How Custody Recovery Works for Terminal Runs with Unpublished Pipeline Commits in no-mistakes
When a no-mistakes run terminates without publishing its pipeline commits, the branch enters a custody state that blocks new work until you run axi sync --recover, which safely reconciles the preserved pipeline head with your local work‑tree using a strict safety matrix.
The no-mistakes system protects your branch state by placing it in custody whenever a run ends in a terminal state—completed, failed, or cancelled—while pipeline commits remain unpublished. This custody mechanism stores the pipeline head in a local gate (a bare repository under NM_HOME) and prevents further work until custody is formally returned through a controlled recovery process.
Understanding the Custody State
Custody acts as a safety lock. When a terminal run leaves commits unpublished, the branch cannot simply continue; the preserved pipeline head must be reconciled with your local work‑tree. The recovery flow is implemented in internal/branchsync/sync.go and triggered via the axi sync --recover command or the TUI “recover custody” action.
The system evaluates the relationship between your current work‑tree and the preserved pipeline head P before allowing any state changes.
The Recovery Decision Matrix
The recovery algorithm follows a strict decision matrix that guarantees data safety. The default path fast‑forwards when safe, while the --keep-local flag preserves your current position at the cost of leaving the pipeline commits dangling.
| Work‑tree relation to preserved head P | Default (--keep-local off) |
--keep-local enabled |
|---|---|---|
| equal | Anchor locally, return custody | Anchor locally, return custody |
| ahead | Anchor locally, return custody | Anchor locally, return custody |
| behind (clean) | Fast‑forward to P, return custody | Return custody at current head |
| behind (dirty) | Fail – requires clean work‑tree | Return custody at current head |
| diverged | Fail – manual reconciliation required | Return custody at current head |
| gate missing | Fail – cannot verify commits | Fail |
Step‑by‑Step Recovery Implementation
The recovery logic spans lines 404–450 of internal/branchsync/sync.go and operates through four distinct phases.
1. State Inspection
The inspect() function (lines 333–368) gathers local Git state, the active run record, and determines whether the run is pipeline_owned (indicating unpublished pipeline commits). This function establishes the baseline for all subsequent decisions.
2. Detecting Recoverable Runs
classifyPipelineOwned() (lines 724–790) identifies terminal runs eligible for custody recovery. When detected, it sets NextAction.Code = "recover_custody", signaling that the branch is locked and requires explicit user intervention.
3. Executing Recovery
The Recover() function (lines 462–526) implements the decision matrix:
- Equal or Ahead: When P is already reachable,
anchorReachablePreserved()anchors the commits locally, followed byfinishRecover()(lines 666–681). - Behind (clean):
recoverFastForward()(lines 670–700) advances the branch to P before returning custody. - Keep‑Local Mode:
recoverKeepLocal()(lines 688–730) performs an atomic compare‑and‑swap usinggit update-ref … <old> <new>to move the gate branch to your current local head without touching the work‑tree.
4. Finalizing Custody Return
finishRecover() (lines 714–727) persists a custody_returned_at timestamp via DB.SetRunCustodyReturned and transitions the state to StateCustodyReturned. The CLI surfaces this through guidance strings defined in internal/cli/axi_guidance.go (lines 24–31) and the skill body in internal/skill/skill.go (lines 225–292).
Safety Guarantees and Atomic Operations
The custody recovery system enforces several critical safety constraints:
- Terminal‑only recovery: Active runs block recovery with code
blocked_recover_run_active. - Anchoring: Preserved commits are anchored locally before any gate modification, preventing loss even if the gate is rewritten.
- Clean work‑tree requirement: Fast‑forward operations require a clean work‑tree to prevent overwriting uncommitted changes.
- Atomic updates: Gate updates use Git’s atomic compare‑and‑swap to eliminate race conditions during concurrent pushes.
Practical Recovery Commands
Detect custody state and return custody using the no-mistakes CLI:
# Check if branch is in custody
no-mistakes axi status
# Output shows `code: recover_custody` with next‑action command
# Standard recovery: fast-forward to preserved head
no-mistakes axi sync --recover
# Branch fast-forwards to pipeline head P
# Run stamped with custody_returned_at
# Start fresh work:
no-mistakes axi run --intent "implement feature X"
# Keep current local head (bypass fast-forward)
no-mistakes axi sync --recover --keep-local
# Gate branch moves atomically to local head
# Preserved commits remain reachable via hidden refs
# Later use `no-mistakes rerun` to validate preserved head
Summary
- Custody locks a branch when a terminal run leaves pipeline commits unpublished, storing the head in a local gate under
NM_HOME. - Recovery follows a strict matrix in
internal/branchsync/sync.gothat evaluates work‑tree cleanliness and divergence. - The
--keep-localflag bypasses fast‑forwarding, preserving dirty work‑trees through atomic gate updates. - All recovery paths conclude with
finishRecover()persisting acustody_returned_attimestamp and unlocking the branch. - Safety mechanisms include atomic
git update-refoperations, mandatory anchoring, and clean work‑tree verification.
Frequently Asked Questions
What triggers a custody state in no-mistakes?
A custody state triggers when a run reaches a terminal status—completed, failed, or cancelled—without publishing its pipeline commits to the remote. The system stores the pipeline head in a local gate and marks the run as pipeline_owned, blocking new runs until custody is returned via axi sync --recover.
How does the --keep-local flag change recovery behavior?
Without --keep-local, the default path fast‑forwards clean work‑trees to the preserved pipeline head P, failing if the tree is dirty or diverged. With --keep-local, the gate branch moves atomically to your current local head regardless of divergence or dirtiness, leaving the work‑tree untouched but potentially leaving P reachable only through hidden refs.
What happens if my work-tree is dirty during recovery?
In default mode, a dirty work‑tree blocks recovery with a failure message instructing you to commit or stash changes first. With --keep-local, dirty work‑trees are allowed; the gate updates to your current head without modifying working directory files, though you must manually reconcile changes later.
Where is the custody recovery logic implemented?
The core algorithm resides in internal/branchsync/sync.go, specifically the Recover() function (lines 462–526) and its helpers recoverFastForward() and recoverKeepLocal(). Supporting infrastructure includes internal/db/run.go for timestamp persistence, internal/cli/axi_guidance.go for user messaging, and internal/tui/branch_sync.go for the interactive recovery dialog.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →