From 676cb1fd70a927b58d9391e7c6c6fa1259ecd0bb Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 17 Oct 2026 13:53:39 -0700 Subject: [PATCH 1/5] Point Every Worktree-Finishing Skill at pr_review.py cleanup The skills that finish a worktree now name `pr_review.py cleanup --checkout ` for a pull request merged into develop, and `cleanup --sweep` for what an interrupted or abandoned session left behind, in place of the raw `git worktree remove`, `git branch -D`, and remote delete the host-safety allowlist does not cover. merge-and-release's hand-written defensive sweep shrinks to one merged pull request lookup per branch plus the command. unattended-handoff's worker and the resync and audit runs gain the cleanup step they lacked. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/audit-a-repo/SKILL.md | 2 + .agents/skills/backlog-burndown/SKILL.md | 3 +- .agents/skills/drive-pr/SKILL.md | 12 ++- .agents/skills/merge-and-release/SKILL.md | 92 +++++-------------- .agents/skills/pr-review-conduct/SKILL.md | 2 +- .agents/skills/repo-worktree/SKILL.md | 36 +++++++- .agents/skills/resync-a-repo/SKILL.md | 4 +- .agents/skills/unattended-handoff/SKILL.md | 8 +- .../fleet-skills/.source-digests/audit-a-repo | 2 +- .../.source-digests/backlog-burndown | 2 +- .../fleet-skills/.source-digests/drive-pr | 2 +- .../.source-digests/merge-and-release | 2 +- .../.source-digests/pr-review-conduct | 2 +- .../.source-digests/repo-worktree | 2 +- .../.source-digests/resync-a-repo | 2 +- .../.source-digests/unattended-handoff | 2 +- .../fleet-skills/skills/audit-a-repo/SKILL.md | 2 + .../skills/backlog-burndown/SKILL.md | 3 +- .../fleet-skills/skills/drive-pr/SKILL.md | 12 ++- .../skills/merge-and-release/SKILL.md | 92 +++++-------------- .../skills/pr-review-conduct/SKILL.md | 2 +- .../skills/repo-worktree/SKILL.md | 36 +++++++- .../skills/resync-a-repo/SKILL.md | 4 +- .../skills/unattended-handoff/SKILL.md | 8 +- .github/skills/audit-a-repo/SKILL.md | 2 + .github/skills/backlog-burndown/SKILL.md | 3 +- .github/skills/drive-pr/SKILL.md | 12 ++- .github/skills/merge-and-release/SKILL.md | 92 +++++-------------- .github/skills/pr-review-conduct/SKILL.md | 2 +- .github/skills/repo-worktree/SKILL.md | 36 +++++++- .github/skills/resync-a-repo/SKILL.md | 4 +- .github/skills/unattended-handoff/SKILL.md | 8 +- 32 files changed, 239 insertions(+), 254 deletions(-) diff --git a/.agents/skills/audit-a-repo/SKILL.md b/.agents/skills/audit-a-repo/SKILL.md index ef69d060e..1bd3c6081 100644 --- a/.agents/skills/audit-a-repo/SKILL.md +++ b/.agents/skills/audit-a-repo/SKILL.md @@ -35,3 +35,5 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards ## After the Report Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). + +The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index b2a31609f..21345adb1 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -286,7 +286,8 @@ working directory in which to delete its branch. So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. -That half is `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout once for each merged group. +For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. +It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. diff --git a/.agents/skills/drive-pr/SKILL.md b/.agents/skills/drive-pr/SKILL.md index d5bf56122..d85d216ce 100644 --- a/.agents/skills/drive-pr/SKILL.md +++ b/.agents/skills/drive-pr/SKILL.md @@ -103,10 +103,12 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. call, it is run from inside the task's own worktree per step 1, where the feature branch is checked out, and `gh pr merge --delete-branch` needs to switch that worktree to the base branch to delete it, which fails when `develop` is already checked out somewhere else, the ordinary - case in this layout. Instead run repo-worktree's post-merge cleanup from the base clone, remove - the worktree and delete the now-merged local task branch, then delete the remote one, which is this - skill's own step rather than that one's, with - `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout. + case in this layout. Instead run repo-worktree's post-merge cleanup with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + run from a hub checkout outside the task's worktree, `` being the repository's + primary clone. It deletes the remote branch, then removes the task's worktree and local branch + and returns the base clone to fast-forwarded `develop`, deciding every local refusal before + anything is deleted, the remote branch included. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It @@ -122,7 +124,7 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. It names each, since the delete would close or retarget it. Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand. Name the pull request number and `--repo` so the maintainer runs the same command. - No direct `git push origin --delete` or ref API delete runs here, since `cleanup` holds the checks. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs here, since `cleanup` holds the checks. A pull request opened from a fork follows `upstream-contribution-workflow` instead, and `cleanup` refuses it. The repo's auto-delete-head-branches setting is kept off fleet-wide (to protect `develop` and diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index a6a397746..50f0450a5 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -169,77 +169,33 @@ skill covers all of it, scoped down by what the maintainer actually asks for. not have, an interrupted loop, a fix landed by hand outside that skill, or a maintainer merge in the GitHub UI. `git worktree list` for any worktree still registered under this task's feature branches, `git branch -vv` for any local feature branch, `git ls-remote - --heads origin` for any matching remote feature branch. For each, verify it finished by - reading GitHub's own state with the exact fields this check needs, not a bare listing, and - stop and report rather than guessing when selection is not exactly one match, on a non-1 - count exit non-zero rather than returning empty with success, an ambiguous or missing match - must fail loud, not read as an empty value still safe to act on: `gh pr list --head - "" --state merged --repo owner/repo --json - number,baseRefName,mergedAt,headRefOid,headRefName,headRepository --jq 'if length == 1 then - .[0] else error("expected exactly one merged PR for this head, got \(length)") end'`. - `--head` is expected to match exactly (verified against `gh` 2.97.0 on this repo, a bare - prefix of a real branch name returned nothing), but confirming `headRefName` equals `` - costs one field and is cheap insurance against a future `gh` behavior change, not a workaround - for a known partial-match case. Confirm `headRepository` is non-null and its `nameWithOwner` equals - `owner/repo`, the owner alone is not enough, a same-owner PR against an identically named - branch in a different repository must never pass this check either. Confirm `baseRefName` - is `develop` (a different merged pull request can share the same head branch name against a - different base, and that is never this sweep's target) and `mergedAt` is set. Compare tips - only where a remote branch actually exists. `git ls-remote --heads --exit-code -- origin - "refs/heads/"` is the exact-match form and must be, in that argument order. `--heads - origin ""` alone still tail-matches, a bare `topic/x` pattern also returns an unrelated - `other/topic/x` if one exists. `--` placed after `origin` instead of before it is not - equivalent either, verified empirically: with a `refs/heads/other/--` ref present, `--heads - origin -- "refs/heads/"` matched both that ref and the intended one, while `--heads -- - origin "refs/heads/"` matched only the one intended. Exit status is a tri-state, not a - stdin-emptiness check: `--exit-code` makes exit `2` mean query succeeded, branch gone, most - likely a prior cleanup attempt got interrupted after the remote delete but before the local - one, so skip straight to the local-tip check below and never attempt the remote delete a - second time. Exit `0` means it matched. Anything else is a failed query, a network or auth - problem, and stops and reports rather than being read as absence, an unreachable remote and a - genuinely gone branch both print nothing to stdout, only the exit code tells them apart. - Where the remote branch does exist, its tip must match that exact pull request's `headRefOid` - before its own delete proceeds, proving nothing landed on it since. Where a local branch - still exists too, its tip (`git rev-parse --verify "refs/heads/"`) must independently - match `headRefOid` before its own delete proceeds. Neither side needs the other to exist, a - prior interrupted attempt may have deleted one side already and left only the other, so - verify and delete whichever side is still there and skip whichever already is not, never - block one side's cleanup on the other side's absence. No `--` on `rev-parse`, verified - empirically: `git rev-parse -- ""` treats the argument after `--` as a path rather - than a revision and never resolves a SHA at all. The fully-qualified form needs no `--` - regardless, since `refs/heads/` never itself starts with `-`, and `--verify` fails - loudly rather than guessing when it does not resolve. Every branch or - worktree-path placeholder below is the real value, substituted as its own quoted argument - (a shell variable expansion such as `"$branch"`, or an argv element), never handed to `eval` - or `sh -c` for a second round of shell parsing, the only way an embedded `$()` or backtick - would actually run. A valid ref can start with `-` or carry a shell metacharacter, which is - why it stays quoted regardless. `--` marks the - end of options wherever a command supports it. - `git merge-base --is-ancestor develop` must never be used for either tip check, a - squash merge (drive-pr's own merge method) never makes the feature tip a literal ancestor of - `develop`, so the check reports every already-finished branch as unmerged. Only once GitHub - confirms it, and only when a local worktree or branch is still there to remove, remove the - worktree by its exact path (a dirty worktree stops cleanup rather than discarding uncommitted - work), `git worktree remove ""`, `git worktree list` names it, then delete the - local branch. `git branch - -d` has the identical squash blindness as `git merge-base --is-ancestor` and refuses too, so - use `git branch -D -- ""` here, safe only because the GitHub-state check just - proved that exact branch finished, the narrow post-squash exception git-commit-conventions - describes, never applied to an unverified branch. Then, only when the remote branch still - exists, delete it with `python3 scripts/pr_review.py cleanup --repo owner/repo`, run - from a hub checkout, `` being the merged pull request the check above returned. It - re-reads that pull request and repeats the merged, base, head-repository, and tip checks in - code, refuses a permanent or protected branch, and confirms the delete by reading the ref - back. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an - unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub - checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs - under the host-safety installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, - which is what lets auto mode complete it. + --heads origin` for any matching remote feature branch. For each such branch, read the one + merged pull request into `develop` it headed, failing loud on any count other than one rather + than reading an empty answer as safe: `gh pr list --head "" --base develop --state + merged --repo owner/repo --json number --jq 'if length == 1 then .[0].number else + error("expected exactly one merged PR for this head, got \(length)") end'`. Then run + `python3 scripts/pr_review.py cleanup --repo owner/repo --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone, per `repo-worktree` "Listing and Cleanup". It re-reads that pull request and + repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or + protected branch, deletes the remote branch and confirms it by reading the ref back, treats + a remote branch already gone as done, so a prior interrupted attempt finishes rather than + stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal + is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed + delete, and `64` a target under an owner other than the hub checkout's own `origin` or that + `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety + installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, which is what lets auto mode + complete it. It also refuses while another open pull request in the repository has the branch as its head or base, naming each. - Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand, naming the pull request number and `--repo`. - No direct `git push origin --delete` or ref API delete runs from this skill, because `cleanup` is the one place holding the checks. + Where this seat cannot run `cleanup`, report the cleanup to the maintainer instead of running its steps by hand, naming the pull request number and `--repo`. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs on a feature branch from this skill, because `cleanup` is the one place holding the checks. Never apply this sweep to `develop` or `main` themselves, only to feature branches a drive-pr loop created. + - What no single pull request names: run + `python3 scripts/pr_review.py cleanup --sweep --repo owner/repo --checkout ` + and report what it lists. It writes nothing but a fetch, and `--apply` runs only on the + maintainer's go-ahead, since the report reaches other sessions' finished work too. ## Mechanics Live Elsewhere diff --git a/.agents/skills/pr-review-conduct/SKILL.md b/.agents/skills/pr-review-conduct/SKILL.md index 4ec78725f..211b5a366 100644 --- a/.agents/skills/pr-review-conduct/SKILL.md +++ b/.agents/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 46bbda6bb..522135528 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -268,10 +268,38 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is in flight across the whole fleet. - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. -- **Verify before removing.** Read the pull request's merged state and head SHA from live GitHub state. Confirm the worktree is clean and resolves to that head. A dirty worktree stops cleanup because force-removing it would discard work. A detached helper worktree needs no pull request, but its commit must be contained in the branch whose completed operation created it. -- **Remove the exact finished worktree, then its local task branch.** Use `git worktree remove `. Try `git branch -d ` after a merge commit. A squash merge does not make the feature tip an ancestor of `develop`, so `-d` cannot recognize it as merged. After the live merged-PR and clean-worktree checks prove that exact branch finished, use `git branch -D ` under the narrow post-squash exception in `git-commit-conventions`. Never apply that exception to an unverified branch or to `develop`. -- **Remove temporary standalone clones and detached helper worktrees too.** Remove the exact `/-` path after confirming it is clean. The remote feature branch follows the repository's normal pull request cleanup policy. Never delete `develop` after a promotion because it is the permanent integration branch. -- **Return the base clone to current `develop`.** Fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. +- **Verify before removing.** A worktree or branch is removed only once live state proves it + finished. For a pull request that is its merged state and head SHA read from GitHub, with the + worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops + cleanup because force-removing it would discard work. A detached helper worktree needs no pull + request, but its commit must be contained in the branch whose completed operation created it. +- **A pull request merged into `develop` is cleaned up by one command.** Run + `python3 scripts/pr_review.py cleanup --repo / --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone. It reads the branch from the pull request, deletes the remote branch, removes + every worktree holding the branch without force, deletes the local branch, and returns the base + clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty + worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` + is done. Any other exit stops and is reported as it stands rather than finished by hand, since + the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the + host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the + full contract. +- **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's + worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is + clean, then remove the exact path with `git worktree remove `, or delete the exact + `/-` directory. Never delete `develop` after a promotion, since + it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is + clean, switch it to `develop` when needed, and fast-forward it with + `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base + clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching + or reconciling it. +- **What an interrupted or abandoned session left behind is swept.** Run + `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` + from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports + each worktree and branch that is clean, idle past `--min-age-hours` (default 24), and provably + finished. Adding `--apply` removes what the report lists through the same checks, the remote + branch of a merged `develop` pull request included. The report reaches other sessions' finished + work as well as this task's, so `--apply` runs on the maintainer's go-ahead. - **Prove the cleanup.** Finish with `git status --short --branch` in the base clone and `git worktree list`. The expected result is a clean base clone at `origin/develop` and no worktree belonging only to the completed task. - A worktree that refuses removal is dirty, and force is not the fix: look at what is uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index 5326c3499..b8faa08f7 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -43,7 +43,9 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and +Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub +worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.agents/skills/unattended-handoff/SKILL.md b/.agents/skills/unattended-handoff/SKILL.md index f701ca677..475011b49 100644 --- a/.agents/skills/unattended-handoff/SKILL.md +++ b/.agents/skills/unattended-handoff/SKILL.md @@ -327,8 +327,12 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Comment on the handoff what merged, which issues it fixed, and - what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one +7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up + first, per `drive-pr` step 4, with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + so no finished worktree or branch outlives the lane. An exit other than `0` is named in the + close-out comment rather than finished by hand. Comment on the handoff what merged, which + issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. 8. **Save no memory**, since state lives in the chain where any session on any machine reads it. diff --git a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo index 0827c9ff8..40925dc6f 100644 --- a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo +++ b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo @@ -1 +1 @@ -db506d0d9484243a +b09763bd493ec71c diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 5573af9d9..8b29a190c 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -1ce0fe48128d5dac +c0316a396936cbf2 diff --git a/.claude-plugin/fleet-skills/.source-digests/drive-pr b/.claude-plugin/fleet-skills/.source-digests/drive-pr index 5fdc624fe..11ac86262 100644 --- a/.claude-plugin/fleet-skills/.source-digests/drive-pr +++ b/.claude-plugin/fleet-skills/.source-digests/drive-pr @@ -1 +1 @@ -cbc0eb52f3bdfc94 +ab156348349f186d diff --git a/.claude-plugin/fleet-skills/.source-digests/merge-and-release b/.claude-plugin/fleet-skills/.source-digests/merge-and-release index e99015190..1dd49c04d 100644 --- a/.claude-plugin/fleet-skills/.source-digests/merge-and-release +++ b/.claude-plugin/fleet-skills/.source-digests/merge-and-release @@ -1 +1 @@ -3053496c0d18bcac +8240808799b201c7 diff --git a/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct b/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct index 16756fca1..8deeedcda 100644 --- a/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct @@ -1 +1 @@ -97815b2d2157b2a4 +e353708498e5145a diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index 1021f3289..72461e344 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -010e5fbece90a0b9 +40dc05c1c28b91a8 diff --git a/.claude-plugin/fleet-skills/.source-digests/resync-a-repo b/.claude-plugin/fleet-skills/.source-digests/resync-a-repo index aa6338a8b..3ac0597b2 100644 --- a/.claude-plugin/fleet-skills/.source-digests/resync-a-repo +++ b/.claude-plugin/fleet-skills/.source-digests/resync-a-repo @@ -1 +1 @@ -4ddd5b2d70797265 +3b35bb4f4ff85e01 diff --git a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff index ed40090d1..ff86e70d8 100644 --- a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff @@ -1 +1 @@ -680119285f154228 +3781961f4f8b9efe diff --git a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md index ef69d060e..1bd3c6081 100644 --- a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md @@ -35,3 +35,5 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards ## After the Report Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). + +The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index b2a31609f..21345adb1 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -286,7 +286,8 @@ working directory in which to delete its branch. So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. -That half is `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout once for each merged group. +For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. +It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. diff --git a/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md b/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md index d5bf56122..d85d216ce 100644 --- a/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md @@ -103,10 +103,12 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. call, it is run from inside the task's own worktree per step 1, where the feature branch is checked out, and `gh pr merge --delete-branch` needs to switch that worktree to the base branch to delete it, which fails when `develop` is already checked out somewhere else, the ordinary - case in this layout. Instead run repo-worktree's post-merge cleanup from the base clone, remove - the worktree and delete the now-merged local task branch, then delete the remote one, which is this - skill's own step rather than that one's, with - `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout. + case in this layout. Instead run repo-worktree's post-merge cleanup with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + run from a hub checkout outside the task's worktree, `` being the repository's + primary clone. It deletes the remote branch, then removes the task's worktree and local branch + and returns the base clone to fast-forwarded `develop`, deciding every local refusal before + anything is deleted, the remote branch included. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It @@ -122,7 +124,7 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. It names each, since the delete would close or retarget it. Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand. Name the pull request number and `--repo` so the maintainer runs the same command. - No direct `git push origin --delete` or ref API delete runs here, since `cleanup` holds the checks. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs here, since `cleanup` holds the checks. A pull request opened from a fork follows `upstream-contribution-workflow` instead, and `cleanup` refuses it. The repo's auto-delete-head-branches setting is kept off fleet-wide (to protect `develop` and diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index a6a397746..50f0450a5 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -169,77 +169,33 @@ skill covers all of it, scoped down by what the maintainer actually asks for. not have, an interrupted loop, a fix landed by hand outside that skill, or a maintainer merge in the GitHub UI. `git worktree list` for any worktree still registered under this task's feature branches, `git branch -vv` for any local feature branch, `git ls-remote - --heads origin` for any matching remote feature branch. For each, verify it finished by - reading GitHub's own state with the exact fields this check needs, not a bare listing, and - stop and report rather than guessing when selection is not exactly one match, on a non-1 - count exit non-zero rather than returning empty with success, an ambiguous or missing match - must fail loud, not read as an empty value still safe to act on: `gh pr list --head - "" --state merged --repo owner/repo --json - number,baseRefName,mergedAt,headRefOid,headRefName,headRepository --jq 'if length == 1 then - .[0] else error("expected exactly one merged PR for this head, got \(length)") end'`. - `--head` is expected to match exactly (verified against `gh` 2.97.0 on this repo, a bare - prefix of a real branch name returned nothing), but confirming `headRefName` equals `` - costs one field and is cheap insurance against a future `gh` behavior change, not a workaround - for a known partial-match case. Confirm `headRepository` is non-null and its `nameWithOwner` equals - `owner/repo`, the owner alone is not enough, a same-owner PR against an identically named - branch in a different repository must never pass this check either. Confirm `baseRefName` - is `develop` (a different merged pull request can share the same head branch name against a - different base, and that is never this sweep's target) and `mergedAt` is set. Compare tips - only where a remote branch actually exists. `git ls-remote --heads --exit-code -- origin - "refs/heads/"` is the exact-match form and must be, in that argument order. `--heads - origin ""` alone still tail-matches, a bare `topic/x` pattern also returns an unrelated - `other/topic/x` if one exists. `--` placed after `origin` instead of before it is not - equivalent either, verified empirically: with a `refs/heads/other/--` ref present, `--heads - origin -- "refs/heads/"` matched both that ref and the intended one, while `--heads -- - origin "refs/heads/"` matched only the one intended. Exit status is a tri-state, not a - stdin-emptiness check: `--exit-code` makes exit `2` mean query succeeded, branch gone, most - likely a prior cleanup attempt got interrupted after the remote delete but before the local - one, so skip straight to the local-tip check below and never attempt the remote delete a - second time. Exit `0` means it matched. Anything else is a failed query, a network or auth - problem, and stops and reports rather than being read as absence, an unreachable remote and a - genuinely gone branch both print nothing to stdout, only the exit code tells them apart. - Where the remote branch does exist, its tip must match that exact pull request's `headRefOid` - before its own delete proceeds, proving nothing landed on it since. Where a local branch - still exists too, its tip (`git rev-parse --verify "refs/heads/"`) must independently - match `headRefOid` before its own delete proceeds. Neither side needs the other to exist, a - prior interrupted attempt may have deleted one side already and left only the other, so - verify and delete whichever side is still there and skip whichever already is not, never - block one side's cleanup on the other side's absence. No `--` on `rev-parse`, verified - empirically: `git rev-parse -- ""` treats the argument after `--` as a path rather - than a revision and never resolves a SHA at all. The fully-qualified form needs no `--` - regardless, since `refs/heads/` never itself starts with `-`, and `--verify` fails - loudly rather than guessing when it does not resolve. Every branch or - worktree-path placeholder below is the real value, substituted as its own quoted argument - (a shell variable expansion such as `"$branch"`, or an argv element), never handed to `eval` - or `sh -c` for a second round of shell parsing, the only way an embedded `$()` or backtick - would actually run. A valid ref can start with `-` or carry a shell metacharacter, which is - why it stays quoted regardless. `--` marks the - end of options wherever a command supports it. - `git merge-base --is-ancestor develop` must never be used for either tip check, a - squash merge (drive-pr's own merge method) never makes the feature tip a literal ancestor of - `develop`, so the check reports every already-finished branch as unmerged. Only once GitHub - confirms it, and only when a local worktree or branch is still there to remove, remove the - worktree by its exact path (a dirty worktree stops cleanup rather than discarding uncommitted - work), `git worktree remove ""`, `git worktree list` names it, then delete the - local branch. `git branch - -d` has the identical squash blindness as `git merge-base --is-ancestor` and refuses too, so - use `git branch -D -- ""` here, safe only because the GitHub-state check just - proved that exact branch finished, the narrow post-squash exception git-commit-conventions - describes, never applied to an unverified branch. Then, only when the remote branch still - exists, delete it with `python3 scripts/pr_review.py cleanup --repo owner/repo`, run - from a hub checkout, `` being the merged pull request the check above returned. It - re-reads that pull request and repeats the merged, base, head-repository, and tip checks in - code, refuses a permanent or protected branch, and confirms the delete by reading the ref - back. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an - unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub - checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs - under the host-safety installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, - which is what lets auto mode complete it. + --heads origin` for any matching remote feature branch. For each such branch, read the one + merged pull request into `develop` it headed, failing loud on any count other than one rather + than reading an empty answer as safe: `gh pr list --head "" --base develop --state + merged --repo owner/repo --json number --jq 'if length == 1 then .[0].number else + error("expected exactly one merged PR for this head, got \(length)") end'`. Then run + `python3 scripts/pr_review.py cleanup --repo owner/repo --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone, per `repo-worktree` "Listing and Cleanup". It re-reads that pull request and + repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or + protected branch, deletes the remote branch and confirms it by reading the ref back, treats + a remote branch already gone as done, so a prior interrupted attempt finishes rather than + stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal + is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed + delete, and `64` a target under an owner other than the hub checkout's own `origin` or that + `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety + installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, which is what lets auto mode + complete it. It also refuses while another open pull request in the repository has the branch as its head or base, naming each. - Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand, naming the pull request number and `--repo`. - No direct `git push origin --delete` or ref API delete runs from this skill, because `cleanup` is the one place holding the checks. + Where this seat cannot run `cleanup`, report the cleanup to the maintainer instead of running its steps by hand, naming the pull request number and `--repo`. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs on a feature branch from this skill, because `cleanup` is the one place holding the checks. Never apply this sweep to `develop` or `main` themselves, only to feature branches a drive-pr loop created. + - What no single pull request names: run + `python3 scripts/pr_review.py cleanup --sweep --repo owner/repo --checkout ` + and report what it lists. It writes nothing but a fetch, and `--apply` runs only on the + maintainer's go-ahead, since the report reaches other sessions' finished work too. ## Mechanics Live Elsewhere diff --git a/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md index 4ec78725f..211b5a366 100644 --- a/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 46bbda6bb..522135528 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -268,10 +268,38 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is in flight across the whole fleet. - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. -- **Verify before removing.** Read the pull request's merged state and head SHA from live GitHub state. Confirm the worktree is clean and resolves to that head. A dirty worktree stops cleanup because force-removing it would discard work. A detached helper worktree needs no pull request, but its commit must be contained in the branch whose completed operation created it. -- **Remove the exact finished worktree, then its local task branch.** Use `git worktree remove `. Try `git branch -d ` after a merge commit. A squash merge does not make the feature tip an ancestor of `develop`, so `-d` cannot recognize it as merged. After the live merged-PR and clean-worktree checks prove that exact branch finished, use `git branch -D ` under the narrow post-squash exception in `git-commit-conventions`. Never apply that exception to an unverified branch or to `develop`. -- **Remove temporary standalone clones and detached helper worktrees too.** Remove the exact `/-` path after confirming it is clean. The remote feature branch follows the repository's normal pull request cleanup policy. Never delete `develop` after a promotion because it is the permanent integration branch. -- **Return the base clone to current `develop`.** Fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. +- **Verify before removing.** A worktree or branch is removed only once live state proves it + finished. For a pull request that is its merged state and head SHA read from GitHub, with the + worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops + cleanup because force-removing it would discard work. A detached helper worktree needs no pull + request, but its commit must be contained in the branch whose completed operation created it. +- **A pull request merged into `develop` is cleaned up by one command.** Run + `python3 scripts/pr_review.py cleanup --repo / --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone. It reads the branch from the pull request, deletes the remote branch, removes + every worktree holding the branch without force, deletes the local branch, and returns the base + clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty + worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` + is done. Any other exit stops and is reported as it stands rather than finished by hand, since + the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the + host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the + full contract. +- **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's + worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is + clean, then remove the exact path with `git worktree remove `, or delete the exact + `/-` directory. Never delete `develop` after a promotion, since + it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is + clean, switch it to `develop` when needed, and fast-forward it with + `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base + clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching + or reconciling it. +- **What an interrupted or abandoned session left behind is swept.** Run + `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` + from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports + each worktree and branch that is clean, idle past `--min-age-hours` (default 24), and provably + finished. Adding `--apply` removes what the report lists through the same checks, the remote + branch of a merged `develop` pull request included. The report reaches other sessions' finished + work as well as this task's, so `--apply` runs on the maintainer's go-ahead. - **Prove the cleanup.** Finish with `git status --short --branch` in the base clone and `git worktree list`. The expected result is a clean base clone at `origin/develop` and no worktree belonging only to the completed task. - A worktree that refuses removal is dirty, and force is not the fix: look at what is uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index 5326c3499..b8faa08f7 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -43,7 +43,9 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and +Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub +worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md index f701ca677..475011b49 100644 --- a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md @@ -327,8 +327,12 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Comment on the handoff what merged, which issues it fixed, and - what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one +7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up + first, per `drive-pr` step 4, with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + so no finished worktree or branch outlives the lane. An exit other than `0` is named in the + close-out comment rather than finished by hand. Comment on the handoff what merged, which + issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. 8. **Save no memory**, since state lives in the chain where any session on any machine reads it. diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md index ef69d060e..1bd3c6081 100644 --- a/.github/skills/audit-a-repo/SKILL.md +++ b/.github/skills/audit-a-repo/SKILL.md @@ -35,3 +35,5 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards ## After the Report Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). + +The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index b2a31609f..21345adb1 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -286,7 +286,8 @@ working directory in which to delete its branch. So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. -That half is `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout once for each merged group. +For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. +It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. diff --git a/.github/skills/drive-pr/SKILL.md b/.github/skills/drive-pr/SKILL.md index d5bf56122..d85d216ce 100644 --- a/.github/skills/drive-pr/SKILL.md +++ b/.github/skills/drive-pr/SKILL.md @@ -103,10 +103,12 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. call, it is run from inside the task's own worktree per step 1, where the feature branch is checked out, and `gh pr merge --delete-branch` needs to switch that worktree to the base branch to delete it, which fails when `develop` is already checked out somewhere else, the ordinary - case in this layout. Instead run repo-worktree's post-merge cleanup from the base clone, remove - the worktree and delete the now-merged local task branch, then delete the remote one, which is this - skill's own step rather than that one's, with - `python3 scripts/pr_review.py cleanup --repo /`, run from a hub checkout. + case in this layout. Instead run repo-worktree's post-merge cleanup with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + run from a hub checkout outside the task's worktree, `` being the repository's + primary clone. It deletes the remote branch, then removes the task's worktree and local branch + and returns the base clone to fast-forwarded `develop`, deciding every local refusal before + anything is deleted, the remote branch included. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It @@ -122,7 +124,7 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. It names each, since the delete would close or retarget it. Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand. Name the pull request number and `--repo` so the maintainer runs the same command. - No direct `git push origin --delete` or ref API delete runs here, since `cleanup` holds the checks. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs here, since `cleanup` holds the checks. A pull request opened from a fork follows `upstream-contribution-workflow` instead, and `cleanup` refuses it. The repo's auto-delete-head-branches setting is kept off fleet-wide (to protect `develop` and diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index a6a397746..50f0450a5 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -169,77 +169,33 @@ skill covers all of it, scoped down by what the maintainer actually asks for. not have, an interrupted loop, a fix landed by hand outside that skill, or a maintainer merge in the GitHub UI. `git worktree list` for any worktree still registered under this task's feature branches, `git branch -vv` for any local feature branch, `git ls-remote - --heads origin` for any matching remote feature branch. For each, verify it finished by - reading GitHub's own state with the exact fields this check needs, not a bare listing, and - stop and report rather than guessing when selection is not exactly one match, on a non-1 - count exit non-zero rather than returning empty with success, an ambiguous or missing match - must fail loud, not read as an empty value still safe to act on: `gh pr list --head - "" --state merged --repo owner/repo --json - number,baseRefName,mergedAt,headRefOid,headRefName,headRepository --jq 'if length == 1 then - .[0] else error("expected exactly one merged PR for this head, got \(length)") end'`. - `--head` is expected to match exactly (verified against `gh` 2.97.0 on this repo, a bare - prefix of a real branch name returned nothing), but confirming `headRefName` equals `` - costs one field and is cheap insurance against a future `gh` behavior change, not a workaround - for a known partial-match case. Confirm `headRepository` is non-null and its `nameWithOwner` equals - `owner/repo`, the owner alone is not enough, a same-owner PR against an identically named - branch in a different repository must never pass this check either. Confirm `baseRefName` - is `develop` (a different merged pull request can share the same head branch name against a - different base, and that is never this sweep's target) and `mergedAt` is set. Compare tips - only where a remote branch actually exists. `git ls-remote --heads --exit-code -- origin - "refs/heads/"` is the exact-match form and must be, in that argument order. `--heads - origin ""` alone still tail-matches, a bare `topic/x` pattern also returns an unrelated - `other/topic/x` if one exists. `--` placed after `origin` instead of before it is not - equivalent either, verified empirically: with a `refs/heads/other/--` ref present, `--heads - origin -- "refs/heads/"` matched both that ref and the intended one, while `--heads -- - origin "refs/heads/"` matched only the one intended. Exit status is a tri-state, not a - stdin-emptiness check: `--exit-code` makes exit `2` mean query succeeded, branch gone, most - likely a prior cleanup attempt got interrupted after the remote delete but before the local - one, so skip straight to the local-tip check below and never attempt the remote delete a - second time. Exit `0` means it matched. Anything else is a failed query, a network or auth - problem, and stops and reports rather than being read as absence, an unreachable remote and a - genuinely gone branch both print nothing to stdout, only the exit code tells them apart. - Where the remote branch does exist, its tip must match that exact pull request's `headRefOid` - before its own delete proceeds, proving nothing landed on it since. Where a local branch - still exists too, its tip (`git rev-parse --verify "refs/heads/"`) must independently - match `headRefOid` before its own delete proceeds. Neither side needs the other to exist, a - prior interrupted attempt may have deleted one side already and left only the other, so - verify and delete whichever side is still there and skip whichever already is not, never - block one side's cleanup on the other side's absence. No `--` on `rev-parse`, verified - empirically: `git rev-parse -- ""` treats the argument after `--` as a path rather - than a revision and never resolves a SHA at all. The fully-qualified form needs no `--` - regardless, since `refs/heads/` never itself starts with `-`, and `--verify` fails - loudly rather than guessing when it does not resolve. Every branch or - worktree-path placeholder below is the real value, substituted as its own quoted argument - (a shell variable expansion such as `"$branch"`, or an argv element), never handed to `eval` - or `sh -c` for a second round of shell parsing, the only way an embedded `$()` or backtick - would actually run. A valid ref can start with `-` or carry a shell metacharacter, which is - why it stays quoted regardless. `--` marks the - end of options wherever a command supports it. - `git merge-base --is-ancestor develop` must never be used for either tip check, a - squash merge (drive-pr's own merge method) never makes the feature tip a literal ancestor of - `develop`, so the check reports every already-finished branch as unmerged. Only once GitHub - confirms it, and only when a local worktree or branch is still there to remove, remove the - worktree by its exact path (a dirty worktree stops cleanup rather than discarding uncommitted - work), `git worktree remove ""`, `git worktree list` names it, then delete the - local branch. `git branch - -d` has the identical squash blindness as `git merge-base --is-ancestor` and refuses too, so - use `git branch -D -- ""` here, safe only because the GitHub-state check just - proved that exact branch finished, the narrow post-squash exception git-commit-conventions - describes, never applied to an unverified branch. Then, only when the remote branch still - exists, delete it with `python3 scripts/pr_review.py cleanup --repo owner/repo`, run - from a hub checkout, `` being the merged pull request the check above returned. It - re-reads that pull request and repeats the merged, base, head-repository, and tip checks in - code, refuses a permanent or protected branch, and confirms the delete by reading the ref - back. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an - unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub - checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs - under the host-safety installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, - which is what lets auto mode complete it. + --heads origin` for any matching remote feature branch. For each such branch, read the one + merged pull request into `develop` it headed, failing loud on any count other than one rather + than reading an empty answer as safe: `gh pr list --head "" --base develop --state + merged --repo owner/repo --json number --jq 'if length == 1 then .[0].number else + error("expected exactly one merged PR for this head, got \(length)") end'`. Then run + `python3 scripts/pr_review.py cleanup --repo owner/repo --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone, per `repo-worktree` "Listing and Cleanup". It re-reads that pull request and + repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or + protected branch, deletes the remote branch and confirms it by reading the ref back, treats + a remote branch already gone as done, so a prior interrupted attempt finishes rather than + stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal + is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed + delete, and `64` a target under an owner other than the hub checkout's own `origin` or that + `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety + installer's `Bash(python3 scripts/pr_review.py:*)` allow rule, which is what lets auto mode + complete it. It also refuses while another open pull request in the repository has the branch as its head or base, naming each. - Where this seat cannot run `cleanup`, report the delete to the maintainer instead of deleting by hand, naming the pull request number and `--repo`. - No direct `git push origin --delete` or ref API delete runs from this skill, because `cleanup` is the one place holding the checks. + Where this seat cannot run `cleanup`, report the cleanup to the maintainer instead of running its steps by hand, naming the pull request number and `--repo`. + No direct `git push origin --delete`, ref API delete, `git worktree remove`, or `git branch -D` runs on a feature branch from this skill, because `cleanup` is the one place holding the checks. Never apply this sweep to `develop` or `main` themselves, only to feature branches a drive-pr loop created. + - What no single pull request names: run + `python3 scripts/pr_review.py cleanup --sweep --repo owner/repo --checkout ` + and report what it lists. It writes nothing but a fetch, and `--apply` runs only on the + maintainer's go-ahead, since the report reaches other sessions' finished work too. ## Mechanics Live Elsewhere diff --git a/.github/skills/pr-review-conduct/SKILL.md b/.github/skills/pr-review-conduct/SKILL.md index 4ec78725f..211b5a366 100644 --- a/.github/skills/pr-review-conduct/SKILL.md +++ b/.github/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 46bbda6bb..522135528 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -268,10 +268,38 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is in flight across the whole fleet. - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. -- **Verify before removing.** Read the pull request's merged state and head SHA from live GitHub state. Confirm the worktree is clean and resolves to that head. A dirty worktree stops cleanup because force-removing it would discard work. A detached helper worktree needs no pull request, but its commit must be contained in the branch whose completed operation created it. -- **Remove the exact finished worktree, then its local task branch.** Use `git worktree remove `. Try `git branch -d ` after a merge commit. A squash merge does not make the feature tip an ancestor of `develop`, so `-d` cannot recognize it as merged. After the live merged-PR and clean-worktree checks prove that exact branch finished, use `git branch -D ` under the narrow post-squash exception in `git-commit-conventions`. Never apply that exception to an unverified branch or to `develop`. -- **Remove temporary standalone clones and detached helper worktrees too.** Remove the exact `/-` path after confirming it is clean. The remote feature branch follows the repository's normal pull request cleanup policy. Never delete `develop` after a promotion because it is the permanent integration branch. -- **Return the base clone to current `develop`.** Fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. +- **Verify before removing.** A worktree or branch is removed only once live state proves it + finished. For a pull request that is its merged state and head SHA read from GitHub, with the + worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops + cleanup because force-removing it would discard work. A detached helper worktree needs no pull + request, but its commit must be contained in the branch whose completed operation created it. +- **A pull request merged into `develop` is cleaned up by one command.** Run + `python3 scripts/pr_review.py cleanup --repo / --checkout ` + from a hub checkout outside the worktree it removes, `` being the repository's + primary clone. It reads the branch from the pull request, deletes the remote branch, removes + every worktree holding the branch without force, deletes the local branch, and returns the base + clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty + worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` + is done. Any other exit stops and is reported as it stands rather than finished by hand, since + the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the + host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the + full contract. +- **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's + worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is + clean, then remove the exact path with `git worktree remove `, or delete the exact + `/-` directory. Never delete `develop` after a promotion, since + it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is + clean, switch it to `develop` when needed, and fast-forward it with + `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base + clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching + or reconciling it. +- **What an interrupted or abandoned session left behind is swept.** Run + `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` + from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports + each worktree and branch that is clean, idle past `--min-age-hours` (default 24), and provably + finished. Adding `--apply` removes what the report lists through the same checks, the remote + branch of a merged `develop` pull request included. The report reaches other sessions' finished + work as well as this task's, so `--apply` runs on the maintainer's go-ahead. - **Prove the cleanup.** Finish with `git status --short --branch` in the base clone and `git worktree list`. The expected result is a clean base clone at `origin/develop` and no worktree belonging only to the completed task. - A worktree that refuses removal is dirty, and force is not the fix: look at what is uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index 5326c3499..b8faa08f7 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -43,7 +43,9 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and +Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub +worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.github/skills/unattended-handoff/SKILL.md b/.github/skills/unattended-handoff/SKILL.md index f701ca677..475011b49 100644 --- a/.github/skills/unattended-handoff/SKILL.md +++ b/.github/skills/unattended-handoff/SKILL.md @@ -327,8 +327,12 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Comment on the handoff what merged, which issues it fixed, and - what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one +7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up + first, per `drive-pr` step 4, with + `python3 scripts/pr_review.py cleanup --repo / --checkout `, + so no finished worktree or branch outlives the lane. An exit other than `0` is named in the + close-out comment rather than finished by hand. Comment on the handoff what merged, which + issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. 8. **Save no memory**, since state lives in the chain where any session on any machine reads it. From 4b28d4a304519def2ea45a6dbd810e97b474b16e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 17 Oct 2026 13:59:20 -0700 Subject: [PATCH 2/5] Correct the Cleanup Claims the Local Review Found in the Skill Wiring The base clone stays put with base=kept on an exit 0, a refusal can follow the remote delete, the local tip may sit behind the merged head, and a branch outside a develop pull request still needs deleting. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/audit-a-repo/SKILL.md | 2 +- .agents/skills/backlog-burndown/SKILL.md | 10 ++--- .agents/skills/drive-pr/SKILL.md | 6 +-- .../skills/git-commit-conventions/SKILL.md | 2 +- .agents/skills/merge-and-release/SKILL.md | 5 ++- .agents/skills/repo-worktree/SKILL.md | 43 +++++++++++-------- .agents/skills/resync-a-repo/SKILL.md | 7 +-- .agents/skills/unattended-handoff/SKILL.md | 8 ++-- .../fleet-skills/.source-digests/audit-a-repo | 2 +- .../.source-digests/backlog-burndown | 2 +- .../fleet-skills/.source-digests/drive-pr | 2 +- .../.source-digests/git-commit-conventions | 2 +- .../.source-digests/merge-and-release | 2 +- .../.source-digests/repo-worktree | 2 +- .../.source-digests/resync-a-repo | 2 +- .../.source-digests/unattended-handoff | 2 +- .../fleet-skills/skills/audit-a-repo/SKILL.md | 2 +- .../skills/backlog-burndown/SKILL.md | 10 ++--- .../fleet-skills/skills/drive-pr/SKILL.md | 6 +-- .../skills/git-commit-conventions/SKILL.md | 2 +- .../skills/merge-and-release/SKILL.md | 5 ++- .../skills/repo-worktree/SKILL.md | 43 +++++++++++-------- .../skills/resync-a-repo/SKILL.md | 7 +-- .../skills/unattended-handoff/SKILL.md | 8 ++-- .github/skills/audit-a-repo/SKILL.md | 2 +- .github/skills/backlog-burndown/SKILL.md | 10 ++--- .github/skills/drive-pr/SKILL.md | 6 +-- .../skills/git-commit-conventions/SKILL.md | 2 +- .github/skills/merge-and-release/SKILL.md | 5 ++- .github/skills/repo-worktree/SKILL.md | 43 +++++++++++-------- .github/skills/resync-a-repo/SKILL.md | 7 +-- .github/skills/unattended-handoff/SKILL.md | 8 ++-- 32 files changed, 143 insertions(+), 122 deletions(-) diff --git a/.agents/skills/audit-a-repo/SKILL.md b/.agents/skills/audit-a-repo/SKILL.md index 1bd3c6081..e950b9c09 100644 --- a/.agents/skills/audit-a-repo/SKILL.md +++ b/.agents/skills/audit-a-repo/SKILL.md @@ -36,4 +36,4 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). -The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. +An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. Each is removed when the run ends, per `repo-worktree` "Listing and Cleanup". The report's pull request is cleaned up once it merges, with `python3 scripts/pr_review.py cleanup --repo ptr727/ProjectTemplate --checkout `. A detached checkout at `main` that only read the hub is removed by hand once it is clean. diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index 21345adb1..a26804bab 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -279,15 +279,13 @@ Brief on `AGENTS.md` "Context and Delegation Discipline"'s subagent shape. `repo-worktree`'s post-merge procedure returns the base clone to current develop before proving the cleanup, and `branching-and-release-model` states that requirement independently. Four workers doing that concurrently mutate one shared checkout, which `GOVERNANCE.md` "Repository Boundaries and -Write Safety" forbids. A worker also cannot -finish the procedure from inside its own worktree, since removing that worktree leaves it with no -working directory in which to delete its branch. +Write Safety" forbids. -So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's +So the whole procedure moves to the orchestrator, which runs it against the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. -It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". +It deletes the remote branch, then removes the group's worktree and local branch, per `repo-worktree` "Listing and Cleanup", which also says what its exit leaves to do. Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. @@ -315,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and branch go the same way a merged group's do. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.agents/skills/drive-pr/SKILL.md b/.agents/skills/drive-pr/SKILL.md index d85d216ce..4723167b0 100644 --- a/.agents/skills/drive-pr/SKILL.md +++ b/.agents/skills/drive-pr/SKILL.md @@ -106,9 +106,9 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. case in this layout. Instead run repo-worktree's post-merge cleanup with `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout outside the task's worktree, `` being the repository's - primary clone. It deletes the remote branch, then removes the task's worktree and local branch - and returns the base clone to fast-forwarded `develop`, deciding every local refusal before - anything is deleted, the remote branch included. + primary clone. It deletes the remote branch, then removes the task's worktree and local branch. + It decides its local refusals before the remote delete, and `repo-worktree` "Listing and + Cleanup" says what a refusal after it and a `base=kept` line each leave to do. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It diff --git a/.agents/skills/git-commit-conventions/SKILL.md b/.agents/skills/git-commit-conventions/SKILL.md index 5c1454f12..1596cfd3e 100644 --- a/.agents/skills/git-commit-conventions/SKILL.md +++ b/.agents/skills/git-commit-conventions/SKILL.md @@ -164,4 +164,4 @@ any rewrite. See `references/history-rewrite.md` for the full two-gate rule. ## Never run destructive git commands without being asked -`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree at the verified head SHA, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. +`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree with the branch at the verified head SHA or behind it, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index 50f0450a5..11dcabf88 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -180,8 +180,9 @@ skill covers all of it, scoped down by what the maintainer actually asks for. repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or protected branch, deletes the remote branch and confirms it by reading the ref back, treats a remote branch already gone as done, so a prior interrupted attempt finishes rather than - stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal - is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + stops, then removes the worktree and local branch. Its local refusals, a dirty worktree + among them, are decided before the remote delete, and `repo-worktree` "Listing and Cleanup" + says what a refusal after it and a `base=kept` line each leave to do. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 522135528..de94ee014 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -270,29 +270,38 @@ since git then runs whatever that directory holds, and nothing where it holds no - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. - **Verify before removing.** A worktree or branch is removed only once live state proves it finished. For a pull request that is its merged state and head SHA read from GitHub, with the - worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops - cleanup because force-removing it would discard work. A detached helper worktree needs no pull - request, but its commit must be contained in the branch whose completed operation created it. + worktree clean and the local branch at that head or behind it, which `cleanup` below checks in + code. A dirty worktree stops cleanup because force-removing it would discard work. A detached + helper worktree needs no pull request, but its commit must be contained in the branch whose + completed operation created it. - **A pull request merged into `develop` is cleaned up by one command.** Run `python3 scripts/pr_review.py cleanup --repo / --checkout ` from a hub checkout outside the worktree it removes, `` being the repository's - primary clone. It reads the branch from the pull request, deletes the remote branch, removes - every worktree holding the branch without force, deletes the local branch, and returns the base - clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty - worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` - is done. Any other exit stops and is reported as it stands rather than finished by hand, since - the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the - host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the - full contract. + primary clone. It reads the branch from the pull request and deletes the remote branch. It then + removes every worktree holding the branch without force, deletes the local branch, and + fast-forwards the base clone's `develop`. Its local refusals are decided before the remote + delete, so a dirty worktree, an operation in progress, or a local tip the merge did not carry + leaves everything in place. A re-check after the delete can still refuse where a concurrent + session changed a tree in between, and its output then names what was already deleted. Any exit + other than `0` stops and is reported as it stands rather than finished by hand. The raw + `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the host-safety + permission allowlist, and the command is. `scripts/README.md` in the hub holds the full + contract. - **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact - `/-` directory. Never delete `develop` after a promotion, since - it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is - clean, switch it to `develop` when needed, and fast-forward it with - `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base - clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching - or reconciling it. + `/-` directory. A named branch such a tree held, a promotion + branch merged into `main` by a merge commit for one, is deleted with + `git branch -d `, and a refusal is reported rather than forced. Its remote branch + follows the repository's normal pull request cleanup policy. Never delete `develop` after a + promotion, since it is the permanent integration branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base + clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with + `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step + to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or + release does not leave the base clone on `main`. Stop and report a dirty base clone or a + non-fast-forward instead of switching or reconciling it. - **What an interrupted or abandoned session left behind is swept.** Run `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index b8faa08f7..0e3255d66 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -43,9 +43,10 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and -Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub -worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`. Remove each one when the run ends, per that skill's "Listing and +Cleanup". A merged pull request's worktree goes through +`python3 scripts/pr_review.py cleanup --repo / --checkout `. A +hub worktree that only read the hub is removed by hand once it is clean, its branch with it. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.agents/skills/unattended-handoff/SKILL.md b/.agents/skills/unattended-handoff/SKILL.md index 475011b49..214b28e72 100644 --- a/.agents/skills/unattended-handoff/SKILL.md +++ b/.agents/skills/unattended-handoff/SKILL.md @@ -327,11 +327,9 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up - first, per `drive-pr` step 4, with - `python3 scripts/pr_review.py cleanup --repo / --checkout `, - so no finished worktree or branch outlives the lane. An exit other than `0` is named in the - close-out comment rather than finished by hand. Comment on the handoff what merged, which +7. **Close the lane out on done.** Each pull request the lane merged into `develop` has had + `drive-pr` step 4's cleanup run, so no finished worktree or branch outlives the lane. A cleanup + that exited other than `0` is named in the close-out comment rather than finished by hand. Comment on the handoff what merged, which issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. diff --git a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo index 40925dc6f..cd85b29dd 100644 --- a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo +++ b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo @@ -1 +1 @@ -b09763bd493ec71c +0cdd1b2a4deea2fc diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 8b29a190c..3dbe4870b 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -c0316a396936cbf2 +fe896045d657105a diff --git a/.claude-plugin/fleet-skills/.source-digests/drive-pr b/.claude-plugin/fleet-skills/.source-digests/drive-pr index 11ac86262..3e26bb539 100644 --- a/.claude-plugin/fleet-skills/.source-digests/drive-pr +++ b/.claude-plugin/fleet-skills/.source-digests/drive-pr @@ -1 +1 @@ -ab156348349f186d +012eac6247ff3cba diff --git a/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions b/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions index 844685ffb..c03a18974 100644 --- a/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions +++ b/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions @@ -1 +1 @@ -5d667d68d62f1f7d +2b97d9ac0a50ab24 diff --git a/.claude-plugin/fleet-skills/.source-digests/merge-and-release b/.claude-plugin/fleet-skills/.source-digests/merge-and-release index 1dd49c04d..4910781ca 100644 --- a/.claude-plugin/fleet-skills/.source-digests/merge-and-release +++ b/.claude-plugin/fleet-skills/.source-digests/merge-and-release @@ -1 +1 @@ -8240808799b201c7 +ed3c680a6d96e804 diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index 72461e344..3e2d4bc59 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -40dc05c1c28b91a8 +029b17de8bd8d6b3 diff --git a/.claude-plugin/fleet-skills/.source-digests/resync-a-repo b/.claude-plugin/fleet-skills/.source-digests/resync-a-repo index 3ac0597b2..b857af525 100644 --- a/.claude-plugin/fleet-skills/.source-digests/resync-a-repo +++ b/.claude-plugin/fleet-skills/.source-digests/resync-a-repo @@ -1 +1 @@ -3b35bb4f4ff85e01 +a7d5b86ccfdd7e69 diff --git a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff index ff86e70d8..403f9fc20 100644 --- a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff @@ -1 +1 @@ -3781961f4f8b9efe +3f5ccf7604397e31 diff --git a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md index 1bd3c6081..e950b9c09 100644 --- a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md @@ -36,4 +36,4 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). -The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. +An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. Each is removed when the run ends, per `repo-worktree` "Listing and Cleanup". The report's pull request is cleaned up once it merges, with `python3 scripts/pr_review.py cleanup --repo ptr727/ProjectTemplate --checkout `. A detached checkout at `main` that only read the hub is removed by hand once it is clean. diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index 21345adb1..a26804bab 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -279,15 +279,13 @@ Brief on `AGENTS.md` "Context and Delegation Discipline"'s subagent shape. `repo-worktree`'s post-merge procedure returns the base clone to current develop before proving the cleanup, and `branching-and-release-model` states that requirement independently. Four workers doing that concurrently mutate one shared checkout, which `GOVERNANCE.md` "Repository Boundaries and -Write Safety" forbids. A worker also cannot -finish the procedure from inside its own worktree, since removing that worktree leaves it with no -working directory in which to delete its branch. +Write Safety" forbids. -So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's +So the whole procedure moves to the orchestrator, which runs it against the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. -It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". +It deletes the remote branch, then removes the group's worktree and local branch, per `repo-worktree` "Listing and Cleanup", which also says what its exit leaves to do. Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. @@ -315,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and branch go the same way a merged group's do. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md b/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md index d85d216ce..4723167b0 100644 --- a/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/drive-pr/SKILL.md @@ -106,9 +106,9 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. case in this layout. Instead run repo-worktree's post-merge cleanup with `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout outside the task's worktree, `` being the repository's - primary clone. It deletes the remote branch, then removes the task's worktree and local branch - and returns the base clone to fast-forwarded `develop`, deciding every local refusal before - anything is deleted, the remote branch included. + primary clone. It deletes the remote branch, then removes the task's worktree and local branch. + It decides its local refusals before the remote delete, and `repo-worktree` "Listing and + Cleanup" says what a refusal after it and a `base=kept` line each leave to do. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It diff --git a/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md b/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md index 5c1454f12..1596cfd3e 100644 --- a/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md @@ -164,4 +164,4 @@ any rewrite. See `references/history-rewrite.md` for the full two-gate rule. ## Never run destructive git commands without being asked -`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree at the verified head SHA, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. +`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree with the branch at the verified head SHA or behind it, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index 50f0450a5..11dcabf88 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -180,8 +180,9 @@ skill covers all of it, scoped down by what the maintainer actually asks for. repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or protected branch, deletes the remote branch and confirms it by reading the ref back, treats a remote branch already gone as done, so a prior interrupted attempt finishes rather than - stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal - is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + stops, then removes the worktree and local branch. Its local refusals, a dirty worktree + among them, are decided before the remote delete, and `repo-worktree` "Listing and Cleanup" + says what a refusal after it and a `base=kept` line each leave to do. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 522135528..de94ee014 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -270,29 +270,38 @@ since git then runs whatever that directory holds, and nothing where it holds no - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. - **Verify before removing.** A worktree or branch is removed only once live state proves it finished. For a pull request that is its merged state and head SHA read from GitHub, with the - worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops - cleanup because force-removing it would discard work. A detached helper worktree needs no pull - request, but its commit must be contained in the branch whose completed operation created it. + worktree clean and the local branch at that head or behind it, which `cleanup` below checks in + code. A dirty worktree stops cleanup because force-removing it would discard work. A detached + helper worktree needs no pull request, but its commit must be contained in the branch whose + completed operation created it. - **A pull request merged into `develop` is cleaned up by one command.** Run `python3 scripts/pr_review.py cleanup --repo / --checkout ` from a hub checkout outside the worktree it removes, `` being the repository's - primary clone. It reads the branch from the pull request, deletes the remote branch, removes - every worktree holding the branch without force, deletes the local branch, and returns the base - clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty - worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` - is done. Any other exit stops and is reported as it stands rather than finished by hand, since - the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the - host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the - full contract. + primary clone. It reads the branch from the pull request and deletes the remote branch. It then + removes every worktree holding the branch without force, deletes the local branch, and + fast-forwards the base clone's `develop`. Its local refusals are decided before the remote + delete, so a dirty worktree, an operation in progress, or a local tip the merge did not carry + leaves everything in place. A re-check after the delete can still refuse where a concurrent + session changed a tree in between, and its output then names what was already deleted. Any exit + other than `0` stops and is reported as it stands rather than finished by hand. The raw + `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the host-safety + permission allowlist, and the command is. `scripts/README.md` in the hub holds the full + contract. - **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact - `/-` directory. Never delete `develop` after a promotion, since - it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is - clean, switch it to `develop` when needed, and fast-forward it with - `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base - clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching - or reconciling it. + `/-` directory. A named branch such a tree held, a promotion + branch merged into `main` by a merge commit for one, is deleted with + `git branch -d `, and a refusal is reported rather than forced. Its remote branch + follows the repository's normal pull request cleanup policy. Never delete `develop` after a + promotion, since it is the permanent integration branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base + clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with + `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step + to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or + release does not leave the base clone on `main`. Stop and report a dirty base clone or a + non-fast-forward instead of switching or reconciling it. - **What an interrupted or abandoned session left behind is swept.** Run `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index b8faa08f7..0e3255d66 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -43,9 +43,10 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and -Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub -worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`. Remove each one when the run ends, per that skill's "Listing and +Cleanup". A merged pull request's worktree goes through +`python3 scripts/pr_review.py cleanup --repo / --checkout `. A +hub worktree that only read the hub is removed by hand once it is clean, its branch with it. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md index 475011b49..214b28e72 100644 --- a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md @@ -327,11 +327,9 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up - first, per `drive-pr` step 4, with - `python3 scripts/pr_review.py cleanup --repo / --checkout `, - so no finished worktree or branch outlives the lane. An exit other than `0` is named in the - close-out comment rather than finished by hand. Comment on the handoff what merged, which +7. **Close the lane out on done.** Each pull request the lane merged into `develop` has had + `drive-pr` step 4's cleanup run, so no finished worktree or branch outlives the lane. A cleanup + that exited other than `0` is named in the close-out comment rather than finished by hand. Comment on the handoff what merged, which issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md index 1bd3c6081..e950b9c09 100644 --- a/.github/skills/audit-a-repo/SKILL.md +++ b/.github/skills/audit-a-repo/SKILL.md @@ -36,4 +36,4 @@ The audit is the fleet's measurement procedure, and the failure shapes it guards Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). -The hub worktree a run measured from is removed when the run ends, per `repo-worktree` "Listing and Cleanup": the report's pull request through `pr_review.py cleanup ` once it merges, and a detached checkout at `main` that only read the hub by hand once it is clean. An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. +An audit leaves no checkout behind it, since one left standing is what a later sweep has to tell apart from live work. Each is removed when the run ends, per `repo-worktree` "Listing and Cleanup". The report's pull request is cleaned up once it merges, with `python3 scripts/pr_review.py cleanup --repo ptr727/ProjectTemplate --checkout `. A detached checkout at `main` that only read the hub is removed by hand once it is clean. diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index 21345adb1..a26804bab 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -279,15 +279,13 @@ Brief on `AGENTS.md` "Context and Delegation Discipline"'s subagent shape. `repo-worktree`'s post-merge procedure returns the base clone to current develop before proving the cleanup, and `branching-and-release-model` states that requirement independently. Four workers doing that concurrently mutate one shared checkout, which `GOVERNANCE.md` "Repository Boundaries and -Write Safety" forbids. A worker also cannot -finish the procedure from inside its own worktree, since removing that worktree leaves it with no -working directory in which to delete its branch. +Write Safety" forbids. -So the whole procedure moves to the orchestrator, which runs it from the base clone at the round's +So the whole procedure moves to the orchestrator, which runs it against the base clone at the round's cleanup step, while no worker is live in a tree it touches. It carries the remote half of `drive-pr`'s post-merge cleanup too. For a merged group both halves are one command, `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout once for each merged group, `` being the repository's primary clone. -It deletes the remote branch, then removes the group's worktree and local branch and fast-forwards `develop`, per `repo-worktree` "Listing and Cleanup". +It deletes the remote branch, then removes the group's worktree and local branch, per `repo-worktree` "Listing and Cleanup", which also says what its exit leaves to do. Taking that step from the worker without naming a new owner would leave a live remote branch behind every group. `cleanup` holds the delete's checks, among them the tip against the pull request's `headRefOid`. It also refuses a branch another open pull request has as its head or base. @@ -315,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and branch go the same way a merged group's do. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.github/skills/drive-pr/SKILL.md b/.github/skills/drive-pr/SKILL.md index d85d216ce..4723167b0 100644 --- a/.github/skills/drive-pr/SKILL.md +++ b/.github/skills/drive-pr/SKILL.md @@ -106,9 +106,9 @@ promotion PR once the fix lands, is the early exit this skill exists to prevent. case in this layout. Instead run repo-worktree's post-merge cleanup with `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout outside the task's worktree, `` being the repository's - primary clone. It deletes the remote branch, then removes the task's worktree and local branch - and returns the base clone to fast-forwarded `develop`, deciding every local refusal before - anything is deleted, the remote branch included. + primary clone. It deletes the remote branch, then removes the task's worktree and local branch. + It decides its local refusals before the remote delete, and `repo-worktree` "Listing and + Cleanup" says what a refusal after it and a `base=kept` line each leave to do. It takes the pull request rather than a branch name and does the verify-then-delete in code. It refuses unless the pull request merged into `develop` from a head in that same repository, and refuses a permanent branch or one any ruleset or branch protection covers. It diff --git a/.github/skills/git-commit-conventions/SKILL.md b/.github/skills/git-commit-conventions/SKILL.md index 5c1454f12..1596cfd3e 100644 --- a/.github/skills/git-commit-conventions/SKILL.md +++ b/.github/skills/git-commit-conventions/SKILL.md @@ -164,4 +164,4 @@ any rewrite. See `references/history-rewrite.md` for the full two-gate rule. ## Never run destructive git commands without being asked -`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree at the verified head SHA, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. +`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree with the branch at the verified head SHA or behind it, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index 50f0450a5..11dcabf88 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -180,8 +180,9 @@ skill covers all of it, scoped down by what the maintainer actually asks for. repeats the merged, base, head-repository, and tip checks in code, refuses a permanent or protected branch, deletes the remote branch and confirms it by reading the ref back, treats a remote branch already gone as done, so a prior interrupted attempt finishes rather than - stops, then removes the worktree and local branch and fast-forwards `develop`. Every refusal - is decided before anything is deleted, a dirty worktree among them. Exit `0` is done, and + stops, then removes the worktree and local branch. Its local refusals, a dirty worktree + among them, are decided before the remote delete, and `repo-worktree` "Listing and Cleanup" + says what a refusal after it and a `base=kept` line each leave to do. Exit `0` is done, and any other exit stops and reports: `1` is a refusal, `2` an unanswered read or an unconfirmed delete, and `64` a target under an owner other than the hub checkout's own `origin` or that `origin` unreadable, a registry refusal being a `1`. It runs under the host-safety diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 522135528..de94ee014 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -270,29 +270,38 @@ since git then runs whatever that directory holds, and nothing where it holds no - **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. - **Verify before removing.** A worktree or branch is removed only once live state proves it finished. For a pull request that is its merged state and head SHA read from GitHub, with the - worktree clean and at that head, which `cleanup` below checks in code. A dirty worktree stops - cleanup because force-removing it would discard work. A detached helper worktree needs no pull - request, but its commit must be contained in the branch whose completed operation created it. + worktree clean and the local branch at that head or behind it, which `cleanup` below checks in + code. A dirty worktree stops cleanup because force-removing it would discard work. A detached + helper worktree needs no pull request, but its commit must be contained in the branch whose + completed operation created it. - **A pull request merged into `develop` is cleaned up by one command.** Run `python3 scripts/pr_review.py cleanup --repo / --checkout ` from a hub checkout outside the worktree it removes, `` being the repository's - primary clone. It reads the branch from the pull request, deletes the remote branch, removes - every worktree holding the branch without force, deletes the local branch, and returns the base - clone to fast-forwarded `develop`. Every refusal is decided before anything is deleted, a dirty - worktree, an operation in progress, and a local tip the merge did not carry among them. Exit `0` - is done. Any other exit stops and is reported as it stands rather than finished by hand, since - the raw `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the - host-safety permission allowlist and the command is. `scripts/README.md` in the hub holds the - full contract. + primary clone. It reads the branch from the pull request and deletes the remote branch. It then + removes every worktree holding the branch without force, deletes the local branch, and + fast-forwards the base clone's `develop`. Its local refusals are decided before the remote + delete, so a dirty worktree, an operation in progress, or a local tip the merge did not carry + leaves everything in place. A re-check after the delete can still refuse where a concurrent + session changed a tree in between, and its output then names what was already deleted. Any exit + other than `0` stops and is reported as it stands rather than finished by hand. The raw + `git worktree remove`, `git branch -D`, and remote delete it replaces are not on the host-safety + permission allowlist, and the command is. `scripts/README.md` in the hub holds the full + contract. - **What no merged pull request into `develop` covers is removed by hand.** That is a promotion's worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact - `/-` directory. Never delete `develop` after a promotion, since - it is the permanent integration branch. Then fetch and prune `origin`, confirm the base clone is - clean, switch it to `develop` when needed, and fast-forward it with - `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base - clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching - or reconciling it. + `/-` directory. A named branch such a tree held, a promotion + branch merged into `main` by a merge commit for one, is deleted with + `git branch -d `, and a refusal is reported rather than forced. Its remote branch + follows the repository's normal pull request cleanup policy. Never delete `develop` after a + promotion, since it is the permanent integration branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base + clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with + `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step + to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or + release does not leave the base clone on `main`. Stop and report a dirty base clone or a + non-fast-forward instead of switching or reconciling it. - **What an interrupted or abandoned session left behind is swept.** Run `python3 scripts/pr_review.py cleanup --sweep --repo / --checkout ` from a hub checkout. It fetches and prunes `origin` and writes nothing else, and it reports diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index b8faa08f7..0e3255d66 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -43,9 +43,10 @@ section 1, a file fetched with the hub rather than present in a carrier, since a answers confidently instead of failing. Never do the resync's own work in a checkout another task may be using, the maintainer's own primary checkout included, even one that looks current: for the hub and for the target repo alike, create a worktree of your own off that base clone and work -there, per `repo-worktree`, and remove each one when the run ends, per that skill's "Listing and -Cleanup": a merged pull request's worktree through `pr_review.py cleanup `, and a hub -worktree that only read the hub by hand once it is clean. On a machine carrying the hub's agent-safety install, Claude Code also +there, per `repo-worktree`. Remove each one when the run ends, per that skill's "Listing and +Cleanup". A merged pull request's worktree goes through +`python3 scripts/pr_review.py cleanup --repo / --checkout `. A +hub worktree that only read the hub is removed by hand once it is clean, its branch with it. On a machine carrying the hub's agent-safety install, Claude Code also refuses a mutating git command run directly in a primary checkout, though its rules classify git and gh commands only, so a plain file write is never denied and the re-vendoring this skill exists to govern goes uncaught. A compound command is judged whole, so a copy chained to a denied git diff --git a/.github/skills/unattended-handoff/SKILL.md b/.github/skills/unattended-handoff/SKILL.md index 475011b49..214b28e72 100644 --- a/.github/skills/unattended-handoff/SKILL.md +++ b/.github/skills/unattended-handoff/SKILL.md @@ -327,11 +327,9 @@ does not qualify, since skipping one costs nothing and a guess costs a revert an Once answered, the label comes off per `GOVERNANCE.md` "Communicating with the User". A declined rule's issue closes, and an adopted one stays open as ordinary work, which the loop may then take, since the maintainer has judged it. -7. **Close the lane out on done.** Each pull request the lane merged into `develop` is cleaned up - first, per `drive-pr` step 4, with - `python3 scripts/pr_review.py cleanup --repo / --checkout `, - so no finished worktree or branch outlives the lane. An exit other than `0` is named in the - close-out comment rather than finished by hand. Comment on the handoff what merged, which +7. **Close the lane out on done.** Each pull request the lane merged into `develop` has had + `drive-pr` step 4's cleanup run, so no finished worktree or branch outlives the lane. A cleanup + that exited other than `0` is named in the close-out comment rather than finished by hand. Comment on the handoff what merged, which issues it fixed, and what it filed along the way, the lesson issue included, then close it. An `auto-*` lane holds one issue, so its work is complete and it is the closed-out lane `session-handoff` "The Chain" names, needing no successor. From 41bade7f1bd28dc01171997368623f27a2f0cd17 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 17 Oct 2026 14:01:09 -0700 Subject: [PATCH 3/5] Fetch Before a Hand Branch Delete and Keep -d for Unmerged Branches Co-Authored-By: Claude Opus 5.5 --- .agents/skills/backlog-burndown/SKILL.md | 2 +- .agents/skills/repo-worktree/SKILL.md | 19 +++++++++++-------- .../.source-digests/backlog-burndown | 2 +- .../.source-digests/repo-worktree | 2 +- .../skills/backlog-burndown/SKILL.md | 2 +- .../skills/repo-worktree/SKILL.md | 19 +++++++++++-------- .github/skills/backlog-burndown/SKILL.md | 2 +- .github/skills/repo-worktree/SKILL.md | 19 +++++++++++-------- 8 files changed, 38 insertions(+), 29 deletions(-) diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index a26804bab..60ee2213b 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -313,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -d `, which refuses rather than discards where that check was wrong, a refusal stopping the group for the maintainer per "Raising a Blocked Question". Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index de94ee014..8fdea96c5 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -291,14 +291,17 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted with - `git branch -d `, and a refusal is reported rather than forced. Its remote branch - follows the repository's normal pull request cleanup policy. Never delete `develop` after a - promotion, since it is the permanent integration branch. -- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base - clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with - `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step - to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` + with `git branch -d `, since `-d` judges the branch against its fetched upstream, + and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into + `develop`. Never delete `develop` after a promotion, since it is the permanent integration + branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself where the base clone + is on `develop` or on the merged branch with no tracked changes. A base clone on the merged + branch that it cannot switch is one of its refusals. One on another branch, or on `develop` with + tracked changes or no fast-forward, exits `0` with `base=kept` on its `status=CLEANED` line. + That line, and every hand removal, leaves this step to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 3dbe4870b..f07496213 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -fe896045d657105a +8ad89af40b630c02 diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index 3e2d4bc59..a53cfbdcf 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -029b17de8bd8d6b3 +0e8525e07fc23fc3 diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index a26804bab..60ee2213b 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -313,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -d `, which refuses rather than discards where that check was wrong, a refusal stopping the group for the maintainer per "Raising a Blocked Question". Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index de94ee014..8fdea96c5 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -291,14 +291,17 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted with - `git branch -d `, and a refusal is reported rather than forced. Its remote branch - follows the repository's normal pull request cleanup policy. Never delete `develop` after a - promotion, since it is the permanent integration branch. -- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base - clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with - `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step - to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` + with `git branch -d `, since `-d` judges the branch against its fetched upstream, + and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into + `develop`. Never delete `develop` after a promotion, since it is the permanent integration + branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself where the base clone + is on `develop` or on the merged branch with no tracked changes. A base clone on the merged + branch that it cannot switch is one of its refusals. One on another branch, or on `develop` with + tracked changes or no fast-forward, exits `0` with `base=kept` on its `status=CLEANED` line. + That line, and every hand removal, leaves this step to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index a26804bab..60ee2213b 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -313,7 +313,7 @@ An abandoned group, and a dead worker's clean tree, have no merged pull request `repo-worktree`'s verify-before-removing step to read, so the check that step gives way to here is what it exists to establish, that nothing unmerged is being thrown away: confirm the branch carries no commit that is not already on develop, and that -its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -D `, the check just made standing in for the merged pull request. +its worktree is clean. Both hold, and the worktree and local branch are removed by hand, since `cleanup` needs a merged pull request to read: `git worktree remove `, then `git branch -d `, which refuses rather than discards where that check was wrong, a refusal stopping the group for the maintainer per "Raising a Blocked Question". Its claim comment is released and its issue returns to the next round's ranking. A remote branch that was pushed is reported to the maintainer rather than deleted, since `cleanup` needs a merged pull request to read. No remote branch is left to report where none was pushed. Either fails, and cleanup stops there and diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index de94ee014..8fdea96c5 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -291,14 +291,17 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted with - `git branch -d `, and a refusal is reported rather than forced. Its remote branch - follows the repository's normal pull request cleanup policy. Never delete `develop` after a - promotion, since it is the permanent integration branch. -- **Return the base clone to current `develop`.** `cleanup` does this itself only where the base - clone is clean and already on `develop` or on the merged branch, and otherwise exits `0` with - `base=kept` on its `status=CLEANED` line. That line, and every hand removal, leaves this step - to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when + branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` + with `git branch -d `, since `-d` judges the branch against its fetched upstream, + and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into + `develop`. Never delete `develop` after a promotion, since it is the permanent integration + branch. +- **Return the base clone to current `develop`.** `cleanup` does this itself where the base clone + is on `develop` or on the merged branch with no tracked changes. A base clone on the merged + branch that it cannot switch is one of its refusals. One on another branch, or on `develop` with + tracked changes or no fast-forward, exits `0` with `base=kept` on its `status=CLEANED` line. + That line, and every hand removal, leaves this step to run: fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. From b6170179602936a7dbca1c50ddabd31b5780401d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 17 Oct 2026 14:08:57 -0700 Subject: [PATCH 4/5] Leave a Promotion Branch to the Sweep Rather Than git branch -d Co-Authored-By: Claude Opus 5.5 --- .agents/skills/repo-worktree/SKILL.md | 7 ++++--- .claude-plugin/fleet-skills/.source-digests/repo-worktree | 2 +- .claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md | 7 ++++--- .github/skills/repo-worktree/SKILL.md | 7 ++++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 8fdea96c5..3380b66a8 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -291,9 +291,10 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` - with `git branch -d `, since `-d` judges the branch against its fetched upstream, - and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + branch merged into `main` by a merge commit for one, is left for the sweep below, which reads it + as finished once `origin/main` or `origin/develop` contains it. `git branch -d` is no substitute, + since it judges a branch with no upstream against `HEAD` and so refuses one merged only into + `main` while the base clone is on `develop`. Where that branch was pushed, its remote branch is reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into `develop`. Never delete `develop` after a promotion, since it is the permanent integration branch. diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index a53cfbdcf..55cd799bd 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -0e8525e07fc23fc3 +b9f7f8c3219781b5 diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 8fdea96c5..3380b66a8 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -291,9 +291,10 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` - with `git branch -d `, since `-d` judges the branch against its fetched upstream, - and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + branch merged into `main` by a merge commit for one, is left for the sweep below, which reads it + as finished once `origin/main` or `origin/develop` contains it. `git branch -d` is no substitute, + since it judges a branch with no upstream against `HEAD` and so refuses one merged only into + `main` while the base clone is on `develop`. Where that branch was pushed, its remote branch is reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into `develop`. Never delete `develop` after a promotion, since it is the permanent integration branch. diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 8fdea96c5..3380b66a8 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -291,9 +291,10 @@ since git then runs whatever that directory holds, and nothing where it holds no worktree, a detached helper worktree, and a temporary standalone clone. Confirm the tree is clean, then remove the exact path with `git worktree remove `, or delete the exact `/-` directory. A named branch such a tree held, a promotion - branch merged into `main` by a merge commit for one, is deleted after `git fetch --prune origin` - with `git branch -d `, since `-d` judges the branch against its fetched upstream, - and a refusal is reported rather than forced. Where that branch was pushed, its remote branch is + branch merged into `main` by a merge commit for one, is left for the sweep below, which reads it + as finished once `origin/main` or `origin/develop` contains it. `git branch -d` is no substitute, + since it judges a branch with no upstream against `HEAD` and so refuses one merged only into + `main` while the base clone is on `develop`. Where that branch was pushed, its remote branch is reported to the maintainer for deletion, since `cleanup` reads only a pull request merged into `develop`. Never delete `develop` after a promotion, since it is the permanent integration branch. From 6d7ecc0695dcd933096b0b1ef861aa7fe08e0e32 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 17 Oct 2026 14:09:53 -0700 Subject: [PATCH 5/5] Name the Promotion Branch Exception in the Loop Completion Rule Co-Authored-By: Claude Opus 5.5 --- .agents/skills/pr-review-conduct/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digests/pr-review-conduct | 2 +- .claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md | 2 +- .github/skills/pr-review-conduct/SKILL.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/skills/pr-review-conduct/SKILL.md b/.agents/skills/pr-review-conduct/SKILL.md index 211b5a366..dacab1446 100644 --- a/.agents/skills/pr-review-conduct/SKILL.md +++ b/.agents/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains, except a promotion branch merged into `main`, which `repo-worktree` leaves for its sweep. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes diff --git a/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct b/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct index 8deeedcda..656d1368f 100644 --- a/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/pr-review-conduct @@ -1 +1 @@ -e353708498e5145a +8e3f5e82dbdaf90c diff --git a/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md index 211b5a366..dacab1446 100644 --- a/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains, except a promotion branch merged into `main`, which `repo-worktree` leaves for its sweep. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes diff --git a/.github/skills/pr-review-conduct/SKILL.md b/.github/skills/pr-review-conduct/SKILL.md index 211b5a366..dacab1446 100644 --- a/.github/skills/pr-review-conduct/SKILL.md +++ b/.github/skills/pr-review-conduct/SKILL.md @@ -273,7 +273,7 @@ is not a stopping condition here, and neither is patience running out. Reporting PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. -After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. For a merge into `develop` that procedure is `python3 scripts/pr_review.py cleanup --repo / --checkout `, run from a hub checkout. The pull request loop is incomplete while its finished worktree or local task branch remains, except a promotion branch merged into `main`, which `repo-worktree` leaves for its sweep. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. ## Every finding ends in one of five outcomes