The Real Disease Behind git pull

Most developers have experienced the frustration: you try to push, git rejects you because the remote moved, so you git pull and get an ugly merge commit that contains zero intentional work. It's reconciliation noise — a coordination failure baked into the data model.

The standard advice is git pull --rebase, which hides the noise by replaying your commits on top of the remote. But this rewrites your commit hashes. In content-addressed terms: it destroys CID stability. Your history is no longer reproducible from its original identifiers. That's not a solution — it's a different flavour of damage.

The deeper issue: git allows multiple people to write to the same ref simultaneously. When two developers push to main, git treats it as a race condition resolved by merge. Every tool that wraps git pull is patching a symptom of this design.

In graf, this problem doesn't exist. Not "we solved it better" — the architecture makes it structurally impossible.


How Graf Eliminates the Problem

Per-Agent Ref Namespaces

Every agent — human or AI — operates in their own ref namespace:

refs/agents/grf:a7fec791/heads/main      ← your main
refs/agents/grf:5d0ad824/heads/main      ← your colleague's main
refs/heads/main                           ← canonical main

When you graf push, you push your agent ref. Nobody else writes to it. The "rejected because remote is ahead" scenario is structurally impossible — you are the only writer to your namespace.

Merge Is Always Intentional

When you want your work on canonical main, you don't "pull and pray." You create a merge request — an mr object (ObjectKind 0x06). The merge is a deliberate architectural act, not an accidental side effect of fetching.

CIDs Never Change

There is no rebase. There is no hash rewriting. Your checkpoint graf1abc123 stays graf1abc123 forever. The DAG is immutable. pull --rebase doesn't exist because the problem it solves doesn't exist.


What graf pull Actually Does

graf pull is deliberately simple. Three steps, in order:

Step 1 — Fetch. Download new CAS objects and ref updates from the remote. Pure data transfer. Your working tree doesn't change. Your local CAS grows; no refs move.

Step 2 — Fast-forward your agent ref. If the remote has a newer version of your own agent ref (because you pushed from another machine), fast-forward to it. This is safe: it's your own history, extended linearly. If it can't fast-forward — you have local checkpoints the remote doesn't — stop and tell you:

Your local agent ref has diverged from remote.
Local:   graf1abc → graf1def → graf1ghi  (2 local checkpoints)
Remote:  graf1abc → graf1jkl              (1 remote checkpoint)

Options:
  graf merge remote/main    Merge remote changes into your branch
  graf explore "reconcile"  Explore a resolution path

No silent merge. No automatic rebase. No hash rewriting. You decide.

Step 3 — Update tracking refs. Update refs/remotes/origin/* to reflect the remote state. Informational only — it tells you what the remote looks like without touching your working tree.

That's it. graf pull is fetch + fast-forward + inform. It never creates a merge commit automatically. It never rewrites hashes.


The Smart Verb: graf sync

After graf pull, you know the remote state. But the reconciliation workflow — "I have local changes, the remote has changes, how do I combine them?" — deserves its own verb.

graf sync

graf sync does everything graf pull does, plus intelligent reconciliation:

If your branch is behind remote: fast-forward. Trivial, silent.

If remote is behind your branch: nothing to do locally. graf push when ready.

If diverged: graf analyses the divergence and presents options:

graf sync

Divergence detected on main:
  Your checkpoints (2):
    graf1def  "refactor KDF pipeline"
    graf1ghi  "add SLIP-0010 path validation"
  Remote checkpoints (1):
    graf1jkl  "fix nullifier scope prefix"  (by grf:5d0ad824)

No conflicting files detected.

Options:
  [1] Merge: create merge checkpoint combining both histories
  [2] Restack: move your 2 checkpoints after remote's checkpoint
  [3] Explore: open an exploration to resolve manually

Recommendation: [2] Restack (no file conflicts, clean separation)
→ 

Restack, Not Rebase

The terminology matters. Git's "rebase" implies rewriting history — changing parent pointers and therefore hashes. Graf's restack is fundamentally different.

Before restack:
  shared:  graf1abc
  yours:   graf1abc ← graf1def ← graf1ghi
  remote:  graf1abc ← graf1jkl

After restack:
  graf1abc ← graf1jkl ← graf1def' ← graf1ghi'

Yes, graf1def' and graf1ghi' have new CIDs — their parent changed. But here's the critical difference from git: graf creates a RestackRecord:

ObjectKind.restack_record = 0x0C

struct RestackRecord {
    original_cids:  []CID,        // [graf1def, graf1ghi]
    restacked_cids: []CID,        // [graf1def', graf1ghi']
    base_cid:       CID,          // graf1jkl (new base)
    reason:         []const u8,   // "sync restack after pull"
    author:         SoulKeyID,
    timestamp:      ThreeClock,
    signature:      Ed25519Sig,
}

The restack is auditable. You can always trace graf1def' back to graf1def. The CIDs changed, but the provenance chain is preserved. Git rebase destroys this information; graf restack preserves it.

refs/restacks/{slug}  →  CID of RestackRecord

Same pattern as deadends and prunes — a discoverable ref that any agent can query: "Was this checkpoint restacked? What was its original CID? Why?"


The Complete Sync Workflow

# Simple case: you're behind, fast-forward
graf sync
→ Fast-forwarded main: graf1abc → graf1jkl (1 new checkpoint)

# Diverged, no conflicts: restack recommended
graf sync
→ Restacked 2 checkpoints after grf:5d0ad824's work
  graf1def → graf1def' (parent changed)
  graf1ghi → graf1ghi' (parent changed)
  RestackRecord: refs/restacks/sync-20260326

# Diverged, conflicts: merge or explore
graf sync
→ Conflict in src/crypto/kdf.zig
  Cannot auto-restack. Options:
  [1] Merge: create merge checkpoint (conflict markers in file)
  [2] Explore: open exploration to resolve manually
→ 1
  Merge checkpoint created: graf1mno
  Resolve conflicts, then: graf checkpoint "resolve merge"

# Force a merge instead of restack
graf sync --merge
→ Merge checkpoint graf1mno created
  Combines your 2 checkpoints + remote's 1 checkpoint

Graf vs Git: Side by Side

Scenario Git Graf
Pull, no divergence git pull (fine) graf sync fast-forward (identical)
Pull, diverged, no conflicts Noise merge commit Restack with provenance record
Pull, diverged, conflicts Merge + conflict markers; or rebase rewrites hashes Presents options: restack, merge, or explore
History after resolution Noise merges or rewritten hashes — pick your poison Clean linear history with auditable RestackRecord
Discovering what happened git reflog (local only, expires) refs/restacks/* (distributed, permanent, signed)

The key insight: git forces a choice between clean history and honest history. Rebase gives you clean history but destroys CIDs. Merge gives you honest history but adds noise. Graf gives you both — clean history with an auditable trail of how it became clean.


The Ergonomics

graf sync              # The smart default. Right thing 90% of the time.
graf sync --merge      # Force merge when restack isn't what you want.
graf sync --dry-run    # Show what would happen without doing it.
graf pull              # Just fetch + fast-forward. No reconciliation.
graf push              # Just push. No fetch, no reconciliation.

Two verbs. pull is fetch-only. sync is fetch + reconcile. Git conflates these into one overloaded command — git pull does fetch + merge or rebase depending on config flags, CLI args, and the phase of the moon. Graf separates the concerns.

git pull is a Swiss Army knife where every blade is slightly rusty. graf pull fetches. graf sync reconciles. Two sharp tools instead of one dull one.


ObjectKind Registry (Updated)

blob              = 0x00
tree              = 0x01
change            = 0x02
checkpoint        = 0x03
release           = 0x04
(reserved)        = 0x05
mr                = 0x06
vouch             = 0x07
governance_action = 0x08
deadend           = 0x09      ← Sackgasse
revive            = 0x0A      ← reserved (future)
prune_record      = 0x0B      ← history sanitisation
restack_record    = 0x0C      ← sync provenance

Four new object types from this design: deadend, revive (reserved), prune, restack. Each encodes negative knowledge or transformation knowledge that git discards. The DAG doesn't just record what you built — it records what you tried, what you cleaned, and how you reconciled.

The Bottom Line

That's not version control. That's institutional memory with cryptographic provenance.