Skip to content

fix(pr-fix-queue): refuse FIXED on no-progress and reopen stale tombstones - #942

Merged
joryirving merged 2 commits into
mainfrom
foreman/wl-misospace-dispatch-940/issue-940
Sep 6, 2026
Merged

joryirving merged 2 commits into
mainfrom
foreman/wl-misospace-dispatch-940/issue-940

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Closes #940.

Why this PR exists

Foreman produced this work and then threw it away. The coder returned GO (turnCount 158, succeededOnTarget: true) and the reviewer returned APPROVE — but the verdict was rewritten to NO-GO by the issueAsk rail:

"verdict":  "NO-GO",
"reviewer": { "outcome": "APPROVE" },
"issueAsk": { "verified": false,
              "claimed": "Record `headSha` at enqueue so a comparison is possible at all." }

That claimed string is item 1 of #940's own "Suggested shape" list, and the branch implements exactly it — headSha: prHeadSha at four call sites. The reviewer opens a PR only on GO, so none was opened; the bridge then retried the whole Workload three times (each attempt identical by construction, since re-running cannot make an unverifiable ask verify) and parked the issue as needs-human.

Opening it by hand so 450 lines of correct work are not lost. Root causes filed separately: misospace/foreman-dispatch-bridge#287 for the wasted retry budget, defilantech/LLMKube#1760 for the demotion itself.

What the change does

Addresses #940's three substantive points:

  • Records headSha at enqueue (src/lib/pr-followup-ingestion.ts, src/app/api/pr-followup/sync/route.ts, src/app/api/pr-followup/webhook/route.ts), so a later FIXED transition has something to compare against. Previously the column was always null.
  • Refuses FIXED when the PR head has not moved (src/lib/pr-fix-queue.ts) — a fix Workload that exits successfully without advancing the PR is not a fix.
  • Reopens stale tombstones — an item already marked FIXED whose evidence is still being re-detected goes back in the queue instead of accumulating another enqueue row.

New helper fetchPullRequestHeadSha in src/lib/github-prs.ts.

What I changed on top

One commit: fetchPullRequestHeadSha was missing from the pinned export list in src/lib/github-facades.test.ts, which was the single failing test in the suite. Nothing else touched.

Verification

Tests  2562 passed | 5 skipped (2567)
typecheck  clean
lint       0 errors, 4 warnings — identical to main (pre-existing)

I checked the lint warnings against origin/main directly rather than assuming; same four.

Worth reviewing carefully

The coder ran to turnCount 158 against a maxTurns of 160, so it finished close to the ceiling. I have reviewed the diff and run the suite, but a change that nearly exhausted its budget deserves a closer read than one that converged early.

Assisted-by: Claude Code (diagnosed the discarded verdict, fixed the failing facade test, verified the suite and opened this PR; the implementation is foreman's).

itsmiso-ai and others added 2 commits September 6, 2026 00:25
…tones (#940)

The PR-fix queue trusted any successful workload exit as a fix and closed the
item as FIXED. When the workload ran without pushing anything (misospace/
miso-gallery#467), the PR kept CHANGES_REQUESTED and the FIXED tombstone
stranded it: re-detection wrote more enqueue rows but never re-dispatched, and
the only recovery was a two-call sequence that lied about the state.

1. Record `headSha` at enqueue: the pr-followup sync and webhook now capture
   `head.sha` from the PR list/detail payload and thread it into the ingestion
   event and PrFixQueueItem.headSha.
2. Verify before FIXED: `markPrFixItem` runs a head SHA comparison via the
   new `assertPrHeadMovedForFix` helper; if unchanged it rolls the item back
   to QUEUED with an audit note instead of writing the untrusted tombstone.
   Legacy rows without headSha and transient GitHub failures degrade to
   accept (don't strand, don't pretend the guard ran).
3. Reopen stale FIXED on re-detection: `enqueuePrFixItem` reopens an item
   when same evidence is re-detected with the same recorded head SHA, the
   safety net for the worked example (271 re-detections vs 1 mark row).
4. One-call recovery: `requeuePrFixItem` now accepts FIXED items, removing
   the "mark BLOCKED first to lie about state" workaround.

Tests cover head SHA capture, the guard (pass / no-record / unchanged /
unavailable), the reopen path (both the worked example and the
new-head-SHA companion case), and the FIXED-requeue path.

Fixes #940

Signed-off-by: Saffron <263493777+itsmiso-ai@users.noreply.github.com>
…t list

The facade test asserts github-prs' exact export list, and the new
fetchPullRequestHeadSha was not added to it. Only failure in the suite.

2562 passing, typecheck clean; the four lint warnings are identical on main.

@its-saffron its-saffron Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — primary route

Recommendation: Approve

This PR correctly implements the three substantive fixes from PR 940. The work is well-structured, thoroughly tested, and handles all edge cases appropriately. CI is green across all completed checks (Tests was pending at time of analysis).

Change-by-Change Findings

New helper: fetchPullRequestHeadSha (src/lib/github-prs.ts)

Fetches the current PR head SHA from the per-PR detail endpoint. Returns null on network failure or missing head.sha. Called by markPrFixItem during the FIXED transition to compare against the recorded head.

headSha capture at sync time (src/app/api/pr-followup/sync/route.ts)

The GithubPR interface now extends Omit<GithubPRBase, "head"> and redefines head to include sha. prHeadSha = pr.head?.sha ?? null threads the value through all event types (comment, review_comment, review, check_run, merge_state, merge_conflict). This is the canonical capture point—GitHub's pulls?state=open returns head.sha on every entry.

headSha capture at webhook time (src/app/api/pr-followup/webhook/route.ts)

Webhook event parsing also captures headSha. Note the explicit null for check_run events (GitHub doesn't include head.sha on that payload): the FIXED guard degrades gracefully for that path.

PrFollowupEvent interface (src/lib/pr-followup-ingestion.ts)

headSha?: string | null added to the event interface. ingestEvent and ingestMergeConflict now forward it to enqueuePrFixItem.

Core fix logic (src/lib/pr-fix-queue.ts)

  • enqueuePrFixItem: When re-detecting known evidence on a FIXED item whose headSha matches the new headSha, sets reopenFixStale = true and flips the item back to QUEUED. History records a #940-tagged note explaining why.
  • assertPrHeadMovedForFix: Returns "passed" | "no-record" | "head-unchanged" | "head-unavailable". Best-effort by design—refuses on "head-unchanged", accepts on "no-record" and "head-unavailable".
  • markPrFixItem: On FIXED transition, runs the guard. If "head-unchanged", rolls back to QUEUED with a history note.
  • requeuePrFixItem: Now accepts FIXED items in addition to BLOCKED (single-call recovery instead of two). History notes "PR 940 recovery" when reopening from FIXED.

Linked Issue Fit

Issue PR 940 Point Status Verification
1. Record headSha at enqueue ✅ Implemented prHeadSha captured at sync, webhook, and ingestion layers; threaded into all event types
2. Verify before marking FIXED ✅ Implemented assertPrHeadMovedForFix + guard in markPrFixItem; refuses FIXED when head unchanged
3. Reopen stale FIXED tombstones ✅ Implemented enqueuePrFixItem reopens FIXED when same evidence + same headSha re-detected
4. Optional: requeue accepts FIXED directly ⚪ Not addressed Issue called it "optionally"; PR body states three substantive points

Standards Compliance

The implementation follows all documented conventions:

  • Error handling: assertPrHeadMovedForFix uses catch with error instanceof Error; GitHub fetch failures are best-effort (warn and accept) rather than blocking.
  • Validation: Status normalization via normalizePrFixStatus before transitions.
  • No commit of secrets: No secrets or env file changes.
  • Prisma: No schema changes in this PR; headSha is an existing nullable column.

CI Results

All completed checks pass: Docker Build (MCP), Docker Build, Build, Lint, Database migrations, Coverage, Database integration, Typecheck, npm audit, smoke. Tests was still pending at analysis time, consistent with the author's reported "2562 passed | 5 skipped".

Edge Cases Covered

Scenario Expected Behavior Test?
FIXED with head moved Accepts FIXED ✅
FIXED with head unchanged Refuses, rolls back to QUEUED ✅
Legacy row (no headSha) Accepts FIXED (cannot compare) ✅
GitHub fetch failure Accepts FIXED (best-effort) ✅
GitHub returns null headSha Accepts FIXED (best-effort) ✅
Non-FIXED transitions No GitHub round-trip ✅
FIXED item reopened via requeue Works as single call ✅
STALE item requeue Still rejected (PR is gone) ✅
check_run webhook (no headSha) Guard skipped for that path ✅ (code comment)

@joryirving
joryirving merged commit e145d89 into main Sep 6, 2026
18 of 19 checks passed
@joryirving
joryirving deleted the foreman/wl-misospace-dispatch-940/issue-940 branch September 6, 2026 03:48
@its-miso its-miso Bot mentioned this pull request Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2] A pr-fix item marked FIXED on workload exit alone strands the PR forever, with no way back

2 participants