Use when changing local branch synchronization, custody recovery, post-review head binding, rebasing, or force-push safety.
48
53%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/branch-sync-and-push-safety/SKILL.mdGuarded Local Branch Synchronization (internal/branchsync)
sync, axi sync, and the TUI u action share one service whose only ordinary worktree mutation is a clean guarded move to an exact freshly verified pipeline push binding: strict fast-forward for behind branches, or an anchored reset to an equivalent diverged pipeline head when local unique work is already represented there. Under --recover, the worktree can only strict-fast-forward to the gate-preserved head, or adopt a diverged preserved head that preservedContainsLocalWork proves carries every local change. Passive status never fetches, and blocked states never reset, stash, merge, rebase, force, switch, delete, or update an external remote.Refresh must not share one deadline across sequential git.LsRemote and git.FetchRemoteBranchToPrivateRef calls, and Apply uses the same per-operation budget for its final live check. The per-operation budget is Service.RemoteTimeout, sourced only from the operator's global branch_sync_remote_timeout setting (default config.DefaultBranchSyncRemoteTimeout, 60s); RepoConfig deliberately has no matching field. Recover's local-gate fetch is outside this network deadline contract. Regressions: TestRefreshSlowSuccessfulLsRemoteDoesNotStealFetchBudget, TestRefreshSlowButSuccessfulLsRemoteAloneExceedsItsOwnBudgetReportsOffline, TestRefreshRaisedRemoteTimeoutAcceptsTheSameLegitimateSlowLsRemote, TestRefreshParentCancellationStopsFetchAfterLsRemoteSucceeds, TestServiceRemoteTimeoutDefaultsToConfigDefault, TestLoadGlobal_InvalidBranchSyncRemoteTimeout, TestLoadRepo_BranchSyncRemoteTimeoutIsNotARepoSetting.head_sha. Structured PR lifecycle retires merged/closed branches. The service rechecks the invoking worktree, target, live remote equality, ancestry or equivalent-divergence proof, generation, and all mutable assumptions immediately before apply.Recover share one eligibility model. Equal/ahead local ancestry can create the local anchor without requiring gate access, but available gate evidence must agree; importing a locally missing preserved head requires exact or safely anchorable gate evidence, a clean worktree, and either ancestry or the content-preservation proof below. Only then does inspection report blocked_pipeline_owned_recoverable + next_action recover_custody with the exact submitted/current-head and relation facts (active runs keep the plain block). Non-commit, symbolic, or conflicting evidence, and import cases that are dirty or genuinely divergent, fail closed with manual reconciliation. A verified head absent from both the worktree and an accessible gate instead offers the explicit --recover --keep-local discard path described below. Plain sync --recover anchors the preserved head at refs/no-mistakes/recover/<run> before stamping runs.custody_returned_at.
Cancellation RELEASES a terminal run that never changed the submitted head (head_sha == submitted_head_sha, no push, no custody stamp): selection keeps it visible so it never misreports as blocked_wrong_branch, and it classifies user_owned - no next_action, non-blocking exit, never represented as recoverable custody, --recover there is an idempotent no-op that mutates nothing, and a fresh axi run or separately authorized direct push is never blocked.
Equal/ahead worktrees anchor locally without requiring gate access, but an available gate's existing recovery ref must agree with the recorded head; behind/diverged worktrees verify and fetch the preserved head from the run-specific recovery ref, fast-forwarding only a clean behind worktree.
A cancelled validation routinely leaves a preserved head that is a REBASE of the local branch, which equality and ancestry read as plain divergence, so a clean diverged worktree is adopted when preservedContainsLocalWork proves containment. That proof is an executable merge-tree three-way merge whose result must equal the preserved head's tree, anchored on the merge-base - never runs.base_sha, the previous gate head. It deliberately does NOT use patch identity: patch IDs discard hunk locations and whitespace, so they cannot tell a genuine replay from a same-shaped edit to another identical block, and a containment claim built on them is not a proof. Everything undecidable escalates, including a rebase whose fix rounds also rewrote operator lines, where nothing separates a deliberate fix from a dropped change.
Adoption anchors the pre-recovery local head at refs/no-mistakes/recover-local/<run>, then moves the branch with Git operations that fail closed on their own rather than after an observation - an atomic update-ref CAS plus read-tree -m -u, never check-then-act followed by reset --hard, which destroys anything landing in the gap. recoverAdoptPreserved owns the reasoning.
Terminalization pins every verified unpublished head at refs/no-mistakes/recover/<run> before the managed worktree can be removed. Recovery reads that run-specific ref rather than requiring the gate branch to match, so aborts, rebases, and pre-push failures remain recoverable while an independently moved gate branch is preserved. Legacy recorded heads that still exist as dangling gate objects are anchored on recovery. When an accessible gate confirms that a verified recorded head is truly missing and both recovery refs are compatible, status offers recover_custody with no-mistakes axi sync --recover --keep-local: that flag is the operator's explicit discard of unpublished pipeline commits. Unverified heads, inaccessible gates, and conflicting refs retain manual reconciliation, and plain --recover still refuses.
A divergent later head can also prove preserved through exactly one append-only recovery_archives binding, but only while its repository, run, branch, required/preserved heads, raw archive ref, and gate recovery ref all revalidate. recoverySourceAvailable remains the one discovery/classification owner and offers only recover_custody with --recover --keep-local; the archived head is never selected. Similar unbound refs and every stale, moved, symbolic, malformed, cross-bound, or ambiguous record fail closed.
When the operator keeps a behind or diverged local head instead of taking the preserved head, --keep-local never touches the worktree and CAS-moves the gate branch to the kept head, staging objects via gate-side fetch - never a push, which would fire the receive hook and start a run.
The full relation matrix and fail-safe rules live in the Recover doc comment in internal/branchsync/sync.go.internal/skill/skill.go plus live AXI strings, then regenerated with make skill. Core regressions live in internal/branchsync (incl. recover_test.go), internal/cli/sync_test.go, internal/tui/branch_sync_test.go, and e2e TestAxiBranchSyncJourney / TestAxiCustodyRecoveryJourney / TestAxiCustodyRecoveryAfterRebaseJourney / TestAxiPrePushAbortUnmovedHeadCustodyJourney.Post-Review Head Continuity and Push Binding
assertPipelineHeadContinuity at entry. The helper is the single semantic owner: equal or descendant live heads continue; backward, sibling, and unverifiable heads fail before the step performs work. Regression: TestPostReviewStepsRefuseHeadClobberAtEntry.runs.review_approved_head_sha; parked, failed, skipped, and legacy reviews carry no inferred authority. Push reads that durable binding, permits only the exact commit or a descendant, and pushes the verified immutable SHA rather than mutable HEAD. Never infer approval from runs.head_sha, a worktree, gate ref, or remote branch. Regressions: TestPushStep_RefusesPostReviewClobberWithoutLaterPipelineCommit, TestPushStep_BindsRemoteAndDatabaseToVerifiedCommitWhenHEADMovesDuringPush, TestExecutor_FullRereviewReplacesApprovalWithoutAuthorizingParkedRound.Rebase Base & Force-Push Safety (data-loss prevention)
internal/pipeline/steps/forcepush.go own the full reasoning; the invariants are the next three bullets.NeedsApproval + AutoFixable=false instead of silently widening the PR (detectBundledLocalDefaultCommits, #283).resolveForcePushDecision, which re-reads the live remote head and allows the push only for a new branch, an already-equal remote, an unchanged lastSeenSHA, or remote commits already incorporated by patch-id (excluding ^baseSHA history the run knowingly rewrites). Anything else refuses, and a failed ls-remote/fetch fails closed; never degrade to a bare --force/--force-with-lease without an explicit anchor.lastSeenSHA must stay the head the run last observed (from run/prior-run push provenance or the remote-tracking ref), never the live remote tip: the rebase step refreshes origin/<branch> only on a normal push, NOT on a force push. CI repairs commit locally and restart validation at Review; the later Push step owns their remote update and force-push safety. Anchoring a lease to a SHA read immediately before pushing is the original #281 bug (it always passes and protects nothing); always-fetching the branch on force push recreates it. Never reintroduce either.TestPushStep_RefusesToClobberAdvancedUpstreamBranch (#305), TestForcePushRun_RefusesToClobberOutOfBandBranchCommit, TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits (#283), TestResolveForcePushDecision_*, TestExecutor_CIRestartRevalidatesBeforePush, TestPushStep_AllowsForcePushAfterMidRunRebaseOverPriorPushedGeneration (#837), TestPushStep_AllowsForcePushOnRerunOverPriorRunPushedGeneration (#837).9a464c2
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.