Skip to content

feat(bin): automate safe task branch cleanup - #2999

Closed
trevorallred wants to merge 37 commits into
kunchenguid:mainfrom
trevorallred:fm/firstmate-branch-cleanup-automation
Closed

trevorallred wants to merge 37 commits into
kunchenguid:mainfrom
trevorallred:fm/firstmate-branch-cleanup-automation

Conversation

@trevorallred

@trevorallred trevorallred commented Aug 25, 2026 •

Copy link
Copy Markdown

Intent

Automate provably safe cleanup of Firstmate task branches across both a project's GitHub origin and its separate local no-mistakes bare remote. Add --delete-branch to fm-pr-merge's gh-axi squash merge so GitHub deletion activates fm-fleet-sync's existing default-on gone-upstream pruning; verify and preserve that established gone-upstream-with-no-worktree proof. Extend the shared fm-branch-merge library as the one owner of cleanup proofs and exact-tip deletion, silently skip projects without a configured no-mistakes remote, wire ordinary landed ship-task cleanup into teardown using its existing GitHub-aware landedness proof, and provide fm-branch-cleanup.sh for a full on-demand sweep without duplicated safety logic. Every ordinary deletion must require an existing strong proof (ancestor of default, established gone-upstream proof, or shared GitHub-aware landedness); uncertainty and checked-out branches must be preserved. Keep the existing explicitly captain-authorized --force discard exception and disposable scout exception unchanged. Validate behavior with executable tests and real scratch repositories/remotes, including origin deletion, no-mistakes cleanup, remote exact-tip leases, repo-root resolution, temporary-worktree cleanup, pushed-but-unmerged preservation, and default-on sweep behavior. Serialize every branch-cleanup path with a repository-common per-branch lock acquired across the complete check/detach/delete sequence; retain in-lock exact-tip comparisons, Git's native branch -D linked-worktree refusal for local deletion, and force-with-lease for remote deletion. Cover competing locked ref movement, concurrent linked-worktree checkout at the native deletion boundary, and the remote-only checkout race. Do not change Herdr lifecycle behavior or fix herdr-preflight-missing-adapter: that test failure is independently confirmed pre-existing on clean main and unrelated to this task.

What Changed

  • Add shared, lock-protected proofs and exact-tip deletion helpers plus an on-demand sweep for eligible fm/* task branches across local, origin, and configured no-mistakes remotes.
  • Extend fleet sync and teardown to remove only provably landed or gone-upstream task branches, preserving checked-out and uncertain branches while retaining force and scout exceptions.
  • Delete GitHub branches after squash merges and add regression coverage for cleanup proofs, remote leases, worktree races, and branch-sweep behavior.

Risk Assessment

✅ Low: The change centralizes locked proof-and-delete operations, preserves exact-tip and native worktree safeguards, and the reviewed call paths conform to the required cleanup behavior.

Testing

Targeted end-to-end Git validation exercised real origin and no-mistakes bare remotes: proven landed branches were cleaned up, unmerged and checked-out branches were preserved, GitHub squash/merge deletion and default-on gone-upstream pruning worked, and exact-tip leases plus per-branch locks protected concurrent ref/worktree races. Evidence transcripts were saved in the designated evidence directory; no UI is involved because this is a shell/Git CLI change.

Evidence: Branch cleanup E2E transcript

Source: Branch cleanup E2E transcript

Focused end-to-end branch-cleanup scenarios exercised real scratch Git repositories and bare remotes. The transcript records deletion of landed branches from both remotes, preservation of unlanded and checked-out branches, squash-merge proof reuse, exact-tip remote lease protection, remote-only checkout locking, and GitHub merge deletion.

FM_TEST_BEGIN 2026-08-25T04:55:05Z tests/fm-branch-cleanup.test.sh family=pr-forge expected_gate_skip=none
ok - full sweep deletes landed refs from both real remotes and preserves unlanded or checked-out branches
ok - on-demand cleanup reuses the merged-PR proof for squash-landed tips across both remotes
ok - an exact-tip lease preserves a remote branch that advances after landedness proof
ok - remote-only cleanup shares the branch lock and preserves a branch checked out before deletion
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ok - fm-pr-merge's delete flag drives branch deletion against a real scratch origin
FM_TEST_END 2026-08-25T04:55:38Z tests/fm-branch-cleanup.test.sh exit=0 duration_ms=32505 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=33258
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=32505 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-branch-cleanup.test.sh duration_ms=32505
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ok - fm-pr-merge's delete flag drives branch deletion against a real scratch origin
Evidence: Fleet-sync cleanup transcript

Source: Fleet-sync cleanup transcript

Fleet-sync scenarios demonstrate default-on gone-upstream pruning, disable behavior, active-worktree preservation, protected-default exclusion, ref-movement locking, and native linked-worktree deletion refusal.

FM_TEST_BEGIN 2026-08-25T04:56:21Z tests/fm-fleet-sync.test.sh family=session-bootstrap expected_gate_skip=none
ok - detached clean ancestor is re-attached and fast-forwarded (recovered)
ok - detached HEAD with unique commits is reported STUCK and left untouched
ok - detached clean ancestor with diverged local default is reported STUCK and left untouched
ok - dirty working tree is reported STUCK and left untouched
ok - non-default named branch is reported STUCK and left untouched
ok - diverged default branch is reported STUCK and left untouched
ok - on-default clean behind clone still fast-forwards
ok - already-current clone is reported unchanged
ok - no-origin clone is skipped (benign), not flagged STUCK
ok - local-only clone is skipped (benign), retaining its merged task branch for authorized cleanup
ok - a branch update between merged proof and deletion is left intact
ok - the per-branch lock serializes a competing ref move and the in-lock exact-tip check preserves it
ok - a worktree checkout between merged proof and deletion leaves the branch intact
ok - git's native branch deletion guard refuses a checkout at the final locked boundary
ok - the gone-upstream sweep prunes an inactive fm/* branch
ok - routine branch pruning defaults on and honors the explicit disable switch
ok - routine fleet sync leaves a branch with an active worktree untouched
ok - the sweep and its shared proof both refuse to ever target the default/protected branch
ok - single-project form accepts a bare project name
ok - single-project bare name resolution is not cwd-sensitive
ok - single-project form accepts a projects/<name> relative name
ok - single-project projects/<name> resolution is not cwd-sensitive
ok - single-project form leaves a genuinely bad name unresolved
ok - whole-fleet form processes every clone under projects/
ok - bootstrap relays recovered: and STUCK: fleet-sync outcomes
ok - orphaned provably-stale packed-refs.lock is cleared and the clone syncs
ok - a live packed-refs.lock is never removed and the sync fails loudly
ok - a live process holding the clone worktree dir blocks lock removal (clone-dir liveness)
ok - a transient packed-refs.lock that self-clears is retried without a force-remove
ok - a non-packed-refs.lock fetch failure keeps today's behavior (no retry)
ok - a non-repo directory under projects/ never fast-forwards the enclosing repo
ok - the single-project form also refuses a directory that is not its own clone root
Evidence: Teardown cleanup transcript

Source: Teardown cleanup transcript

Teardown scenario demonstrates ordinary landed ship-task cleanup removes the exact branch tip from both origin and the no-mistakes bare remote.

FM_TEST_BEGIN 2026-08-25T04:57:03Z tests/fm-teardown.test.sh family=pr-forge expected_gate_skip=none
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 1s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ok - teardown prompts tasks-axi backlog refresh when compatible
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 1s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression), and its task branch is dropped inline
ok - no-mistakes teardown preserves a pushed-but-unmerged task branch
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
ok - ordinary teardown removes the exact landed task tip from origin and no-mistakes
ok - forced teardown discards the unpushed worktree and its task branch
ok - reported scout teardown drops its disposable task branch
ok - teardown completes when an exact busy-state sidecar is already absent
ok - herdr teardown removes pane-owned escalation dedupe state

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • 🚨 bin/fm-branch-cleanup.sh:65 - Captain, the required "lock acquired across the complete check/detach/delete sequence" is not met for remote-only sweep candidates: this path checks worktree state, fetches the tip, and establishes GitHub/content landedness before entering fm_branch_delete_remote_proven_tip's lock. Move this whole candidate proof and delete into one locked callback so the final deletion is authorized by an in-lock proof.

🔧 Fix: Moved remote candidate proof inside branch lock
1 error still open:

  • 🚨 bin/fm-branch-merge-lib.sh:231 - The required “lock acquired across the complete check/detach/delete sequence” is still violated for the remote-delete callers. fm_branch_delete_remote_proven_tip acquires the lock only after its caller has established landedness: fm-branch-cleanup.sh proves merged/gone/content at lines 95–115, and fm-fleet-sync.sh proves [gone] at line 232. While waiting for the lock, a fetch can rewind the default ref or restore the upstream tracking ref without changing the candidate tip; this helper then checks only tip/worktree state and deletes the local and remote refs although the original proof is no longer true. Add shared locked merged/gone/landed proof-and-delete operations in this library and route those callers through them, so the proof is re-established under the same lock as deletion.

🔧 Fix: Lock remote cleanup proofs with deletion
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-branch-cleanup.test.sh (first four end-to-end scenarios completed before the harness’s fixed 30-second command window)
  • isolated execution of test_pr_merge_delete_flag_drives_real_origin_deletion from tests/fm-branch-cleanup.test.sh
  • bin/fm-test-run.sh tests/fm-fleet-sync.test.sh (relevant cleanup/locking/default-on scenarios completed before the harness window)
  • isolated execution of test_teardown_prunes_landed_task_from_both_remotes from tests/fm-teardown.test.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

trevorallred and others added 15 commits August 21, 2026 17:54
fm-teardown.sh already dropped a task's own fm/<task-id> branch inline once
its work was confirmed landed, but a local-only-mode (or origin-less) project
had no backstop for a branch that survived past that point - fm-fleet-sync.sh's
existing periodic prune only recognized a squash-merged PR's now-gone remote
branch, and skipped the whole remote-backed sync (including that prune) for
exactly the projects with no remote to notice "gone" tracking on. That gap
matched a concrete trigger: five rapid local-only ship tasks against a
local-only project each left their fm/<task-id> branch behind even though
bin/fm-merge-local.sh had already fast-forward-merged every one of them, and
firstmate cleaned them up by hand with `git branch -d`.

Add bin/fm-branch-merge-lib.sh, the one owner of "is this branch provably safe
to delete": not checked out in any worktree, and either an ancestor of the
merged-into ref (a clean fast-forward or non-squash merge - git's own safe
`branch -d` can verify this itself) or its upstream tracking reads "[gone]"
(a squash-merged PR's remote branch was deleted). Never force-deletes past
that proof.

fm-fleet-sync.sh gains prune_merged_fm_branches, a git-only sweep of a
project's own fm/* branches using that shared proof, run unconditionally
before any mode/remote gate - so it also covers local-only and no-origin
projects, which the existing remote-backed prune_gone_branches never reaches.
fm-teardown.sh's own inline branch-drop is refactored (no behavior change)
into one local helper instead of two duplicated copies.

Regression coverage: fm-fleet-sync.test.sh gains cases proving the sweep
prunes a genuinely fast-forward-merged fm/* branch (including with no origin
remote at all), leaves an unmerged/diverged branch and one still checked out
in an active worktree untouched, and never targets the project's own default
branch. fm-teardown.test.sh's existing local-only-merged and
no-pr-recorded/externally-merged-PR cases now also assert the task branch is
actually dropped, closing the concrete trigger and confirming today's
externally-merged-PR reconciliation already covers that case end to end.
The prior CI fix round gated prune_merged_fm_branches behind a new
FM_FLEET_PRUNE_MERGED opt-in (default off), responding to an automated
review concern that a destructive fleet-sync mutation should not default
on. That concern does not hold here: fm-fleet-sync.sh is AGENTS.md's own
named exception to "never write to a project" (fleet sync, secondmate
sync, and a few other guarded paths are explicitly carved out), and
prune_gone_branches in this exact file already deletes local branches
from a project clone by default, gated only by the pre-existing
FM_FLEET_PRUNE variable. prune_merged_fm_branches extends that same,
already-authorized mechanism to close a coverage gap (local-only and
no-origin projects), not new destructive authority - so it belongs under
the same default-on gate, matching the concrete trigger this whole change
exists to fix (a task branch left behind with nothing to notice or clean
it up automatically).

Reverts the gate to FM_FLEET_PRUNE (default on, matching
prune_gone_branches), removes every FM_FLEET_PRUNE_MERGED reference from
comments and docs, and drops the now-inapplicable
test_merged_task_branch_requires_explicit_prune_authority test along with
its run_sync_with_merged_prune helper, restoring the other prune-positive
tests to plain run_sync. The atomic branch-d-from-a-detached-worktree
delete mechanism, the expected-tip and worktree-race regression tests, and
the [gone]-without-merge test from the intervening CI fix rounds are kept
unchanged - only the gating variable and the prose/tests describing it
move back to default-on.
…missing-adapter-origin

fix(bin): refuse teardown before cleanup when Herdr prerequisites are missing
@greptile-apps

greptile-apps Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Confidence Score: 4/5

The PR is not yet safe to merge because routine fleet synchronization still performs destructive local branch cleanup without explicit captain authorization.

The default fleet-sync path invokes gone-upstream pruning whenever FM_FLEET_PRUNE is unset and ultimately runs git branch -D; this leaves the previously reported explicit-authority conflict outstanding.

Files Needing Attention: bin/fm-fleet-sync.sh, bin/fm-branch-merge-lib.sh

Reviews (9): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread bin/fm-fleet-sync.sh Outdated
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread bin/fm-teardown.sh
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: scheduled 7:10pm PT 8/24 pass (FM-FMOSS-CRON). First look on current main 038d0f7ec6ba7238a151722931434dcf06ff37c4 (#2942). VISION.md read in full. Thank you @trevorallred.

class=default-behavior. Inspected THIS DIFF, not the #2981 hold writeup. Related #2981 was mixed (adapter guard + default-on prune_merged_fm_branches). This follow-up does not include the adapter guard. What it does:

  • bin/fm-fleet-sync.sh prune_gone_branches: FM_FLEET_PRUNE default flipped 0 / opt-in ([ "${FM_FLEET_PRUNE:-0}" = "1" ]). Tests pin default fleet sync does not delete. That part is no longer the fix(bin): guard missing backend adapters and prune merged task branches #2981 default-on sweep.
  • bin/fm-pr-merge.sh: GitHub merges always send --delete-branch unless the caller already passed it. Default-on destructive remote-ref delete at merge time.
  • bin/fm-teardown.sh teardown_drop_task_branch: ordinary (non-force, non-scout) teardown now deletes the exact landed local tip and matching origin / no-mistakes remote refs. Force/scout retain unproven refs (Greptile's later P1 is addressed in the DIFF). Remote prune on ordinary teardown is still default-on destructive.
  • bin/fm-branch-cleanup.sh: on-demand command; opt-in.
  • docs/architecture.md still claims a default-on merged-fm/* backstop before local-only/no-origin gates. Inspected sync_project at THIS HEAD: that sweep is not there; prune_gone_branches runs after those skips and only when opted in. Docs overclaim.

On-demand cleanup + opt-in fleet prune would be opt-in. Default-on --delete-branch and default-on remote teardown prune make the PR default-behavior. Never auto.

VISION.md (inspected bin/fm-branch-cleanup.sh, bin/fm-branch-merge-lib.sh proofs/-d/exact-tip lease, bin/fm-fleet-sync.sh prune_gone_branches, bin/fm-pr-merge.sh --delete-branch, bin/fm-teardown.sh teardown_drop_task_branch, tests fm-branch-cleanup.test.sh / fm-fleet-sync.test.sh / fm-teardown.test.sh):

  • One captain, one interface: does not align as auto. Silent remote-ref deletion is machinery, not an outcome the captain asked for this invocation.
  • Authority is explicit: does not align. Destructive/irreversible ref deletion ships as the default merge and ordinary-teardown path. FM_FLEET_PRUNE=1 is the one honest opt-in; --delete-branch and teardown remote prune are not.
  • Scripts own the mechanics: aligns. Ancestor / gone-upstream / GitHub-aware landedness proofs, worktree guards, expected-old-value remote lease. Uncertain candidates left untouched.
  • A restart is a non-event: mixed. Proofs try not to discard unlanded work; deleting the only recovery ref for a squash-landed tip is still irreversible.
  • Delegation with a spine: does not align as auto. "Unlanded work is never torn down" is a finding, not an obstacle; landed-ref policy still needs an explicit captain word (Track safe pruning of landed pooled task branches #2994 tracks that separately).
  • The fleet outlives any vendor: aligns (GitHub --delete-branch is forge-specific but GitLab path is unchanged).
  • Scope: aligns as command-layer git hygiene; still a product call.

Overlap / holds: bin/fm-teardown.sh also moves in standing hold #2804 (pool isolation, fm-spawn/fm-teardown) and herdr pair #2637 / #2692. Instruction is do not land teardown/spawn overlap with those holds. Related unstamped issue #2994 (safe pruning of landed pooled task branches) is the policy this would pre-empt. Not a lock PR. Not spawn-freshen code (fm-spawn.sh untouched) but teardown file overlap is enough.

This HEAD: 14556acbddd9e06fc04afbdbd053cd5ad14f7951. MERGEABLE / UNSTABLE, ahead 16 / behind 0.
Attestation no-mistakes-pipeline-attestation:v1 head_sha b6adfaa14b67d91bd4d4b1271d05c7bc1722eead ≠ THIS HEAD. Author/CI blocker; not escalated.
CI: Greptile SUCCESS only. action_required CI 32799747663 / Require no-mistakes 32799747881. Not first-time (trevorallred #2768 had full Behavior/Lint SUCCESS); workflows not approved this pass.

Security: git-ref deletion with fail-safe proofs; no secrets. Land-eligible: NO. Captain-flag NOW: no (NM mismatch is an author blocker; default-behavior is a captain-decision only when otherwise ready). waiting-on-author for HEAD-matching attestation. Did not squash.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: first look on current main 038d0f7ec6ba (#2942). Vehicle for #2994 (stamped existing-pr). Never messaged the captain.

class=default-behavior (mixed). Fleet-sync prune is now opt-in (FM_FLEET_PRUNE defaults 0; #2981 had default-on). teardown_drop_task_branch still deletes a proven-landed task branch on ordinary teardown with no captain flag — that is default-on destructive cleanup. Mixed is never auto. #2997 swallow (|| true on delete) is still present on this branch.

VISION.md (inspected bin/fm-branch-cleanup.sh, bin/fm-fleet-sync.sh prune, bin/fm-teardown.sh teardown_drop_task_branch, tests):

This HEAD: 14556acbddd9e06fc04afbdbd053cd5ad14f7951. MERGEABLE / UNSTABLE, ahead 16 / behind 0.
Attestation b6adfaa14b67d91bd4d4b1271d05c7bc1722eead ≠ THIS HEAD.
CI action_required: CI 32799747663, Require no-mistakes 32799747881. Not first-time (trevorallred has #2981/#2768/#2723); workflows not approved. Greptile is not a merge gate.

Security: none (fail-safe git-ref delete; force/scout retain). Overlap / holds: bin/fm-teardown.sh also in open #2804, #2637, #2692. Docs overlap docs/architecture.md / docs/configuration.md. Not a lock PR.

Land-eligible rec: NO. Captain-flag NOW: no (NM mismatch + teardown hold overlap; do not escalate the product call while those stand).

This is a captain-decision on default-on landed teardown drop, and waiting-on-author for a HEAD-matching attestation. Not a merge I will recommend.

@trevorallred trevorallred changed the title feat(bin): automate landed task branch cleanup feat(bin): automate safe task branch cleanup Aug 25, 2026
Comment thread bin/fm-fleet-sync.sh Outdated
Comment thread bin/fm-branch-cleanup.sh
@trevorallred
trevorallred force-pushed the fm/firstmate-branch-cleanup-automation branch from 132f21a to 9c9ac9f Compare August 25, 2026 05:49
@trevorallred

Copy link
Copy Markdown
Author

Closing because this PR was opened against the public upstream template by mistake. The branch has been rebased onto trevorallred/firstmate's current main and will be submitted to that repository instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants