10 minDEXgitproductarchitecture

I built a time machine, then deleted half of it

Four timeline verbs, parallel variants in git worktrees, a record mode and five branch families. Two cleanup specs removed almost all of it — including, awkwardly, the feature whose existence justified the first round of deletions.

Dex commits after every stage of an autonomous run, which means a finished run is a git history with a node per unit of work. Once you have that, an obvious product idea follows: let people navigate it. Rewind to any stage. Try that stage again. Try it five ways at once and keep the best.

I built all of that. Here is what shipped in the checkpoint release:

  • Go back — restore the tree to any past stage
  • Try again — re-run a stage as a new attempt
  • Try N ways — fan out N parallel variants of the next stage into git worktrees, compare, pick
  • Keep this — promote a stage’s output to canonical
  • Unmark kept — the inverse
  • Record mode — a REC badge in the top bar; auto-promote every completed stage for shared baselines and CI fixtures
  • Step mode — pause after each stage and prompt for a decision in a modal

Underneath, that needed five families of git refs, two extra directories of runtime state, a variant-group engine, worktree lifecycle management, compare and resume modals, and a candidate-prompt modal.

Two specs later almost none of it exists. This is the story of the deletions, including one whose justification did not survive contact with the next spec.

First, the thing that made it possible to delete

The checkpoint release wired up the data correctly and left the canvas behind. The Timeline tab rendered disconnected dots in lanes — no edges, no commit hashes, no way to tell what was on the active path. Which meant navigation had to happen somewhere else, and “somewhere else” was a right-click menu full of verbs.

So the next spec rebuilt the canvas as an actual branching DAG. One column per git ref, one node per stage commit, edges showing parent → child, colour showing what is on the active path. And crucially, a plain left-click on any node does the correct git operation for that node without asking:

You clicked What happens
A branch tip Check out that branch
A commit mid-history Fork a throwaway selected-<ts> branch from it
Anything, with a dirty tree Confirm first: Save or Discard

That is the entire navigation model. And the moment it worked, most of the verb menu became a second way to do things the canvas already did in one click.

Deletion round one: the verbs

The first cleanup spec retired Try N ways, Keep this and Unmark kept from the right-click menu, along with the step-mode candidate modal — and with them, the whole variant-groups engine: parallel worktrees, attempt branches, compare modals, resume modals.

The step-mode change is a good miniature of the whole argument. Before, pausing after a stage popped a modal asking what you wanted to do next. After, the orchestrator just pauses and the Resume button on the dashboard becomes available. The event still fires, the timeline marker still refreshes, the debug payload still carries the candidate SHA — the plumbing was fine. It was the modal that was redundant, because the dashboard already had a control for “continue” and now the user had two.

Losing Try N ways was the one I argued with myself about, because it was the most technically interesting thing in the build. Spec-only stages ran their variants in parallel worktrees, so three variants of plan cost roughly one variant’s wall-clock time rather than three. I was proud of that. It is also exactly the kind of pride that keeps a feature alive past its usefulness: the wall-clock problem was solved, and the token cost was still 3× per fan-out, on a stage whose output you then had to read three times to choose between.

There is a version of variants I would still defend — and I built it, right before deleting the feature. The variant work had an obvious flaw: three variants of the same stage ran the same model with the same system prompt and the same tools, so the only thing that varied was the model’s own randomness. Three rolls of one die. The fix was agent profiles — folders on disk under .dex/agents/<name>/ carrying a runner config plus a small Dex-side knob file, so a fan-out could be Opus / Sonnet / Haiku, or conservative / standard / innovative, as orthogonal axes.

That is the right design. Building it is also what made the cost legible enough to delete the feature, which is a lesson I did not enjoy learning: fixing a feature properly is sometimes how you find out it should not exist. A half-built feature is easy to keep, because its cost is still hypothetical.

Deletion round two: the justification

Here is the awkward bit.

The first cleanup spec justified retiring the promote verbs like this:

The verbs being retired (Keep / Unmark / Try N ways) duplicate behaviour Record Mode already handles automatically.

Perfectly reasonable. Record mode auto-promoted every completed stage, so a manual “keep this” was a worse version of a thing that could just be on.

Three days later, the second cleanup spec deleted record mode.

Not out of spite — because it turned out to be unreachable. Record mode could only be switched on with a DEX_RECORD_MODE=1 environment variable or by hand-editing a JSON state file. There was no UI for it. There had never been a UI for it. It was developer-only machinery that had acquired a badge in the top bar, a branch family (capture/*), a tag family (checkpoint/done-*), and an auto-promote code path threaded through the run loop.

So the chain reads: verbs deleted because record mode covers them → record mode deleted because nobody can turn it on. Which leaves the original capability covered by neither, and that is fine — it turned out nobody wanted a manual promote verb once clicking a node worked — but it is not what the paper trail says, and I would rather point at the gap than let two specs quietly imply a tidier story than actually happened.

The same spec also killed the last producer of the attempt-* branch family. It survived in exactly one place: when you clicked a timeline node with a dirty working tree and chose Save, your changes were committed onto a side branch called attempt-<ts>-saved. The dialog even told you so, by name.

Now Save makes a normal commit on the branch you are already on, subject dex: pre-jump autosave, and the dialog copy stops mentioning branches entirely. The user-visible change is one sentence of dialog text. The system-visible change is an entire branch family with its own lifecycle, pruning rules and edge cases — gone.

The namespace, before and after

This is the number I would put on the fridge:

Before After
Branch families dex/*, selected-*, attempt-*, capture/* dex/*, selected-*
Tag families checkpoint/*, checkpoint/done-* checkpoint/*, created out-of-band only
Runtime dirs .dex/variant-groups/, .dex/worktrees/
Modals in the run path candidate prompt, compare, resume

Four families to two. And the surviving checkpoint/* tags are no longer minted by the running app at all — a small script creates them when you want a named, shareable save point for a fixture or a baseline. The app’s own storage model is just commits on dex/* branches, which is the smallest thing that could possibly have worked and, it turns out, the thing that did.

What replaced the verbs

Not more verbs. Two primitives, both on the timeline, both aimed at gestures that previously forced you into a terminal:

Remove this saved version. Deletes a branch Dex owns. If that branch holds work that exists nowhere else, you get a confirmation that names the stages in plain English — “Cycle 2 — Plan” — rather than a list of SHAs. main and branches you created yourself have no remove control at all, so the destructive gesture is structurally unavailable where it would hurt most.

Make this the new main. Promotes any saved version into the primary line. And since real promotion means real merge conflicts, the conflicts are resolved by a one-shot agent, per file, under a deliberately small tool surface:

const ALLOWED_TOOLS = ["Read", "Edit"];

const SYSTEM_PROMPT_OVERRIDE = [
  "You are resolving a merge conflict in a single file.",
  "You MUST use the Edit tool to modify the file. Do NOT only describe the resolution in text",
  "Goal: remove all conflict markers (<<<<<<<, =======, >>>>>>>) so the file parses cleanly,",
  "while preserving the intent of both branches when possible.",
  "Do not modify any other file. Do not add commentary outside the Edit tool call.",
].join("\n");

Read and Edit. Nothing else. No Bash, no Write, no ability to touch a file it was not handed. And every way it can fail is a named value rather than a stack trace:

export type ResolverFailReason =
  | "max_iterations"
  | "cost_cap"
  | "verify_failed"
  | "agent_gave_up"
  | "user_cancelled";

An iteration cap so it cannot loop. A dollar cap so it cannot run away. The project’s own verify command as the thing it must satisfy — if the merged file does not build, the resolution does not count, regardless of how confident the explanation was. If any cap trips, the merge stops and hands you back a working tree with the conflicts still in it, which is exactly where you would have been without it.

The general lesson

The first design was not wrong. Every verb in that opening list answered a real question someone could have about a pipeline they were not watching. What changed was that a better answer to all of them at once arrived — a canvas where the obvious gesture does the right thing — and once it did, the verbs stopped being features and became alternate paths to the same outcome.

That is the actual smell, and it is harder to spot than dead code: not functionality nobody uses, but functionality that now has a shorter route. Dead code announces itself with zero call sites. A redundant verb has call sites, has tests, works fine, and quietly taxes every future change to the surface it lives on.

The question I now try to ask after shipping any navigational UI: if this canvas does its job, which of my existing controls become the second-best way to do something? Those are the deletions, and they are easiest to make immediately — before someone builds on them, before they acquire their own branch family, and before the justification for keeping them becomes another feature you were about to delete.

Next: six hundred lines or it does not ship — the refactor that made this codebase editable by the same kind of agent it runs.