{"slug": "two-loops-and-a-human", "title": "Two loops and a human", "summary": "An engineer described a six-week experiment running coding agents on real projects, settling on a two-loop architecture — a daily/weekly/monthly slow loop and a short-lived fast loop — with a single human approval gate between them. The design keeps prompts as reviewed files in the repo, has jobs propose verdict tables with hashes rather than per-item questions, and gates merges on a standing approval, no new commits, green CI, and a clean security pass. The slow loop has run unattended since mid-September, while the fast loop ran for about ten days in September and is no longer active.", "body_md": "*Six weeks of running coding agents on real projects, and what the design looks like now*\n\n**Status, stated up front.** The slow loop in this piece has run unattended since mid-September: daily digest, weekly sweep, monthly dreaming, each ending in a pull request a human approves. The fast loop has run too, on one project, for about ten days in September, and taught most of what follows. It is not running today. Every project has since moved to a different orchestrator, and the headless shape for it is undecided. I would rather say that than describe a diagram as live when it isn't.\n\nOne person, several projects, and agents doing most of the typing. The failure mode isn't that the agents are bad at code. It's that they are confident, fast, and have no memory of last week. Left alone, they will re-derive a rule you already learned, write into a directory you meant to keep clean, call a half-finished job \"complete\", and do it all in well-formed prose.\n\nSo the design question was never \"how do I make the agent smarter\". It was: **where does judgment live, where does state live, and what is the one thing a human must do?**\n\nThe answer that held up is two loops with one gate between them.\n\nThe human sits on the gate. Everything else is machinery to make the gate cheap.\n\n| Job | When | Proposes | The human does | \n|---|---|---|---|\n| Machine checks | daily | a status file: every check pass / fail / unknown; a missing check is unknown, never clean | reads it through the digest | \n| Daily digest | daily, on a hosted runner | one comment: open PRs each with a one-line mergeable-or-blocked verdict, decisions falling due, open blockers | answers batched decisions in one reply | \n| Weekly sweep | Sundays | drains the drop, routes notes, regenerates packs, refreshes the export, and proposes a verdict table for every aged follow-up | says `go` or`go except …` | \n| Monthly dreaming | 1st of the month | rewrites the distilled memory file and nothing else | approves one PR | \n\nTwo design choices matter more than the schedule.\n\n**Prompts are files.** Every job's prompt lives in the repo and runs through the agent's command line, headless. Changing how the sweep behaves is a reviewed PR, with the same gate as code.\n\n**Jobs propose tables, not questions.** The first version of the sweep listed aged items and asked \"close or park?\" per item. That is a chore, and chores don't get done; the list grew to 22. The current version proposes a verdict per item with the evidence, prints a hash of the table, and the human answers once. The apply step refuses if the table changed after approval. Every \"the system asks me the same thing every week\" became a table with a hash.\n\nThe hosting platform won't let an identity approve its own PR. That constraint became the design:\n\nA PR is mergeable when four things hold at the same commit: a standing approval from one of those two identities, no commits since it, CI green (no checks at all is not green), and a clean security pass. One script prints that as one line, and the digest prints it for every open PR. \"Is this ready?\" stopped being a question.\n\nTwo smaller rules carry weight. A **scope ratchet**: after the first review verdict, later commits may change what is already in the diff but not widen it, unless a human says so explicitly. And a **bootstrap exception**: a PR that changes the reviewer itself can't earn the reviewer's approval, so it takes a one-time exception only the human grants, recorded where it can't be lost.\n\n**Out.** The export is generated from a committed revision, never a working tree. That rule came from finding the export four days and 174 commits behind the main branch because it had been copied from someone's checkout. Each export carries a manifest the projects can read but not write; a project's copy of the rules is verified against it, so a project can't vouch for its own copy. Every number the verdict uses comes from files only the exporter wrote. Both of those came from review findings on the same PR, and the fix for the second was to delete the \"grace period\" that trusted the project's copy at all.\n\n**In.** The drop. One file per item, written as `.<name>.partial` and renamed when complete. Intake moves files into a hub-owned staging directory before reading them, and skips byte-identical repeats by hash. A failed move is never logged as \"vanished\". Another lesson from a run that printed \"drop is clear\" over 58 unfiled files.\n\nThe first fast loop wrapped a phase-based orchestrator so a phase could run headless on a project's own runner, with exactly one human decision in the middle: approve the plan. It ran on one project, a small game, for about ten days. In that time:\n\nThen the orchestrator changed. A newer one shipped its own gates, its own pre-approval flags, and its own disk state. Rebuilding the wrapper around it would mean two state machines over one pipeline. So the fast loop is being redesigned as: a thin environment-owning wrapper, the orchestrator's own gates as the stops, and the hub only watching. That design exists on paper. The first headless milestone under it hasn't run. The first *supervised* milestone on the new orchestrator hit an upstream bug on its own patch route, and is parked until the fix lands upstream, which I've filed and offered to test.\n\nThat parked milestone is also the clearest statement of the discipline: nothing in the project's state was hand-edited to get around it.\n\n\"Autonomous\" was a mode a project was either in or not. The headless runs showed that was the wrong shape: every phase was a series of stops, and whether a machine could answer a stop depended on one thing, **what the project's tests decide without a human**.\n\nSo autonomy became a per-project level, derived from two measured numbers:\n\nThe ladder: L0 unassessed; L1 supervised phase (the human answers every stop as it happens); L2 supervised plan (the human approves the plan, then the phase runs unattended); L3 unattended phase; L4 unattended milestone; L5 unattended roadmap (the project proposes its own next phase). **L5 is deliberately unspecified.** Nothing in the repo has evidence for what would make it safe, and writing criteria from imagination is how a gate becomes a formality.\n\nThe level is a standing state the last window of phases supports, never the highest a project ever reached, and it caps how a phase may run.\n\nThe distilled memory is one file, under 200 lines, that a session carries in. Daily and weekly jobs change the hub constantly; why rewrite the memory only monthly? (\"Dreaming\" is the hub's name for that rewrite.)\n\nBecause it is a **full rewrite reviewed as a diff**, and a full-rewrite diff is only reviewable when enough has changed to matter. Weekly, most of the diff would be noise and the review would become a rubber stamp, which is the one thing a memory rewrite can't afford: a bad rewrite deletes what you knew. Monthly keeps each review real and each revert cheap. The fast-changing state already has a home: the digest carries today's, the sweep carries the week's. Dreaming is for what stayed true long enough to be worth remembering.\n\nThe repo's own record of the choice is less principled than that: it was sequenced last among the unattended jobs because the lowest-frequency job is the slowest to reveal a fault. The test of whether monthly is right is simple and not yet run: measure how much the file changes per rewrite, and whether the digest catches it contradicting itself mid-month. Small diffs, no contradictions: monthly holds. Otherwise, a weekly *staleness check* that proposes lines but rewrites nothing.\n\nThe threat model isn't a malicious agent. It's a fallible one with write access and confidence, plus the ordinary supply chain around it. The rules fall into five groups, and every one came from an incident:\n\nThe pattern across all five: every check that could be satisfied by the thing it checks got rewritten until it couldn't.\n\n`go`.\nI'm publishing anyway because the slow loop is real and the lessons are real. The design's main claim is modest: **put the human on one gate, make everything else propose, and make every check unfakeable by the thing it checks.** Six weeks in, that part holds.", "url": "https://wpnews.pro/news/two-loops-and-a-human", "canonical_source": "https://dev.to/shai_kappel/two-loops-and-a-human-41ig", "published_at": "2026-10-22 13:35:59+00:00", "updated_at": "2026-10-22 13:46:16.433874+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/two-loops-and-a-human", "markdown": "https://wpnews.pro/news/two-loops-and-a-human.md", "text": "https://wpnews.pro/news/two-loops-and-a-human.txt", "jsonld": "https://wpnews.pro/news/two-loops-and-a-human.jsonld"}}