feat(pr): queue and drain verbs (#473 Task 4) - #518
Conversation
Adds `forgectl pr queue` (list queued reviews, FIFO by createdAt) and `forgectl pr drain` (launch queued reviews as concurrency-cap slots free up, once or on a --watch interval). One drain pass takes the lifecycle lock, counts occupancy, refuses on any unreadable record, claims the oldest free-slot's-worth of queued records to `preparing` under that same hold, then prepares and launches each through the identical Prepare -> Launch path `pr <ref>` uses. A launch failure records attempts/lastError/lastAttemptAt and returns the record to `queued` for a later pass; at 3 attempts it parks in `needs-repair` instead. `Client.Drain` performs exactly one pass per call, matching the one-report-per-call shape every other composite verb in this package follows; the --watch loop, its per-pass stdout line, and its three-consecutive-refusals exit rule live in the CLI. Dry-run tested against two real open PRs on cameronsjo/forgectl via scripts/dogfood-drain.sh; the live (non-dry-run) launch is left for the orchestrator. Session-Id: c2d13fd6-30bc-409a-989c-cd5ad22073fd Model: claude-sonnet-5 Harness: claude-code 2.1.269 Machine: cf6e768835c7 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…opt-in (#473) The script launched real reviewer sessions unless --dry-run was remembered. Muscle memory now runs the safe path; the destructive one has to be named. Verified: the default path passes under a live tmux server and dispatches no window; without a server it fails closed on the unreadable cap. Session-Id: c2d13fd6-30bc-409a-989c-cd5ad22073fd Model: claude-fable-5-1 Harness: claude-code 2.1.269 Machine: cf6e768835c7 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and release the lock before teardown (#473) 1. A retry no longer tears down a workspace whose review agent may be live. Launch parks the record in needs-repair with a reason naming the window on both branches where the window already exists, and settleDrainFailure was erasing that reason, removing the clean room under the running agent, and returning the ref to queued — so a later pass would launch a second agent for the same ref. The settlement now re-reads the record and asks tmux first: an already-parked record, a window resolvable by the derived name, or an unreadable tmux (fail-closed) all mean leave it parked, keep the workspace, stop retrying. Only a failure with neither signal — a gh or clone failure before dispatch — requeues. 2. Drain mirrors Queue's local-ref refusal at the reader. A queued record marked local is refused at claim time: never claimed to preparing, never cloned, reported as a refused item. Dispatch was previously stopped only by Launch's own refusal, which its comment calls an incidental second barrier. 3. The lifecycle lock is no longer held across os.RemoveAll of a clone. The transition mutator is side-effect-free and idempotent (transitionLocked may run it twice); it records the workspace it cleared, and the teardown runs after the lock is released, through the same bounded sandboxTeardown path. 4. scripts/dogfood-drain.sh sets XDG_CONFIG_HOME and XDG_STATE_HOME alongside HOME on every forgectl invocation. os.UserConfigDir reads XDG_CONFIG_HOME first on Linux, so a HOME-only scratch isolated nothing there. Dry-run stays the default. 5. Nits: CheckDispatchCapability runs once at the top of a pass, so an undispatchable host refuses the pass instead of parking N records; drainItem resolves provenance through provenanceFromRecord, so the forged-authorship warning fires on the unattended path; a requeue clears WindowID; the refusal and watch-refusal strings in internal/cli/pr_drain.go go through safeTerm like the pass line already does. Session-Id: c2d13fd6-30bc-409a-989c-cd5ad22073fd Model: claude-opus-5 Harness: claude-code 2.1.269 Machine: cf6e768835c7 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… owes (#473) Session-Id: c2d13fd6-30bc-409a-989c-cd5ad22073fd Model: claude-fable-5-1 Harness: claude-code 2.1.269 Machine: cf6e768835c7 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Warning Review limit reached
On-demand reviews are free for the next 28 days. After that, they cost $0.25 per reviewed file. Or wait 9 minutes for your next included review. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (11)
Comment |
cameronsjo
left a comment
There was a problem hiding this comment.
Code review (crit=0 imp=1) and an independent security review (crit=0 imp=4, probed) ran against the diff at 8c04073. The code finding, the dogfood script launching by default, is fixed in dd49816f (dry-run default, --launch opt-in, verified under a throwaway tmux server with nothing dispatched). The four security findings in the retry path (teardown under a possibly-live agent and requeue; local queue entries not refused at claim; the lifecycle lock held across the workspace removal; script isolation on Linux) are fixed in 4e73dfac, each with a test seen red against a reverted control. A second security pass at 4e73dfac verified all four closed by its own reads and found nothing new at Important; the delta to this head is plan bookkeeping only.
Verified by the security passes: every launch-time gate re-runs through the ordinary Prepare and Launch rather than trusting the stored record; the claim is one transition under one hold; a drainer killed after the claim leaks a slot rather than double-launching; the retry parks on a resolvable window or an unreadable tmux, keeping the workspace and the window pointer; the post-lock teardown is bounded by the sandbox prefix and symlink checks and cannot run twice on a mutator re-run.
Recorded, not actioned: a mass park after a mid-pass tmux outage prints N identical launch errors with no pointer to the recovery verb (operability, not security); a crash between the requeue write and the teardown leaves an unreferenced quarantined clone in the temp dir; no test drives the watch loop timing, since the codebase has no clock seam. Gates re-run at this head: build, vet, go test ./... -timeout 5m, golangci-lint, gofmt, bash -n on the script, all clean.
Closes #473
Task 4 of the review-autonomy spine (
docs/plans/2026-09-11-review-autonomy-spine.md), the last of four. With this the plan's contract is delivered: lock and atomic records (#497), phases andpr repair(#502), the cap on every launch path (#516), and now the queue and its drainer.What lands
pr queuelistsqueuedrecords oldest first,--json, withno queued reviewsand[]when empty.pr drainruns one pass by default (--once), or loops with--watch --interval. A pass: take the lock, count occupied slots, refuse the pass if any record is unreadable, refuse a local queue entry at claim, claim the oldest queued records topreparingunder the same hold, release, then prepare and launch each through the same pathpr <ref>uses, with every gate re-run at launch time rather than trusted from the record. One line per pass reaches stdout even with logging off;--dry-runnames what would launch and creates nothing;--intervalwithout--watchrefuses; exit codes follow the design.attemptsandlastError, tearing the failed attempt's workspace down after the lock is released. A failure that already parked the record, or that left a window tmux can resolve, or that happened while tmux was unreadable, stays parked with a reason namingpr repair --adopt-window; the workspace and the window pointer are kept. The drainer therefore never launches a second agent over a live one. At three attempts a record parks withdrain: 3 attempts, last: ….scripts/dogfood-drain.shqueues two refs inside a scratch home (withXDG_CONFIG_HOMEandXDG_STATE_HOMEpinned, so the isolation holds on Linux), drains, and asserts the report. Dry-run is the default;--launchis the opt-in for the live pass.Dogfood
The dry-run path was run against two real open PRs under a live tmux server: both named as would-launch, no workspace created, no window dispatched. The live
--launchpass is still owed.Review
Code review at
8c04073: clean apart from the script defaulting to a live launch, fixed indd49816f. Security review at8c04073, probed: launch-time gates hold; four Important in the retry path (teardown under a possibly-live agent and requeue; local entries not refused at claim; lock held across the removal; script isolation on Linux), all fixed in4e73dfacwith tests seen red against reverted controls. Re-check at4e73dfacis in the marker comment. Recorded, not actioned: a dispatch failure now parks rather than retries, so the three-attempt budget applies toghand clone failures; no test drives--watch's own loop timing, since the codebase has no clock seam yet.Verification
From the worktree:
go build ./...,go vet ./...,go test ./... -timeout 5m,golangci-lint run,gofmt -l .,bash -n scripts/dogfood-drain.sh, all clean, re-run by the orchestrator at each head.Session-Id: c2d13fd6-30bc-409a-989c-cd5ad22073fd
Model: claude-sonnet-5
Harness: claude-code 2.1.269
Machine: cf6e768835c7