HEDDLE

Designing fork-from-capture

The moment you reach for git stash is the moment your tooling has failed you. You hit a fork in the road in the middle of a refactor, two approaches both defensible, and the only thing your version control offers is a volatile scratchpad. You stash. You try B. You hope you'll remember to come back to A. Forty percent of the time you don't.

Add an agent to the picture and the failure mode gets worse. Now there are two of you in the working tree: the human who reached for stash, and the agent who's halfway through a capture cycle and doesn't know the stash exists. The agent's context is a function of which files it has open, which symbols it's resolved, which prompts it's seen. Stash a worktree and the agent's context falls off a cliff. You don't get back to A by popping the stash; you get back to A by re-warming an agent from scratch.

We knew this was the wound for a while before we built the primitive. Two false starts on what to build instead:

Prototype 1: scoped worktrees

The first attempt was a wrapper around git worktree add. You'd say "fork this" and Heddle would spin up a second checkout in a sibling directory, hand it to the second agent, and you'd hop between them with a shell shortcut. Each worktree had its own index, its own working tree, its own copy of the agent's working context.

Two of everything. Two open editors. Two terminals. Two .env files (or, more accurately: one shared .env and a lot of confusion). The cost of having a second exploration running wasn't twice the cost of having one. It was four or five times. Most teams stopped using the feature within a week.

The lesson was that physical isolation isn't what we want. The two paths don't need to live in separate directories; they need to live in separate threads. The working tree can stay the same; what changes is the state pointer.

Prototype 2: capture-level fork

The second attempt was the shape we shipped. You don't fork a directory; you fork a state. From any capture on any thread, heddle fork branches the state forward into a new thread. The original keeps running. The fork has its own captures, its own agent identity, its own event log starting from the fork point. Both threads share the same working tree as their starting point; the fork diverges from there.

Switching threads is cheap because there's no second checkout to materialise; Heddle hands the agent a different state to resolve from. Coming back to an abandoned fork three months later is one command: heddle goto <state>. The fork's captures are still there. The agent that took the fork is still attributed. The reason for abandoning, if one was given, is in the oplog.

Two design calls in this prototype shaped what shipped:

"Both paths persist" is not negotiable

The first prototype let you drop a fork by deleting the worktree directory. We pulled that out. heddle thread drop now marks a thread abandoned: it stops appearing in default listings, but everything in it stays. Captures stay attributed. The reason for abandoning is part of the audit trail. heddle thread show renders it exactly as it would render a live thread.

This matters because the value of an abandoned path isn't apparent at the moment you abandon it. The value shows up later, when conditions change, when a teammate asks a question you don't immediately remember the answer to, when an agent in 2027 wants to know why we decided against the JWT compat layer in 2026. Volatile abandonment kills that value. Durable abandonment keeps it.

Annotations follow the code, not the thread

The hard problem in the second prototype was annotations. Heddle has a context system, notes attached to symbols and files that re-anchor on every capture. When you fork, what happens to the contexts on the symbols you're about to fork from?

Two options. Attach to the thread. The contexts follow the working state into the fork; both threads carry the same notes. Cheap; obvious; wrong, because a context written on the parent thread shouldn't quietly migrate into the fork's voice. Attach to the symbol. The contexts stay attached to the symbol on the shared ancestor; both threads see them via the symbol, but neither thread owns them. This is what we shipped.

Practically: when a fork edits a function that has a context attached, the context stays on the original symbol. If the fork moves or splits the function, re-anchoring runs on the fork; the parent's view is unchanged. If the parent later modifies the same function differently, both threads now have divergent versions of the context's anchor, and that's fine, because each thread re-anchors against its own working state. The two views are reconciled at merge time, the same way edits to the same file are.

What we didn't build

Two paths we walked back from after thinking about them:

We didn't build cross-fork merge. If a fork pays off, you ship it; the parent thread becomes the abandoned one. We don't support cherry-picking captures from a fork back into its parent. The reason is that the fork and the parent share an ancestor on purpose: a divergence is meant to be a real divergence. If you wanted the changes from both, you weren't forking; you were branching, and Heddle has threads for that.

We didn't build automatic forks. We considered detecting "this capture looks like it's exploring two paths" and proposing a fork. Wrong direction. The decision to fork is editorial: it's the moment a human or an agent declares "this is a meaningful divergence", and automating it would make the record full of forks nobody intended.

More on the operational shape of fork: the forking-and-recovering guide. More on the primitive itself: the fork-from-capture concept page.