Repository navigation
fix(land-watch): leave a PR another session holds to that session - #469
Conversation
Quest-Claim: fc9dee9c610f484b
The watcher read no hold labels, so it fixed, pushed to, and re-enqueued a PR while a /quest-iterate session held it. It now checks for a hold when it handles the removal, before it starts the fixer, and again before it pushes a result or re-enqueues. A hold ends the pick, and the PR gets one comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watcher enqueues an armed pick GitHub left out of the queue, including its own re-enqueue that only armed auto-merge. That path never checked holds, so a session's hold taken while the checks ran did not stop the watcher from enqueuing the PR. It now skips a held PR, keeping the pick armed for the pass after the hold clears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. WalkthroughThe watcher now checks pull-request hold labels before starting fixer work, publishing results, or re-enqueueing. It records held removal outcomes before posting a best-effort comment. The fixer and flaky-test disable flows leave held pull requests for a maintainer to enqueue after the hold clears. Tests cover holds observed at multiple stages. Merge Risk:
|
There was a problem hiding this comment.
ℹ️ One minor wording accuracy point inline — nothing blocking.
Reviewed changes — reviewed head bd6deef against main (full diff, all 6 files), traced every push and enqueue site in the watcher path against the hold checks, and ran the suite locally (test/land-watch.test.ts: 23 passed; bun run check: tsc + prettier pass).
- Hold detection plumbing: labels join the open-PR snapshot and the
PULLquery;holdOfreads anyquest:*label but the legacyquest:merge, reusingisHoldLabelfromsrc/core/hold.ts, so the semantics match the hold machinery the other commands use. - Removal handling: a fixable-reason removal of a held PR gets verdict
heldinstead offix— no fixer plan is recorded, the pick is ended, and the removal comment says the watcher leaves it to the holding session. - Fixer gates: the hold is re-checked at each action point — after the already-started-job check in
takeand before the conflict merge and the slot wait, on a fresh read incollectbefore anything is pushed, with the pick upheld when the round's own re-enqueue was already accepted by GitHub, again right beforepublishHeadto close the validation-window race, and inrequeuebefore an enqueue of a head GitHub has not taken.Heldthrown from any of these is folded into a singleheldcomment per pick, with the fixer's note dropped. - Armed picks and disable PRs:
enqueueArmedleaves a held armed pick armed for the pass after the hold clears, andopenDisabletreats a held disable PR like a refused one — commented once and left for a maintainer, not retried each pass. - Tests and docs: the new fixer test covers a hold at the removal, one taken while the fixer runs, one taken during fix validation (verifying the origin branch is untouched), and one taken while a removal waits for a fixer slot, plus
quest:mergeholding nothing; the armed-pick test covers both hold timings and the enqueue once the holds clear. The README's watcher section documents the behavior and quest records were cleaned up consistently.
glm-5.3-flash | 𝕏
A hold found by the re-enqueue, after the fixer's push, or on a restart whose head is already the fix, posted that the result "was not applied". The comment now names the pushed head, so the holding session knows its branch moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A held round whose comment GitHub refused stayed pending once it had asked to re-enqueue, so if the hold cleared before the next pass, that pass re-enqueued it without a maintainer's pick. The job now records the hold first, and every retry leaves the PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A removal's or a waiting round's held comment that GitHub refused left nothing recorded, so if the hold cleared before the retry, the watcher fixed the PR as if it had never been held. The removal record, with its hold, is now saved before the removal's comment, and take records the hold on the removal before commenting; a retry reuses either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fails A disable PR found held, or refused, at its enqueue posted before it recorded the outcome, so a refused comment left it to be enqueued once the hold cleared. The refusal is now recorded first, and a retry only posts the comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # test/land-watch.test.ts
…a taken requeue A held round whose comment GitHub kept refusing was set aside without ending its pick, so a later App enqueue continued it. The pick now ends when the hold is recorded, before the comment, in collect and take. After a crash that lost a round's requeue result, a hold taken since tripped the pre-push check even though the head was already pushed and queued. That check now runs only when a push is still needed; requeue still checks the hold before any enqueue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — the new persistence-ordering commits check out; one minor wording suggestion inline, of the same class as the comment from the previous review.
Reviewed changes — reviewed the delta since pullfrog's prior review of bd6deef: commits 411c13b, 58823be, fa70c6b, bb74740, and 1ec89e3 plus the merges of main. Traced every hold-recording site end to end and ran the suite locally (test/land-watch.test.ts: 25 passed; bun run check: tsc + prettier pass).
- Hold recorded before the comment (
411c13b,58823be,fa70c6b,bb74740): each site that posts a held or refused comment now persists the hold first —leave()writesjob.heldand short-circuits a retry into comment-only,takesavesremoval.holdbefore its comment,handlesaves the removal record (withhold) and ends the pick before its comment, andopenDisablerecordsDisable.refusedbefore its comment — so a hold seen once ends the pick even if GitHub refuses every comment and the label clears before the retry. - Says the fix was pushed when it was (
411c13b):collecttracks whether the PR's head is the round's fix, and the held comment picks between "was pushed as<head>, but not re-enqueued" and "was not applied"; the held test asserts both comment texts. - Recovery keeps a taken requeue (
1ec89e3): the pre-push hold read runs only when a push is still needed, so a crash that lost a round's requeue result no longer drops a head GitHub already queued;requeuestill checks the hold before any enqueue, and the pick ends when the hold is recorded, so a round set aside for refused comments leaves no pick open. - Tests and docs: the flaky-disable test is parametrized over a hold on the disable PR as it opens (comment refused once, left for a maintainer after the hold clears), the held test grows a scenario for a hold while the removal waits with refused comments retried on the next pass,
World.refusebecomes a list, andWorld.onPullfires on each single-PR read. The README's watcher section documents the behavior and the quest records were cleaned up consistently.
Only one minor finding inline — see the suggested one-line derivation of pushed.
glm-5.3-flash | 𝕏
…st-effort Per the design checkpoint settled on 2026-11-04: every step that sees a hold ends the pick in its state first and stops acting on the PR, then posts one comment best-effort, with one wording whether or not a fix was pushed. Drop job.held, Disable.refused, Removal.hold, and the comment retry paths. A removal recorded held still ends the pick if a crash makes the pass handle it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: simplify the held PR's bookkeeping, and comment on it best-effort. Settled by the maintainer in
The next local review starts a new session with (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — reviewed the delta since pullfrog's prior review of b46de7c0: commits a2b69f7 (clean merge of main) and 2ccd142c, the simplification settled at the design checkpoint. Traced every site that can see a hold end to end against its pick's state writes, and ran test/land-watch.test.ts (25 passed) and bun run check (tsc + prettier) locally.
- One wording, best-effort comments (
2ccd142c):holdOf/heldTextare replaced byisHeldand a singleHELDconstant, and every hold comment goes through the newsayHeld, which warns about a GitHub refusal and never retries. - State before comment, everywhere:
collect'sleave()now runsendPick→finish→sayHeld,takerunsmark("held")→endPick→sayHeld,openDisablerecordsdone: "held"before its comment, andhandlesaves the removal record before its comment with only a held verdict's comment best-effort — a refusal on any other verdict still fails the pass, as before. The persisted-hold bookkeeping is gone (Removal.hold,job.held,Disable.refused, and their retry tests), so no site can claim a pushed/worded state anymore. - Pre-push hold read keyed off
ours: the fresh label read beforepublishHeadruns unless the round's fix is already the PR's head, andrequeuestill checks the hold before enqueuing a head GitHub has not taken; a head this round already had queued stays queued. - A held round is dropped cleanly: its fixer note and flaky verdict are not recorded, so the hold's comment is the only one after the removal's; a removal the fixer took while held keeps
taken: "held", whichadvanceFixes'taken === undefinedfilter never re-takes, andhandle'sprior.verdict === "held"covers a crash before its comment. - Tests and docs: the held test asserts exactly one held comment per PR once the holds clear (
[1, 1, 0, 1, 1]for 5–9), simulates the crash losing a handled mark, and keeps the timings at the removal, during validation, after the push, while the fixer runs, and while a slot is waited on; the flaky-disable test recordsdone: "held"with the comment refused; the armed-pick checks stand. The README's watcher section documents the best-effort comment and the held disable PR.
glm-5.3-flash | 𝕏

Finishes quest/m0/land-fixer/holds.md.
Problem
quest land watchnever read a PR's labels, so it ran the fixer on, pushed to, and re-enqueued a PR another session held. On moq-dev/moq#6949, the host gate pushed a fixer merge and re-armed the PR while a/quest-iteratesession heldquest:iterateand was pushing its own fixes; the watcher, which replaced the gate there, had the same gap.Change
A hold is any label
isHoldLabelaccepts (quest:active, or an older pin'squest:iterate; the leftoverquest:mergeholds nothing). Every step where the watcher can see a hold ends the held PR's pick in its state first and stops acting on the PR, then posts one comment best-effort: a comment GitHub refuses is warned about and never retried. The comment has one wording (HELDinsrc/cli/land-watch-fix.ts), whichever step found the hold and whether or not a fix was already pushed.handleinsrc/cli/land-watch.ts): the pass's open-PR snapshot now reads labels. A fixable removal of a held PR gets the new verdictheldinstead offix, recorded before the removal's comment, which then says the watcher leaves the PR to its session. A removal recordedheldstays held if a crash makes a pass handle it again after the hold clears.take): the PR re-read now carries labels. A hold taken while the removal waits for a fixer slot marks the removalheldand ends the pick.collect): a held PR's round is dropped before its result is recorded, like a round whose pick ended: no fixer note, no flaky verdict, no push. The PR is read again right after the fix is validated and beforepublishHead, since validation takes a while; a head already pushed (after a crash) needs no push and skips that read.requeue): a held PR is not re-enqueued. A head this round already asked GitHub to queue, and GitHub took, stays, since the hold came after it.openDisable): a held one is recordeddone: "held"and left for a maintainer, as a refused one is.enqueueArmed): the watcher does not enqueue an armed pick GitHub left out of the queue while the PR is held. The pick stays armed, and the first pass after the hold clears enqueues it if its head and request are unchanged.The README's watcher section documents it.
Public API: none.
quest land watchbehavior changes as above. Its state files change: the removal record'sverdictgainsheld, and a disable'sdonecan beheld. Nothing is in production, so no migration.Choices made without asking
handlerecords every removal before its comment, not only a held one. A held removal's comment is best-effort; any other verdict's refused comment still fails the pass before the event is marked handled, so the next pass handles it again and posts it, as before.enqueueArmed) is a writer's request, not a watcher's pick, so a hold leaves it armed rather than ending it, as decided before the checkpoint.Validation
bunx vitest run test/land-watch.test.ts: 25 passed.quest:mergealone does not hold. GitHub refuses the hold comments of 5 (at the removal) and 7, and neither is retried once the holds clear. A crash that loses 5's handled mark still leaves it held after its hold clears.requeued 5 <fixed>.handle's recorded and live hold,take,collect's finished-round check, its pre-push read and that read's skip for a pushed head, the kept queued head,requeue,enqueueArmed, and ending the pick incollect); each fails the suite.just checkandjust test(throughquest gate) on2ccd142c: both pass.origin/main(at844c9934) cleanly.Decisions
Settled by the maintainer in
/quest-completeon 2026-11-04:endPick), the watcher records it, comments once, and forgets the pick. A maintainer enqueues the PR again once the session finishes.Settled by the maintainer in
/quest-completeon 2026-11-04, at the design checkpoint (Design change). Seven local review rounds plus Pullfrog kept finding gaps in the held comment's bookkeeping (persisting it before posting, retrying refused comments, "pushed as X" vs "not applied" wording), not in the rule itself:job.held,Disable.refused,Removal.hold, the comment-retry paths, and their tests are gone; the tests for the rule at each step stay.Review
Codex (
quest review, run from this branch's source), gpt-6.1-sol, xhigh effort, in a new session after the design checkpoint (quest review --new).2ccd142c, againstorigin/mainat05aebae9: no findings.The earlier sessions' rounds (seven, plus Pullfrog) reviewed the held comment's bookkeeping this change removes; see the Design change.
Pullfrog's threads: discussion_r4470352858 (the held comment said "was not applied" after a push) and discussion_r4472838625 (a recovered retry re-derived that wording) are both moot: the comment has one wording that claims nothing about the fix, and the retry path is gone. The second is answered and resolved.
Suggested follow-ups
None.
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)