Skip to content

chore: mark an approved PR ready for review in implement-issue Step 11 - #603

Merged
johanzander merged 2 commits into
mainfrom
worktree-skill-ready-on-approval
Aug 15, 2026
Merged

chore: mark an approved PR ready for review in implement-issue Step 11#603
johanzander merged 2 commits into
mainfrom
worktree-skill-ready-on-approval

Conversation

@johanzander

Copy link
Copy Markdown
Owner

Summary

  • implement-issue Step 11 now runs gh pr ready <n> when the Stage 4 bot returns APPROVED, instead of leaving a reviewed, green PR sitting in draft.
  • The merge constraint is separated out and hardened: no agent ever merges, and nothing flips out of draft before an approval.

Root cause

Not a bug — a gap in the skill, found by running it. Step 11 (added in #600) drives the independent review loop to APPROVED, but Steps 10 and 12 were written before that loop existed, back when the skill handed over an unreviewed PR. So they still said "do not take it out of draft" and "Draft PR only", and the skill stopped at "approved, green, still a draft".

That end state makes the maintainer re-derive something the loop already established: notice the PR, judge whether it is finished, flip it, then merge. Three manual steps standing in for one.

Fix

  • Step 11, APPROVED branch — runs gh pr ready, with the reasoning for why an approval is what earns the flip. Guarded: if commits landed after the approving review (a TODO.md nit, a git merge origin/main), re-check gh pr checks and mergeable first and report which commits landed after the review and whether they touched reviewed code. That is the one way this step could mislead.
  • Step 12 — first bullet is now "never merge, ever", stated on its own and explicitly not weakened by a green CI run or an approval. Draft-until-approval is a separate bullet, so the two rules stop being one sentence that had to be read as both.
  • Step 10 — no longer says "do not take it out of draft" flatly; says it stays a draft there, because nothing has reviewed it yet.
  • CI mode (row 11) — unchanged behaviour, now explicit: CI opens a draft and leaves it, because Step 11 never runs there and there is no approval to earn the flip.
  • CLAUDE.md — "PRs are always opened as drafts" clarified to "opened as drafts, ready on approval, never merged by an agent". It was the one line elsewhere that could be read as "must stay draft forever".
  • Plus the matching Quick Reference row, one Rationalizations row, and two Red Flags (handing over an approved-but-draft PR; flipping ready early or merging at all).

Test plan

Evidence the test discriminates

Not applicable, and not skipped silently: this diff is prose in .claude/skills/ and CLAUDE.md with no executable behaviour, so there is no mutation that could redden a test. The equivalent evidence is the manual execution above — the flow was run against a real PR first, and the wording follows what that run actually needed (including the post-approval-commit guard, which only surfaced because it happened).

Outcome-level coverage

None, because no optimizer behaviour changes — no DP, intent, control-mapping or economics code is touched. The affected files are .claude/skills/implement-issue/SKILL.md and CLAUDE.md.

Documentation check

Grepped docs/agents/bess-knowledge.md and docs/SOFTWARE_DESIGN.md — neither mentions PR draft state or the review loop, so nothing to update there. docs/agents/workflow.md and docs/agents/rules.md mention drafts only in the CI/Stage-3 sense, which this change deliberately leaves alone. CHANGELOG.md is intentionally not touched: agent tooling has no user-visible effect, matching #600, which changed the same skill and added no entry.

The skill drove the PR all the way to a green, bot-approved state and then
left it a draft, which forced the maintainer to re-derive "is this actually
finished?" by hand before merging — three manual steps to restore a fact the
review loop had already established.

Step 11's APPROVED branch now runs `gh pr ready`, guarded on re-checking CI
and mergeability when commits landed after the approving review. The merge
constraint is untouched and now stated on its own: no agent ever merges, and
nothing flips out of draft before an approval. CI mode keeps opening drafts
and leaving them, since Step 11 never runs there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Jzvtwrzajuy8jKFKVj2rE
@bess-agent

Copy link
Copy Markdown
Collaborator

@claude-bot review

@bess-manager-claude-bot bess-manager-claude-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: mark an approved PR ready in implement-issue Step 11

No linked issue — this is a self-identified skill gap (per the PR body), not an issue fix, so the "matches issue root cause" checklist item doesn't apply. It's pure prose (.claude/skills/implement-issue/SKILL.md, CLAUDE.md), no executable code, so the "would the test catch a regression" item is also N/A — agreed with the PR's own "Evidence the test discriminates" section on that.

1. CLAUDE.md and SKILL.md now contradict each other on CI-mode PRs (file:line)

CLAUDE.md:132-134 (new):

Never push directly to main. PRs are always opened as drafts, and stay that way until an independent review approves them — implement-issue Step 11 then marks the PR ready (gh pr ready) so the only thing left is the maintainer's merge. No agent ever merges.

This is stated as a blanket rule under "General bot rules" — the section governing the GH Actions bot pipeline (Stages 1–5), including Stage 3 (issue-fix.yml, which runs implement-issue in CI mode).

But this same PR's own CI-mode table row says the opposite for that exact case:

SKILL.md:51 (new):

  1. Independent review loop | Skip — CI opens the PR as a draft and the owner triggers Stage 4 by hand after reading it. A CI run that requested its own review would be the fix bot grading itself on a PR nobody has looked at yet. Since the loop never runs here, the PR also stays a draft: gh pr ready is Step 11's, and there is no approval in CI mode to earn it.

So a Stage-3-opened PR that later gets APPROVED via a manual Stage-4 @claude-bot review (not the Step 11 loop, since that's skipped in CI mode) has no mechanism in either file that ever runs gh pr ready on it — it stays draft forever unless a human does it by hand. That directly contradicts the new CLAUDE.md claim that PRs "stay [draft] until an independent review approves them ... Step 11 then marks the PR ready" — for CI-opened PRs, an approval does not lead to ready via this skill. Either scope the CLAUDE.md bullet to the interactive/local flow explicitly, or say who/what flips a CI-mode PR to ready after Stage 4 approves it.

2. New autonomous gh pr ready <n> isn't guarded in .claude/settings.json (worth resolving explicitly)

SKILL.md:484-488 has Step 11 run gh pr ready <n> unattended on APPROVED, with no confirm gate. CLAUDE.md's own Permissions section (this repo's normative, heavily-worked-out policy) draws the guard line at "escapes to GitHub" / "visible to others" — and lists exactly which gh subcommands get an ask prompt for that reason: gh pr merge, gh release, gh repo edit, gh secret, gh workflow run, and blanket gh api. gh pr ready isn't in that list, isn't in .claude/settings.json's ask/deny, and this PR doesn't add it — so per CLAUDE.md's own description of defaultMode: auto, it "decides without involving you," meaning the skill can flip a PR's visibility/notify-reviewers state with zero human confirmation.

Flipping ready-for-review is exactly the class of action the top-level system guidance calls out as needing confirmation by default ("actions visible to others... creating/closing/commenting on PRs"). This may be a deliberate call (it's not a merge, and it's reversible via gh pr ready --undo), but the PR doesn't discuss it, and it's the kind of gap CLAUDE.md's Permissions section is explicit about needing to be a deliberate decision, not an oversight. Worth either adding Bash(gh pr ready*) to ask, or stating explicitly in this PR why it's fine to leave to the auto classifier.

3. Post-approval-commit guard identifies the risk but doesn't resolve it (minor)

SKILL.md:500-507:

If you push anything after the approval (parking a nit in TODO.md, a git merge origin/main), re-check gh pr checks and mergeable/mergeStateStatus before flipping, and say in your report which commits landed after the approving review and whether they touched the reviewed code. gh pr ready on a PR whose approved diff has since changed underneath it is the one way this step can mislead.

This names the failure mode correctly (stale-approved diff) but the only mitigation given is CI/mergeability re-checks plus a report — nothing tells the agent not to run gh pr ready when the post-approval commits did touch reviewed code. As written, the step still flips it ready regardless, just with a note in the report. If a post-approval commit touched reviewed code, the safe action is presumably to loop back into Step 11 for a fresh review before running gh pr ready, not just disclose it after the fact.

Fitness of approach

  • Scope is minimal and matches the stated intent (two files, both plausibly need updating for this change).
  • Internally, most of the diff is consistent (Step 10, Step 12, Rationalizations, Red Flags, Quick Reference all line up with the new Step 11 behavior) — the CLAUDE.md/CI-mode contradiction above is the one place it isn't.
  • No docs/agents/rules.md architecture/testing violations apply (no code).

Given #1 is a direct self-contradiction introduced by this PR's own two edited files, and #2 is a real gap against the repo's own documented permissions philosophy, requesting changes rather than approving as-is.

…al guard

Three findings from the Stage 4 review, two fixed and one answered.

1. The CLAUDE.md bullet stated ready-on-approval as a blanket rule, but it
   sits under "General bot rules", which governs the Stage 1-5 pipeline —
   and this same PR says CI mode skips Step 11. A Stage 3 PR approved by a
   manual Stage 4 review therefore had nothing that would ever flip it. Now
   split: interactive runs flip their own, CI-opened PRs stay draft for the
   owner, who is triggering that review by hand anyway.

2. The post-approval guard named the stale-approved-diff risk and then
   flipped the PR regardless, disclosing it in the report. Disclosure is not
   a mitigation. It now branches on whether the new commits touched reviewed
   code: outside the reviewed diff, flip and justify; inside it, another
   review round, because the approval being held is for different code.

3. gh pr ready is deliberately left unattended, and CLAUDE.md's permissions
   section now says so rather than leaving it to be read as an oversight. The
   documented standard has two halves — escapes the repo AND git cannot undo
   it — and this clears the second: --undo reverts it, no content changes,
   and the push that made the diff public already prompted. Gating it would
   stall the one flow whose point is reaching that state unattended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Jzvtwrzajuy8jKFKVj2rE
@bess-agent

Copy link
Copy Markdown
Collaborator

Findings 1 and 3 were right and are fixed in 92e5136.

1 — CLAUDE.md / CI-mode contradiction. Confirmed by reading it back: the bullet sits under "General bot rules", which governs Stages 1–5, so it claimed ready-on-approval for exactly the flow this PR says skips Step 11. A Stage 3 PR approved by a manual Stage 4 review had nothing that would ever flip it. Now split in two — interactive runs flip their own; a CI-opened PR stays a draft for the owner, who is triggering that review by hand anyway.

3 — post-approval guard. Agreed, and the sharper way to put it is that disclosure isn't mitigation. The guard now branches on the answer instead of just reporting it: if the post-approval commits fall outside the reviewed diff, flip and justify which commits and why; if they touched reviewed code, don't flip, run another round, because the approval being held is for different code. The round cap counts it, and hitting the cap hands over an honest draft.

2 — gh pr ready left unattended: deliberate, and now documented. Not adding Bash(gh pr ready*) to ask, for the reason CLAUDE.md's own standard gives: "the effect escapes the repo and git cannot undo it." Both halves have to hold. gh pr ready clears the first and fails the second — gh pr ready --undo puts it straight back, it changes no content, and the diff was already public because the git push that created it prompted. Compare the row above it: gh pr merge is the same category and is gated, because nothing undoes a merge to main. That is the line, and gh pr ready sits on the safe side of it.

The other half is that gating it would defeat the change. This flip is the codified endpoint of Step 11's loop, which exists to reach that state without the maintainer in the loop; a prompt there stalls the one flow whose whole point is not stalling. You were right that it shouldn't be silent, though, so the permissions section now states the call and its reasoning rather than leaving the absence to read as an oversight.

Worth adding: the guard from finding 3 has already been exercised for real. This session flipped #601 after parking a TODO.md nit following its approval — outside the reviewed diff, hence legitimate under the rule as now written, which is also where the guard came from.

@bess-agent

Copy link
Copy Markdown
Collaborator

@claude-bot review

@bess-manager-claude-bot bess-manager-claude-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: mark an approved PR ready in implement-issue Step 11 (round 2)

No linked issue (self-identified skill gap per the PR body), and no executable code changes (.claude/skills/implement-issue/SKILL.md, CLAUDE.md only) — the "matches issue root cause" and "test would catch a regression" checklist items are N/A, matching the PR's own "Evidence the test discriminates" section.

All three findings from the prior review round are resolved in 92e5136:

  1. CLAUDE.md/SKILL.md contradiction on CI-mode PRs — fixed. The old blanket "Step 11 marks the PR ready" bullet sat under "General bot rules" (which governs Stages 1–5, including CI-mode Stage 3), but SKILL.md's own CI-mode table row skips Step 11 entirely — so an approved CI-opened PR had no mechanism that ever flipped it. CLAUDE.md now explicitly splits this: interactive runs flip their own PR via Step 11; a Stage-3 PR "stays a draft even after Stage 4 approves it... Flip it yourself when you're satisfied." Consistent with SKILL.md's CI-mode row, which now says the same thing from the other side.

  2. Post-approval stale-diff guard just disclosed the risk instead of resolving it — fixed. The new branch is correct: commits that "didn't" touch reviewed code (nit in TODO.md/CHANGELOG.md, a no-op git merge origin/main) get a mergeability re-check and a named justification before flipping; commits that "did" touch reviewed code block the flip and require another review round instead. That's the right shape — a stale approval no longer gets silently honored.

  3. gh pr ready left unattended with no discussion — now explicitly addressed rather than left as an apparent oversight. CLAUDE.md states the reasoning against the file's own stated two-part standard ("escapes the repo and git cannot undo it"): gh pr ready --undo reverses it and it changes no content, so it fails the second half, unlike gh pr merge one row up. This was one of two options offered in the prior round (add an ask rule, or state explicitly why it's fine to leave to the auto classifier) — documenting the reasoning is a legitimate resolution, and the reasoning holds up (checked: no workflow in .github/workflows/*.yml gates on draft vs. ready_for_review, so flipping ready doesn't trigger any new CI side effect in this repo either).

Fitness of approach

  • Scope stays minimal — two files, both plausibly needed for this change, no unrelated edits.
  • Internally consistent end to end: Step 10, Step 12, the Rationalizations table, and the Red Flags list all line up with the new Step 11 behavior; the round-1 CI-mode contradiction was the one place it wasn't, and that's now fixed.
  • No docs/agents/rules.md architecture/testing violations apply (no code touched).

Approving — no blockers remain.

@johanzander
johanzander marked this pull request as ready for review August 15, 2026 13:41
@johanzander
johanzander merged commit 626d043 into main Aug 15, 2026
8 checks passed
johanzander added a commit that referenced this pull request Aug 22, 2026
…ove (#653)

* fix: stop the fleet prune from destroying the worktrees it cannot remove

`git worktree remove` is sandbox-denied, and unlike `git worktree add` it
fails DESTRUCTIVELY. Removal deletes the working tree first and only then
unlinks `.git/worktrees/<name>` -- and that unlink is the denied one:

    error: failed to delete '.../worktrees/backlogger': Operation not permitted
    error: failed to delete '.git/worktrees/backlogger': Operation not permitted

By then ~393 tracked files are gone. It does not roll back. What is left is a
carcass: a registered worktree whose `git status` is a few hundred ` D` lines
and nothing else. Both prune loops read that as "uncommitted tracked changes"
and correctly refuse to auto-delete it -- so the failure makes the worktree
permanently unprunable BY ITSELF. Re-running hits the no-`--force` refusal;
`--force` re-hits the denial. `git worktree prune` performs the same unlink,
so it cannot clear the wreckage either.

13 carcasses accumulated across three sweeps (#568, #596, #597, #600, #601,
#603, #609, #612, #617, #629, #633, #634, #641) before anyone read the diff.
The last sweep reported them back as "a real backlog of stranded edits worth
reviewing" -- they were its own wreckage from the previous runs, and not one
byte of real work was in them.

Because the filename set is identical in every worktree, so is APFS's readdir
order, so every carcass loses the SAME ~393 paths (`core/`, `frontend/`,
`bess_manager/`, `pyproject.toml`, ...). Identical damage across many
worktrees is the signature, not a coincidence.

- Both prune loops now report `PRUNE` and emit one `!`-prefixed command for
  the maintainer to run unsandboxed, instead of removing anything themselves.
- Both classify a dirty set that is entirely ` D` as `CARCASS`, distinct from
  real edits. The predicate anchors on `^ D ` (unstaged deletions only), so a
  STAGED deletion still reads as intentional work.
- `local-agent-environment.md` gains the `remove`/`prune` half of the
  `.git/worktrees` denial, next to the `add` half it already documented.

`verify-sandbox.sh:88` predicted this in a parenthetical ("a stray directory
when `git worktree remove` then fails"); nothing acted on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FQA38o22RkHZJ4zEHDZS3

* fix: measure the prune half, which exits 0 while failing

The previous commit inferred that `git worktree prune` was denied because it
performs the same `.git/worktrees/<name>` unlink as `remove`. Measured now,
using a carcass as the probe -- `rm -rf` its directory, then prune:

    $ git worktree prune -v; echo "exit=$?"
    Removing worktrees/backlogger: gitdir file points to non-existent location
    error: failed to delete '.../.git/worktrees/backlogger': Operation not permitted
    exit=0

Denied as expected, but it EXITS 0. `remove` at least exits 255; prune reports
success having done nothing, so `git worktree prune && echo done` prints done.
That is the nastier of the two and was not in the inferred version.

Two further measured facts change the guidance:

- The denial is on the `.git/worktrees/<name>` unlink ONLY. `rm -rf` of the
  working tree from Bash SUCCEEDS. An agent can therefore always destroy the
  files and never the registration, and half-doing it converts a carcass into
  a `prunable` phantom -- no better, still needing the same unsandboxed fix.
- A phantom breaks both classification loops. Its directory is gone, so
  `git -C "$wt" branch --show-current` fails, `$b` comes back empty, and the
  detached-HEAD guard swallows it as "SKIP (detached)". Both loops now test
  `[ -d "$wt" ]` first and report PHANTOM. Verified over four cases: missing
  dir with and without a branch name, detached, and on a branch.

The emitted cleanup command gains a trailing `git worktree prune` (a phantom
has no directory, so `remove` has nothing to work with), uses `[ -n "$b" ]`
instead of `&&` since a phantom yields no branch name, and now says it must
run from a NON-worktree-isolated session -- an isolated one refuses the `cd`
to the shared checkout, which is why the first cleanup attempt silently did
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FQA38o22RkHZJ4zEHDZS3

* docs: record that `git branch -D` warns on .git/config but strands nothing

Third instance of the `.git/config` denial, alongside `checkout -b --track`
and `push -u`, and the same exit-0-while-partially-failing shape as the prune
bullet:

    $ git branch -D worktree-backlogger; echo "exit=$?"
    error: could not lock config file .../.git/config
    warning: update of config-file failed
    Deleted branch worktree-backlogger (was 6c70a77).
    exit=0

The first draft of this bullet asserted the denial leaves a stale
`[branch "<name>"]` stanza behind. Checked before committing, and it does not:
grepping .git/config afterwards found no stanza for the deleted branch, and
the only stale one in the file is an unrelated `undefined`. The branch had no
stanza to drop in the first place -- writing one needs `push -u` or
`checkout -b --track`, both denied by the two bullets above, so branches
created under this sandbox never have one. Recorded as the general case, with
the pre-sandbox branch explicitly marked untested rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FQA38o22RkHZJ4zEHDZS3

* docs: reconcile worktree-remove bullets with the rest of the permissions doc

The new destructive-failure bullets for `git worktree remove`/`prune`
contradicted three claims in the same file: the unattended list (which still
named `git worktree remove`), the "sandbox makes the unattended list safe"
thesis, and the "git already refuses the dangerous case" reasoning for the ask
list. A reader of only the Permissions section could conclude the command was
safe to call from sandboxed Bash — the exact bug the skills no longer call.
Carve the two verbs out as explicit exceptions and scope the git-refuses
reasoning to the cases git actually covers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: report worktree paths, not branch names, in the prune output

The sweep's emitted maintainer command reconstructed `.claude/worktrees/<name>`
from branch names, but `git worktree list` yields paths and includes sibling
worktrees outside `.claude/worktrees/` — the branch name alone cannot locate
the worktree, so removal would silently miss its target. Report `$wt` in the
PRUNE/CARCASS lines and drive the command from those paths. Also count PHANTOM
in implement-issue's emit-one-command line, matching sweep-prs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fold the branch delete into Step 11's deferred worktree removal

The After-Merge cleanup splits in two: `ExitWorktree action=remove` clears the
worktree in-session, or removal is handed to the maintainer when the session
has already left. The old item 3 ran `git branch -D` right after either path,
but git refuses to force-delete a branch while its worktree registration
persists — the exact state the deferred path leaves behind, since only the
maintainer's not-yet-run command clears the registration. Emit the branch
delete as part of that same deferred command (remove first, then delete, as
sweep-prs does), and scope item 3 to the in-session path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants