Debug the recipe, not the run
There is a moment, working with agents, when you stop debugging runs and start debugging recipes. A run is one execution: it fails, you curse, you retry. A recipe is the workflow definition itself — in Tixim, a Nix-built DAG of sandboxed steps. When a run fails and you patch the recipe, that class of failure is gone. Not for the next attempt; for every attempt after it, in every project that imports the flake.
This post is the story of that loop, told through one repository: vx-maintainer, the per-project maintainer service that grew into the recipe library our development now runs on. The service itself was finished in three weeks of May. The next three months — 300 commits — were spent teaching the recipes what their own failed runs had learned.
The authorship stats tell you where this is going:
git shortlog -sn
158 Daniel Poelzleithner
105 Hermes Agent
26 tix-repair-bot
11 vx-maintainer
tix-repair-bot and vx-maintainer are not people. They are the workflows,
committing fixes to their own repository — landed by the landing workflow they
define, gated by the gate declaration in their own tree.

June: make it finish one damn feature
The first workflow fixtures landed on June 6. The same day produced seven consecutive fix commits — reserved output names, missing tokens, a reviewer whose errors vanished into a green run. That last one became the first gate: fail the workflow when the reviewer errored. It sounds obvious. Everything in this post sounds obvious after the run that proved it wasn’t.
By late June the pattern had a shape. A run called wf-oaken-prairie wasted 37
minutes spawning fifteen diagnostic workers — each a ~150-second container
cold-start — to chase a phantom error that any file read would have settled in
milliseconds. The fix wasn’t a smarter agent. The recipe gave the manager
preloaded inspect.* tools and moved the git plumbing into host-side
capsules: small writeShellApplication scripts that run in about a second
and cost zero tokens. My commit message from June 27 captures the mood of the
era; I asked one session to “fix the fucking spec-kitty workflow and make it
finnally finish a damn feature.”
July: retry instead of fail
Early July was the resilience batch, written in a single frustrated weekend.
Workers died before their first turn; reviewers took whole steps down with
them; a manager once declared victory with zero commits on the branch. The
recipe answers, in order: dead sessions get classified and respawned instead of
failing the step; a mechanical verify-landing bounces incomplete work back
for one fixup round (the step exits 3, a declared success code, and the DAG
routes to a fresh manager); and durable state files stopped being rewritten on
every resubmit — the gate semantics had been correct all along, but the inputs
were lying.
The doctrine that crystallized: agents are the exception handler, not the CPU. Every fix that mattered replaced “trust the agent to remember” with a deterministic capsule or a mechanical gate.

Failures in the DAG stopped being fatal and started being routed. In the screenshot above, two review workers show red — and next to them, the respawn that finished the job. The run doesn’t care. The gate decides.
The autopsy that changed the economics
On July 22 a change-management run called wf-silken-umbra spent 77 workers,
4,133 tool calls, and 19.7 million tokens over three hours to land 356 lines
— and silently defaulted a blocking scope question it should have asked me. The
autopsy became a commit message, and the commit became policy:
- 28 of the 77 workers were doing pure git plumbing → capsules now do it in a second, for free.
- Workers were respawned while their sessions were still alive → a reuse-first ladder: an undelivered-but-done session gets a message, not a successor; one researcher session per run; dismiss promptly.
- A typo’d model name (
falbe-5) had been accepted silently → everyharness#modelis validated at dispatch. - Blocking decisions now surface first in the design, and an empty answer stops the run instead of defaulting.
Model selection itself became data instead of prompt folklore — one ordered ladder, shared by every workflow:
# Ordered weakest -> strongest: escalation walks this list ONE index at a time,
# so array order, not the `tier` number, is what actually drives the ladder.
[
{ harness = "opencode"; model = "minimax/minimax-m3"; tier = 1; }
{ harness = "claude-code"; model = "claude-sonnet-5"; tier = 1; }
{ harness = "opencode"; model = "gpt-5.6-terra"; tier = 1; }
{ harness = "opencode"; model = "z-ai/GLM-5.2"; tier = 2; }
{ harness = "claude-code"; model = "claude-opus-5"; tier = 3; }
{ harness = "claude-code"; model = "claude-fable-5"; tier = 4; }
]
August: cache the expensive part
The most expensive thing a landing does is review: a panel of models reading the whole diff, plus a code-review pass that can take half an hour. In July, a run that died after review re-bought all of it on resubmit — one such run re-paid ~45 minutes of model time to rediscover findings that were already on disk.
So the recipe learned to remember. Findings are sealed into a cache keyed by
the aggregate head SHA — the tree, not the branch name, because findings
about a different tree are worse than no findings. A week later the key grew a
second dimension, harness#model, so a restart restores the reviewers that
already ran and dispatches only the missing ones:
# Per-MODEL findings cache, keyed by (aggregate head SHA, harness#model slug).
# A restart restores the reports of models that already ran for this exact
# tree and re-dispatches only the ones still missing -- so a run that died
# mid-panel does not re-pay for the reviewers that finished. Emits one of:
# REVIEW_CACHE=hit every panel model restored -> skip the swarm entirely
# REVIEW_CACHE=partial some restored, some missing -> swarm the missing only
# REVIEW_CACHE=miss nothing cached -> swarm the full panel

Even the cache had to learn from a failure: the first version wrote its entry after the quorum check — which raised on partial panels — so it only ran when the panel was already complete, exactly when the cache is useless. Survivors are now sealed first; the failure marker comes second. The design rule that fell out of it: every failure mode of a cache must degrade to “do the work again,” never to a false hit.
And once, the false hit came from below: git’s own rerere cache silently
replayed a stale conflict resolution during an automated rebase and dropped a
branch’s auth fix — 77 of 83 commits landed, run green, code gone. No gate can
catch code that never appears in the diff. The fix was six lines: automated
rebases run with rerere off, so conflicts reach the resolver agent. Human
worktrees keep it. The dropped commits themselves were carried through by the
next landing, original SHAs intact — the regression test now sits on main
with a comment narrating the bug it guards against. Nothing stayed lost except
the illusion that a green run means a complete one.
Trust the gate, not the agent
The last stretch of commits all rhyme:
- A repair worker committed a correct fix, then exited non-zero — and the run threw the landing away without re-running the gate it had just turned green. Now the gate, not the repair agent’s exit code, decides.
- Task completion is gated on commit count, because “a worker’s own claim of success is not evidence.”
- A red gate used to latch the run failed forever, even after the re-check
passed. The engine grew
supersedes: a green re-gate now retires the failure it re-verifies, and the run resumes into merge. - The gates themselves moved into the target repository —
.config/tix/land-gate.jsondeclares which suites gate a landing, and the three-hour full-flake build moved off the merge path entirely, into a nightly workflow that repairs into its own branch. vx-maintainer’s own repo declared its gate the same day the feature shipped. The tool gates itself.
Some lessons are now literally comments in the recipe, so no future run can unlearn them:
# Doctrine: cleanup may only touch what the vx-meta branch manifest or the
# caller's explicit parameters say is OURS. It never discovers deletion
# candidates by globbing refs, and it NEVER runs `git worktree prune`
# (walks EVERY registered worktree and evicts entries other runs depend on).
Why this is the interesting part
None of these commits made a model smarter. Almost every one made the next run cheaper: a capsule instead of a worker, a cache instead of a re-review, a skipped step instead of a repeated one, a gate instead of a judgment call. That is the paradigm in practice — intelligence is spent once, at composition time, and the recipe is where it accumulates. The failure isn’t an interruption of the process. The failure is the process.
Somewhere in mid-July, between two autopsies, I wrote: “we create resilience by breaking things. we are alone with our little project, we can do this with no real impact. can’t do this when we are live.” And a few days earlier, after a landing quietly rebased four branches, spawned a fixer for one conflict, took review findings, fixed them and merged: “this is how stuff should flow.”
It does, now. Mostly. The commits that say otherwise are already teaching the next recipe.