Conversation
An area can hold several open reports and only one can land: the gate requires a byte-exact append at the cursor in `PROGRESS.md`, so once `main` moves, every other open report for that area is unmergeable for good. Nothing retired them, so they accumulated one per area per round. Retire them from the merge workflow, after the compare-and-swap has already chosen a winner, on the one rule that needs no prediction: the window does not start at the live cursor. That is decidable from the branch name and the cursor alone. Doing it beforehand, on the grounds that another open report covers more of the window, is wrong in three ways that each cost real work. The favoured report may fail its build, leaving nothing landed and the other closed-unmerged, which `apply` treats as permanently refused. The close races the merge workflow, which reads a pull request's state when it collects and trusts that snapshot until it writes. And "covers more" has to be read from a pull request body, which anyone may edit, so a stale or forged one retires a live report. Waiting for the ref update removes all three. The publishing path therefore closes nothing at all, and the merge workflow gains a checkout of the validator at the ref it was already pinned to, so the retiring rules are reviewed alongside the gate rather than separately. Also re-read the pull request immediately before the compare-and-swap, so that closing one stops it rather than usually stopping it. Three attempts: an answer that says the pull request moved is a considered non-landing and exits clean, while no answer at all is infrastructure failing and exits non-zero. Conflating those would let one timeout report a green run that landed nothing, after which the planner sees the pull request still open and in flight and never retriggers it. 🤖 Prepared with Claude Code
`TauCetiRoadmap/<area>` and `Completed/<area>` are different roadmaps whose cursors are different values in different files, which is why the collector derives the parent from the changed paths rather than probing in a fixed order. The sweep hard-coded `TauCetiRoadmap/`, so a report landing under `Completed/` read the active roadmap's cursor, or none at all when no active roadmap of that name exists. Carry the validated parent through the bundle and the workflow alongside the area, and build the path from it. Report branches are `progress/<from7>-<to7>/<Area>` and record no parent, so when a name exists under both, the open reports cannot be attributed to one roadmap or the other. Retire nothing in that case and say so: leaving a few orphans for a human beats discarding another roadmap's live work on the cursor we happen to be holding. 🤖 Prepared with Claude Code
…cleanup Retiring on "its cursor is not the current one" reads a mutable snapshot and treats every disagreement alike, so a stale contents response retires a report that starts *ahead* of it -- the live one. Retire instead on positive evidence from committed history: the log shows that cursor already appended at and moved past. Reading less history shrinks the evidence, which can only retire fewer reports, so staleness fails in the safe direction. Seven hex characters are not a commit, so a prefix matching both a spent cursor and the live one proves nothing and the report is kept. Match the gate's exact branch grammar and require the base branch a report must target. The looser parser would accept `progress/nothex-whatever/Area`, which is somebody's ordinary pull request; failing an automated gate is not a reason to close a human's work. Thread the repository through to the close, rather than reading one repository's pull request numbers and closing another's by the same number. List open pull requests by pagination rather than a capped fetch that filters afterwards. The cap was a starvation lever: enough newer pull requests of any kind, needing neither to merge nor to be plausible, push an area's stranded reports out of the window indefinitely. Move the cleanup into its own job. It does not need the bypass credential and must not hold it -- retiring a report is an ordinary `pull-requests: write` operation, while the App token exists to write to a protected branch. The separate job also stops a failing checkout reddening a merge whose content is already on `main`, which no `|| true` on a later step could catch, and replaces a step-level `if:` whose implicit `success()` would have skipped cleanup after any earlier hiccup. 🤖 Prepared with Claude Code
|
I did a fresh pass over the current head (
Aside from those, I like the direction here: retiring only after the CAS, removing mutable PR-body metadata from the destructive decision, separating cleanup from the bypass token, carrying the parent through validation, and removing the capped PR listing are all improvements. CI is green on the current head. Prepared with GPT 6. |
`file_on_default_branch` now returns None only for a 404 and raises on any other failure, so the sweep retires nothing when it cannot tell whether the area also exists under the other parent. A report is retired only when the full `from_sha` of the section its own head appends is a spent cursor; the seven-character branch prefix merely nominates candidates. This keeps a live report whose start shares a prefix with a spent cursor that a stale log still shows. The late pull-request re-read is described as narrowing the close-vs-land window, not closing it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This PR retires an area's unmergeable reports from the merge workflow, once the compare-and-swap has already chosen a winner, and stops the publishing path closing anything at all.
An area can hold several open reports and only one can land. The gate requires a byte-exact append at the cursor recorded in
PROGRESS.md, so the moment one lands and the cursor moves, every other open report for that area is unmergeable for good. Nothing retired them, so they accumulated: one per area per round, never shed.The rule is now the one that needs no prediction: a report is retired only when committed
PROGRESS.mdonmainshows the cursor its window starts at already appended at and moved past. The branch name's seven-character prefix only nominates candidates; the decision compares full SHAs, taking the report's starting SHA from the section its own head appends, and keeps the report whenever that cannot be read. No window comparison, no pull-request metadata, no ordering. Nothing is retired when the area exists under bothTauCetiRoadmap/andCompleted/, or when a failed lookup leaves that unknown, since report branches do not record their parent.Retiring beforehand, on the grounds that another open report covers more of the window, is wrong in three ways that each cost real work. The favoured report may fail its build, leaving nothing landed and the other closed-unmerged, which
applytreats as permanently refused and no later run repairs. The close races this very workflow, which reads a pull request's state when it collects and trusts that snapshot until it writes. And "covers more" has to be read from a pull request body, which anyone may edit, so a stale or forged one retires a report that was alive. Waiting for the ref update removes all three at once, because there is then nothing left to predict.Ownership is deliberately not consulted, unlike in
apply. There the question is whose work may be superseded and the answer has to be "only our own", or a stranger can be vetoed. Here the report is unmergeable for its author as much as for anyone, and leaving it open marks the area in flight against them.The merge job gains a checkout of the validator at the ref it was already pinned to, since it is the first thing in that job to run code from this repository rather than call
gh. The retiring rules are therefore reviewed alongside the gate, not separately.This also re-reads the pull request immediately before the compare-and-swap, which narrows the window in which a pull request closed mid-run still lands. It does not eliminate it: a close between that read and the ref update is not seen. It reads to refuse and never to authorise: the tree and the parent still come from the two pinned SHAs, so nothing read there can cause a landing, only prevent one. Three attempts, because the distinction that matters is between an answer saying the pull request moved, which is a considered non-landing and exits clean, and no answer at all, which is infrastructure failing and exits non-zero. Conflating those would let a single timeout report a green run that landed nothing, after which the planner finds the pull request still open and in flight and never retriggers it.
Supersedes #15.
🤖 Prepared with Claude Code