Back to Writing

Experimenting With Version Control for AI Workflows

/

The shape of the work changed

I've been using Git for years. It works. It's the backbone of basically everything I ship.

But lately the way I work doesn't map to it as cleanly as it used to.

When AI is part of the loop, a change isn't just a diff anymore. It's a task, a few attempts, some validation, a decision about which attempt to keep, and then the final state. Git only sees that last part. Everything else disappears into my head or gets lost in PR descriptions nobody reads six months later.

I kept running into this and eventually started building something to see if the model could be better.

What Git actually captures

Git captures snapshots and diffs. You change files, commit, push. If you squint you can reconstruct what happened.

But most of the context lives outside of it. Why the change happened. What I tried before. What failed. What I validated. What I rejected and why.

That stuff ends up in commit messages if I'm being good about it. Or in PR descriptions. Or in Slack. Or nowhere.

It was fine when changes were manual and mostly linear. One person, one edit, one commit. The mapping was close enough.

With AI it starts to feel off.

What AI workflows actually look like

When I use AI to build something, the process doesn't look like a clean sequence of commits.

It looks more like: I describe a task. The model gives me a candidate. Sometimes I run it, sometimes I tweak it, sometimes I throw it away and try another direction. At some point something works well enough. I clean it up and move forward.

Git sees the cleaned-up version. That's it.

The three attempts that didn't work, the one that almost worked but had a subtle issue, the validation I ran to confirm the final version - none of that is a first-class part of the history.

# What Git sees:
commit a1b2c3d  "fix: update sync handler"

# What actually happened:
attempt 1 → model rewrote the handler, broke edge case
attempt 2 → manual fix, passed unit tests, failed integration
attempt 3 → hybrid approach, all tests pass
validation → integration suite green, manual check on staging
decision  → kept attempt 3, promoted to main

All of that middle part just vanishes.

The missing pieces

A few things stand out once you pay attention.

Candidates don't have a place to live. It's normal to try a few variations before something feels right. In Git that turns into messy branches or half-baked commits you squash later. The attempts matter though. They tell you what paths were explored. They help when something breaks later and you need to understand how you got here.

Validation is always somewhere else. You run tests. You check behavior. You look at logs. That information lives in CI pipelines, local terminals, random screenshots. There's no direct link between a change and the evidence that it worked. You just trust that it did at the time.

Promotion is implicit. There's always a moment where you decide: this is the version I keep. In Git it's just another commit or merge. There's no explicit record that this was one of several candidates and you chose it for specific reasons. It's flattened into linear history.

I'm not the only one thinking about this

A video that really stuck with me was Theo Browne's piece on why it's time to rethink Git. He laid out something I'd been feeling but hadn't fully articulated.

He talks about this spectrum of primitives in version control. On one end you have a line of code. On the other end you have the repo. In between you've got commits and pull requests. And his point is that there's all this unexplored space between those primitives. Before commits, nothing is tracked. Between commits and PRs, there's no native way to stack work, review pieces, or keep metadata about what led to a change.

That framed the problem for me in a way that clicked. Because when I look at my own AI-assisted workflow, most of the interesting stuff happens in exactly that space. The attempts, the validation, the decision about what to keep - all of it lives before the commit. Git doesn't see any of it.

He also makes the point that Git was built for Linus Torvalds' specific needs - decentralized patch submission over email for the Linux kernel. We've been hacking workflows on top of that ever since. Pull requests, stacked diffs, LFS, CI integrations. All of it bolted on because the core primitive wasn't enough.

And now with AI agents writing code alongside us, the gaps are getting wider. Theo mentions wanting to see what prompt led to a line of code being added, wanting to revert an agent's work without losing your own changes. That kind of provenance tracking doesn't exist in Git at all.

Other people are building in this space too. Zed announced Delta DB, their vision for turning the editor into a collaborative workspace with a Git alternative underneath. JJ is a version control system built on top of Git that handles conflicts differently. Graphite built stacked PRs as a workflow layer. Meta uses Mercurial with stacked diffs internally.

I think all of these efforts are pointing at the same thing: the commit is too flat a primitive for how we work now. What I wanted to explore with syft is one specific part of that - the task-to-candidate-to-promotion flow that AI-assisted work naturally produces.

So I started building syft

syft is what came out of this. It's a version control experiment for AI-heavy development. Written in Rust, local-first, still early.

The idea is straightforward. Instead of treating a commit as the unit of work, syft treats the whole change process as the unit.

A ChangeNode in syft ties together:

  • the task (what you were trying to do)
  • the base snapshot (where you started)
  • the result snapshot (where you ended up)
  • the intent (why)
  • the semantic delta (what structurally changed)
  • the validation artifacts (proof it works)
  • the promotion state (whether you decided to keep it)

Commits still matter. Diffs still matter. Git still matters underneath. But the unit you reason about is bigger and holds more of the actual story.

The workflow

Here's what using syft looks like right now:

You initialize a syft repo. Import a Git commit as your base snapshot. Create a task describing what you're doing. Do your work - with AI, without, doesn't matter. Capture the result as a new snapshot. Propose it as a change against the base. Run validation. If it's good, promote it. Optionally export it back to Git for the rest of the world.

In practice:

syft init
syft repo import-git
syft task create "refactor sync handler to support batch operations"
# ... do the work ...
syft snapshot capture
syft change propose --base <snapshot-id>
syft change validate
syft change promote

The key difference from Git is that if you tried three approaches, all three exist as change nodes against the same task. You can compare them. You can see which ones passed validation and which didn't. The one you promoted is explicitly marked as the winner.

Where it's actually better

I think syft does a few things better than plain Git for this kind of work.

Task stays attached to the change. A lot of AI work goes sideways because the actual goal gets separated from the patch. The model gives you something that looks right but drifted from what you were trying to do. Having the task as part of the change record keeps that connection visible.

Validation belongs to the candidate. In Git, CI runs sit next to a commit in some external system. In syft the validation artifacts are part of the change node itself. When you're comparing three attempts at the same task, that makes a real difference.

Promotion is explicit. You can have several candidates for one task and make one clear decision about which one moves forward. That decision is recorded. It's not just "this commit came after that commit."

Parallel exploration has a home. Branches sort of work for this in Git. But they're not really the same thing as "three candidate implementations of the same intent." Syft models that directly.

Where it's still just a bootstrap

I want to be honest about where this is.

Everything is local. There's no API layer, no remote sync, no multi-user coordination. Metadata lives in SQLite. Objects are on disk under .syft/. Validation runs locally.

The semantic analysis layer exists but it's narrow - Rust only right now. It can tell you if a change touched a public symbol or shifted a dependency edge. That's useful but it's far from full language coverage.

There's no native storage backend replacing Git yet. Syft imports from Git and exports back to Git. That was deliberate. I wanted to prove the model works in a real repo with a real CLI before building infrastructure around it.

That's fine. The first job was to get the model right. Infrastructure comes after.

Git comparison

Here's how the mental model maps:

Gitsyft
Unit of workcommitChangeNode
Contextcommit messagetask + intent
Historylinear logtask → candidates graph
Attemptssquashed awaypreserved as candidates
Validationexternal (CI)attached to change
Decision to keepimplicit (merge)explicit promotion
Semantic infononedelta analysis
Parallel triesbranches (awkward)multiple change nodes

Git is the better tool for general version control. For collaboration, hosting, review tools, developer habits - all of that runs through Git and that's not changing.

Syft is trying to be better at the specific problem of tracking AI-assisted change exploration. Tasks, candidates, validation, promotion. The stuff that happens before and around the final diff.

Design decisions

A few choices I made early that shaped how this works.

Git stays underneath. Syft imports from Git and exports back. That keeps it usable without asking anyone to abandon their existing setup. You can test it in a normal repo today.

Changes are heavier than commits on purpose. A change node carries more context because AI-assisted work needs more context. The task, the validation, the semantic delta. That extra weight is the whole point.

Local-first. Everything under .syft/. No services, no accounts, no network dependency. I can prove the model works on my own machine before adding complexity.

Branches are secondary. Internally it's more about tasks, snapshots, changes, and promotions. Branches still matter at the Git boundary. They're just not the main shape of the system.

What I learned building it

Building this in Rust was the right call for the storage and CLI parts. The type system catches a lot of the modeling mistakes early - when you're defining what a ChangeNode contains and how snapshots relate to each other, having the compiler enforce those relationships matters.

The hardest part hasn't been the code. It's been figuring out where the model should be opinionated and where it should stay flexible. How much structure is enough to be useful without being annoying. When to require validation and when to let you skip it.

I've been using it on my own repos while building it. That feedback loop is worth more than any amount of design up front. Some things I thought would be useful turned out to be noise. Some things I added as an afterthought turned out to be the parts I use the most.

Where this goes

I don't know yet if syft becomes a real tool other people use or if it stays as a personal experiment that taught me something.

What I do know is that the gap exists. Theo talked about it. Zed is building around it. JJ and Graphite are patching parts of it. The whole space is moving because the same frustration keeps showing up everywhere - Git doesn't model the way we work with AI.

The number of attempts, the validation evidence, the promotion decisions, the provenance of who or what generated a change - all of that is either lost or scattered across half a dozen systems.

Whether syft specifically is the right answer, I'm not sure. The shape of the model feels right. Task, candidates, validation, promotion. That maps to how the work actually happens.

For now I'm still building it, still using it, still figuring out which parts hold up and which don't.

The repo is at github.com/chaqchase/syft if you want to look at it or try it. It's MIT licensed. Current version is v0.2.1. Runs on macOS, Linux, and Windows.

# Install on macOS/Linux
curl -fsSL https://raw.githubusercontent.com/chaqchase/syft/main/scripts/install.sh | sh