Blog · 2026.10.06
From one line to a pull request
Hand Elanous a single line — something like “put the cause in this error message too” — and a little later a pull request appears in the repository; when the conditions are met, it goes all the way to merge without a human hand. What carries the work in between is the “harness.” The internal documents that described the harness had swollen over a few months to hundreds of kilobytes, and nobody had read them to the end. So this time we redrew it as a one-page map. This post follows that map and walks outside developers through the path a one-line request travels — both the path when things go well and where it goes when it falls.
한 줄이 PR 이 되기까지 — 하니스 한 런의 길.
Beta — until the general release, Elanous is provided as is (Apache-2.0). Get news of the general release at elanous.ai.
The whole path on one page
First, the whole picture. Each box is unpacked in turn below.
1. The door — it comes in as one line
There are two doors in. One is for telling it what to do in a single line at the terminal; the other is for someone who has already written a goal document and hands that document over. Inside the TUI, a slash command or a fixed opening phrase that starts with “implement with the harness…” leads onto the same path. An ordinary conversation that does not use the opening phrase does not go to the harness. That split is intentional — there is no need to put even a light edit through isolation and review every time.
There used to be more doors. We have now closed two old ones; calling a closed door starts nothing and only returns a note saying “please come in through this door.” The reason for cutting the doors down is simple. Each door had slightly different default capabilities, so the same request produced different results depending on which door it came through.
2. Writing the goal — turning one line into “a document that can be judged”
The one line is not passed to implementation as it is. First it is turned into a “goal document.” A goal document holds what is being fixed, which files are the target, what signals “done,” and the boundaries that must not be touched.
There is one thing we learned from experience. If you give a child agent only “don’t do this” without “do this instead,” the child circles at a dead end. So in a goal we write a “path” and “a signal that can be disproved” rather than prohibitions.
While the goal is being written, the authoring step sometimes raises a question. It used to stop and wait for a person to answer it. Now, at all three doors, the authoring step reads the context, answers the question itself and carries on. It leaves only the questions a person has to answer.
3. Isolation — it does not touch anyone else’s working tree
Once the goal stands, a place to work is chosen. There are two cells.
- Local — a separate working tree is created on the machine that made the request, and changes are made only inside it. The tree a person was using never becomes the child’s write area.
- Remote container — a slot is leased from a pool made up of several machines. Each machine’s image is kept in line, and just before launch the coding model’s credentials are renewed ahead of time. That renewal was added to stop runs dying for the wrong reason because credentials expired mid-run.
The principle we kept here is “context is decided once and passed down only as arguments.” The moment a lower step starts reading “which folder am I in right now?” for itself, the isolation the parent set up is quietly broken.
4. Build · gate · review — a few laps around the same place
In the isolated place, the implementation child changes the code. When it finishes, the run goes straight to the gate.
The gate does not run the whole repository’s tests. It runs only the tests and type checks for the scope of the changed files, and what it asks is not “are there failures?” but “did this change cause a new failure?” Repositories whose full test suite is always green are rare. If failures that were already red get blamed on the child, the child spends all its rounds fixing things that have nothing to do with it. So every failure is labelled as one of “caused by this change · already there · unknown · precondition not met.”
Once the gate passes, an unattended review reads the diff. If the review raises a “must fix,” the run goes back to implementation. That is a rework round. A lap feeds back two kinds of things. Coming back from the gate, the failing tests and type errors go into the next round’s instructions; coming back from review, the list of things to fix does.
The review is not always right. So the child can “rebut” a must-fix, and a re-judgement that receives the rebuttal looks at it again. If every must-fix is dismissed, the run leaves the loop. Rounds have a cap (by default, two rework rounds).
5. The supervisor — it names “why did it stop?” in a word
Outside the rounds sits the supervisor. Every time a run stops, the supervisor records the reason as one word from a fixed set: things like “converged,” “no progress,” “round budget used up,” “model usage exhausted” and “review not observed.” There are fifteen of these words today, and the next cell each word goes to is fixed in a table as one of five: harvest, split the scope, review again on its own, human gate, or proposal.
The line in this table we care about most is the last one. When a new word that is not in the table appears, it is not read as a “pass”; it is left only as a “proposal.” That closes off the path by which something unknown gets quietly approved.
6. Landing — it is stopped in one place only, just before merge
A run that passes opens a pull request. Even with automatic merge switched on, the pull request is not merged right away. The gate does not look only at “the review passed.” It also checks whether the review actually ran, whether the reviewer read the diff to the end, and whether the evidence the goal asked for by name has been filled in. If any one of them is missing, the pull request stays open and is handed to a person.
There are times landing has to stop, such as while a release is being cut. It used to be checked separately on each landing path, which left gaps. Now every landing path checks for a freeze in one place, just before merge. A merge that arrives during a freeze is not thrown away; it goes into a hold queue and is merged in turn once the freeze lifts.
A run whose merge succeeds clears away the working tree and branch it made. If any one of the checks before clearing trips, nothing is deleted and both are left in place. That is so “something we could not confirm” is never read as “nothing there.”
7. When it falls — nothing is thrown away, and the reason is stated
Not every run ends in a merge. There are three paths on the failure side.
Rework and splitting the scope. When the gate or review catches something, the run first laps the same place again. If the rounds are used up with no progress, the supervisor does not fire the same goal again; its verdict is to split the scope and go again.
Harvest. The output of a run that stopped half-done is kept as a “harvest branch.” The next run, or a person, can pick it up from there. If it failed in a remote container, the result line is interpreted to show “why it actually died.” Remote failures used to be left as a single lumped-together line, so finding the cause meant digging through the logs by hand. Failure events are passed on to classification on the self-healing side, and that classification starts only on the host, not inside the remote container. That is so the same failure is not being fixed in several places at once.
Human gate — decision cards. When a run hits a wall it cannot get over on its own — the round budget used up, model usage exhausted, a step timing out — it goes to the human gate. A decision card with the options and the evidence goes up to a person. The rule we are aiming for is “a human gate only for payment · publication · credentials · irreversible decisions.” Today, budget and time-outs going to the human gate is a value we have left as an exception to that rule, and it is pinned in tests so that any change to it shows.
We are also building a “task worker” that makes this failure handling one step more automatic. It is split into a decision part, which takes the supervisor’s verdict and picks “the next move,” and an execution part, which carries that move out. If the same move fails twice it moves on to a different one, and the last move is a decision card. The execution part runs only as a shadow, without writing its results outside, and the decision part is not wired in yet.
What works today / what does not yet
| Item | Status |
|---|---|
| One-line request → writing the goal → isolated build → gate → unattended review → pull request → conditional automatic merge | ✅ Works today |
| Running in two cells, a local working tree or a remote container (remote: lease · image sync · credentials renewed ahead) | ✅ Works today |
| Answering authoring questions itself, at all three doors | ✅ Works today |
| Changed-file-scope gate · only “new failures” are put on the child | ✅ Works today |
| Supervisor exit words → next-cell table · unknown words left only as proposals | ✅ Works today |
| Freeze check just before merge on every landing path · resume after a hold | ✅ Works today |
| Reclaiming the working tree and branch after merge | ✅ Works today |
| Showing the real reason for a remote failure · keeping a harvest branch | ✅ Works today |
| Queueing requests that come in through the doors | ⚠️ Built in, but off by default |
| A lightweight authoring tier | ⚠️ Local only — for remote containers: Planned — not yet |
| Task worker execution part | ⚠️ Runs only as a shadow (does not write results outside) |
| Wiring in the task worker decision part (picking the next move itself after a failure) | 🔲 Planned — not yet |
| Narrowing the human gate to payment · publication · credentials · irreversible decisions | 🔲 Planned — not yet |
Why we built it this way
There are not many cells on this path where a person really has to stand. Instead, if the child receives “a goal that can be judged,” the gate asks only about “failures it newly caused,” and the supervisor states in a word why a run stopped, then a person only needs to be at the cell where decision cards come up — that is our hypothesis. Whether it holds, we measure by whether the share of runs that go to the human gate goes down.
We cut the map down to one page, and decided that any change to the harness will update that map within the same change. The next time this path changes, we will write about it again, along with the cells that changed.
Source: github.com/ElanvitalAI/elanous · Docs: docs.elanous.ai
Beta — provided as is (Apache-2.0). Get update news at elanous.ai. Read also Redrawing the loop map with three cores.