The Vision Log, Part 2: What Breaks When an Agent Edits Its Own Repository

Written by

in

The first time an automated writer commits into a repository a person is also working in, it takes something that is not its own. Not maliciously: it stages everything it can see, because that is what the simplest working version does. Letting an agent write its own code is the well-served half of this problem, and the half nobody demos is the rest — proof, isolation, reversal, and the strange last step of restarting the process you happen to be standing on. That is what this part is for, and the parts of it that failed in production are all here.

This is The Vision Log, a series about one long-running agent that reads, changes and verifies its own source code, and this part is the machinery that lets it do that without wrecking the repository it lives in. Vision is not an assistant and not an automation framework: it is designed as a digital counterpart to the developer who built it, and autonomy is a means it uses rather than the point of the exercise. Like every part of the series, this one states only what the project’s own engineering log records, including the parts of this mechanism that broke. Part 0 explains what the project is and the wish it came from.

The unit of self-repair is a recorded refusal

If you let an agent work on your repository, the first thing worth designing is what it does when it cannot proceed. When a run cannot do something, the interesting output is not the failure, it is the record of it. A run that is blocked writes a wall: a short line naming what it could not do and why. Walls accumulate, and a wall is what the self-fix path consumes.

Not every wall is the agent’s to act on. The developer drew that line as a scope rule (engineering log, 2026-09-03). Machine scope is anything that changes this computer outside the project: permission rules, installs, launchers, continuous integration, host settings. Those wait for the developer’s word, always. Project scope is Vision’s own code, tests, documentation and log, and a project-scope wall opens on its own once the run that recorded it finishes.

The word "opens" is doing real work there. It does not mean the change is accepted. It means a run is allowed to start, and that is the distinction to hold on to when you decide what your own automation may open by itself.

Two writers in one tree cannot be told apart afterwards

Two writers in one working tree leave a diff you cannot attribute afterwards. A self-fix run gets its own detached checkout, created with a worktree add under a temporary path named after the wall (engineering log, 2026-09-03). The command-line agent inside it can stage everything it likes, because everything it can see belongs to this one fix.

That isolation was not the first design, and the reason it exists is worth stating plainly. Before it, self-fix ran in the live checkout and committed with a blanket stage-everything. Two incidents measured the cost:

  • A run whose instruction never arrived produced an empty answer, and the commit it made swept up twelve repair scripts a parallel session was in the middle of writing, filing another person’s unfinished work under the run’s name (engineering log, 2026-08-28).
  • A later self-fix commit swept nine uncommitted files out of a person’s working tree while they were editing them (engineering log, 2026-09-03).

The first mitigation was to record which paths were already dirty when the run started and unstage them afterwards (engineering log, 2026-08-28). It is a reasonable rule and it covers exactly half the problem: it cannot see a file that became dirty while the run was working. There is no rule that can. Nothing inspecting a tree afterwards can tell you who wrote a line in it.

So the tree was split instead. When the run ends, its head is merged back into the main line with fast-forward only, and the worktree is removed.

Takeaway. If two writers share a working tree, which of them made a given change is not recoverable after the fact. Give the automated writer a checkout of its own and the question stops being asked.

The gates run again, and they are not run by the claimant

You cannot take the report as the verdict. A run ends by reporting that it fixed something. That report is an input, not a verdict (engineering log, 2026-09-03). The change counts as an improvement only when four separate things hold.

The ground What it rules out
A commit exists A run that describes a fix it never wrote
Every touched path is one the gates measure A change in a directory nothing verifies, which counts as unmeasured, and unmeasured is held
No test file was deleted Passing the suite by removing the suite
The linter, the type checker and the tests are run again after the commit, by the system rather than by the run A run reporting green from a state it has since changed

A fifth ground was added later (engineering log, 2026-09-03): the run must append its own numbered entry to the log, and if the commit is there but the entry is not, the change is held. A fix without its record is not applied.

The second ground is the one that surprises people. Parts of this repository are not covered by the gates that run on a self-fix commit, and rather than trust a change there, the system reads not-measured as not-proven and holds it. That is deliberately conservative and deliberately visible: the hold says which ground failed, so you can tell an unproven change from a rejected one without reading a line of the code.

"Held" has to be a place, not a label

You cannot leave an unproven change where the proven ones live and mark the difference with a word. For a while it was only a label. A self-fix commit landed on the main line whether or not it proved itself, and held was a status written beside it. Once restarts became automatic, held code came alive at the next restart like any other commit, which is what happened to one commit that was marked held and was running an hour later (engineering log, 2026-09-03).

The repair was to move the commit rather than annotate it (engineering log, 2026-09-03). A finished run parks its commit on a branch named after the wall. The gates are re-verified inside the worktree. If the proof stands, the branch is fast-forwarded into the main line and deleted. If it does not, the commit stays on the branch, the wall stays open carrying the branch name, and the main line never saw it. Undoing a held fix is deleting a branch, which leaves no revert commit and no trace in the main history.

Refusing to fast-forward is a feature in the same way. The merge fails when the main line moved underneath the run, or when the same file has uncommitted edits in the main checkout. Both are cases where landing would be wrong, so the refusal is the correct outcome and the commit waits on its branch.

Takeaway. A hold that leaves the code in the path of execution is not a hold. Make the unproven state a different location, not a different adjective.

A fresh checkout made the toolchain’s own output look like authored work

Give an agent a clean checkout and your own tooling starts looking like its work. The first real self-fix run in its own worktree was held despite passing everything, and both causes were created by the isolation (engineering log, 2026-09-03).

The sandbox could not write the repository’s own bookkeeping. A worktree’s git directory is a file pointing at metadata that lives outside the run’s root, and a sandbox that restricts writes to that root refuses the path. The commit failed on the index lock, and the run honestly reported itself blocked.

The proof read the working tree instead of the commit. The tree showed thirty changed files. Twenty-eight of them were caches and data files that the gate run had just created, invisible in the main checkout only because they already existed there. In a fresh checkout they are all additions, they sit in directories the gates do not measure, and the second ground above fails. The repair was to derive the changed set from the commit’s own diff rather than from the tree.

Takeaway. A fresh checkout makes your toolchain’s own output indistinguishable from your work. Ask the commit what changed, never the directory.

Restarting the thing you are standing on

You will hit this one whatever your version control looks like. This is the part that is genuinely hard, and it is not about version control at all. A code change is not live until the process running the old code is replaced, and the process asking for the replacement is the process being replaced.

Every property below exists because the obvious version of it failed first (engineering log, 2026-09-03). Read what follows as a list of traps already paid for, not as a design to admire: each one is a bill you would otherwise settle yourself.

  • The restart command has to answer before it acts. The synchronous form is for a human at a shell. The form the agent uses writes its response first and restarts afterwards, because an inline restart kills the caller before the caller can be told it was accepted.
  • The record cannot be written by the process that dies. The request is written to disk before it is sent, and the outcome is written by the supervisor, the only participant that survives. The next process to start reads that file and reports what happened to its predecessor.
  • It has to wait to be idle. A restart request waits for the work board to clear, pauses briefly, then goes. If the machine stays busy it gives up after thirty minutes rather than killing live work.
  • Calling your own launcher can hang forever. Invoking the launcher behind a pipe did not return for two thousand one hundred and fifty-six seconds, right up to the moment the daemon was shut down, because the daemon inherited the calling shell’s pipe handle and the reader never saw end of file. The daemon itself had been up and healthy within two seconds.
  • Asking a running stack to start again can be worse than doing nothing. A second start command on a live stack moved the supervisor from running to degraded, with a cleanup-required error naming forty-three process ids. Four of them were alive. The other thirty-nine were helper processes that had already finished. Degraded was sticky, so every later restart was refused, which is precisely the door a self-restart has to walk through — check what your own supervisor does with a second start command before you rely on one.

The one step that cannot report its own failure

The step you cannot instrument from the inside is the one that replaces the process doing the instrumenting. Every mechanism above worked. Then, at 09:21 on 2026-09-11, a self-fix run committed a fix, proved it, landed it and asked for a restart — the last step in the chain this article has been building — and the stack came back seventy-eight minutes later with nothing written anywhere in between (engineering log, 2026-09-11). The isolation held, the gates held, the hold-as-a-place held, and the part that was supposed to make the proven change live simply stopped, silently, with the agent inside the process it had just asked to be replaced.

Moment, Asia/Seoul What was recorded
09:21:40 The fix is committed
09:22:55 The run reports it applied, and a restart is requested
09:27:04 The running process stops
09:27:37 The release step of the restart fails, naming no process
09:27:37 to 10:46:06 Nothing is written to any log or data file
10:46:06 The stack is running again

The failure itself is understood and even expected. The supervisor kills the processes it owns, verifies they are gone, and if something it believes it owns is still alive it refuses to start a second copy beside it. Refusing is correct, because a duplicate stack is a worse accident than an outage.

What makes this an honest failure mode rather than a bug report is the rest of it.

The record does not say what remained. The line written to disk carried an exception type name and nothing else. The log already contains the fix for this: after three restarts died the same way, each leaving the stack down for five minutes until a person typed a start command, the message was changed to name the leftover process ids and their executable names (engineering log, 2026-09-04). That naming applies on one path through the supervisor’s error handling. This morning’s failure came through a different one and arrived as a type name.

The leftovers, when they were named, were not ours. Both times they were measured, the processes blocking a restart were strangers that had been given process ids previously belonging to dead children of the stack: a graphics utility in one case (engineering log, 2026-09-04), and on 2026-09-08 the developer’s game client together with its crash handler (engineering log, 2026-09-08). An escape hatch exists for exactly this. If every process the supervisor published as an anchor is gone, then the stack is already down, whatever remained is not the stack, and the restart proceeds (engineering log, 2026-09-04 and 2026-09-08). It did not fire this morning. Why it did not is not in the record, because the record does not name what it was looking at.

The automatic rollback declined, correctly. When a restart carrying a named commit fails, the supervisor reverts that commit and tries once more. This time it refused, because the repository was not confirmed clean, which is the right refusal. It also left the stack down.

Nobody was told. For seventy-eight minutes there was no health record, no error entry, no state change and no notification. The agent does have a status field carrying the outcome of its own last restart, and that field is filled in by the process that comes up afterwards. If nothing comes up afterwards, nothing fills it in.

That is the shape of the thing. A restart is the one step where the agent cannot report its own failure, because reporting it requires the process the step just removed. So if you build one, put the watcher for that step outside the process it restarts: nothing inside will survive to tell you.

What changes for you

If you are about to let an agent write into your repository, four of these are cheap to adopt on day one and will save you the incidents above.

Give the automated writer its own checkout, because every rule that tries to work out afterwards which edits belonged to it is guessing, and it is guessing about someone else’s unsaved work — the dirty-path rule of 2026-08-28 covers the files that were already dirty when the run began and can never cover the ones that went dirty while it worked. Make the unproven state a location rather than an adjective, because a commit on the main line with a cautionary label attached is on the main line. Ask the commit what changed instead of the working directory, because in a fresh checkout your own build caches look exactly like authored changes. And treat unmeasured as unproven: it is tempting to let a change through because nothing objected, but nothing objecting is not the same as something checking.

The fifth is harder and cannot be adopted, only budgeted for. Two safety checks behaved exactly as designed on the morning described above, and the combined result was seventy-eight minutes of silent downtime — a correct refusal still costs you an outage, so design the refusal and the recovery together rather than one and then the other. Whatever restarts the agent lives outside the agent, which means a supervisor that can stop the process is a supervisor that can strand it, and the path by which a stranded stack reaches a person must not run through the thing that was stopped.

The hard part of letting an agent modify itself is not the writing. Writing the change is the easy, well-served half, and every mechanism in this article exists to answer a question that arrives after the writing is finished: whether the change is proven, where it lives while it is not, and who is left to tell you when the last step fails.

Where this goes next

Part 3 — four wrong diagnoses before the real one. The same discipline applied to a content pipeline that was holding four topics in five: four explanations that each measured flat, and a real defect four steps upstream of the symptom.

Part 4 — where this stands, against the standard it was given closes the series. The founding document says a digital counterpart is to be judged on eight measures — future possibility, growth for the developer, shared growth, capability growth, knowledge growth, contribution, continuity and trust — and it explicitly forbids judging one by task counts, run counts and model counts. Twenty of the twenty-six things this series measured are on the forbidden list, and only one of the eight has an instrument at all (engineering log, 2026-09-04 and 2026-09-11).

Part 0 is here, and part 1, on the two systems built and abandoned before this one, is here.