Guarded Local Branch Synchronization (internal/branchsync)
sync,axi sync, and the TUIuaction 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 thatpreservedContainsLocalWorkproves carries every local change. Passive status never fetches, and blocked states never reset, stash, merge, rebase, force, switch, delete, or update an external remote.- Give each network remote operation its own bounded child context derived from the caller:
Refreshmust not share one deadline across sequentialgit.LsRemoteandgit.FetchRemoteBranchToPrivateRefcalls, andApplyuses the same per-operation budget for its final live check. The per-operation budget isService.RemoteTimeout, sourced only from the operator's globalbranch_sync_remote_timeoutsetting (defaultconfig.DefaultBranchSyncRemoteTimeout, 60s);RepoConfigdeliberately 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. - Successful pipeline pushes persist the exact SHA, credential-free target fingerprint/ref, and generation; legacy rows remain nullable and must never infer provenance from mutable
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. - A TERMINAL run with unpublished pipeline commits (moved head) is recoverable only from verified, non-conflicting evidence: inspection and
Recovershare one eligibility model. Equal/ahead local ancestry can create the local anchor without requiring gate access, but available gate evidence must agree; importing a 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 reportblocked_pipeline_owned_recoverable+next_action recover_custodywith the exact submitted/current-head and relation facts (active runs keep the plain block). Missing, non-commit, symbolic, or conflicting evidence, and import cases that are dirty or genuinely divergent, fail closed with manual reconciliation instead.sync --recoveranchors the preserved head atrefs/no-mistakes/recover/<run>before stampingruns.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 asblocked_wrong_branch, and it classifiesuser_owned- nonext_action, non-blocking exit, never represented as recoverable custody,--recoverthere is an idempotent no-op that mutates nothing, and a freshaxi runor 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 whenpreservedContainsLocalWorkproves containment. That proof is an executablemerge-treethree-way merge whose result must equal the preserved head's tree, anchored on the merge-base - neverruns.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 atrefs/no-mistakes/recover-local/<run>, then moves the branch with Git operations that fail closed on their own rather than after an observation - an atomicupdate-refCAS plusread-tree -m -u, never check-then-act followed byreset --hard, which destroys anything landing in the gap.recoverAdoptPreservedowns the reasoning. Terminalization pins every verified unpublished head atrefs/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; a truly missing recorded head reports a distinct manual-reconciliation action instead of advertising an impossiblerecover_custodycommand. When the operator keeps a behind or diverged local head instead of taking the preserved head,--keep-localnever 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 theRecoverdoc comment ininternal/branchsync/sync.go. - Public guidance is owned by
internal/skill/skill.goplus live AXI strings, then regenerated withmake skill. Core regressions live ininternal/branchsync(incl.recover_test.go),internal/cli/sync_test.go,internal/tui/branch_sync_test.go, and e2eTestAxiBranchSyncJourney/TestAxiCustodyRecoveryJourney/TestAxiCustodyRecoveryAfterRebaseJourney/TestAxiPrePushAbortUnmovedHeadCustodyJourney.
Post-Review Head Continuity and Push Binding
- Every step after Review in the fixed pipeline order (Test, Document, Lint, Push, PR, CI) calls
assertPipelineHeadContinuityat 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. - A successfully completed full review atomically records
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 mutableHEAD. Never infer approval fromruns.head_sha, a worktree, gate ref, or remote branch. Regressions:TestPushStep_RefusesPostReviewClobberWithoutLaterPipelineCommit,TestPushStep_BindsRemoteAndDatabaseToVerifiedCommitWhenHEADMovesDuringPush,TestExecutor_FullRereviewReplacesApprovalWithoutAuthorizingParkedRound.
Rebase Base & Force-Push Safety (data-loss prevention)
- The whole job of this tool is to not lose people's code; favor refusing the push and surfacing a finding over any clever recovery. The comments in
internal/pipeline/steps/forcepush.goown the full reasoning; the invariants are the next three bullets. - Rebase bases come from the freshly fetched authoritative remote refs, never local or stale state; and a branch built on unpushed local-default-branch commits parks with
NeedsApproval+AutoFixable=falseinstead of silently widening the PR (detectBundledLocalDefaultCommits, #283). - Every force-push routes through
resolveForcePushDecision, which re-reads the live remote head and allows the push only for a new branch, an already-equal remote, an unchangedlastSeenSHA, or remote commits already incorporated by patch-id (excluding^baseSHAhistory the run knowingly rewrites). Anything else refuses, and a failed ls-remote/fetch fails closed; never degrade to a bare--force/--force-with-leasewithout an explicit anchor. lastSeenSHAmust 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 refreshesorigin/<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.- Regressions:
TestPushStep_RefusesToClobberAdvancedUpstreamBranch(#305),TestForcePushRun_RefusesToClobberOutOfBandBranchCommit,TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits(#283),TestResolveForcePushDecision_*,TestExecutor_CIRestartRevalidatesBeforePush,TestPushStep_AllowsForcePushAfterMidRunRebaseOverPriorPushedGeneration(#837),TestPushStep_AllowsForcePushOnRerunOverPriorRunPushedGeneration(#837).