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.