CtrlK
BlogDocsLog inGet started
Tessl Logo

branch-sync-and-push-safety

Use when changing local branch synchronization, custody recovery, post-review head binding, rebasing or rebase.strategy, private-mirror reconciliation, or force-push safety.

SKILL.md
Quality
Evals
Security

Guarded 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.
  • Give each network remote operation its own bounded child context derived from the caller: 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.
  • 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 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.
  • Public guidance is owned by 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

  • Every step after Review in the fixed pipeline order (Test, Document, Lint, Push, PR, CI) calls 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.
  • 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 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)

  • 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.go own 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=false instead 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 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.
  • Regressions: TestPushStep_RefusesToClobberAdvancedUpstreamBranch (#305), TestForcePushRun_RefusesToClobberOutOfBandBranchCommit, TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits (#283), TestResolveForcePushDecision_*, TestExecutor_CIRestartRevalidatesBeforePush, TestPushStep_AllowsForcePushAfterMidRunRebaseOverPriorPushedGeneration (#837), TestPushStep_AllowsForcePushOnRerunOverPriorRunPushedGeneration (#837).

Rebase Step Integration Shape (rebase.strategy)

  • rebase.strategy (rebase default, or merge) selects how the rebase step integrates a moved base. Under merge each target is merged with git merge --no-ff instead of rebased onto, so the reviewed head stays on the branch as the merge commit's FIRST parent. Three things follow and are what the option exists for: ciRepairContinuityGap's ancestry test then passes natively (no patch-id or content guess), publication is a fast-forward, and the merge commit's two parents keep a conflict resolution auditable from outside the pipeline afterwards. To match, mergeWithAgent's resolver prompt requires an ADDITIVE resolution, which rebaseWithAgent's "minimal necessary changes" prompt deliberately does not; do not unify the two prompts.
  • mergeWithAgent PROVES the merge instead of trusting the agent's conclusion, and target ancestry alone is not that proof (a rebase satisfies it too). It snapshots the pre-merge head and resolves the target to a SHA BEFORE git merge runs - resolving by refname afterwards lets an agent that fetches mid-resolution fail a merge that actually landed - and afterwards requires HEAD to have moved off the pre-merge head with BOTH snapshots ancestors of it. shouldSkipRebase has already consumed every non-divergent case, so two divergent commits can only both be ancestors of HEAD through a merge commit; that admits a follow-up commit on top of the merge while still rejecting merge --abort, a rebase, and reset --hard onto the target, and an unrelated commit made after abandoning it. A conflicted rebase is the other mid-operation state the worktree can be left in and git sets no MERGE_HEAD for it, so rebaseInProgress is checked alongside mergeInProgress and a leftover rebase is aborted and rejected too. Every rejection routes through restorePreMergeHead, which resets the worktree back to the head that merge started from before returning so a rejected attempt is never left checked out as the branch's state, and is fail-closed: a restore that does not land exactly there, or that leaves a rebase in progress underneath it, is reported inside the returned error. Regressions: TestRebaseStep_MergeStrategyUnconcludedMergeIsAbortedAndFails, TestRebaseStep_MergeStrategyConflictedRebaseLeftInProgressIsAbortedAndFails, TestRebaseStep_MergeStrategyAbortedMergeFails, TestRebaseStep_MergeStrategyRebasedInsteadOfMergedFails, TestRebaseStep_MergeStrategyUnrelatedCommitInsteadOfMergeFails, TestRebaseStep_MergeStrategyResetOntoTargetFails, TestRebaseStep_MergeStrategyFollowUpCommitAfterTheMergeIsAccepted.
  • The flag is trusted-default-branch-only regardless of allow_repo_commands, for the same reason no_ci is: a pushed branch must not opt its own integration out of the evidence shape the maintainer chose, in either direction. An unrecognized value fails the config closed rather than falling back.
  • forcePushDecision.fastForward is a separate, unconditional path: an append-only update publishes with a plain push, no --force, so the remote rather than our lease bookkeeping enforces the no-rewrite property. It applies under either strategy.
  • test/ci scope is deliberately untouched: both still run in full on the integrated head, and ci_fix.go's merge-conflict repair still rebases and therefore still always revalidates.
  • User-facing semantics are owned by docs/src/content/docs/reference/repo-config.md (rebase.strategy). Regressions: internal/pipeline/steps/rebase_merge_test.go, internal/config/rebase_strategy_test.go, TestResolveForcePushDecision_FastForwardNeedsNoForce, TestResolveForcePushDecision_RewriteIsNotAFastForward.

Private Mirror and Empty-Index Handoffs

  • internal/gate owns private-mirror reconciliation. The accepted Decision 41-A exception, containment and final-tree survival requirements, exact archive-before-delete contract, and descendant preservation are owned by docs/src/content/docs/concepts/gate-model.md (Private mirror reconciliation). Decision 41-A permits only a mirror head exactly equal to the publishing run's Run.SubmittedHeadSHA or its durable Run.LastPushedSHA (read from the database, because publication does not update the executor's in-memory run) to bypass patch proof; it is an explicit exception, not a claim that ownership proves containment. push.go must settle shared gate/worktree refs only once. Separately, and not a head exception (#1233): a private-only commit reachable from a refs/no-mistakes/recover/<run> anchor is preserved and is re-derived out of the at-risk set, so the sanctioned sync --recover -> rerun -> push loop cannot deadlock against the anchor that loop wrote. That is preservation evidence only - containment is still proven by ancestry, patch-ID or tree survival, Decision 41-A is still the only head exception, archive-before-delete is unchanged, and an anchor that is symbolic or non-commit credits nothing. A refusal names every satisfier. The credit scan's membership walk excludes the live head and every anchor, so it is bounded by the private-only range; with no anchors it credits nothing and the ordinary refusal stands, otherwise candidates are scanned in batches of maxRecoveryCandidates, never truncated or refused by size. Behavioral coverage lives in internal/gate/reconcile_test.go, internal/pipeline/steps/push_test.go, and internal/pipeline/steps/push_published_rewrite_test.go.
  • The correction and CI repair handoff contract is owned by docs/src/content/docs/reference/pipeline-steps.md; stagedChangesPresent implements the index check in internal/pipeline/steps/common_fix.go.

Custody Recovery (internal/branchsync)

  • Missing-head keep-local (#958): classifyPipelineOwned checks missingHeadKeepLocalRuns first. When a verified recorded head is absent from both the worktree and an accessible gate and recovery refs are compatible, status offers recover_custody with --recover --keep-local as the explicit discard of those unpublished commits. Recover takes that early keep-local path, then ordinary keep-local uses recoverKeepLocalAtCurrentHead / finishKeepLocalRecover so a stranded stack is preflighted, available heads are anchored, and custody stamps atomically. Unverified or conflicting evidence stays manual reconciliation; plain --recover still refuses.
  • Bound-archive keep-local (#954): after the missing-head check, recoverySourceAvailable remains the discovery/classification owner and returns a recoverySourceProof (not a bool). A divergent later head affects status only through exactly one append-only recovery_archives binding whose repository, run, branch, required head, preserved head, and raw non-symbolic refs/heads/archive/* target all revalidate, alongside the exact gate recovery ref. The only offered action is recover_custody with --recover --keep-local; it never selects the archive. recoverKeepLocalFromArchive revalidates the bound archive at the recovery boundary and rolls the gate branch back if custody stamping fails. Any missing, moved, replaced, malformed, stale, cross-bound, or ambiguous evidence fails closed without Git mutation. Regressions: TestBoundArchive*, TestBoundArchiveProofMatrixFailsClosedWithoutGitMutation, TestAxiArchiveBackedRecoveryKeepsExactRequiredHeadAndBothHistories, missing-head tests in recover_test.go / sync_test.go.
Repository
kunchenguid/no-mistakes
Last updated
First committed

Is this your skill?

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.