The Default Was to Push
The builder’s configuration has a field called autoPush, and the line that reads it says: if unset, true. Nothing sets it. There was no flag to set it with. So every unattended run of the improvement loop has been committing the model’s edits onto whatever branch it started on and pushing them, and I found this by reading the line, not by watching it happen.
Where a push is a publish
Most of my repositories are private, and a push to a private repository is a backup. Three are not like that. The game deploys its pages from its default branch. This site publishes on every push to its main branch, through a pipeline that runs the guards and builds and deploys. The tax estimator deploys on every push to main through the hosting provider’s git integration, which means there is no workflow file, no pipeline definition, nothing in the tree that a program could read to learn that a push is a deploy. The repository’s own README says so, in a sentence, for people.
The builder had no idea any of this was the case, and had no reason to. Its job is to make a repository better and keep going. Pushing is how a run’s work survives the run. From inside the loop that is the safe default. From outside, for three repositories, the safe default was the single act in the whole loop that cannot be taken back.
As far as I can tell it never got the chance. The loop has run against throwaway targets for months and against real repositories rarely, and I have no record of it pushing to any of the three. That is not the same as knowing it did not. It is the state of my evidence.
Reading prose to find a deploy
The fix was not to turn pushing off. The loop is allowed to push where a push is a backup; that is most places, and a builder that never pushes is a builder whose work dies with the session. The fix was to teach it the question: would pushing this branch publish something?
That question has two sources of evidence, and they are not the same kind. The first is a workflow file: a push trigger that fires for this branch, combined with an action that deploys. That is structured, and a program can parse it. A trigger that only fires on tags does not count. A branch filter that does not match the current branch does not count. The repository’s own test workflow does not count, because tests do not publish. Judged this way, most of my repositories come back clean, including the builder’s own.
The second source is the one I did not want to write. The tax estimator’s deploy is configured in a provider’s dashboard, not in the repository, so the tree carries no trace of it. The only place the fact is recorded is a sentence in the README: pushing main deploys the site. So the detector reads the README and the project’s instructions file for a sentence that asserts a push deploys, and a match there counts the same as a workflow match. It is a guard that reads prose, and I dislike that, and I could not find anything else to read.
What the default became
autoPush is gone. In its place is a push policy with three values, auto, always and never, exposed as a flag and announced in the run’s banner. Under auto, the default, the loop commits its work as before, asks the question, and where the answer is yes it withholds the push and keeps the commit, printing the file it read, the marker it matched and the text that matched. A person who wants the loop to push there anyway says always, or runs it on a work branch that publishes nothing. The judgment happens after the branch is known, so a run on a scratch branch is judged on that branch, not on main.
I checked it read-only against the real repositories before trusting it. This site: flagged, by both sources. The tax estimator: flagged, by the README alone. The game: flagged, by its workflow. Everything else: no findings, push allowed.
The part I keep
The thing I want to remember is not the default. Defaults are wrong all the time and this one was wrong in the obvious direction. It is that the most consequential fact about three repositories, that pushing them is publishing, lived in three different places: a workflow file, a provider’s settings page, and a sentence. A program can read one of those. A program can be told about the second. Nobody had written the third down anywhere a program would look, and the only reason it was written down at all is that I keep a file in each repository explaining the repository to the agents that work in it.
That file was written for a reader. It turns out the builder is a reader too, and now it reads that sentence before it pushes.