Repository navigation
An epic-child PR's merge ref goes stale when the epic branch moves, and nothing retriggers it #8880
Description
An epic child's PR is opened against the epic assembly branch. When the driver pushes that
assembly branch mid-flight (merging origin/main in to clear a base-side red, per the epic-child
claim step in claude-plugins/fabrika/skills/operate/SKILL.md), the child PR's checks do not
re-run. The red the child inherited from the old base stays on the PR, and no verb in the pipeline
retriggers it.
On lane 8716, child #8766's PR #8859 sat red on roadmap-guard after epic/8716 was moved. The
operator cleared it by closing and reopening the PR twice, because the first reopen raced the
merge-ref recomputation and re-ran the guard against the stale base anyway. Close/reopen is also
the wrong lever for a second reason: it tears the PR's preview stage down mid-deploy (#8881).
Triage note. The stated cause needs checking before the fix is chosen. The issue says GitHub does
not rebuild pull/N/merge on a base push. The likelier mechanism is that GitHub does recompute the
merge ref (asynchronously, which is what the first reopen raced) but emits no pull_request event
for a base push, so nothing schedules a run. The two readings point at different fixes: if the ref
does rebuild, a check-suite re-request at the unchanged head is enough; if it does not, only a head
push (empty commit) will do. Settle it against the real platform first, per this repo's
ground-falsifiable-claims rule.
Triage note. heal-ci cannot cover this today. Its one rerun arm is gated to a recognised
transient signature (claude-plugins/fabrika/skills/heal-ci/SKILL.md step 4), and a stale-base
red is not transient; there is no diagnosis token for "the base moved under this PR".
Related, not duplicates: #8881 (the close/reopen fallout), #6918 (no verb refreshes a lane branch
from origin/main), #8839 (build pr has no --base), #8863 (the roadmap-guard red this surfaced
through, closed, wrong stated cause).
Acceptance criteria
- The platform behaviour is verified against a real PR whose base moved. Which of the two
readings above holds is recorded on this issue or in the PR body, with what was read. - After an epic assembly branch is pushed, the operator has a documented, non-destructive way to
retrigger every open child PR's checks. Close/reopen is explicitly named as forbidden, with
Closing and reopening a PR tears down its preview stage mid-deploy and fails the deploy on a deleted D1 #8881 as the reason. - The step lands where the driver will hit it: the epic-child section of
claude-plugins/fabrika/skills/operate/SKILL.md, adjacent to the base push it follows. - If the retrigger is mechanical enough to be a verb, it is one under
packages/fabrika-cli/src/lane/rather than hand-rolledgh apiin skill prose (ADR 0228). - The retrigger is idempotent: running it against a child whose checks are already current is a
no-op, not a second run. -
BaseStanding.behindByis read by a consumer or dropped from the type.compareStandingrefuses the whole sweep when GitHub omitsbehind_by, andretriggerOnereads onlystatus— so a field nothing consumes can fail a sweep. -
lane retrigger's exit 11 either means what the CLI description and the verb-reference row say ("before anything was written") or the wording matches the behaviour. Today a comparison that fails on the second child, after the first child's head was already moved, returns 11 with a write behind it.
Original report (verbatim)
Summary
GitHub does not rebuild a PR's pull/N/merge ref when the PR's base branch is pushed. After the driver moved epic/8716 to clear a roadmap-guard red, PR #8859's checks kept running against the old merge commit and stayed red. The only way through was closing and reopening the PR, twice.
What I was doing
Driving epic lane 8716. Child #8766's PR #8859 (base epic/8716) went red on roadmap-guard because the epic branch had drifted from main. I merged origin/main into epic/8716 and pushed, expecting the child's checks to go green.
What I observed
The PR's check suite did not re-run against the moved base. The roadmap-guard job kept evaluating the stale pull/8859/merge commit. The operator closed and reopened the PR to force a merge-ref rebuild; the first reopen raced the rebuild and re-ran the guard against the stale base again, so it took a second close/reopen. The reviewer filed the same red as #8863 with a wrong stated cause (it named the guard, the real cause is the stale merge ref).
Why it matters
Every epic run that moves its assembly branch mid-flight will leave its open child PRs red with no clean retrigger. Close/reopen is the wrong tool: it also tears down the preview stage mid-deploy (filed separately). Each occurrence is a hand fix the founder has ruled should not exist (#8807 R4.1).
Pointers
- claude-plugins/fabrika/skills/operate/SKILL.md (epic child claim, base push)
- packages/fabrika-cli/src/lane/ (
lane provereads the PR head) - .github/workflows/roadmap-guard.yml
- roadmap-guard I3 reds on milestone #56 though ROADMAP.md carries a paused campaign row for it #8863, A command-declared spell that emits needs ProcessPorts, which the Kernel context does not carry #8766, defineProgram: the Tuval program authoring API #8716, chore: A command-declared spell that emits needs ProcessPorts, which the Kernel context does not carry #8859, epic lanes need hand fixes: main drift, collisions, cleared rounds #8807
Suggested next step (non-binding)
After an epic-branch push, have the operator retrigger the child PRs' checks deliberately, via gh api re-requesting the check suite or an empty commit on the child branch, instead of close/reopen. Document the merge-ref behavior in the operate skill.
Filed by an agent · session 94113692-2733-4534-9963-c7ae534247d0 · branch main · 2026-09-10T05:09:49Z
Activity
usirin commentedon Sep 10, 2026
usirin commentedon Sep 17, 2026
Built
fabrika lane retrigger <epic>plus the operate step that runs it afterlane push.Read first: the platform question the issue asked to settle. It is answered in the PR body off a live
read of this repository, not off docs alone — GitHub rebuilds
refs/pull/<n>/mergeon a base pushand emits no event for it, so a re-run (which replays the original event's pinned SHA) cannot clear a
stale-base red and the head has to move.
Then
packages/fabrika-cli/src/lane/retrigger-verb.ts: the no-op arm is the one to grade againstcriterion 5 — staleness is the platform's own comparison against the base as it stands, so a child
already carrying the base tip is read and never written to. The 422 split (conflict versus a head a
sibling moved) is the other judgement call: GitHub documents one status for both, so the verb
re-reads the head to tell them apart.
Tests run:
packages/fabrika-cli/src/lane/retrigger-verb.unit.test.ts(10, new),src/io/pulls.unit.test.ts,src/lane/command-help.unit.test.ts,src/lane/verbs.unit.test.ts,src/short-description.unit.test.ts.build check --surface codeand--surface proseboth green,and the package typecheck reports 0 errors.
fix(lane): retrigger reports behind_by and keeps exit 11's untouched-…
usirin commentedon Sep 17, 2026
Lane transcript for 8880. Terminal state:
complete(LANE-TERMINAL). PR #9379 merged atb6b98ffcf95411e4c5b07356c170a4e91778fbb1.One build round, one review FAIL, one repair round, review PASS, ship landed through the queue.
lane print + lane history
{ "phases": [ { "name": "pipeline", "tasks": [ "issue" ] } ], "terminals": { "complete": "complete", "tripped": "tripped" }, "tasks": { "issue": { "initial": "queued", "maxRetries": 3, "maxWaits": 3, "maxLaps": 16, "states": { "queued": [ "WIP", "BLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "build": [ "DONE", "BLOCKED", "LAP", "CLEARED", "CANCELLED", "LANDED" ], "build:ui": [ "DONE", "BLOCKED", "LAP", "CLEARED", "CANCELLED", "LANDED" ], "review": [ "PASS", "BLOCKED", "LAP", "FAIL", "CLEARED", "CANCELLED", "LANDED" ], "review:ui": [ "PASS", "BLOCKED", "LAP", "FAIL", "CLEARED", "CANCELLED", "LANDED" ], "ship": [ "LAP", "DONE", "WIP", "BLOCKED", "FAIL", "CLEARED", "CANCELLED", "LANDED" ], "ship:queued": [ "LAP", "DONE", "BLOCKED", "WIP", "FAIL", "CLEARED", "CANCELLED", "LANDED" ], "blocked": [ "UNBLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "human:cp-approval": [ "UNBLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "human:queue-stall": [ "UNBLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "human:machinery-stall": [ "UNBLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "human:budget-spent": [ "UNBLOCKED", "CLEARED", "CANCELLED", "LANDED" ], "shipped": [ "CLEARED", "CANCELLED", "LANDED" ], "diagnosed": [ "CLEARED", "CANCELLED", "LANDED" ], "board:cancelled": [ "CLEARED" ], "board:landed": [ "CLEARED" ] } } } } [ { "task": "issue", "event": "ISSUE.WIP", "at": "2026-09-17T00:51:03.740Z" }, { "task": "issue", "event": "ISSUE.DONE", "at": "2026-09-17T01:08:09.920Z", "pr": "https://github.com/kamp-us/phoenix/pull/9379" }, { "task": "issue", "event": "ISSUE.FAIL", "at": "2026-09-17T01:24:41.616Z", "pr": "https://github.com/kamp-us/phoenix/pull/9379" }, { "task": "issue", "event": "ISSUE.DONE", "at": "2026-09-17T01:34:12.739Z", "pr": "https://github.com/kamp-us/phoenix/pull/9379" }, { "task": "issue", "event": "ISSUE.PASS", "at": "2026-09-17T01:50:00.089Z", "pr": "https://github.com/kamp-us/phoenix/pull/9379" }, { "task": "issue", "event": "ISSUE.DONE", "at": "2026-09-17T01:59:00.238Z", "pr": "https://github.com/kamp-us/phoenix/pull/9379", "partial": false, "landed": [ 9379 ] } ]