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 pullis a Swiss Army knife where every blade is slightly rusty.graf pullfetches.graf syncreconciles. 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.
That's not version control. That's institutional memory with cryptographic provenance.