I built a time machine, then deleted half of it

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

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 four 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.

One correction, because the paragraph above reads like profiles died with the fan-out and they did not. The variant consumer is gone; the profiles themselves are live, user-facing, and now doing a better job than the one they were built for. .dex/agents/<name>/ still holds a dex.json of Dex-side knobs plus an optional runner-native overlay, there are IPC handlers behind it, and the type it resolves to has outgrown its origin entirely:

export type AgentRunnerKind = "claude-sdk" | "codex" | "copilot";
export type AgentProfile = ClaudeProfile | CodexProfile | CopilotProfile;

Which is the happier half of this story. The feature was deleted; the mechanism built to fix it turned out to be the seam where non-Claude backends plug in. The SDK post covers what it grew into. That is not a defence of building things speculatively — I did not foresee this, and one salvaged mechanism does not pay for a deleted engine. But it is worth noting that the thing worth keeping was the smallest, most boring artifact in the whole feature: a folder naming convention and a JSON file.

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 — Remove this saved version and Make this the new main — both on the timeline, both aimed at gestures that previously forced you into a terminal. The git post covers how they work, including the capped, Read-and-Edit-only agent that resolves the merge conflicts promotion produces.

What matters here is the ratio. Seven verbs and a variant engine became two gestures and a canvas — and the two survivors are not survivors from the original list. Neither Remove nor Make this the new main appears anywhere in the checkpoint release’s verb menu. They are the things people actually dropped to a terminal to do, which is a category I could only see after deleting the things I had guessed at.

That is the part I would tell my earlier self. The verbs were not wrong answers to the question “what would someone want to do with a run’s history”. They were answers to a question I had asked myself rather than watched anyone try.

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.