Repository navigation
Description
Problem
Publish in _stage_one (stage.py) has these steps in order:
- Push the branch.
- Open the draft PR.
- Write the marks in one
set_marks(BRANCH, PR, SHA, GROUPS, TEXT_HASH and others). - Add the labels.
- Post the comment.
_open_pull removes the branch when the draft cannot be made. A label failure is caught (_label_pull and _label_needs_fix catch GitHubOperationError). A failed comment comes after the marks write, so the item is recorded.
Only a failed set_marks (step 3) leaves a pushed branch and a draft PR with no stage.pr. A kill of the worker between step 1 and step 3 leaves the same state.
The item then stays out of every later round as held by a pushed branch (_Judge._held, issue_holder). _reconcile reads closed items only. A person must delete the branch and close the draft by hand.
Why a search by issue number is not safe. The held check finds a branch by the issue number. A branch of a person or of an agent for the same issue number looks the same. OpenPull has no author field. The only author guard is _foreign_commits. It compares commit authors with the git user.name of the clone. If the clone has the identity of that author, the guard passes. A rule that deletes every pushed branch of an Item with no stage.pr would then delete the work of a person. It would also defeat _Judge._held (#2654).
Expected
Mark the intent before the push. Then the job can tell its own orphan from any other branch.
- Add the key
stage.pendingtoStageRecordKey(stores/store.py). Its value is JSON: the branch name and the time._stage_onewrites it with oneset_marksbefore the push. The marks write after the draft sets it to None, together with the other keys. - Add a pass
_orphansnext to_reconcile. It reads the open items that havestage.pendingand nostage.pr.- The pass uses the branch name from the mark. A retitle between the failure and the next round changes the slug, but not the mark.
- A mark younger than a constant age (1 hour) is left alone. A run in another clone can be in the middle of its publish.
- A branch that is not on
origingivesRemoval(..., gone=True): only the record is cleared. - A pushed branch with
_foreign_commitsequal to 0 gives aRemoval._remove_stagedcloses the draft, deletes the branch and clears the record. The draft is the open pull request with that head. The pass readsstore.open_pulls()once per round, and only when a pushed branch needs it. - A pushed branch with a commit of another author is kept and reported as a
KeptBranch, as_reconciledoes for closed items. It costs no extra request.
- A pushed branch with no
stage.pendingmark is never touched. It belongs to a person or an agent.
The item is then planned again in the next round. The plan facts (groups with measured tokens, the hash of the text) are in the marks write that failed. They cannot be rebuilt without the planner.
Request cost: each staged item adds 2 requests (one read and one PATCH of the issue for stage.pending). Staged items are rare. A round with an orphan to remove adds 1 read of the open pull requests. Other rounds add none.
Acceptance criteria
-
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k marks_write_fails- a test fails the marks write after the draft and runs the next round. It finds the draft closed, the branch deleted and the item planned again. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k killed_after_push- a kill between the push and the draft leaves a branch with no draft. The next round deletes the branch and clears the record. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k retitle_between- a retitle between the failure and the next round changes the slug. The orphan is still found by the mark. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k foreign_commit- a branch with a commit of a person is kept and reported, not deleted. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k other_branch_kept- in one round, the orphan of the job is removed. A pushed branch of an agent for another Ready item, with no mark, is not touched. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k young_mark- a mark younger than the age limit is left alone. -
uv run pytest tests/roadmap/stage/test_roadmap_stage_orphan.py -k request_count- a staged item adds 2 requests for the mark. A round with an orphan to remove adds 1 read of the open pull requests. A round with a kept branch adds none.
Every command above fails on the unchanged code. The pull request cites the fail-to-pass run (devops ci fail-to-pass --base <ref>).
Feasibility
I read these files at origin/main:
- stage.py:
_stage_one,_open_pull,_label_pull,_label_needs_fix,_reconcile,_rejections,_remove_staged,_foreign_commits,_Judge._held. - git/worktree_add.py:
issue_holder. - stores/store.py:
OpenPull(it has no author field) andStageRecordKey.
I checked that _stage_one already writes BRANCH only after the push, so a change of that order gives nothing. I checked that the marks write is the single write that sets PR, BRANCH and SHA.
I did not run the code. I did not check GitHub behavior for the deleted head branch of an open pull request. The pass closes the draft itself with _remove_staged, so it does not depend on that behavior.
The first idea, a search by issue number and branch pattern, is dropped. The reason is in the Problem section.
Existing tool considered
_remove_staged, Removal, KeptBranch, _foreign_commits and the JSON mark pattern of stage.rejected (all in the repository). No library is needed.
Cost
Build: about 50k tokens and 2.5 h (one key, one write, one pass, 8 tests). Keep: about 2k tokens per quarter. Fix: about 10k tokens and 0.5 h.
Related
Part of #3027. #3387 (items whose draft plan fails the check; a different fault), #3648 (display of stopped items; this change adds no explain line). Order: after the issues that change stage.py before it.
Activity
dan-petty commentedon Nov 7, 2026
Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.
fixprefix of the title gives the type.(roadmap)scope of the title gives the scope.It is a rare failure path that needs manual cleanup, but it blocks an item from all later rounds until a person intervenes.It removes a manual recovery step for a real but infrequent operational failure.It adds a new mark key, modifies the publish order, and adds a multi-branch _orphans pass with edge cases for age, foreign commits, and gone branches.Both deal with the stage job and pushed branches, but #3387 re-stages items whose plans fail checks while the candidate cleans up orphans left by a failed marks write.