Skip to content

feat: split fleet PR maintenance out of implement-issue into sweep-prs - #594

Merged
johanzander merged 4 commits into
mainfrom
feat/sweep-prs-skill
Aug 15, 2026
Merged

feat: split fleet PR maintenance out of implement-issue into sweep-prs#594
johanzander merged 4 commits into
mainfrom
feat/sweep-prs-skill

Conversation

@johanzander

Copy link
Copy Markdown
Owner

Summary

  • implement-issue now watches its own PR to green, and merges origin/main before pushing.
  • Fleet-wide PR maintenance moves to a new sweep-prs skill, runnable under /loop.
  • implement-issue Step 4 is unchanged — it still prunes merged worktrees and nothing more.

Problem

implement-issue stopped at draft-PR-open. It never checked whether that PR went green, and local quality-check.sh does not reproduce the CI matrix. Separately, a PR that was green when its session ended goes CONFLICTING as soon as other PRs merge ahead of it — and GitHub creates no workflow run at all for a conflicted PR, so it presents as "CI never fired" rather than as a conflict. Nobody investigates a PR that appears to have no checks.

A dry run against the current fleet found #437 and #579 sitting CONFLICTING DIRTY, both invisible for this reason.

Changes

implement-issue (its own PR only)

  • Step 9: git fetch origin && git merge origin/main before pushing. Step 4 cuts the branch from a current origin/main, but Steps 5–8 take hours and other PRs merge during them.
  • New Step 10: gh pr checks --watch --fail-fast, fix failures in the worktree, re-check mergeability. Scoped explicitly to this PR — widening a single-issue session into fleet cleanup is how two sessions end up pushing to one branch. --watch blocks rather than polls, so it costs one wait, not a context re-read every 60s.
  • Hard constraints renumbered to 11; CI-mode table, Quick Reference, 3 Rationalizations rows, 3 Red Flags updated.

sweep-prs (new)

Skip gate → classify → act → report, over every worktree.

The skip gate is the piece implement-issue must not grow: a live Claude session at that cwd (any status — idle and blocked included, since an agent parked between Step 6 and Step 8 reads as idle and still holds its branch), uncommitted tracked changes, a HEAD under 30 minutes old, or detached/locked all mean hands off. Ownership is read with claude agents --json run unscoped, because sibling worktrees never appear in the project-scoped view.

Then: merged → prune; open and CONFLICTING/BEHIND → merge origin/main, auto-resolve mechanical conflicts only (CHANGELOG ## [Unreleased], imports, lockfiles), quality-check.sh, push; checks failing → auto-fix only Black/Ruff/missing-changelog, report real test failures; branch with no PR → report, never delete.

Two behaviours found by dry-running the sweep

Both would have shipped silently:

  1. GitHub computes mergeable lazily. The first query on a cold PR returns UNKNOWN and triggers the computation. A single pass therefore reports UNKNOWN for precisely the stale PRs the sweep exists to find — it would have found nothing. Now retries 3× with a 3s backoff; verified all three PRs resolve on retry.
  2. 11 worktrees hold branches with local commits and no PR. Defined as report-only, never delete.

Test plan

Notes

Docs-only change — no CHANGELOG.md entry, since .claude/skills/ is agent tooling with no user-visible effect. Neither docs/agents/bess-knowledge.md nor docs/SOFTWARE_DESIGN.md describes anything this touches.

No Closes # — this isn't tracked by an issue.

johanzander and others added 4 commits August 15, 2026 00:56
Step 4 only pruned worktrees whose PR had merged. It never looked at an
open PR, so it could not see either way a parked PR rots: red CI, or a
branch that went CONFLICTING because other PRs merged into main ahead of
it. A CONFLICTING PR creates no workflow run at all, so it presents as
"CI never fired" and nobody investigates.

Step 4 becomes 4a (fleet sweep) + 4b (worktree + branch). The sweep keeps
the merged-prune arm unchanged and adds an open-PR arm that merges
origin/main into stale branches, auto-resolves mechanical conflicts only,
and reports everything else. A skip gate runs before both arms so the
sweep never touches a worktree another agent owns — any live session at
that cwd, uncommitted tracked changes, or a HEAD under 30 minutes old.
The same section is what to run under /loop for continuous fleet
maintenance, rather than forking a second skill that drifts.

Step 9 gains the matching pre-push `git merge origin/main`: Step 4b cuts
the branch from a current origin/main, but Steps 5-8 take hours and other
PRs merge during them.

Two behaviours found by dry-running the sweep against the real fleet:
GitHub computes `mergeable` lazily, so the first query on a cold PR
returns UNKNOWN and only triggers the computation — a single pass reports
UNKNOWN for precisely the stale PRs the sweep exists to find, hence the
retry. And 11 worktrees hold branches with local commits and no PR; those
are reported, never deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYV1WdkadzkeUUgULhC3Dk
implement-issue owns exactly one PR — the issue it was invoked for — and
had no idea whether that PR ever went green. It stopped at draft-PR-open,
so a PR that failed the CI matrix (which quality-check.sh does not
reproduce) or went CONFLICTING minutes later sat there unreviewed.

Step 9 now merges origin/main before pushing: Step 4 cuts the branch from
a current origin/main, but Steps 5-8 take hours and other PRs merge during
them. Opening an already-CONFLICTING PR is worse than it sounds, because
GitHub creates no workflow run at all for one — it presents as "CI never
fired" rather than as a conflict.

New Step 10 watches that PR to green via `gh pr checks --watch`, fixing
failures in the worktree, and is explicitly scoped to this PR alone. Step 4
is unchanged: it still prunes merged worktrees only.

Fleet-wide maintenance moves to a new sweep-prs skill, which walks every
worktree, prunes merged ones, merges main into stale branches, and reports
red CI. It has the piece implement-issue must not grow: an ownership skip
gate. A live session at that cwd (any status — idle and blocked included),
uncommitted tracked changes, or a HEAD under 30 minutes old means hands
off, because merging under a running implement-issue moves its HEAD and
puts two sessions on one branch.

Two behaviours found by dry-running the sweep against the real fleet:
GitHub computes `mergeable` lazily, so the first query on a cold PR returns
UNKNOWN and only triggers the computation — a single pass reports UNKNOWN
for precisely the stale PRs the sweep exists to find, hence the retry. And
11 worktrees hold branches with local commits and no PR; those are
reported, never deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYV1WdkadzkeUUgULhC3Dk
The sweep acted on everything that survived the skip gate, in one pass,
with no way to choose. Merging and pushing to a PR the user has not looked
at should not be what happens because they forgot to pass a flag.

Three modes: bare `/sweep-prs` classifies the fleet and reports what it
would do, changing nothing; `/sweep-prs 437 579` acts on the listed PRs
only; `/sweep-prs --all` acts on everything eligible and is the one to pair
with /loop. The safe mode is now the default.

Pruning merged worktrees stays automatic in every mode. It deletes only
work already merged into main, so nothing is at risk, and gating it behind
a flag recreates the failure it was added to fix -- a cleanup nobody
chooses to run doesn't run, which is how 39 worktrees accumulated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYV1WdkadzkeUUgULhC3Dk
Step 10 told you to run `gh pr checks --watch` and read the result. On
this skill's own PR that command returned "no checks reported on the
'feat/sweep-prs-skill' branch" about 8 seconds after the push -- the run
existed and was in_progress, but --watch returned before it registered.

That string has two causes that need opposite responses. A CONFLICTING PR
has no run and never will, because GitHub does not build one; the fix is
to merge origin/main. A just-pushed PR has a run that hasn't appeared yet;
the fix is to wait on the run id. Reading either as green is how a red PR
gets handed over as finished.

Step 10 now requires telling them apart via `gh pr view --json mergeable`
plus `gh run list --branch`, then `gh run watch <id> --exit-status`. Same
correction in sweep-prs' rationalization table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYV1WdkadzkeUUgULhC3Dk
@johanzander
johanzander marked this pull request as ready for review August 15, 2026 07:02
@johanzander
johanzander merged commit 74c346b into main Aug 15, 2026
8 checks passed
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.

1 participant