<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bes-dev.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bes-dev.com/" rel="alternate" type="text/html" /><updated>2026-07-31T17:54:04+00:00</updated><id>https://bes-dev.com/feed.xml</id><title type="html">Sergei Belousov</title><subtitle>Personal website and blog</subtitle><entry><title type="html">From App Factories to a Reasoning Compiler</title><link href="https://bes-dev.com/posts/reasoning-compiler/" rel="alternate" type="text/html" title="From App Factories to a Reasoning Compiler" /><published>2026-06-11T00:00:00+00:00</published><updated>2026-06-11T00:00:00+00:00</updated><id>https://bes-dev.com/posts/reasoning-compiler</id><content type="html" xml:base="https://bes-dev.com/posts/reasoning-compiler/"><![CDATA[<p>I didn’t plan to build a compiler — I just wanted to maximize out of the AI agents I had.</p>

<p>What is an AI agent today? It’s actually quite simple. There is a language model — the brain and the center of decision making. And there is a harness around the model: the environment where the model works — the thing that makes the model an agent. Without the harness the model is just a text generator, sometimes quite a smart one.</p>

<p>Most of the resources of the labs around the world go into improving the models, which we use <code class="language-plaintext highlighter-rouge">as is</code> — and thank god, it’s not us who pay for their training. The harness gets much less attention from the research community. So I have good news for you: the harness is exactly the place where an indie researcher can make a contribution, without having the resources of the frontier labs.</p>

<h2 id="wishes-not-guarantees">Wishes, Not Guarantees</h2>

<p>Today the harness mostly means two things: MCP and Skills. Skills explain to the agent in free text how to approach a task; MCP gives it the tools for that. The idea is quite elegant: give the model a minimal kit — and let it assemble the solution on its own.</p>

<p>But all this elegance rests on one assumption, that the model will follow the received instructions. And a skill is just text in the context window, which the model is not obliged to follow. Skills today are basically prompt engineering on a new lap: they raise the odds, but they give no guarantees. And the problem is not the quality of skills: you can write an excellent instruction, covering all the nuances. But if the model can simply ignore these instructions — it’s a bad foundation for a reliable system. A good foundation needs something else — a structure that the model doesn’t read but executes, with no way around it.</p>

<h2 id="the-splinter">The Splinter</h2>

<p>I got here not by theorizing but by hitting walls, project after project: I built <a href="/posts/etnamute/">an AI mobile developer</a>, <a href="/posts/teaching-claude-to-test/">taught an agent to actually test things</a>, <a href="/posts/autoharness/">made a harness repair itself between tasks</a>, and finally <a href="/posts/harness-matters/">moved everything onto a local 27B model, replacing the LLM orchestrator with a finite state machine</a>. I wanted my agents to solve complex tasks autonomously, for many hours, without my intervention. And ideally — to do all of it with lightweight local models that can run right on my laptop. And in every new project, while raising the bar for autonomy and quality, I had to make the harness a bit less advisory and a bit more structural, until it stopped being instructions and became code. Why orchestration with deterministic state machines beats orchestration through reasoning is <a href="/posts/harness-matters/">a separate post</a>; the one-line version: control flow lives in code, and the model is only responsible for judgment at the leaves.</p>

<p>But when the harness of my agents collapsed mostly into code, I was still writing it by hand. For every new task I decided what the topology of the state machine will be, what happens in each state, what contracts between the agents, etc. The essence of this process is reasoning about the task.</p>

<p>An attentive reader will say: but this is how we solved agentic tasks a few years ago, and then we dropped it in favor of orchestration through reasoning, when the models became smart enough — am I trying to sell an old idea? And he will be absolutely right: deterministic orchestration returns control, but it takes away the main achievement of the reasoning era — the model’s ability to derive the solution on its own. To fix this problem, today we will go up to the level of meta-agents and bring the model back into the loop of orchestration decisions. But we will do it in a clever way: we will build an agentic system that spends reasoning once to build a deterministic machine, which then can run as many times as you want, without spending any additional reasoning on orchestration.</p>

<p>This is what reharness does — a reasoning compiler. It is open source, published on npm and installs with one command — at the end of the post I will show how to try everything in a couple of minutes, but first let’s see how it works.</p>

<h2 id="a-trick-from-1971">A Trick from 1971</h2>

<p>Let me defend my right to use the word “compiler” for reharness. For this I will use two ideas.</p>

<p>First, a compiler takes something expensive to interpret and makes it cheap to execute. A JIT analyzes the program execution, finds the hot paths and compiles them into machine code — so it stops re-interpreting them on every pass. reharness does essentially the same. The hot path is the model’s reasoning, which the agent repeats every time it faces the same class of tasks. The machine code is a finite state machine. Here the analogy works on the level of intuition.</p>

<p>To explain the second idea, we need to recall a rather beautiful trick from the distant 1971 — the first Futamura projection. Let’s start with partial evaluation: if a part of the program’s input is fixed, you can hardcode it inside. For example, for <code class="language-plaintext highlighter-rouge">pow(x, n)</code> with fixed <code class="language-plaintext highlighter-rouge">n=3</code>, we can do an in-place substitution <code class="language-plaintext highlighter-rouge">pow(x, 3) = x * x * x</code>. Now take an interpreter — it’s also a program, it just receives another program as input. And if we specialize the interpreter with respect to one concrete program, we get a standalone artifact that does the same as the original program, but without the interpretation overhead. Futamura’s observation here was that the essence of specializing on a program is exactly compilation.</p>

<p>Now let’s draw the analogies between classic compiler theory and modern agentic systems. The agentic loop — think, call a function, look at the result, think again — is an interpreter. A concrete request, or a trace of a task solved once, is a program. And if you clean out everything that depends on the concrete run, what remains is a deterministic pipeline. So “compiling reasoning” is the same formal move, just one level higher up the stack. This way, I allow myself the word.</p>

<p>Now let’s look at the economics of compiling reasoning. The compilation itself costs real money, because you need to spend reasoning to build the deterministic pipeline. As for the runtime, let’s look at the limit cases. If the task is fully mechanical, the compiler pulls everything into code and the pipeline doesn’t call the model at all. If the task is purely creative, the compiler will build for it a machine with one agent state, and the number of spent tokens will be the same as before compilation. Everything between these two cases is the most interesting part — tasks that are hard to turn into pure code, but where the amount of reasoning can be reduced a lot. As we can see, by construction, after compilation the cost of every new run at least doesn’t grow, and often becomes lower.</p>

<p>And the nicest thing here is that compilers are a well-researched area with a known anatomy, which we now can use. There is a target language (the deterministic FSM runtime). There is an intermediate representation (skeleton.xml — the single artifact that all the passes work on). There is a static analyzer that checks the correctness of the built machine. There is a pipeline from the source language to the executable code — with a multi-language frontend, exactly one creative pass, and everything else deterministic. And there is a profile-guided optimization loop from real runs, which gcc doesn’t have, but every grown-up JIT does.</p>

<p>Let’s walk the whole system in the order it is actually designed — from the target up. Because the promises of a compiler are based on the guarantees of the target.</p>

<h2 id="the-mouthful">The Mouthful</h2>

<p>The runtime itself appeared before any attempts to formalize it strictly, and it was naive. Since then it grew up and became formal, and its formal name is a mouthful: a <strong>deterministic hierarchical Moore-action transducer with run-to-completion semantics</strong>. Sounds scary, but the name is a specification, where every word corresponds to some design decision. So let’s unpack it word by word, and for each one understand what problems it saves us from.</p>

<p><strong>…transducer</strong> is simply an automaton with output: not a textbook recognizer that answers “accept / reject”, but a machine that does work while it moves. Formally the pipeline is a six-tuple <code class="language-plaintext highlighter-rouge">(Q, q₀, F, Σ, δ, λ)</code>: states, the start one, the terminal ones (each marked <code class="language-plaintext highlighter-rouge">success</code> or <code class="language-plaintext highlighter-rouge">error</code>), the alphabet of events, the transition function δ and the action function λ. All the useful work — agent calls, code, artifacts — lives in λ. And all the other words of the name are restrictions on how δ and λ are allowed to behave.</p>

<p><strong>Moore-action</strong> means that the action is tied to the state — and only to it. Each state runs its action to the end and emits exactly one event — the result of its own computation, not a reaction to something arriving from outside. Thanks to this we can answer the question “who did that?”, because all the computations happen only in the nodes and never on the transitions between them. Want to understand what a state does — read it, there is nowhere else to look.</p>

<p><strong>…with run-to-completion semantics</strong> means that the machine fully finishes the state’s action and only after that thinks about transitions. No event queues, no preemption — strictly one event per step. There is only one allowed source of external signals — the <code class="language-plaintext highlighter-rouge">wait</code> state (timer, file, shell, webhook), and it is an explicit state type, not a side door. This allows to always understand at which stage of computation the machine is right now, because a run is a sequence of finished computations, not a pile of half-done actions.</p>

<p><strong>Deterministic</strong> is the most loaded word, it saves us from several problems at once.</p>

<p>First, δ resolves an event by taking <em>the first transition with a true guard, in written order</em>. UML, as we know, doesn’t define the order of guards — we nail it down. So the situation “this time it somehow went differently” is impossible for the same scalars — the route will always be the same.</p>

<p>Second, we forbid the machine to hang silently. For any reachable pair (state, event), either a transition is defined, or the machine stops with an explicit error pointing at the problem — an unhandled event, no guard matched, a switch with no suitable branch. And the <code class="language-plaintext highlighter-rouge">fail</code> path first persists the machine state and then dies: even a failure leaves a resumable run behind. This, by the way, forbids skipping the verification steps — paths around the checks just don’t exist in the graph.</p>

<p>Third, guards execute at the transition points, when no stage is active, and they can read only the scalar bus — small values in memory: flags, counters. Try to do something with the file workspace inside a guard — and the machine will fail loudly. Thanks to this, the whole configuration of the machine is a pair (current state, dictionary of scalars). So restoring an interrupted machine is trivial — you saved this pair, and then you restored it. This also simplifies the static analysis: to trace the transitions, the analyzer needs to model a handful of scalars, not a file system.</p>

<p>But it’s worth noting that while the machine itself is deterministic, the agent state stays stochastic inside, because it’s an actual LLM call. But with fixed outputs of the agent states — the full run of the machine is reproducible. Splitting the task into a deterministic skeleton and stochastic reasoning only where you can’t avoid it — this is basically the product.</p>

<p><strong>Hierarchical</strong> means that a state can be not only a leaf but also a composite: <code class="language-plaintext highlighter-rouge">parallel</code> — fork/join over an array, <code class="language-plaintext highlighter-rouge">loop</code> — bounded iteration, <code class="language-plaintext highlighter-rouge">call</code> — invoking another compiled pipeline. A composite executes by recursion as an RTC computation, it doesn’t break any of our machine’s restrictions.</p>

<p>Infinite loops are forbidden by design: for <code class="language-plaintext highlighter-rouge">loop</code> the maximum number of iterations is mandatory, though an early exit by a corresponding predicate is also allowed. The reason is simple: if the exit predicate is written by a stochastic LLM, you can easily end up in a situation where the loop diverges — we strictly forbid this.</p>

<p>As for <code class="language-plaintext highlighter-rouge">parallel</code>, the parallelism is real only where it actually matters: for agent states the parallelism is implemented at the level of OS processes. Each branch gets its own copy of the bus, which excludes the race for shared data; the branches communicate through isolated output directories, which the join then reads as a list.</p>

<p>And now if we return to the name of our runtime, it stops being scary and turns out simple and logical:</p>

<ul>
  <li><strong>Transducer</strong>: the machine does work.</li>
  <li><strong>Moore-action</strong>: the work happens in the nodes.</li>
  <li><strong>Run-to-completion</strong>: the steps don’t interrupt each other.</li>
  <li><strong>Deterministic</strong>: one route for the same scalars, no silent stalls, with stochasticity only in the leaves.</li>
  <li><strong>Hierarchical</strong>: composites nest by recursion and loops are always bounded.</li>
</ul>

<p>The poverty of the target is not a bug, it’s a feature. We deliberately restricted the machine to the necessary minimum to get checkable guarantees.</p>

<h2 id="one-file-of-truth">One File of Truth</h2>

<p>Our runtime is a processor that executes programs. The programming language for this processor is the intermediate representation of the machine in XML format. The DSL consists of 12 state types (<code class="language-plaintext highlighter-rouge">agent</code>, <code class="language-plaintext highlighter-rouge">interactive</code>, <code class="language-plaintext highlighter-rouge">code</code>, <code class="language-plaintext highlighter-rouge">set</code>, <code class="language-plaintext highlighter-rouge">switch</code>, <code class="language-plaintext highlighter-rouge">check</code>, <code class="language-plaintext highlighter-rouge">parallel</code>, <code class="language-plaintext highlighter-rouge">loop</code>, <code class="language-plaintext highlighter-rouge">wait</code>, <code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">approval</code>, <code class="language-plaintext highlighter-rouge">final</code>), which the runtime collapses to eight constructs — the first four are one active state with different actions, <code class="language-plaintext highlighter-rouge">check</code> is sugar over <code class="language-plaintext highlighter-rouge">switch</code>.</p>

<p>Here is an example program describing a multi-model code review:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;skeleton</span> <span class="na">id=</span><span class="s">"multi-review"</span> <span class="na">initial=</span><span class="s">"ingest"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;usage&gt;</span>multi-review <span class="ni">&amp;lt;</span>repo<span class="ni">&amp;gt;</span><span class="nt">&lt;/usage&gt;</span>
  <span class="nt">&lt;inputs&gt;</span>
    <span class="nt">&lt;arg</span> <span class="na">name=</span><span class="s">"repo"</span> <span class="na">positional=</span><span class="s">"true"</span> <span class="na">required=</span><span class="s">"true"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;arg</span> <span class="na">name=</span><span class="s">"models"</span> <span class="na">type=</span><span class="s">"list"</span> <span class="na">default=</span><span class="s">"claude-sonnet,claude-opus"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/inputs&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"ingest"</span> <span class="na">type=</span><span class="s">"code"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;contract&gt;</span><span class="cp">&lt;![CDATA[Collect the diff of config.repo into c.out()/diff.patch;
      set c.data.empty = true if there are no changes.]]&gt;</span><span class="nt">&lt;/contract&gt;</span>
    <span class="nt">&lt;on</span> <span class="na">event=</span><span class="s">"DONE"</span> <span class="na">target=</span><span class="s">"has_changes"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"has_changes"</span> <span class="na">type=</span><span class="s">"check"</span> <span class="na">expr=</span><span class="s">"data.empty"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;on</span> <span class="na">event=</span><span class="s">"TRUE"</span> <span class="na">target=</span><span class="s">"ok"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;on</span> <span class="na">event=</span><span class="s">"FALSE"</span> <span class="na">target=</span><span class="s">"review"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"review"</span> <span class="na">type=</span><span class="s">"parallel"</span> <span class="na">over=</span><span class="s">"config.models"</span> <span class="na">branch=</span><span class="s">"reviewer"</span> <span class="na">concurrency=</span><span class="s">"4"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;on</span> <span class="na">event=</span><span class="s">"DONE"</span> <span class="na">target=</span><span class="s">"gate"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"reviewer"</span> <span class="na">type=</span><span class="s">"agent"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;contract&gt;</span><span class="cp">&lt;![CDATA[Read the diff from the input directory, write findings.md
      with a list of findings (severity: blocking | advice).]]&gt;</span><span class="nt">&lt;/contract&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"gate"</span> <span class="na">type=</span><span class="s">"code"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;contract&gt;</span><span class="cp">&lt;![CDATA[Read c.dirs('reviewer') - one directory per model - merge
      findings into c.out()/report.md, count blocking ones into c.data.blocking.]]&gt;</span><span class="nt">&lt;/contract&gt;</span>
    <span class="nt">&lt;on</span> <span class="na">event=</span><span class="s">"DONE"</span> <span class="na">target=</span><span class="s">"route"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"route"</span> <span class="na">type=</span><span class="s">"switch"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;go</span> <span class="na">guard=</span><span class="s">"expr:data.blocking &gt; 0"</span> <span class="na">target=</span><span class="s">"bad"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;go</span> <span class="na">target=</span><span class="s">"ok"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/state&gt;</span>

  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"ok"</span> <span class="na">type=</span><span class="s">"final"</span> <span class="na">status=</span><span class="s">"success"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;state</span> <span class="na">name=</span><span class="s">"bad"</span> <span class="na">type=</span><span class="s">"final"</span> <span class="na">status=</span><span class="s">"error"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/skeleton&gt;</span>
</code></pre></div></div>

<p>Every working state has a short <code class="language-plaintext highlighter-rouge">&lt;contract&gt;</code> — this is the only place that stores the intent of the node. The <code class="language-plaintext highlighter-rouge">reviewer</code> state is an example of the <code class="language-plaintext highlighter-rouge">parallel</code> composite: it runs once for each model, and then control returns to the parent. The <code class="language-plaintext highlighter-rouge">gate → route</code> pair exists because of our machine’s restriction that agents can’t touch the scalars, so there is always a small code bridge for choosing the direction, which reads the files and puts one number on the bus.</p>

<p>If you look at the code carefully, you won’t find a single file path there. The reason is not that I simplified the example — the language has no such constructs. In the whole IR there is only one declaration — <code class="language-plaintext highlighter-rouge">&lt;inputs&gt;</code>, which defines the external arguments that are impossible to derive from the graph. From <code class="language-plaintext highlighter-rouge">&lt;inputs&gt;</code> the codegen generates the argument parser, and a static check requires that every argument the pipeline reads is declared. Implicit declaration of global variables is forbidden. Everything else is derived from the graph.</p>

<p>One more deliberate poverty of the target is the guard language, which is <strong>not Turing-complete on purpose</strong>. It allows only identifiers over <code class="language-plaintext highlighter-rouge">config.*</code> / <code class="language-plaintext highlighter-rouge">data.*</code> / <code class="language-plaintext highlighter-rouge">retries.*</code>, comparisons, boolean logic, arithmetic and literals — no function calls, no assignments, no ternary. A guard must be a cheap, total, statically checkable expression over scalars, and the grammar is simply not able to express anything else. Routing inside the machine is a function of scalars, and the guard grammar makes violating this requirement impossible.</p>

<h2 id="the-wiring-nobody-wrote">The Wiring Nobody Wrote</h2>

<p>You may object: the stages obviously pass data to each other (diffs, reports, build artifacts, etc.). Who declares what flows where? Nobody. And this is not a hole in the language — this is my favorite part of the whole system.</p>

<p>Working on the compiler, at first I tried the obvious ideas. And the most obvious one here is to make the model somehow understand the data flow inside the graph and annotate it on its own. All the attempts failed for a simple reason: the model’s opinion on this question is a second source of truth about the graph, and there is no guarantee it won’t diverge from the real graph. To be honest, in my experiments it diverged almost always, which led to generating a broken machine. Things were especially bad with loops and parallel branches.</p>

<p>The important insight for solving this problem was that a graph edge already is a contract by itself. If stage B is reachable from stage A, then the output of A is by definition available to B. So the most reliable solution turned out to be deriving the visibility from the topology: the producers visible to a node are its ancestors, and there is nothing more to invent here.</p>

<p>One important question remains, which is easy to miss and run into bugs: how many instances of the producer’s output does the reader see? In compiler theory there is an instance-wise rule for this, in the spirit of the polyhedral model. The same classic that is used in loop optimizers for resolving array accesses in loop nests. If you ever wrote <code class="language-plaintext highlighter-rouge">results[i][j]</code> inside two nested loops — you have all the necessary theory: a thing that executes inside loops is addressed by its loop indices. For us it’s all the same: every state has an iteration space — the chain of composites enclosing it, from the outer to the inner. An instance of a state is addressed by an instance vector: one index per enclosing composite (which parallel branch, which loop iteration). For a top-level stage this vector is empty — there is one of it.</p>

<p>The cardinality rule then fits in one sentence, verbatim from the code:</p>

<blockquote>
  <p>Producer <code class="language-plaintext highlighter-rouge">P</code> is visible to reader <code class="language-plaintext highlighter-rouge">N</code> as a <strong>collection</strong> ⇔ <code class="language-plaintext highlighter-rouge">P</code>’s chain is longer than the common prefix of the two chains. Otherwise — as a <strong>single instance</strong>.</p>
</blockquote>

<p>All of this is easy to understand on our review example. As long as <code class="language-plaintext highlighter-rouge">reviewer</code> works in its own process, it sees its own copy of the bus and its own working directory, and it doesn’t see the neighbors — the question “which instance?” is resolved trivially for it: for example, there is exactly one <code class="language-plaintext highlighter-rouge">ingest</code> for it. But <code class="language-plaintext highlighter-rouge">gate</code> runs already outside the parallel, and from its point of view <code class="language-plaintext highlighter-rouge">reviewer</code> is not one result, but one per each parallel branch. From here the simple rule: from inside we read one branch (<code class="language-plaintext highlighter-rouge">c.dir</code>), from outside — a list (<code class="language-plaintext highlighter-rouge">c.dirs</code>). For loops it unrolls the same way. Even the tricky case “the actor reads the critic from the previous iteration” resolves by itself: the runtime gives the latest existing instance. In compiler theory this analysis is called instance-wise dataflow — the classic theory of array accesses, which fit the agentic pipeline like it was made for it.</p>

<p>And physically it’s all almost disappointingly simple: every output directory is named by the full instance vector — <code class="language-plaintext highlighter-rouge">work/&lt;stage&gt;/&lt;i0&gt;/&lt;i1&gt;/…</code>. The producer’s write, the branch bookkeeping and the consumer’s read are computed from the same pair (stage, vector). They can’t diverge — there is nothing for them to diverge about.</p>

<p>So in total the data travels over three channels:</p>

<ul>
  <li><strong>The scalar bus</strong> — used for routing.</li>
  <li><strong>Per-stage directories</strong> — artifacts, with the wiring derived from the graph.</li>
  <li><strong>External targets</strong> — the only channel of communication with the outside world, declared through <code class="language-plaintext highlighter-rouge">&lt;inputs&gt;</code>.</li>
</ul>

<h2 id="other-peoples-theorems">Other People’s Theorems</h2>

<p>We have the runtime, we have the IR to program it, the last important step before building the generator is static analysis. We must be able to tell AI slop from correctly working programs. For this we will again turn to compiler theory, instead of writing our own bicycles whose robustness I really don’t want to prove myself. In reharness there are just two lightweight engines for static analysis, together they take 78 lines of code.</p>

<p>The first engine checks the reachability of states in the graph using breadth-first search, nothing much to discuss here.</p>

<p>The second engine is more tricky, and it’s easiest to understand on our same review example. <code class="language-plaintext highlighter-rouge">gate</code> writes the scalar <code class="language-plaintext highlighter-rouge">data.blocking</code>, and <code class="language-plaintext highlighter-rouge">route</code> reads it. What we want to know here: is this scalar actually written on all the paths leading into <code class="language-plaintext highlighter-rouge">route</code>? Because the graph was drawn by a model — and if it created a path around <code class="language-plaintext highlighter-rouge">gate</code>, then on that path <code class="language-plaintext highlighter-rouge">route</code> will read something that doesn’t exist.</p>

<p>We could do a full enumeration over all the paths for this, but computationally it’s hard, and besides, a more beautiful solution was already invented for this. The trick is this: instead of paths, we compute for every <em>node</em> the set “what is guaranteed to be written by the moment we arrived here”. There are just two rules: a node that writes a scalar adds it to its set, and where several branches merge, we take the <em>intersection</em> of what arrived along them — only what is guaranteed on every branch stays guaranteed. If the question is not “is it guaranteed” but “is it possible on at least some path”, everything works the same but with the <em>union</em> — this is exactly how “whose outputs are visible to a node” from the previous section is computed. Loops remain, but they are also simple: we walk the graph again and again until the sets stop changing, and they can’t grow forever — there is a finite number of scalars, so the stop is guaranteed.</p>

<p>That’s the whole engine: in the textbooks it’s called the Kam–Ullman monotone dataflow framework, where the version with intersection is the MUST analysis, the version with union is the MAY analysis, and “the sets can’t grow forever” is its convergence theorem. The whole machinery is two lines:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IN[n]  = merge of OUT[p] over all predecessors p      (entry: IN = ∅)
OUT[n] = IN[n] ∪ gen(n)
</code></pre></div></div>

<p>Every check in reharness is a thin instance on top of these two engines:</p>

<table>
  <thead>
    <tr>
      <th>check</th>
      <th>what it is, formally</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>every state is reachable from start</td>
      <td>forward reachability</td>
    </tr>
    <tr>
      <td>every state can reach a final (no dead ends)</td>
      <td>backward reachability from the finals</td>
    </tr>
    <tr>
      <td>no scalar is read before it’s written</td>
      <td>forward MUST dataflow = <em>definite assignment</em></td>
    </tr>
    <tr>
      <td>visible producers and their cardinality</td>
      <td>MAY reachability + the common-prefix rule</td>
    </tr>
  </tbody>
</table>

<p>Look at the bottom row of the table: deriving the data wiring and <em>checking</em> it is one and the same analysis, not two pieces of code that have to negotiate with each other. And a second trick of the same kind: the question “where does a state lead?” is answered in the whole system by <em>one</em> function — <code class="language-plaintext highlighter-rouge">successors</code>, and it is used by the reachability check, the data analysis and the codegen. Three consumers look at the graph with the same eyes — and the whole class of bugs “the validator assumed one thing, the codegen did another” disappears by construction.</p>

<h2 id="the-rite-of-passage">The Rite of Passage</h2>

<p>So, piece by piece our compiler is almost ready, what’s left is to tie everything together. And here is a fun thing: the compiler is a reharness pipeline, a machine running on its own runtime. This proves that the target language is expressive enough.</p>

<p>The machine itself looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>research → prd → [review_prd: approval] → design → construct → fill → check_dataflow → polish → verify → done
                                                              verify FAIL → fix_verify (≤2) → verify
                                                              polish escalate → redesign (rare)
</code></pre></div></div>

<p><strong>The frontend is multi-language</strong> — like gcc, which compiles C, C++ and Fortran into one IR. There are three source languages: a <strong>request</strong> in natural language; an <strong>amendment</strong> (<code class="language-plaintext highlighter-rouge">amend</code>) — folding a new feature into the existing PRD as an intent delta, keeping the already filled leaves; and a <strong>recorded trace</strong> — the log of a task solved once. All three converge on the PRD: a short document about <em>what</em> should be built.</p>

<p>Of the three source languages, the trace deserves a separate pause — it’s the most unusual one. A recorded session is a <em>demonstration</em>, and turning a demonstration into a reusable program is programming by demonstration: generalization from one example. The generalization here is <strong>explanation-based</strong>, and the recipe is exactly what it sounds like: explain <em>why</em> the trajectory reached the goal, keep the weakest preconditions that this explanation needs, and throw away everything specific to the run. The concrete repository becomes a parameter, dead ends and wandering are discarded, and what survives is the structure — and it is exactly the thing worth compiling. Two properties make this robust in practice. First, the trace is <em>grounded</em> before generalization, and the priority order matters: what the trace actually shows (the real call, the real response) beats the documentation, the documentation beats the web, and the web beats the model’s memory. Second, the whole construction is format-agnostic <em>by design</em>: the session is read raw — JSONL, markdown export, a pasted chat, doesn’t matter — because the universal parser here is the model itself, and you need no per-vendor adapters and no lock-in to anyone’s log format.</p>

<p><strong>Next comes the only human gate, placed exactly where it’s cheap.</strong> The human approves the PRD — the intent, never the structure. It’s worth spelling out the asymmetry here: validating intent is cheap, and people are good at it; reviewing a generated state graph for semantic correctness is expensive, and people are, let’s be honest, bad at it. So the gate stands on the only truly catastrophic failure — “built the wrong thing entirely” — and everything downstream belongs to the compiler. A demonstration, by the way, buys no special trust: traces go through the same gate.</p>

<p><strong>There is exactly one creative pass — <code class="language-plaintext highlighter-rouge">design</code>.</strong> It emits the only thing that really requires a model: the topology graph plus a behavioral contract per node — that same IR. And it self-corrects in a hot context: design runs under RPC, and after every turn the runtime re-prompts it with the errors from the analysis library, until the IR comes out clean. Essentially these are semantic compile errors returned to the author — it’s just that the author here is a model, and it fixes them without leaving the room.</p>

<p><strong>Everything below design is deterministic.</strong> <code class="language-plaintext highlighter-rouge">construct</code> is the lowering: codegen from IR into TypeScript, stubs for the agent prompts, the argument parser from <code class="language-plaintext highlighter-rouge">&lt;inputs&gt;</code>. <code class="language-plaintext highlighter-rouge">fill</code> writes the leaf implementations against their contracts — the only LLM work below design, and strictly local: a leaf sees its contract, not the graph. <code class="language-plaintext highlighter-rouge">check_dataflow</code> runs the definite assignment. <code class="language-plaintext highlighter-rouge">polish</code> is one review pass of the whole pipeline against the PRD, editing only the leaves; deliberately <em>not</em> a review→fix→re-review loop — the loop was expensive and bought nothing. <code class="language-plaintext highlighter-rouge">verify</code> is the objective backstop: type compilation plus structural checks; a failure goes to a bounded <code class="language-plaintext highlighter-rouge">fix_verify</code> (two rounds maximum), which edits only the implementations.</p>

<p>And the final check is done <strong>by execution, not by faith</strong>. The result of compilation is an executable graph, which means it can be dry-run with the model leaves stubbed out: zero tokens, while the control flow, the guards and the derived wiring actually walk all the way to a terminal. Don’t trust that it “compiled” — watch how it runs. And on real runs <strong>the cost is observed, not estimated</strong>: every agent leaf reports its actual spend, the runtime sums it into the verdict. A fully amortized pipeline prints <code class="language-plaintext highlighter-rouge">0 agent runs · 0 tokens · $0.0000</code>. The central claim of this post is falsifiable with one line of output — any day.</p>

<h2 id="the-ring-closes">The Ring Closes</h2>

<p>One organ remains — the one that AOT compilers don’t have, but grown-up JITs do: <strong>profile-guided optimization</strong>. A compiled pipeline keeps improving from its own runs; it’s just that the profile is not branch counters, but agent traces. The precise name for this is <strong>speedup learning</strong>, a relative of explanation-based generalization, at the granularity of a sub-routine.</p>

<p>Every run leaves a verdict behind, and on failure the repair step reads the trace, finds the root cause and fixes the offending leaf, without touching the node’s contract — it escalates to a graph change only if the fix doesn’t fit into a leaf. After the repair the pipeline is re-verified and re-run on the original arguments: “fixed” is confirmed by execution, not by faith — the same principle that verify lives by, the system is consistent in its epistemology.</p>

<p>On success the system walks the agent leaves: it refines the attached skills, if the trace showed that a skill lied, and it hunts for a <em>repeating deterministic sub-routine</em> that the agent reinvents by hand on every run — to freeze it into a callable tool.</p>

<p>And here the eighties have a warning prepared for us, called the <strong>utility problem</strong>: if you cache everything you learned, you become <em>slower</em> — every cached item itself costs context and selection time. The field burned itself on this decades before LLMs made it expensive also in dollars. So a frozen tool passes two gates: <em>acquisition</em> — it is correct by construction (parses, passes its self-test, respects the sandbox) — and <em>retention</em> — it keeps its place only while the call frequency justifies it. If nobody calls it — the tool gets unbound and goes to the archive, and this way the system learns what pays off and unlearns what didn’t.</p>

<p>If you step back, you can see the ring that closes here: the runtime executes what the pipeline built, and produces traces. The traces feed the frontend — compiling a demonstration is, after all, compiling someone’s trace — and they feed the optimizer, which improves the pipeline from its own runs. The same artifact ends up both on the input and on the output, and for a reasoning compiler this is not a coincidence: reasoning is its raw material.</p>

<h2 id="four-rungs">Four Rungs</h2>

<p>So, the compiler exists — but what to compile with it? The ladder of applications starts on your own desk and ends, surprisingly, in the enterprise.</p>

<p><strong>Rung one: your own routine.</strong> Everything you do with an agent regularly is a candidate for compilation. Mail digests, log triage, weekly reports, dependency checks, changelogs: a recurring task compiles once for a couple of dollars and then runs on cron for months. The sweet spot is exactly the <em>repetition</em>: a one-off task doesn’t justify the compilation, a live agent is cheaper; but everything you run at least weekly amortizes the compilation within a couple of runs. And if the task is mechanical, the pipeline doesn’t call the model at all — I have compiled commands that haven’t spent a single token since the compilation day, and they are doing fine. The cheapest agent is the one that never calls the model.</p>

<p><strong>Rung two: compiling traces — including other people’s.</strong> This is where it gets interesting, because the source language stops being your head. Solve a task with a live agent once, take the trace, compile it — and nobody solves this task from scratch anymore. In a team it looks like this: the most experienced engineer <em>shows</em> once how to deploy, review or migrate properly — and his session becomes a tool that everyone runs. A demo instead of a spec, and no broken telephone. And since HuggingFace started putting agent traces right on the Hub, you can compile <em>other people’s</em> demonstrations too: I ran reharness on open trace datasets — someone else’s session turns into your working pipeline. The more open traces exist, the more ready-made programs lie in the commons — for now, in unassembled form.</p>

<p><strong>Rung three: a backend for assistants.</strong> Conversational harnesses of the OpenClaw/Hermes class are great at dialogue and judgment — and they pay tokens for <em>every</em> run of the routine they grind daily. The industry already considers it normal that an agentic task burns 10-100x more tokens than a direct model call — and the lion’s share of this multiplier goes not to judgment, but to re-interpreting the same structure: what’s next, did I check, what was there last time. Plug the compiler in as a backend — and the assistant keeps the conversation and the judgment, while the recognized routine drives off into a compiled pipeline at ~$0. This hits hardest for small local models: their token budget is tight as it is, and burning it on orchestration is exactly the waste this whole series started from.</p>

<p><strong>Rung four: the enterprise, surprisingly.</strong> The main brake on agents in serious organizations is not the price, it’s the nondeterminism. The auditor needs “which rule fired and why”; the agent reasons differently on every run; compliance reaches for the cigarettes. A compiled pipeline cuts this knot: the agent’s flexibility stays at compile time, and the execution gets the auditability of the good old RPA — deterministic transitions, a full trace of every run, observed cost, byte-for-byte reproducibility on the mechanical parts, and exactly one labeled node where the judgment happened. The classic automation cases — invoice processing, ticket triage — are exactly high volume plus one recurring decision: one agent leaf in a deterministic skeleton, straight from the textbook.</p>

<p>Four rungs, one product: <strong>a recurring task stops paying for reasoning on every run.</strong> Big models made us lazy — “the model will figure it out” — and we pay for this figuring-out again and again. A compiler is a refusal to pay twice.</p>

<p>And the part that still makes me smile: the foundation for all of this was found not in fresh papers about agents, but in compiler theory that is decades old. Futamura, Moore machines, polyhedral analysis, Kam–Ullman fixpoints — it all was lying on the shelf, waiting for a domain that didn’t exist when it was written. The machine I used to write by hand for every task, the compiler now derives on its own.</p>

<h2 id="try-it">Try it</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">-g</span> reharness

<span class="c"># compile a pipeline from a request</span>
reharness compile <span class="s2">"code review FSM for this project"</span>

<span class="c"># or from a recorded session</span>
reharness compile <span class="nt">--from-session</span> ./session.jsonl

<span class="c"># dry-run: the whole graph, stubs instead of agents, zero tokens</span>
reharness &lt;<span class="nb">command</span><span class="o">&gt;</span> <span class="nt">--dry-run</span>

<span class="c"># run it for real</span>
reharness &lt;<span class="nb">command</span><span class="o">&gt;</span> args
</code></pre></div></div>

<p>For the agent leaves you need at least one backend on <code class="language-plaintext highlighter-rouge">PATH</code>: <a href="https://github.com/badlogic/pi-mono">Pi</a> (the default) or Claude Code (<code class="language-plaintext highlighter-rouge">--provider claude</code> — the agents run on a subscription instead of per-token billing). A compiled pipeline is just a regular <code class="language-plaintext highlighter-rouge">reharness/</code> directory in your project: read it, version it, carry it with you.</p>

<p>Apache 2.0: <a href="https://github.com/bes-dev/reharness">github.com/bes-dev/reharness</a> · npm: <a href="https://www.npmjs.com/package/reharness">npmjs.com/package/reharness</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[I didn’t plan to build a compiler — I just wanted to maximize out of the AI agents I had.]]></summary></entry><entry><title type="html">Launching the Shuttle with 1970 Code: How We Revived a Forgotten NASA Program</title><link href="https://bes-dev.com/posts/shuttle-launch-1970/" rel="alternate" type="text/html" title="Launching the Shuttle with 1970 Code: How We Revived a Forgotten NASA Program" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>https://bes-dev.com/posts/shuttle-launch-1970</id><content type="html" xml:base="https://bes-dev.com/posts/shuttle-launch-1970/"><![CDATA[<blockquote>
  <p><em>T+0.0. ALT 10. VEL 0. GAM 90.0.</em>
<em>Vertical launch. Cape Canaveral, Pad 39.</em>
<em>Computer: not a UNIVAC 1100.</em></p>
</blockquote>

<p><em>“I have not been able to get the test case from the report working yet.”</em></p>

<p>Ralph Carmichael, author of PDAS – the largest public archive of aerospace software. Program MSC-13914 is listed on his site as “work in progress.” Since 2009. Seventeen years.</p>

<p>The program was written in 1970. In FORTRAN IV. For a UNIVAC 1100 computer that hasn’t existed for half a century. Documentation – four volumes, 700 pages, test data preserved only in illegible dot-matrix printer scans. Nobody has been able to run it.</p>

<h2 id="the-archive">The Archive</h2>

<p>In 2024, NASA published the COSMIC archive – a catalog of software the agency distributed through the University of Georgia from the 1960s through the 1990s. Hundreds of programs for computing orbits, aerodynamics, structural loads. Most in FORTRAN, targeting long-vanished machines.</p>

<p>Among them – MSC-13914. Space Shuttle Synthesis Program. Developed by General Dynamics in 1970 under contract NAS9-11193 for the Manned Spacecraft Center in Houston. 7,811 lines of FORTRAN IV. Automates trajectory, weight, and performance calculations for a two-stage Space Shuttle system.</p>

<ol>
  <li>The Shuttle hadn’t flown yet – Columbia’s first flight was still 11 years away. The program was built for predesign studies: “will this configuration work?” Not for controlling a real flight – for evaluating concepts.</li>
</ol>

<p>I found it as a single text file. 608 kilobytes. ELT format – a punch card image for the UNIVAC 1100 loader. No Makefile, no readable documentation. Just code.</p>

<h2 id="autopsy">Autopsy</h2>

<p>Remarkably, after nearly sixty years, getting the program to run on a modern computer required no major surgery. Ten fixes total. None touching logic – only half-century-old syntax. <code class="language-plaintext highlighter-rouge">IFIX()</code> instead of <code class="language-plaintext highlighter-rouge">INT()</code>. <code class="language-plaintext highlighter-rouge">ALOG()</code> instead of <code class="language-plaintext highlighter-rouge">LOG()</code>. A <code class="language-plaintext highlighter-rouge">PUNCH</code> statement for card output. A typo in the original: <code class="language-plaintext highlighter-rouge">HARTBL87)</code> instead of <code class="language-plaintext highlighter-rouge">HARTBL(7)</code>. UNIVAC loader directives that have nothing to do with FORTRAN.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gfortran -ffixed-form -std=legacy -fno-automatic -o sssp sssp_fixed.f
</code></pre></div></div>

<p>It compiles. 2026, Apple Silicon, macOS.</p>

<p>But <code class="language-plaintext highlighter-rouge">-fno-automatic</code> isn’t cosmetic. It’s the key to everything. Original FORTRAN IV on UNIVAC stored local variables statically. Subroutine <code class="language-plaintext highlighter-rouge">STORE</code> saved orbiter data through entry point <code class="language-plaintext highlighter-rouge">ORBSTO</code> and restored it through <code class="language-plaintext highlighter-rouge">ORBCAL</code> – a different entry point in the same subroutine. Without <code class="language-plaintext highlighter-rouge">-fno-automatic</code>, gfortran puts local variables on the stack, and data evaporates between calls. The program compiles, runs, and produces garbage. A silent, insidious bug that Ralph Carmichael probably never found.</p>

<h2 id="dead-end">Dead End</h2>

<p>First quest cleared. The program builds. But running it requires input data – 300 weight coefficients, aerodynamic tables, a pitch schedule.</p>

<p>The original test case exists – in Appendix VII of NASA CR-114985. I found the PDF. Opened it. Scanned pages. Dot-matrix printouts. Every page marked: “REPRODUCIBILITY OF THE ORIGINAL PAGE IS POOR.” Digits blur together. 3 is indistinguishable from 8.</p>

<p>But even if the scans were legible – the 1970 test case doesn’t describe the real Shuttle. It’s a General Dynamics concept: a fully reusable two-stage system, both stages winged. Nothing in common with what actually flew in 1981.</p>

<p>If we can’t read the old data – we’ll create new data. For the real Shuttle.</p>

<h2 id="wikipedia-as-a-data-source">Wikipedia as a Data Source</h2>

<p>We have a program that can calculate weights and trajectories of spacecraft. But it has 300 tuning coefficients and not a single known value.</p>

<p>What we do have are answers. Not the coefficients – but what the right coefficients produce.</p>

<p>Orbiter dry weight: 151,000 pounds. Wikipedia. Wing weight: 16,200 pounds. NASA weight statements. Thermal protection: 18,895 pounds. Wikipedia. RS-25 engines: 7,004 pounds each, 512,300 pounds vacuum thrust, ISP 452.3 seconds. Wikipedia. Solid rocket boosters: 1,100,000 pounds of propellant, 123 seconds burn time. Wikipedia. Launch pad LC-39 coordinates: 28.608 N, 80.604 W. Google Maps.</p>

<p>Data from an encyclopedia and fact sheets. Rounded. Averaged across missions. But sufficient.</p>

<h2 id="program-in-the-loop">Program-in-the-Loop</h2>

<p>The classic approach to reviving such code: read the documentation, understand the meaning of each of the 300 coefficients, tune them by hand. For a program with illegible documentation and a half-century gap in context – a dead end.</p>

<p>A different approach: reverse engineering. We know the answer – wing weight equals 16,200 pounds. We know the formula inside SSSP: WWING = C(1) * f(geometry) + C(2) * area + C(3). We need to find C(1), C(2), C(3) such that the formula gives the right answer.</p>

<p>Not for one component – for all of them simultaneously. 14 target categories: wing, fuselage, thermal protection, landing gear, engines, hydraulics, electrical, avionics, orientation control, crew, payload. Each category – a number from Wikipedia. Each depends on several C() coefficients. The coefficients are coupled: changing C(15) (fuselage structure) affects total weight, which affects required propellant, which affects tank volume, which affects body surface area, which affects thermal protection.</p>

<p>A nonlinear system with cross-dependencies. Manual tuning is impossible – changing one coefficient shifts ten others.</p>

<p>Solution: scipy.optimize.minimize, Nelder-Mead method. The objective function is the sum of squared relative errors across all 14 categories. At each iteration of the optimizer:</p>

<ol>
  <li>Generate an SSSP input file with current C() coefficients</li>
  <li>Run the Fortran program as a subprocess</li>
  <li>Parse stdout – find “WEIGHT BREAKDOWN – ORBITAL STAGE”</li>
  <li>Extract 14 numbers, compare against Wikipedia targets</li>
  <li>Return the error to the optimizer</li>
</ol>

<p>SSSP-in-the-loop. A 1970 Fortran program as a black box inside a 2026 Python optimizer. Each call – 2 seconds. 429 calls. Five minutes.</p>

<p>The same method – for the trajectory. Different target points: altitude, velocity, and flight path angle at T+60, T+90, T+120, T+200, T+300 seconds – from STS mission data. Different parameters: average SRB thrust, specific impulse, liftoff weight. Same loop: generate input, run SSSP, parse trajectory, compare against targets, minimize error.</p>

<p>And for aerodynamics – the same method. Drag coefficient scale as a parameter, target points – altitude and flight path angle from real flight reconstructions.</p>

<p>Three optimizers. One principle: don’t guess parameters – fit them to known answers.</p>

<h2 id="not-overfitting">Not Overfitting</h2>

<p>A critical question: did we simply rig the numbers to produce a pretty result? Did we create an unphysical vehicle that exists only in the optimizer’s parameter space?</p>

<p>Every coefficient found maps to real components.</p>

<p>C(15) = 39,448 pounds – “basic fuselage structure.” This isn’t a magic number. It’s forward fuselage (10,800) + mid fuselage (8,000) + aft fuselage (8,000) + payload bay doors (4,600) + secondary structure (8,048). A sum of real components, each known from NASA weight statements.</p>

<p>C(180) = 1.44 pounds per square foot – thermal protection insulation unit weight. Real HRSI tiles: 1.5 lb/ft2. LRSI: 0.8. RCC on the nose and leading edges: 4.0. Our value is a weighted average. An engineer designing thermal protection would arrive at a similar number.</p>

<p>Average SRB thrust: 2,814,000 pounds. Real peak: 3,300,000, at liftoff: 2,700,000. Our value falls between them. For a constant-thrust model, this is the only physically correct approximation.</p>

<p>Aerodynamics: the optimizer returned CD_SCALE = 1.0. It didn’t touch the drag coefficients. Open-source data used as-is. Transonic drag rise of 2.2x – within the 2.0-2.5x range typical of launch vehicles.</p>

<p>The optimizer didn’t create an unphysical vehicle. It found the Shuttle in parameter space. Every coefficient is explainable. Every one falls within real engineering ranges. An aerospace engineer looking at these numbers would recognize the configuration.</p>

<p>This isn’t overfitting. It’s calibration.</p>

<h2 id="the-flight">The Flight</h2>

<p>The weight model is statics. Dynamics is where it gets interesting.</p>

<p>SSSP contains a complete trajectory module: three-dimensional equations of motion over a rotating oblate Earth with J2-J4 gravity harmonics. A fifth-order Runge-Kutta-Butcher integrator with adaptive step control – 245 lines, double precision for intermediate values (a luxury in 1970). Bilinear interpolation of aero coefficients by Mach number and angle of attack. Cape Kennedy Reference Atmosphere 1963 – 193 data points from sea level to 2.3 million feet. A propulsion model with ten operating modes.</p>

<p>All of it – original 1970 code. Not a single formula changed.</p>

<p>Getting it to work was its own adventure: dozens of undocumented flags, each silently disabling an entire module if not set correctly. Defaults in subroutine TRAJC that overwrite input parameters after they’re read. A singularity at zero velocity. But that’s engineering archaeology, of interest mainly to specialists. Details are in the EXPERIMENT_LOG on GitHub.</p>

<p>The result: aerodynamics from open data (11 points of axial force coefficient vs Mach), atmosphere from BLOCK DATA, pitch schedule from published I-load profiles of the real Shuttle – alpha=-2 degrees at max-Q, alpha=-4 degrees post max-Q, alpha=+3.5 degrees during second stage. All loaded through standard $DATA1 input blocks, using the original ALPC=22 mode – one of 31 attitude control modes built into the program.</p>

<p>T+0. Vertical launch. Three SSMEs and two SRBs. Liftoff mass 4,575,000 pounds.</p>

<p>T+60. Max-Q. Altitude 13.5 km. Velocity 549 m/s. Flight path angle 53 degrees. Gravity turn.</p>

<p>T+123. SRB separation. Altitude 50 km. Velocity 1,895 m/s. Boosters jettisoned – mass drops by 400,000 pounds in an instant.</p>

<p>T+300. Altitude 115 km. Velocity 5,205 m/s. Flight path angle 0 degrees. Horizontal flight. No atmosphere left – drag is zero.</p>

<p>T+389. MECO. Main Engine Cut-Off. Velocity 7,803 m/s. Orbital. Engines silent.</p>

<p>T+500. Altitude 109 km. Velocity 7,803 m/s. Flight path angle 0.4 degrees. Orbital coast. Stable.</p>

<p>SSSP has no built-in MECO – the 1970 program assumed engines burn until propellant depletion. Without MECO, the rocket accelerates to twice orbital velocity, and the program reports “VEHICLE ATTEMPTED SUBTERRANEAN FLIGHT.” Five lines in subroutine PRPSN: if velocity exceeds orbital – thrust equals zero.</p>

<h2 id="accuracy-and-its-limits">Accuracy and Its Limits</h2>

<p>Gravity turn: GAM=75.6 degrees at T+30s versus 75.0 real. Half a percent error.</p>

<p>Velocity at T+200s: 9,843 m/s versus 10,000 real. 1.6% error.</p>

<p>MECO altitude: 109 km versus 115 km real. 5% error.</p>

<p>The discrepancies are explainable from two sides.</p>

<p>On our side – SSSP doesn’t model SSME throttle-down through max-Q (the real Shuttle reduces thrust to 67%; we hold 100%). Doesn’t model closed-loop PEG guidance – the real Shuttle computes thrust direction in real time; we use a fixed alpha(Mach) table. Doesn’t model wind loads, roll program, yaw steering.</p>

<p>But there’s another side. Our “real data” are numbers from Wikipedia and NASA press kits. Rounded. Averaged across 135 missions, each flying with its own payload mass, in its own weather, to its own orbit. SRB separation altitude varied from 150,000 to 174,000 feet between missions – a 15% spread. Some of our target points (T+200s, T+300s) are interpolations, not measurements from a specific flight. We couldn’t find detailed per-second telemetry from real missions in the public domain.</p>

<p>We’re comparing an approximate model against approximate data. The 5-15% error isn’t just a limitation of SSSP. It’s also the uncertainty of the targets themselves.</p>

<p>For a predesign tool – that’s enough. It answers the question “will this configuration work?” And it answers correctly.</p>

<hr />

<p>The experiment started as curiosity – code from a NASA archive, illegible scans, a forgotten tool. It ended with a working simulation of a real spacecraft’s flight.</p>

<p>The Space Shuttle program ended in 2011. 135 missions. The technology that defined an era of crewed spaceflight. The Artemis program is preparing a return to the Moon – SLS uses the same modified RS-25 engines and 5-segment SRBs.</p>

<p>And program MSC-13914, written 11 years before Columbia’s first flight, can still calculate its trajectory. On a laptop. In 10 seconds.</p>

<p>In subroutine SOLVE, line 6439, there’s a construct <code class="language-plaintext highlighter-rouge">IF(0) 5005,10,5005</code> – an arithmetic IF that always branches to label 10. The developer’s comment: <em>“THIS CARD NECESSARY TO PROGRAM AROUND COMPILER OPTIMIZATION BUG ON UNIVAC 1108 EXEC II.”</em> A workaround for a 1970 compiler bug. Still works in gfortran 2026. We didn’t touch it.</p>

<p>7,811 lines. 44 subroutines. 10 fixes to compile. Three optimizers for calibration. One <code class="language-plaintext highlighter-rouge">make run</code> to launch.</p>

<p>Video: <a href="https://youtu.be/aavo1POfrZg">youtu.be/aavo1POfrZg</a></p>

<p>Code: <a href="https://github.com/bes-dev/sssp">github.com/bes-dev/sssp</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[T+0.0. ALT 10. VEL 0. GAM 90.0. Vertical launch. Cape Canaveral, Pad 39. Computer: not a UNIVAC 1100.]]></summary></entry><entry><title type="html">Environment Over Intelligence: How a 27B Model on a Laptop Builds Mobile Apps</title><link href="https://bes-dev.com/posts/harness-matters/" rel="alternate" type="text/html" title="Environment Over Intelligence: How a 27B Model on a Laptop Builds Mobile Apps" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://bes-dev.com/posts/harness-matters</id><content type="html" xml:base="https://bes-dev.com/posts/harness-matters/"><![CDATA[<p>Late at night. The room is dark. A phone simulator loads on my laptop screen — running a habit tracker.
With bar charts for daily stats, pie charts for categories, a calendar with colored dots for each habit, and streak counting.
An hour earlier none of this existed. Just a terminal with a blinking cursor and a single line: <code class="language-plaintext highlighter-rouge">/build habtrack "Habit tracker with daily check-ins, streak counting, weekly bar chart..."</code>.</p>

<p>52 minutes. No cloud providers. No API keys. Just a 27-billion-parameter model running right on my laptop.</p>

<p>I didn’t babysit the process. Didn’t fix bugs in a chat window. Didn’t yell at the model in all caps for building the wrong thing.
Fired off one command, switched to another task, checked back an hour later — the app was ready to test. Before that, an RSS reader: one hour. Before that, a calculator: 37 minutes.</p>

<p>Three apps in one evening. Zero hands-on. Not a single dollar spent on tokens. And the model is the least interesting part of this story.</p>

<h2 id="the-hired-hand">The Hired Hand</h2>

<p>For the past few months I’ve been experimenting with an autonomous AI publisher for mobile apps.
<a href="https://bes-dev.com/posts/etnamute/">Etnamute</a> is one of the intermediate iterations, released as an open source agent built on Claude Code — 4,500 lines of instructions on how to build mobile applications.
A single run takes anywhere from thirty minutes to two hours — a full cycle from idea to a finished cross-platform React Native app.</p>

<p>It works. But it’s a hired hand. We pay Anthropic for access to the models that serve as the agent’s brain.
And the company can raise prices at any moment, decide that my use case violates their ToS, or the service can go down under load at the exact moment I need to run the agent.
And my employee vanishes. Can’t do the job. Not because it was poorly built. Because I don’t own it.</p>

<p>When Qwen3.6-27B dropped (a fresh open source model from Alibaba) and Anthropic’s servers went down under load yet again, I started wondering: can I move this entire machinery onto hardware sitting right on my desk?
A model whose weights live on my hard drive. Local inference on my machine. Zero dependencies on external providers or APIs.</p>

<p>No, you can’t move it. But you can reinvent it.</p>

<h2 id="clean-slate">Clean Slate</h2>

<p>The very first idea — just take the existing Etnamute and feed it to Qwen3.6-27B.
Fired up the model in opencode. Typed <code class="language-plaintext highlighter-rouge">/build-app feedwise "RSS reader with support for major feed formats. Dark theme."</code>. Nothing.
The model went into deep meditation. Not metaphorically — it literally stopped responding, lost in reasoning chains that burned through the entire 128K token context before writing a single line of code.
The problem isn’t that the model couldn’t understand the instructions (a 27B model is perfectly capable of understanding what’s in Etnamute) — it’s the cascade effect: many rules spawn complex reasoning chains, and reasoning chains devour context and available compute.</p>

<p>The brute-force approach doesn’t work. But even if it did, there’s the controllability problem. OpenCode with oh-my-opencode plugins is a big system with its own logic.
MCP servers, background sub-agents, hooks that modify model behavior on top of my prompts, a long system prompt. I had no control over what exactly happens between “I asked” and “the model did.” A black box inside a black box.</p>

<p>I didn’t need a Swiss Army knife with a million modes. I needed a system where I control every layer.
I switched to <a href="https://github.com/badlogic/pi-mono">Pi</a> — a minimalist CLI agent.
Four tools: read, write, edit, bash. System prompt — under a thousand tokens. Nothing else.
A thin layer between my instructions and the LLM that lets me understand and control everything from prompt input to final output.</p>

<h2 id="strip-away-everything-unnecessary">Strip Away Everything Unnecessary</h2>

<p>Michelangelo said the sculpture is already inside the stone — you just need to remove the excess marble.
Turned out the same was true for Etnamute’s instructions. Not adapting 4,500 lines, but finding within them those without which a correct app cannot be built.
Each rule — one question: what breaks if you remove it?</p>

<p>User interview: cut. The model can generate a spec directly from the user’s prompt.
Market research: cut. RevenueCat integration: cut. None of that is needed for an MVP.
E2E testing with visual analysis: painful, but cut too. Replaced with a shell script running grep checks and a smoke test for runtime errors.
Instead of NativeWind, which gave the 27B model serious trouble, we use React Native Paper — Material Design out of the box.
Three-level feature priorities — MUST, SHOULD, WONT: cut the middle tier. The gray zone for a small model means stubs, not implementations.</p>

<p>Total: 935 lines. Five thousand tokens. Five percent of the context window instead of “context exhausted.”</p>

<p>With a big model you think about what else to add. With a small one: what else to cut.</p>

<p>The compressed prompt worked. First test — 13 hours generating an RSS reader, ending with a white screen on launch.
NativeWind, which I hadn’t cut yet, was silently crashing the renderer. DOMParser, which doesn’t exist in Hermes, was dropping the app on the first feed addition.
Second run after accounting for these — 2 hours, works immediately. Third — a working pomodoro timer, a different class of app with its own set of problems.
Then — a calculator, then — a habit tracker with charts.</p>

<p>Each run isn’t just a test of the model. It’s a test of the environment. An app breaks not because the model is dumb — but because the environment didn’t warn about potential problems.
White screen from NativeWind — a line “use React Native Paper” in the prompt. DOMParser — have the model use fast-xml-parser instead. Buttons in a column — a flex grid layout template.
Each bug isn’t an app fix, isn’t a long debugging session with ALL CAPS in the chat. No. We fix the harness. Apps are disposable. The harness is cumulative.</p>

<p>But this isn’t vibe coding in reverse — not “got an error, shoved a fix into the prompt, repeat until convergence.”
Each rule must earn its place in the instructions. If the bug is specific, it means we missed something important in the overall methodology.
DOMParser crashes Hermes — that’s not a rule “don’t use DOMParser.” It’s a runtime pitfalls table with ten entries: APIs that compile but don’t exist at runtime.
Buttons in a column — that’s not a rule that creates grid layouts. It’s a section explaining how layouts work and the general principles behind them.</p>

<p>The harness isn’t a cookbook of errors and fixes. It’s a description of a <a href="https://bes-dev.com/posts/autoharness/">methodology</a> that sidesteps entire classes of problems.
A specific fix helps with one specific problem. Well-formulated principles close hundreds.</p>

<p>After several runs the prompts stabilized. New generations didn’t produce unique problems — all the pitfalls had been covered not by fixes, but by rules.
But for generating complex apps, our approach wasn’t enough.</p>

<h2 id="when-memory-runs-out">When Memory Runs Out</h2>

<p>All this time our harness was a single instruction describing how to properly generate mobile apps.
A monolithic instruction that the Pi agent followed during generation. One single agent.
Everything lived in one context: the spec, reasoning about the plan, package installation logs, TypeScript errors it had already fixed, old file versions it had already rewritten.
In one run the agent burned through 12 million tokens — most of them on repeated <code class="language-plaintext highlighter-rouge">npm install</code> attempts with wrong versions.</p>

<p>The context window is a budget. Not a buffer you can dump into endlessly, but a budget that runs out.
And in a monolithic architecture it runs out at the most interesting part.</p>

<p>To solve this, I did something the author of Pi <a href="https://mariozechner.at/posts/2025-11-30-pi-coding-agent/">considers an anti-pattern</a>: I split the monolith into sub-agents.</p>

<p>Each pipeline step is a separate Pi process with a clean context.
The PRD agent sees only the app idea and nothing else.
The skeleton agent — only the spec.
The logic agent — TypeScript interfaces.
The UI agent — types and stores.
A few dozen lines of prompt per agent.
Each agent sees exactly what it needs to do its job. Nothing more.</p>

<p>Mario is right: parallel agents working in the same layer can generate garbage.
But my agents aren’t parallel. They’re sequential and layered.
Each with its own zone of responsibility: Logic doesn’t touch UI, UI doesn’t touch stores.
The contract between agents — TS interfaces created by the skeleton agent first.
If the UI agent calls a method that doesn’t exist in the store — <code class="language-plaintext highlighter-rouge">tsc</code> catches it in a second.</p>

<p>Context passes through the file system. One agent writes a file. The next one reads it.
No variables, no shared memory, no intermediate formats. Unix way. Simple debugging.</p>

<p>Sub-agents solved the context problem. But created a control problem.</p>

<h2 id="rails-instead-of-a-steering-wheel">Rails Instead of a Steering Wheel</h2>

<p>Launching sub-agents in Pi is done through extensions, and above them sits a parent agent — the main Pi process from which everything is launched.
Inside it — an orchestrating model that was supposed to manage the process.
In practice, it hijacked it.
It would see a sub-agent’s problem and try to solve it itself, through long trial and error.
Often it burned millions of tokens on something a shell script could handle in seconds — it just never ran the script.</p>

<p>A parent agent is an LLM that makes decisions about the process.
And every such decision costs tokens.
And every such decision can be wrong.
The model decides “let’s try a different approach” — and gets stuck in an infinite loop.
The model can skip pipeline steps, and does.
The model refuses to delegate to a sub-agent and breaks everything itself.</p>

<p>I solved this with finite state machines.
A deterministic state machine where transitions are defined in advance, conditions are formalized, and terminal states are inevitable.</p>

<p>The model can’t skip verification — there’s no transition bypassing that state.
Can’t jump straight to UI development when the app skeleton isn’t ready — that transition is invalid.
Can’t endlessly fix code — after several failed attempts, the machine automatically transitions to an error state.</p>

<p>The model doesn’t decide what happens next. The machine decides. The model just executes the current step’s task.</p>

<p>For this I built a small framework — <a href="https://github.com/bes-dev/reharness">reharness</a>.
An FSM engine for orchestrating Pi agents, deterministic functions, and the like.</p>

<h2 id="not-vibe-coding">Not Vibe Coding</h2>

<p>It’s worth noting that our approach is fundamentally different from what many people have gotten used to as vibe coding.</p>

<p>Vibe coding is when you chat with an agent, ask it to build something, get a result you don’t like, ask it to redo, still wrong, you get frustrated and start writing in ALL CAPS — and every unstructured comment of yours pollutes the context and degrades the quality of the next response.
You pay for every iteration. The provider is happy.</p>

<p>Our pipelines are different. You don’t chat with the agent in real time. You design the environment: which prompts, in what order, with what checks, with what transition conditions.
Then you set the task. The machine attempts to solve it, operating within the designed environment.
You go about your business in the meantime. If something breaks — you fix the environment, not the app.
Add a check. Refine a prompt. Run again.</p>

<p>This isn’t “vibes.” This is engineering. Building a vertical that predictably produces results.
And the fundamental difference is in scalability. Vibe coding scales linearly: more apps = more of your time in the chat.
Pipelines scale differently: every improvement to the environment benefits all future app generations.</p>

<h2 id="the-skeleton-inside-the-stone">The Skeleton Inside the Stone</h2>

<p>Pipeline: <code class="language-plaintext highlighter-rouge">scaffold → prd → skeleton → logic → ui → verify ↔ fix → complete</code>. Each step is a pure function of the file system: files in, files out.
No information passes outside this rule. Makes debugging straightforward.</p>

<p>Before anyone writes a single line of implementation, the skeleton agent creates all necessary TypeScript interfaces with JSDoc documentation.
These aren’t TODO stubs — they’re contracts. What each method is expected to do, what its edge cases are, what happens under concurrent calls.
The logic agent implements these contracts, and the UI agent builds screens on top of them. If anyone violates the contract — a simple <code class="language-plaintext highlighter-rouge">tsc</code> check catches it immediately.</p>

<p>Why does this matter? The skeleton constrains the solution space.
If the model sees an interface with three methods — it will implement those three methods.
Give it a blank slate — it implements whatever it sees fit, forgetting half of it along the way and making up the rest.
Types are rails for each subsequent agent. This is the core principle of our entire approach: the narrower the corridor, the more precise the movement.</p>

<p>The second pillar is scoping.
Each agent sees only what it needs to do its job.
The UI agent doesn’t care what problems the logic agent ran into — that’s not its zone of responsibility.</p>

<h2 id="trust-but-verify">Trust, but Verify</h2>

<p>Generating code is only half the job. The other half — figuring out whether it actually works.</p>

<p>In the full Etnamute, a <a href="https://bes-dev.com/posts/teaching-claude-to-test/">dedicated QA agent</a> handles this — it can launch the app, analyze screenshots, tap buttons simulating user actions, and compare results against expectations.
Heavy artillery that even in Etnamute runs slowly but catches bugs with high confidence. On a local 27B model it’s an unaffordable luxury — we don’t have that much compute.</p>

<p>Here we make do with smoke testing: launch the app in a headless simulator and check the logs.
If it crashed — we see the stack trace. Didn’t crash — we call it working.
Five mechanical checks with zero LLM involvement: tsc, bundle, runtime smoke, stub detection, anti-pattern scanning.</p>

<p>But catching bugs isn’t enough — you need to fix them. And here another contract turned out to be critical — the file <code class="language-plaintext highlighter-rouge">verify-report.md</code>.
During testing, the verify step writes a detailed report when it finds an error — which file, which line, what type.
The fix agent reads it and makes surgical fixes, without wasting time exploring the codebase or guessing at the problem.</p>

<p>Before this contract existed, the fix agent had free rein to run even longer than all other generation steps combined.
Adding this single file cut the cost of error correction by an order of magnitude.</p>

<h2 id="the-environment-remembers">The Environment Remembers</h2>

<p>Between the first and last run, the model didn’t get any smarter. We used the same Qwen3.6-27B throughout. What changed was the environment.</p>

<p>DOMParser crashes Hermes — that’s not an app bug, it’s a gap in the prompt that didn’t mention fast-xml-parser.
Calculator buttons in a column instead of the familiar button grid — our UI agent didn’t receive instructions on how to design that type of interface.
app.json conflicts with Expo Router — that’s not an app bug, it’s a scaffold bug that didn’t prohibit creating that file.</p>

<p>Every bug becomes a line in a prompt, a check in the verify scripts, or code in the scaffold.
The model doesn’t learn between sessions — it has no memory, and we don’t have the resources to fine-tune.
But the environment remembers the minefield and won’t let anyone step on the same mine twice.
Shell scripts don’t forget checks. Grep doesn’t miss patterns.
The FSM won’t let you skip steps or call them out of order.</p>

<p>I truly saw this the moment when, after the first 13-hour run that produced a broken app, the agent spent just 37 minutes generating a fully working calculator.
The pipeline ran, verify caught one error (SafeAreaView from the wrong package), the fix agent surgically replaced one import line, verify passed, smoke passed, the app works.
37 minutes from command to working application. The model didn’t do anything remarkable — it simply fulfilled the contracts. The environment didn’t let it fail.</p>

<p>And this is the key insight. Improving the environment closes the gap between open and closed models more effectively than improving the model itself.
Scaffold installs the right packages — the model doesn’t need to guess versions through trial and error.
Verify catches errors deterministically — the model won’t waste time on diagnostics. The model doesn’t need to remember the pipeline step order — the FSM remembers for it.</p>

<p>Large models make us lazy. Massive context — no need to think about prompt structure, and you can fix problems by piling on even more instructions.
No need to design the environment, because the big model “will figure it out anyway.” Small models don’t forgive that laziness. Every token is expensive. Every rule must earn its place.
Every check must pay for itself — if its false positive rate is higher than the frequency of real bugs, it hurts more than it helps.</p>

<p>And the most interesting part: systems designed under these constraints work better for <em>any</em> model. Minimalism doesn’t hinder a strong model, but it saves a weak one. This isn’t a limitation. It’s a design principle.</p>

<h2 id="six-files">Six Files</h2>

<p>Three apps, all built in one evening:</p>

<table>
  <thead>
    <tr>
      <th>App</th>
      <th>What’s inside</th>
      <th>Time</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Feedwise</strong></td>
      <td>RSS + Atom, bookmarks, full article text</td>
      <td>~1 hour</td>
    </tr>
    <tr>
      <td><strong>Calculus</strong></td>
      <td>iOS-style calculator</td>
      <td>37 min</td>
    </tr>
    <tr>
      <td><strong>HabTrack</strong></td>
      <td>Bar chart, pie chart, calendar, streaks</td>
      <td>52 min</td>
    </tr>
  </tbody>
</table>

<p>The trend is obvious. Models are getting cheaper. Hardware is getting cheaper. Quality is going up.
Qwen3.6-27B, released just days ago, can generate mobile apps in reasonable time on an ordinary laptop.
What will local models be capable of in a year? Two?</p>

<p>Last night I looked at the habit tracker and thought not about which model built it. I thought about the six markdown files that told it how. A few dozen lines each. Not a single line wasted.</p>

<p>Code: <a href="https://github.com/bes-dev/reharness">github.com/bes-dev/reharness</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Late at night. The room is dark. A phone simulator loads on my laptop screen — running a habit tracker. With bar charts for daily stats, pie charts for categories, a calendar with colored dots for each habit, and streak counting. An hour earlier none of this existed. Just a terminal with a blinking cursor and a single line: /build habtrack "Habit tracker with daily check-ins, streak counting, weekly bar chart...".]]></summary></entry><entry><title type="html">Teaching an Agent to Improve Its Own Scaffolding Between Tasks</title><link href="https://bes-dev.com/posts/autoharness/" rel="alternate" type="text/html" title="Teaching an Agent to Improve Its Own Scaffolding Between Tasks" /><published>2026-04-22T00:00:00+00:00</published><updated>2026-04-22T00:00:00+00:00</updated><id>https://bes-dev.com/posts/autoharness</id><content type="html" xml:base="https://bes-dev.com/posts/autoharness/"><![CDATA[<p>Most of my time I spend vibe-coding on small projects — it works great there. Drop in some context, get code back, tweak it, ship it. But at some point I got curious: how does this hold up on a genuinely large codebase with serious legacy baggage?</p>

<p>So I picked one. Multiple languages, multiple databases, zero documentation, an architecture that clearly accumulated over years without any coherent plan. The kind of project where most people turn around and walk away already at the “how do I even run this” stage. Over a weekend, Claude Code and I rewrote it from the foundation up and got a working prototype running.</p>

<p>This isn’t a post about how fast things are now. It’s about a specific technique without which that scale would have been impossible.</p>

<h2 id="the-problem-with-large-codebases">The problem with large codebases</h2>

<p>Vibe-coding works on small projects because the context fits in your head and in the context window at the same time. On large legacy code that breaks fast: the agent doesn’t know what’s already in the codebase, repeats the same mistakes, and loses the thread between tasks.</p>

<p>The obvious answer is more context in the prompt. But that scales poorly and gets expensive. The right answer is to structure what the agent needs to know — and automatically update that knowledge between tasks.</p>

<h2 id="the-architecture-three-phases-per-ticket">The architecture: three phases per ticket</h2>

<p>The key insight: each logical task needs its own session with a properly prepared scaffold. Not one giant context for the entire project, but targeted preparation for the specific ticket at hand.</p>

<p><strong>Phase 1 — Harness assembly.</strong> Before each task, a supervisor agent reads the project (manifests, schemas, API surface, existing scaffold) and does something like a deep research pass: how do other people solve this kind of problem in this stack, what MCP servers exist for the tools we need, what anti-patterns are typical. The output is a harness tailored to the ticket:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CLAUDE.md</code> with project context and critical rules</li>
  <li>Skills with step-by-step instructions for the type of work (migration, refactor, external API integration — each has its own structure)</li>
  <li>Rules — one file per anti-pattern: “Never X, because Y”</li>
  <li>MCP servers with documentation for the relevant libraries</li>
</ul>

<p>The harness isn’t rebuilt from scratch each time — it reads what already exists and adds only what’s missing for the new task.</p>

<p><strong>Phase 2 — Work session.</strong> A normal interactive Claude Code session. Claude works within the prepared context, I steer and course-correct. Nothing magical.</p>

<p><strong>Phase 3 — Analysis and update.</strong> After the session, the supervisor reads the JSONL log (Claude Code writes a full transcript of every session) and looks for signals:</p>

<ul>
  <li>Tool results with <code class="language-plaintext highlighter-rouge">is_error: true</code></li>
  <li>User corrections — messages containing “no”, “wrong”, “instead”</li>
  <li>Retry patterns — the same tool called with near-identical arguments across several consecutive exchanges</li>
  <li>Rule violations — actions that contradict rules already defined in the harness</li>
</ul>

<p>A single incident gets documented as inherent complexity. A repeating pattern (×2 or more) becomes a concrete harness patch: a new rule file, a strengthened existing rule, an MCP addition for missing docs, or an entry in the project Wiki.</p>

<h2 id="wiki-as-compiled-memory">Wiki as compiled memory</h2>

<p>Between tasks the harness doesn’t reset — it accumulates. The key artifact is <code class="language-plaintext highlighter-rouge">project-wiki.md</code>, a living document of compiled project knowledge. Four sections:</p>

<p><strong>Reuse Map</strong> — what’s already implemented and where. Agent tried to write its own implementation on top of something that already existed? The path goes here, and the next task reads it first — before any research.</p>

<p><strong>Anti-Patterns</strong> — not generic “don’t use raw SQL”, but project-specific: “this codebase already has a client for X right here — don’t reinvent it.” Rules in <code class="language-plaintext highlighter-rouge">.claude/rules/</code> hold general principles; the Wiki holds what’s specific to this project.</p>

<p><strong>Tips</strong> — approaches that worked in non-obvious ways.</p>

<p><strong>Gotchas</strong> — non-linear dependencies, initialization order, hidden contracts between components.</p>

<p>The idea comes directly from Karpathy’s LLM Wiki: don’t store raw observations and RAG over them — compile them into a structured knowledge base that updates incrementally. The Wiki is read before each new task and before each iteration inside loop skills. The effect is noticeable by the third or fourth task: the agent stops hitting the same walls and navigates the codebase faster — not because it got smarter, but because the accumulated context won’t let it forget what’s already been figured out.</p>

<p>From Karpathy’s AutoResearch comes another idea: each successful iteration becomes the new baseline. Here that’s the harness — it never rolls back, only grows. By task N the agent operates as if it’s done this in the project many times before.</p>

<h2 id="the-upshot">The upshot</h2>

<p>By the end of the weekend I had a strong sense of working with someone who was gradually learning the project — not a tool that starts from zero every time.</p>

<p>Building a harness for each task feels like overhead, but in practice it pays for itself by the second or third task within a project. And the most surprising part: harness assembly itself is fully automatable. You don’t need domain expertise in every stack — you just need to describe the scope of work and let the agent do the deep research. It finds what you need more accurately than you would in the same time.</p>

<p>This direction — automatic context adaptation per task, accumulating project memory across sessions — feels like where the next meaningful jump in AI-assisted productivity is going to come from.</p>

<hr />

<p>Code: <a href="https://github.com/bes-dev/autoharness">github.com/bes-dev/autoharness</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Most of my time I spend vibe-coding on small projects — it works great there. Drop in some context, get code back, tweak it, ship it. But at some point I got curious: how does this hold up on a genuinely large codebase with serious legacy baggage?]]></summary></entry><entry><title type="html">How I Built a Junior QA Engineer on Top of Claude Code</title><link href="https://bes-dev.com/posts/teaching-claude-to-test/" rel="alternate" type="text/html" title="How I Built a Junior QA Engineer on Top of Claude Code" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://bes-dev.com/posts/teaching-claude-to-test</id><content type="html" xml:base="https://bes-dev.com/posts/teaching-claude-to-test/"><![CDATA[<p>I’m building Etnamute — a system where AI agents write mobile apps from idea to App Store. User interview, market research, spec, design, code, marketing — all automated. My role came down to one thing: open the finished app and check if it works.</p>

<p>I used to joke that while my AI agents do the real work, I’m their errand boy — the tester. The joke stopped being funny around the third app, when I toggled the theme to dark and the screen stayed white. The agent swore everything works. Fifty tests green. The app — white.</p>

<p>I decided it was time to build one more agent. A junior QA engineer that would take over testing. Here’s how we raised it — and what it taught us.</p>

<h2 id="suspect-number-one-unit-tests">Suspect number one: unit tests</h2>

<p>First thing Claude does when you ask it to test an app — it writes unit tests. Lots of them. Fifty in five minutes. Every state action verified. Every screen renders without crash. Every button press calls the right handler.</p>

<p>Looks solid. There’s one problem.</p>

<p>The test confirms that <code class="language-plaintext highlighter-rouge">setTheme('dark')</code> writes <code class="language-plaintext highlighter-rouge">'dark'</code> to settings. Test passes. Screen stays white. The value got saved — the UI didn’t re-render. From the code’s point of view, everything is correct. From the user’s point of view — the dark theme toggle doesn’t work.</p>

<p>I asked Claude what’s going on. It said: “Visual theme application is a design gap.” Not a bug. A gap.</p>

<p>That’s when I got the fundamental problem. Claude was testing code. It should have been testing promises.</p>

<h2 id="the-case-of-ui-promises">The case of UI promises</h2>

<p>Every button on screen is a promise to the user. A toggle labeled “Dark Theme” promises the screen will go dark. A currency selector “€” promises all prices will show euros. A “Save” button promises data will be saved.</p>

<p>A human tester gets this intuitively. They look at the screen, tap the toggle, look at the result. If the toggle says one thing and the screen shows another — that’s a bug. Not a “design gap.” A bug.</p>

<p>I turned this into what I call an <strong>interaction map</strong>. Before writing any test, Claude has to go through every screen and answer one question for each interactive element: <strong>what does the user expect when they look at this?</strong> Not what the handler code does — what the button label promises.</p>

<p>Then Claude reads the code and compares. User expectation vs actual behavior. If they don’t match — it’s a broken promise.</p>

<p>The difference is subtle but it matters. A “Notifications” switch saves a setting for a background service — no visible change on screen, and the user doesn’t expect one. That’s fine. But a “Dark Theme” toggle with no visible effect — that’s a broken promise. The label says one thing, the screen shows another.</p>

<p>The first run with this rule found something real. A subscription tracker app had a currency setting. User picks euro — the spending widget on home shows <code class="language-plaintext highlighter-rouge">€15.49</code>. The subscription card right next to it — still <code class="language-plaintext highlighter-rouge">$15.49</code>. Same screen, two components, one has a hardcoded dollar sign. The interaction map caught it because it asked: “currency change — which screens does it affect?” — and checked each one.</p>

<p>Could you write a unit test that catches this? Sure. But you’d have to predict that this specific component would forget to read the currency setting. You can’t write a test for every possible oversight. A human tester solves this differently: change the currency, look at the screen. If something didn’t update — you see it right away. No prediction needed.</p>

<h2 id="you-need-hands-well-fingers">You need hands. Well, fingers.</h2>

<p>The interaction map is a plan. But the plan needs execution. Someone has to open the app and tap through it.</p>

<p>That’s what Maestro does — a UI testing framework for mobile apps. You describe a scenario in YAML: launch, tap here, type text, check what appeared. Maestro runs it on a real iOS simulator or Android emulator.</p>

<p>The key difference from unit tests: Maestro tests the built app. Same binary the user will install. If a library crashes on launch — Maestro sees it. If the keyboard covers the submit button — Maestro can’t tap it. If an animation leaves an element in the wrong spot — the check fails.</p>

<p>Here’s what it looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">launchApp</span><span class="pi">:</span>
    <span class="na">clearState</span><span class="pi">:</span> <span class="no">true</span>
<span class="pi">-</span> <span class="na">extendedWaitUntil</span><span class="pi">:</span>
    <span class="na">visible</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Add</span><span class="nv"> </span><span class="s">Subscription"</span>
    <span class="na">timeout</span><span class="pi">:</span> <span class="m">10000</span>
<span class="pi">-</span> <span class="na">tapOn</span><span class="pi">:</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">catalog-netflix"</span>
<span class="pi">-</span> <span class="na">tapOn</span><span class="pi">:</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">btn-submit"</span>
<span class="pi">-</span> <span class="na">extendedWaitUntil</span><span class="pi">:</span>
    <span class="na">visible</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Netflix"</span>
    <span class="na">timeout</span><span class="pi">:</span> <span class="m">15000</span>
<span class="pi">-</span> <span class="na">takeScreenshot</span><span class="pi">:</span> <span class="s">home_with_netflix</span>
</code></pre></div></div>

<p>Every action waits for a specific result. Not <code class="language-plaintext highlighter-rouge">sleep(3)</code> — “wait until this text shows up.” If it doesn’t — the test fails and you get a screenshot of what the screen actually showed.</p>

<p>The scenarios come from the interaction map. Visual effects — verified by Maestro. Cross-screen effects (currency change in settings affects home and statistics) — navigation between screens plus screenshots. Effects with no visual feedback — regular unit tests. Each effect type is covered by the right method.</p>

<p>The whole thing is wrapped in one shell script — <code class="language-plaintext highlighter-rouge">smoke.sh</code>. It builds the app, boots a headless simulator (no GUI window), installs the binary, runs all scenarios, kills processes when done. One command. The agent is not allowed to run these steps by hand — if the script breaks, fix the script.</p>

<h2 id="the-third-eye-visual-check">The third eye: visual check</h2>

<p>Maestro confirms that taps worked and text appeared. But it doesn’t know how the app <em>should</em> look. The button might be there, but the color is wrong. Spacing is off. Layout doesn’t match the mockup.</p>

<p>Claude can look at images. After all scenarios pass, it opens every saved screenshot and compares against three sources of truth:</p>

<p><strong>Stitch mockups.</strong> If the app was designed with Google Stitch, the original screen designs are saved next to the code. Claude compares the real screenshot to the intended layout — element placement, proportions, visual hierarchy.</p>

<p><strong>Design tokens.</strong> A DESIGN.md file has exact colors, font sizes, spacing values. Claude checks if the screenshot matches.</p>

<p><strong>The interaction map.</strong> If the scenario changed currency to euro, Claude checks that the screenshot actually shows euro signs everywhere, not dollars.</p>

<p>That’s exactly how the currency bug got caught. Maestro confirmed settings accepted the choice. The interaction map pointed to which screens should change. Claude looked at the screenshot — and saw that one component didn’t update.</p>

<h2 id="three-layers-one-qa-engineer">Three layers, one QA engineer</h2>

<p>Put it all together and you get something that looks a lot like how a real QA team works:</p>

<p><strong>Planning.</strong> Read the spec. For each UI element, decide what the user should see. Flag anything suspicious before testing starts. That’s the interaction map.</p>

<p><strong>Execution.</strong> Open the app. Go through every scenario. Check that buttons work. Data survives restarts. Bad input shows error messages. That’s Maestro.</p>

<p><strong>Visual review.</strong> Compare each screen to the mockup. Check colors, typography, spacing. If you changed a setting — make sure the change reached every screen. That’s Claude looking at screenshots.</p>

<p>Each layer is a separate Claude Code skill — a markdown file that other skills can use. <code class="language-plaintext highlighter-rouge">/build-app</code> runs the full testing cycle after building. <code class="language-plaintext highlighter-rouge">/improve-app</code> uses just the interaction map and visual review — enough to check the impact of small changes. <code class="language-plaintext highlighter-rouge">/test-app</code> runs everything end to end.</p>

<h2 id="how-this-is-different-from-ai-writes-tests">How this is different from “AI writes tests”</h2>

<p>In practice, this approach catches bugs in four categories that standard AI testing misses:</p>

<p><strong>Broken promises.</strong> Theme toggle that saves the setting but doesn’t re-render. Currency selector that updates some components but not others. Search button with an empty handler.</p>

<p><strong>Runtime crashes.</strong> Libraries that compile fine but crash on launch — because they need features not available in the user’s environment. TypeScript compiler and bundler see nothing. Maestro catches the crash on first launch.</p>

<p><strong>Cross-screen mismatch.</strong> A setting that reaches one screen but not another. Data visible on one tab, missing on the next.</p>

<p><strong>Design drift.</strong> Layout that shifted from the mockup. Colors that don’t match the design system. Inconsistent spacing between similar screens.</p>

<p>Nothing exotic. Normal problems every mobile developer deals with. But they’re invisible to unit tests — because unit tests check pieces in isolation. The interaction map checks promises. Maestro checks the assembled app. Visual review checks the visual contract.</p>

<h2 id="try-it">Try it</h2>

<p>The testing pipeline is part of Etnamute, the AI app factory I described <a href="/posts/etnamute/">earlier</a>.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">interaction-map/SKILL.md</code> — builds a test plan from UI analysis</li>
  <li><code class="language-plaintext highlighter-rouge">visual-review/SKILL.md</code> — compares screenshots to mockups and design system</li>
  <li><code class="language-plaintext highlighter-rouge">maestro/SKILL.md</code> — scenario templates, Expo Router gotchas</li>
  <li><code class="language-plaintext highlighter-rouge">testing/SKILL.md</code> — runs all three layers in order</li>
  <li><code class="language-plaintext highlighter-rouge">scripts/smoke.sh</code> — build, simulator, Maestro, cleanup — one command</li>
</ul>

<p>The approach is not tied to Expo or React Native. The interaction map works with any UI framework. Maestro supports iOS and Android. Visual review works with any screenshot. The idea — test promises, not code — works anywhere.</p>

<p>Honest note: this is not a silver bullet. The agent skips steps sometimes. Complex forms need extra work. And it all depends on how well your UI elements are addressable — if a button has no test ID, Maestro can’t tap it.</p>

<p>But between “AI runs tests and declares victory” and “AI opens the app and checks what the user sees” — that’s where most real bugs live.</p>

<hr />

<p>Code: <a href="https://github.com/bes-dev/etnamute">github.com/bes-dev/etnamute</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m building Etnamute — a system where AI agents write mobile apps from idea to App Store. User interview, market research, spec, design, code, marketing — all automated. My role came down to one thing: open the finished app and check if it works.]]></summary></entry><entry><title type="html">I Built an AI Mobile Developer. Here’s What It Actually Does.</title><link href="https://bes-dev.com/posts/etnamute/" rel="alternate" type="text/html" title="I Built an AI Mobile Developer. Here’s What It Actually Does." /><published>2026-03-19T00:00:00+00:00</published><updated>2026-03-19T00:00:00+00:00</updated><id>https://bes-dev.com/posts/etnamute</id><content type="html" xml:base="https://bes-dev.com/posts/etnamute/"><![CDATA[<p>I’ve been experimenting with automating mobile app development end-to-end. Not the “type a prompt and pray” kind — a structured pipeline where AI interviews you, researches the market, writes a spec, builds the app, checks its own work, and deploys to the App Store.</p>

<p>The whole thing runs on top of Claude Code. No custom framework, no cloud service, no SaaS subscription beyond what you’re already paying for. Just markdown files telling Claude how to do its job.</p>

<h2 id="the-problem-i-was-solving">The problem I was solving</h2>

<p>I wanted to ship mobile apps fast. Like, multiple-apps-per-week fast. Not prototypes — actual store-ready products with monetization, ASO materials, and marketing copy.</p>

<p>Tools like Rork exist, but they’re a black box. You pay per prompt, the code lives in their cloud, and you can’t customize the pipeline. I wanted something I own and can plug into a larger system.</p>

<h2 id="what-etnamute-actually-is">What Etnamute actually is</h2>

<p>A folder with ~25 markdown files and a few scripts. That’s it.</p>

<p>The markdown files define a pipeline:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">discovery.md</code> — adaptive interview, generates questions based on your specific app idea</li>
  <li><code class="language-plaintext highlighter-rouge">spec.md</code> — PRD generation with web research and user approval</li>
  <li><code class="language-plaintext highlighter-rouge">qa.md</code> — self-review with dynamic checklists generated from the PRD</li>
  <li><code class="language-plaintext highlighter-rouge">release.md</code> — fastlane config, Maestro screenshots, local builds</li>
  <li><code class="language-plaintext highlighter-rouge">headless.md</code> — accepts a PRD file, no interactive steps, runs autonomously</li>
</ul>

<p>Claude Code reads these files and follows the instructions. Skills and rules in <code class="language-plaintext highlighter-rouge">.claude/</code> get auto-discovered. An MCP server serves Expo and RevenueCat docs on demand so Claude doesn’t hallucinate API calls.</p>

<h2 id="how-it-works">How it works</h2>

<p>You run <code class="language-plaintext highlighter-rouge">claude</code> in the project directory and describe your app idea. From there:</p>

<p><strong>Phase 0 — Discovery.</strong> Claude analyzes your idea and asks 5-8 adaptive questions via structured UI — not generic “what type of app” stuff, but domain-specific questions with relevant options. Then it runs web searches to find competitors, validate pricing, check market demand. You get a PRD summary to approve before any code is written.</p>

<p><strong>Phase 1 — Plan.</strong> A 9-section implementation plan generated from the approved PRD. Tech stack, file structure, milestones with checklists.</p>

<p><strong>Phase 2 — Build.</strong> Five milestones executed sequentially: scaffold, screens, features, monetization (if you chose it — it’s optional), polish + launch materials. QA check after each milestone.</p>

<p><strong>Phase 3 — Finalize.</strong> Final QA pass across the entire app.</p>

<p><strong>Phase 4 — Release.</strong> Optional. Generates fastlane config, captures screenshots via Maestro on a simulator, builds IPA/AAB locally, uploads to stores. You confirm before it submits.</p>

<p>There’s also an <strong>Improve Mode</strong> — point it at an existing app in <code class="language-plaintext highlighter-rouge">apps/</code> and ask for changes. It reads the PRD and code, clarifies what you need, applies targeted changes, verifies.</p>

<h2 id="the-honest-part">The honest part</h2>

<p>This is not magic. Claude can still ignore instructions. The QA step is self-review — the same model checking its own output. The “97% quality score” is a number Claude assigns to itself.</p>

<p>But the difference between “generate an app” and “follow a structured pipeline with checkpoints” is real. It’s the difference between a junior dev with no process and a junior dev with code review. Not bulletproof, but way less random.</p>

<h2 id="the-headless-mode-is-the-interesting-bit">The headless mode is the interesting bit</h2>

<p>The whole point of having a formal PRD schema is that other agents can generate it. An upstream agent analyzes the market, picks a niche, writes a PRD, hands it to Etnamute — and out comes an app. No human in the loop.</p>

<p>That’s what I was actually building: an AI-first app publisher. Etnamute is just the developer in that chain.</p>

<h2 id="try-it">Try it</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./setup.sh
claude
<span class="o">&gt;</span> I want an app <span class="k">for </span>tracking daily water intake
</code></pre></div></div>

<p>Apache 2.0: <a href="https://github.com/bes-dev/etnamute">github.com/bes-dev/etnamute</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve been experimenting with automating mobile app development end-to-end. Not the “type a prompt and pray” kind — a structured pipeline where AI interviews you, researches the market, writes a spec, builds the app, checks its own work, and deploys to the App Store.]]></summary></entry></feed>