<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>sglavoie.com – Tag: Automation</title><link>https://www.sglavoie.com/tags/automation/</link><description>Posts and writings by Sébastien Lavoie. Customization setups and articles on productivity for developers who like minimalism.</description><language>en</language><atom:link href="https://www.sglavoie.com/tags/automation/feed.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Fri, 09 Oct 2026 16:20:00 -0600</lastBuildDate><item><title>From a plan to unattended agent runs: how I drive coding agents one session at a time</title><link>https://www.sglavoie.com/posts/from-plan-to-unattended-agent-runs/</link><description>Long conversations with a coding agent drift, and “done” too often means “the agent said so”. This is the pipeline I built to turn a plan into small sessions with checks the agent can’t talk its way around — a continuation of the same instinct behind systematic reviews and parallel work with Git worktrees.</description><content:encoded>&lt;p&gt;&lt;em&gt;Written with AI: This post was drafted with an AI assistant, working from my notes, code and tools, then reviewed and edited by me. The experiences and opinions are mine, and so are any mistakes.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Coding agents got good enough, quickly enough, that the bottleneck in my day moved. It&amp;rsquo;s no longer typing the code; it&amp;rsquo;s making sure the code that gets typed is the code I wanted, and that it actually works. The first few weeks went the way I suspect they go for a lot of people: one long conversation per feature, a growing pile of context, and a moment near the end where the agent cheerfully announces it&amp;rsquo;s done and I discover, ten minutes later, that &lt;em&gt;&amp;ldquo;done&amp;rdquo;&lt;/em&gt; meant &lt;em&gt;&amp;ldquo;the parts I remembered are done&amp;rdquo;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Trying to resume progress on large features led me to try different frameworks such as &lt;a href="https://github.com/github/spec-kit"&gt;Spec Kit&lt;/a&gt; from GitHub, &lt;a href="https://github.com/Fission-AI/openspec"&gt;OpenSpec&lt;/a&gt;, the &lt;a href="https://kiro.dev/"&gt;Kiro IDE&lt;/a&gt;, standalone Claude skills and so on. I even built my own harness-agnostic spec-driven framework which I used for a while with (slow) success with GitHub Copilot CLI, Codex and Claude Code. That level of ceremony was right for less capable models, but the picture evolved: the models got better at producing what I wanted without being walked through every step, and I found I needed a different kind of help.&lt;/p&gt;
&lt;p&gt;What I needed wasn&amp;rsquo;t more guidance for the agent. It was less trust in it. Generated code reads fluently, compiles, and is sometimes wrong in ways that only show up later: a concurrency guard that guards nothing, a test that faithfully asserts the bug. &lt;strong&gt;Fluency and correctness are uncorrelated&lt;/strong&gt;, and anyone who has reviewed enough agent output has caught that gap personally. Two problems kept coming back:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Long conversations drift.&lt;/strong&gt; The further a session goes, the more the early decisions fade, and the agent starts re-solving problems it already solved, or quietly undoing them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;Done&amp;rdquo; was decided by the agent.&lt;/strong&gt; There was no external definition of done that the agent couldn&amp;rsquo;t argue with, so every finished session still needed me to establish, from scratch, whether it was actually finished.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second one matters more than it looks. When an agent writes the code, the review doesn&amp;rsquo;t go away; it moves, and it gets harder, because I&amp;rsquo;m now checking work I didn&amp;rsquo;t build the understanding for. Review is the scarce resource. Anything that makes the agent produce more code without making that code cheaper to verify just moves the bottleneck around.&lt;/p&gt;
&lt;p&gt;What I ended up with is a small (well&amp;hellip; not so small anymore) tool called &lt;code&gt;asl&lt;/code&gt;, short for &lt;em&gt;agent session loop&lt;/em&gt;. The idea is simple: write a plan, cut it into sessions that each fit comfortably in one fresh conversation, attach to every session a check that a program — not the agent — runs, and then let a runner go through the sessions one at a time while I do something else. This post walks through that pipeline, why each step exists, and what I learned building it.&lt;/p&gt;
&lt;h2 id="why-plans-not-prompts"&gt;Why plans, not prompts&lt;/h2&gt;
&lt;p&gt;Most of the leverage happens before any code is written. A vague prompt to an agent yields a confident implementation of something adjacent to what you wanted; a precise plan yields something much closer. So the pipeline starts with a plain Markdown plan, and with a step whose entire job is to make that plan better before anything runs.&lt;/p&gt;
&lt;p&gt;The bar I hold a plan to is that it should be &lt;em&gt;decision-complete&lt;/em&gt;: every choice the implementer shouldn&amp;rsquo;t be making on their own has already been made. The test I use is a slightly hostile question: &lt;strong&gt;could two competent implementers follow this plan and end up with meaningfully different behaviour?&lt;/strong&gt; If the answer is yes, the plan is hiding a decision — about an API, a data format, an error case, a migration order — and the agent will happily make that decision for me, silently, somewhere in the middle of a diff.&lt;/p&gt;
&lt;p&gt;It helps to separate facts from decisions. &amp;ldquo;Which callers use this function?&amp;rdquo; is something the agent can discover by reading the repository, and it should. &amp;ldquo;Which of those callers are we willing to break?&amp;rdquo; is a judgement call, and it stays with me. A good plan records the first kind as discovery and the second kind as an explicit decision.&lt;/p&gt;
&lt;p&gt;The step that enforces this is a &lt;a href="https://docs.claude.com/en/docs/claude-code/skills"&gt;Claude Code skill&lt;/a&gt; called &lt;code&gt;plan-iterate&lt;/code&gt;. It reads the plan, assesses how complete it is (scope, interfaces, tests, risks), asks me targeted questions about the weakest areas, and edits my answers back into the file. With &lt;code&gt;--ideal&lt;/code&gt;, it first does one research pass over the codebase and describes what the &lt;em&gt;unconstrained&lt;/em&gt; ideal version of the change would look like, then ranks the gaps between my plan and that ideal by impact, and asks me to adopt, defer or reject each one. Deferred items get written into the plan so the decision is recorded rather than forgotten.&lt;/p&gt;
&lt;p&gt;This is the same instinct behind &lt;a href="https://www.sglavoie.com/posts/systematic-reviews/"&gt;systematic reviews&lt;/a&gt;: it&amp;rsquo;s cheaper to catch a bad idea on paper than in a diff.&lt;/p&gt;
&lt;h2 id="the-pipeline-step-by-step"&gt;The pipeline, step by step&lt;/h2&gt;
&lt;p&gt;Here is the whole flow, from a plan to a finished run:&lt;/p&gt;
&lt;p&gt;&lt;img
 src="https://www.sglavoie.com/images/posts/0046_from_plan_to_unattended_agent_runs/pipeline.svg"
 alt="Flowchart of the pipeline. PLAN.md is refined by plan-iterate, then asl prepare runs session-split, session-expand, asl baseline and asl preflight. Red or yellow preflight findings go through plan-preflight-iterate and back to preflight; an all-green plan goes to asl run, which runs one session per fresh agent turn. Python runs each session’s Verify checks: a failure triggers a repair turn with the failing evidence, a pass moves to the next pending session, and the run ends as all-complete. A blocked, unsafe or guard-violating turn, or an exhausted repair budget, pauses the run with a named reason."
 loading="lazy"
 decoding="async"/&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.sglavoie.com/files/posts/0046_from_plan_to_unattended_agent_runs/pipeline.txt"&gt;Mermaid source of this diagram&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The steps in the middle used to be separate commands I had to remember to run in order. These days a single &lt;code&gt;asl prepare PLAN.md --cwd REPO_ROOT&lt;/code&gt; runs split, validation, expansion, baseline and the readiness gate deterministically and, on success, prints the exact &lt;code&gt;asl run&lt;/code&gt; command to use next.&lt;/p&gt;
&lt;h3 id="splitting-where-to-cut"&gt;Splitting: where to cut&lt;/h3&gt;
&lt;p&gt;Splitting turns one plan into a directory: a &lt;code&gt;manifest.md&lt;/code&gt; listing the sessions and their readiness state, plus one &lt;code&gt;session-&amp;lt;id&amp;gt;.md&lt;/code&gt; per session. Each session has a scope, the files it&amp;rsquo;s expected to touch, a handoff section the agent fills in when it&amp;rsquo;s done, and a completion contract (more on that below).&lt;/p&gt;
&lt;p&gt;Where to cut took me a while to get right. A good session has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One primary outcome&lt;/strong&gt;, with evidence that can fail on its own. If I can&amp;rsquo;t write a check that distinguishes &amp;ldquo;this session worked&amp;rdquo; from &amp;ldquo;this session didn&amp;rsquo;t&amp;rdquo;, it isn&amp;rsquo;t a session yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A useful state if the later sessions never run.&lt;/strong&gt; If the run stops after session 3, sessions 1 to 3 should still leave the codebase in a coherent, reviewable state, not half of a refactor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Room to work in one fresh context.&lt;/strong&gt; Context is a split criterion in its own right: the files involved, their size, the instructions that apply and the handoffs from earlier sessions all need to fit, with space left over for the agent to think. It&amp;rsquo;s much cheaper to re-split before running than to discover forty minutes in that a session can&amp;rsquo;t hold everything it needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two anti-patterns I now avoid on purpose: splitting implementation from its tests (the test session ends up testing whatever the implementation happened to do), and a &amp;ldquo;cleanup later&amp;rdquo; session whose absence would leave the change unsafe. Sessions are ordered by dependency, not by whatever is easiest to edit first: shared contracts and scaffolding before their consumers, additions before removals.&lt;/p&gt;
&lt;h3 id="expanding-the-plan-is-a-set-of-claims"&gt;Expanding: the plan is a set of claims&lt;/h3&gt;
&lt;p&gt;Every path, function signature, command and test name in a plan is a &lt;em&gt;claim&lt;/em&gt; about the repository, and plans are often written against a mental model that&amp;rsquo;s a few commits out of date. Expanding is where the agent reads the actual codebase and checks each of those claims: do these files exist, do these functions have the signatures the plan assumes, is this session secretly three sessions?&lt;/p&gt;
&lt;p&gt;The rule that makes this useful is that &lt;strong&gt;corrections go into the plan, not into the implementation.&lt;/strong&gt; Problems get written into a &lt;code&gt;## Contract Issues&lt;/code&gt; section of the session rather than silently patched around. If the agent compensates for a stale fact while implementing, the only record of that decision is the diff, and the next session inherits the stale fact. A session that turns out to be too big can be re-split into child sessions with &lt;code&gt;session-resplit&lt;/code&gt; while the rest of the plan is left untouched.&lt;/p&gt;
&lt;h3 id="preflight-the-gate"&gt;Preflight: the gate&lt;/h3&gt;
&lt;p&gt;Preflight produces red, yellow and green findings, each with a machine-readable repair hint that names the failing file, the check that failed and the smallest repair. A red or yellow finding blocks the plan from becoming &lt;code&gt;loop-ready&lt;/code&gt;, and &lt;code&gt;asl run&lt;/code&gt; refuses to start on anything else. When there are findings, a companion skill, &lt;code&gt;plan-preflight-iterate&lt;/code&gt;, walks through them with me as a short Q&amp;amp;A and reruns preflight until it&amp;rsquo;s clean. Because each finding names its smallest repair, that pass only touches what broke.&lt;/p&gt;
&lt;p&gt;Preflight also checks the contracts themselves: every expected file either exists or is explicitly marked as new, every command is available, and no placeholder or open decision survived into a session.&lt;/p&gt;
&lt;p&gt;One design rule from this part spread to the whole tool: &lt;strong&gt;keep authored state separate from derived state.&lt;/strong&gt; The plan, the sessions, the decisions and the handoffs are authored; the manifest&amp;rsquo;s readiness flags and status tables are derived, and get rebuilt from the authored files. When the two disagree, the tool pauses rather than picking a winner. A readiness table can always be regenerated; a decision that got silently overwritten can&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;The point of all this ceremony is to move failures earlier. A missing file discovered during preflight costs a few seconds; the same missing file discovered forty minutes into a run costs a confused agent, a half-finished session and a messy diff.&lt;/p&gt;
&lt;h2 id="completion-contracts-who-decides-what-done-means"&gt;Completion contracts: who decides what &amp;ldquo;done&amp;rdquo; means&lt;/h2&gt;
&lt;p&gt;This is the part I&amp;rsquo;d keep if I had to throw everything else away.&lt;/p&gt;
&lt;p&gt;Every session file ends with a block like this:&lt;/p&gt;
&lt;div class="code-block"&gt;
 &lt;div class="code-block__bar" data-pagefind-ignore&gt;
 &lt;span class="code-block__lang"&gt;markdown&lt;/span&gt;
 &lt;button class="code-block__wrap" type="button" aria-pressed="false" hidden&gt;Wrap&lt;/button&gt;
 
 &lt;/div&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Completion Contract
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Done when
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; The &lt;span class="sb"&gt;`export`&lt;/span&gt; command writes one JSON file per project.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Existing CLI tests still pass.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Verify
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;session&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;02&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;checks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;unit-suite&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;argv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;unittest&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-q&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;timeout_seconds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&amp;ldquo;Done when&amp;rdquo; is for humans (and for the agent to aim at). &amp;ldquo;Verify&amp;rdquo; is for the runner: a list of checks — commands to run, files that must exist, patterns that must appear in a file — that the Python runner executes after the agent says it&amp;rsquo;s finished. If a check fails, the session isn&amp;rsquo;t complete, no matter how convincing the agent&amp;rsquo;s summary was. Commands are given as an argument list and run without a shell, so there&amp;rsquo;s no quoting to get subtly wrong.&lt;/p&gt;
&lt;p&gt;Writing these contracts taught me that a check is only as good as what it can &lt;em&gt;falsify&lt;/em&gt;. A test suite that would also pass for a plausible wrong implementation isn&amp;rsquo;t evidence of anything, whoever wrote the code. Agents make that old truth urgent, because they produce plausible wrong implementations at volume. So before I accept a contract, I try to describe one implementation that would be wrong but would still pass it. If that&amp;rsquo;s easy, the contract needs a sharper check, or the session is claiming more than it can prove.&lt;/p&gt;
&lt;p&gt;Three details make this more robust than it sounds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Baselines.&lt;/strong&gt; Before the run, &lt;code&gt;asl baseline&lt;/code&gt; executes every contract and records what it reports on the unchanged codebase, and preflight won&amp;rsquo;t let a plan start without that evidence. A check can declare how it should behave before any work happens: a reproducer is &lt;em&gt;expected to fail&lt;/em&gt; (proving it actually reaches the thing being changed), while protected behaviour is &lt;em&gt;expected to pass&lt;/em&gt; throughout. A protected check that already fails is a broken contract, not a goal — and a reproducer that already passes means the session is either done or testing the wrong thing. Either way, better to know up front than to blame the session for a pre-existing red.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every check backs a named claim, with a limit.&lt;/strong&gt; &amp;ldquo;The focused test passes&amp;rdquo; says nothing about the database, the deployed service or the code paths it doesn&amp;rsquo;t exercise. Keeping that limit written next to the check stops a green session from quietly turning into &amp;ldquo;it works&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The split between judgement and enforcement.&lt;/strong&gt; The agent decides &lt;em&gt;how&lt;/em&gt; to implement a session. Python decides whether it&amp;rsquo;s &lt;em&gt;done&lt;/em&gt;. The skill the agent runs for each session, &lt;code&gt;session-run&lt;/code&gt;, is under a hundred lines of instructions, and that&amp;rsquo;s on purpose: when the runner launches it, Python already holds the plan lock and has checked readiness, and after the agent returns, Python runs the verification, syncs the progress journal and refreshes readiness. Nothing that matters is left for the model to remember.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point turned into a design rule for everything else: &lt;strong&gt;anything that must happen is code; only things that require judgement go in a prompt.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="what-a-run-looks-like"&gt;What a run looks like&lt;/h2&gt;
&lt;p&gt;Starting a run looks like this:&lt;/p&gt;
&lt;div class="code-block"&gt;
 &lt;div class="code-block__bar" data-pagefind-ignore&gt;
 &lt;span class="code-block__lang"&gt;bash&lt;/span&gt;
 &lt;button class="code-block__wrap" type="button" aria-pressed="false" hidden&gt;Wrap&lt;/button&gt;
 
 &lt;/div&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;asl run ~/.local/share/agent-plans/export-command/ --harness claude&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The runner then loops:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Re-read the plan files from disk.&lt;/li&gt;
&lt;li&gt;Pick the first pending session.&lt;/li&gt;
&lt;li&gt;Start one agent turn, in a fresh context, running &lt;code&gt;/session-run&lt;/code&gt; on the plan.&lt;/li&gt;
&lt;li&gt;When the turn ends, run the session&amp;rsquo;s Verify checks.&lt;/li&gt;
&lt;li&gt;If they pass, mark the session complete and go back to step 1. If not, put the session back to pending and hand the failing evidence to a repair turn; when the repair budget runs out, stop with a reason.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Re-reading from disk on every turn matters more than it looks. The plan directory is the single source of truth, so I can edit a future session while a run is in progress, and the next turn picks up the change. Nothing about the plan lives only in the runner&amp;rsquo;s memory.&lt;/p&gt;
&lt;h3 id="fresh-context-durable-handoffs"&gt;Fresh context, durable handoffs&lt;/h3&gt;
&lt;p&gt;Every session starts in a brand new conversation, loaded with only what it needs: the instructions that apply, the plan, its own session file and contract, and the handoffs from the sessions it depends on. That&amp;rsquo;s the fix for drift: there&amp;rsquo;s no long transcript for early decisions to fade out of.&lt;/p&gt;
&lt;p&gt;It also means the handoff has to carry everything. When a session finishes, the agent fills in its handoff: the files it modified, the key decisions it made, anything blocking the next session, and notes such as why it reran a check. Two optional sections are worth singling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Discovered facts&lt;/strong&gt; record what implementation proved about the plan&amp;rsquo;s assumptions (&amp;ldquo;the config loader already caches; no need for a second layer&amp;rdquo;), so the next session doesn&amp;rsquo;t spend its context re-researching a settled question.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open questions&lt;/strong&gt; hand forward what&amp;rsquo;s still uncertain, named, instead of leaving it to be rediscovered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My test for a handoff is blunt: if losing the transcript changes what the next session is allowed to do, the handoff is incomplete. Conversations are disposable; the plan directory is the memory.&lt;/p&gt;
&lt;h3 id="pausing-is-a-feature"&gt;Pausing is a feature&lt;/h3&gt;
&lt;p&gt;Agents fail in loops more than in crashes: rereading the same files, rerunning the same green check, restating the same hypothesis as a &amp;ldquo;new&amp;rdquo; fix. Left alone, an unattended run will happily burn an hour that way. So a session that can&amp;rsquo;t make progress doesn&amp;rsquo;t keep trying; it pauses, and a pause is a durable state transition with a stable reason, not an abandoned conversation.&lt;/p&gt;
&lt;p&gt;The vocabulary is deliberately small and fixed: &lt;code&gt;blocked&lt;/code&gt; for a decision only I can make, &lt;code&gt;unsafe&lt;/code&gt; when continuing could do damage, &lt;code&gt;verify-failed&lt;/code&gt; when the contract didn&amp;rsquo;t pass after repairs, &lt;code&gt;no-contract&lt;/code&gt; or &lt;code&gt;malformed-contract&lt;/code&gt; when there&amp;rsquo;s nothing trustworthy to verify against, a guard violation, and so on. The agent isn&amp;rsquo;t allowed to invent new reasons. Each pause records what blocked it and the evidence, so the next step is obvious. That makes &amp;ldquo;why did it stop?&amp;rdquo; a question with an answer, and it means a retry has to come with something new — a changed hypothesis, a fixed fixture, a decision — rather than the same attempt with more hope.&lt;/p&gt;
&lt;p&gt;It took me a while to stop treating pauses as failures. &lt;strong&gt;A necessary pause is the system working.&lt;/strong&gt; A run that pushes through an unresolved decision to keep its completion rate up is worse than one that stops and asks.&lt;/p&gt;
&lt;h3 id="the-controls-i-use-all-the-time"&gt;The controls I use all the time&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;--harness&lt;/code&gt; picks the agent: Claude Code, Codex or Copilot CLI. The same plan runs on any of them, and I can switch mid-run for the remaining sessions with &lt;code&gt;asl control select-harness&lt;/code&gt;. A few other controls I use all the time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;asl control stop-after-current&lt;/code&gt;&lt;/strong&gt; lets the current session finish and then exits cleanly, so the next &lt;code&gt;asl run&lt;/code&gt; picks up where it stopped. It&amp;rsquo;s the polite version of Ctrl-C.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--max-sessions N&lt;/code&gt;&lt;/strong&gt; runs at most &lt;code&gt;N&lt;/code&gt; sessions and stops successfully. Useful when I want to look at the first session&amp;rsquo;s output before trusting the rest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pauses with questions.&lt;/strong&gt; If an agent hits a decision it shouldn&amp;rsquo;t make alone, the session pauses with &lt;code&gt;reason: blocked&lt;/code&gt;. &lt;code&gt;asl questions PLAN_DIR&lt;/code&gt; renders the questions, and &lt;code&gt;asl run PLAN_DIR --reply &amp;quot;...&amp;quot;&lt;/code&gt; resumes the paused turn with my answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--review-harness codex&lt;/code&gt;&lt;/strong&gt; adds a final session where a &lt;em&gt;different&lt;/em&gt; agent reviews the finished work and writes its findings to a &lt;code&gt;final-review.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-second-agent-reviewing-cold"&gt;A second agent, reviewing cold&lt;/h3&gt;
&lt;p&gt;That last control deserves a little more space. The reviewer runs on a different harness from the implementer, isn&amp;rsquo;t allowed to change any code, and reviews &lt;em&gt;cold&lt;/em&gt;: from the plan, the contracts and the actual changes, not from the implementer&amp;rsquo;s account of them. Its job is to find where the change violates the intent, the protected behaviour, the scope or the limits of its own evidence. Its output is structured, not &amp;ldquo;looks good&amp;rdquo;: every finding has a severity, the files it points at, an explanation, what &amp;ldquo;fixed&amp;rdquo; means, and the checks that would prove it, and the run refuses a review whose summary line and findings don&amp;rsquo;t agree.&lt;/p&gt;
&lt;p&gt;Repairs for its findings are bounded too. Findings are grouped, each group gets a capped number of repair attempts, and the review-and-repair cycles themselves are capped, so a reviewer and an implementer can&amp;rsquo;t ping-pong forever. The principle I hold myself to on top of that: a repair fixes the finding inside the reviewed boundary, and anything that would change the design goes back to the plan as a new session instead of being absorbed into the fix. The cap limits rework; it never lowers the bar for acceptance.&lt;/p&gt;
&lt;p&gt;Having one model check another&amp;rsquo;s work catches a surprising number of things, but I try not to over-read it. A different model brings different habits for finding failures, not independent evidence. What actually decides acceptance is still the checks, the reproducers and me reading the diff.&lt;/p&gt;
&lt;!-- TODO(seb): an example of a review finding from a second harness that the first one missed. --&gt;
&lt;p&gt;When the run ends, it ends with an explicit reason: &lt;code&gt;all-complete&lt;/code&gt;, &lt;code&gt;max-sessions&lt;/code&gt; or &lt;code&gt;stopped&lt;/code&gt; for the happy paths, or a named pause like a failing check or a guard violation. There is no &amp;ldquo;it just stopped&amp;rdquo;. And &amp;ldquo;all-complete&amp;rdquo; still means &lt;em&gt;the contracts passed&lt;/em&gt;, not &lt;em&gt;the change is correct&lt;/em&gt;: it&amp;rsquo;s the point where I go and read the diff, the handoffs and the review, not the point where I merge.&lt;/p&gt;
&lt;h2 id="guardrails-briefly"&gt;Guardrails, briefly&lt;/h2&gt;
&lt;p&gt;Once a run is unattended, you need a way to steer it, and the most important distinction I ended up drawing is between &lt;em&gt;advice&lt;/em&gt; and &lt;em&gt;enforcement&lt;/em&gt;. Telling an agent not to deploy is steering; making sure it can&amp;rsquo;t reach the deploy credentials is a boundary. A prompt expresses intent, never authority.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guidance is advice.&lt;/strong&gt; I can send a running session a note (&amp;ldquo;prefer the existing date helper over adding a dependency&amp;rdquo;) and it&amp;rsquo;s delivered into the conversation. It can shape implementation choices, but it can&amp;rsquo;t change the session&amp;rsquo;s scope or its completion contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only a guard enforces.&lt;/strong&gt; &lt;code&gt;asl control guard add&lt;/code&gt; can deny a command (matched as an exact argument prefix and enforced by a per-run &lt;code&gt;PATH&lt;/code&gt; shim) or protect a path (compared against a snapshot taken at the start of the turn). A violation rejects the turn and pauses the run. It never rolls anything back, so I can always see exactly what the agent did.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A guard also reports, per harness, whether it is &lt;em&gt;active&lt;/em&gt;, &lt;em&gt;degraded&lt;/em&gt; or &lt;em&gt;unsupported&lt;/em&gt;. Not every agent CLI exposes the same hooks, and I&amp;rsquo;d rather have a guard say &amp;ldquo;I can&amp;rsquo;t enforce this here&amp;rdquo; than quietly imply it did.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s more to supervising a run than this, like a mailbox that lets a running agent ask questions without stopping, and a separate &amp;ldquo;overseer&amp;rdquo; that watches runs across projects. They all follow the same rule: an AI that can be argued into an action must never be the guard.&lt;/p&gt;
&lt;h2 id="when-this-is-overkill"&gt;When this is overkill&lt;/h2&gt;
&lt;p&gt;Most changes don&amp;rsquo;t need any of this. A local, reversible edit with a test that already covers it needs a boundary, a check and a review — that&amp;rsquo;s it. I reach for a full plan and an unattended run when a change spans several sessions&amp;rsquo; worth of work, when the order of the steps matters, or when I want the run to make progress while I&amp;rsquo;m doing something else. The pipeline exists so every decision has a home when a change is big enough to need one, not so every typo fix carries maximum ceremony.&lt;/p&gt;
&lt;p&gt;A quick way to tell: before handing a task to an agent, can I state the observable outcome and one behaviour that must not change, bound the files involved, name a check that would fail for a plausible wrong implementation, and point to someone (usually me) who can actually absorb the diff? If one of those answers is &amp;ldquo;no&amp;rdquo; or &amp;ldquo;not sure&amp;rdquo;, the right next step is usually a smaller precursor — reproduce the bug, inventory the callers, write the failing test — rather than the whole change.&lt;/p&gt;
&lt;h2 id="lessons-from-four-months-of-building-it"&gt;Lessons from four months of building it&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;asl&lt;/code&gt; started as a small loop in a script in early June 2026. Four months later it&amp;rsquo;s about 180,000 lines of standard-library-only Python with about 270,000 lines of tests, built largely by running plans through the tool itself. A few things I&amp;rsquo;d tell myself at the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;More tests than code is not a vanity metric.&lt;/strong&gt; When most of the code is written by agents, the tests are the specification, and they&amp;rsquo;re what makes the next change safe. There is also a &lt;code&gt;reliability-stress&lt;/code&gt; suite that reruns timing-sensitive scenarios under load and across time zones, because &amp;ldquo;it passed on my machine at 3 p.m.&amp;rdquo; is not good enough for something that runs unattended overnight.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make failures early and loud.&lt;/strong&gt; Every hour spent on preflight, baselines and repair hints has paid itself back many times over in runs that didn&amp;rsquo;t die halfway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Promote automation by maturity, not enthusiasm.&lt;/strong&gt; Almost every piece of &lt;code&gt;asl&lt;/code&gt; went through the same ladder: do it by hand while watching, write it down as a checklist or skill, script the deterministic parts, and only then enforce it in the runner. Skipping a rung always produced something that failed in a way I didn&amp;rsquo;t yet understand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn each run failure into a control.&lt;/strong&gt; The best features came from runs that went wrong. Agents pausing the whole run for every small question became a live question-and-reply channel; answers that were accepted but never actually read by the agent became receipts proving the agent consumed them. Each one started as a reproducer from a real run and landed as its own reviewed change — never as a quick patch in the middle of the run that exposed it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write down the reason for every stop.&lt;/strong&gt; Every pause and every exit has a stable, named reason. That made the tool scriptable, and made it possible to build a supervisor on top of it at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t trust a harness to enforce what it can&amp;rsquo;t.&lt;/strong&gt; Supporting three agent CLIs taught me that they differ a lot in what they can actually guarantee, and that the honest answer is to report capabilities instead of assuming them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be suspicious of feeling faster.&lt;/strong&gt; A &lt;a href="https://arxiv.org/abs/2507.09089"&gt;2025 randomized trial by METR&lt;/a&gt; found experienced open-source maintainers were about 19% &lt;em&gt;slower&lt;/em&gt; with early-2025 AI tooling on their own repositories, while believing they&amp;rsquo;d been faster. That&amp;rsquo;s not a verdict on today&amp;rsquo;s tools, but it&amp;rsquo;s a good reason to judge a workflow by what passes review rather than by how productive a session felt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard library only has been worth it.&lt;/strong&gt; No dependency upgrades, no virtual environment to break, and the whole thing installs by symlinking a directory with GNU Stow, alongside the rest of &lt;a href="https://www.sglavoie.com/posts/managing-dotfiles-with-git-bare-repository/"&gt;my dotfiles&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- TODO(seb): what broke along the way, and what held up better than expected. --&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The biggest change wasn&amp;rsquo;t any single feature; it was moving &amp;ldquo;done&amp;rdquo; out of the agent&amp;rsquo;s hands. Once every session has a contract that a program checks, the rest follows naturally: sessions can be small and start fresh, runs can be unattended, pauses become information instead of mysteries, a second agent can review the first one&amp;rsquo;s work, and I can spend my attention on the plan and the review instead of on babysitting a conversation.&lt;/p&gt;
&lt;p&gt;If you work with coding agents a lot, my advice is to start there, and to start small. Pick one bounded task you&amp;rsquo;d normally do yourself in under an hour. Before handing it over, write down what &amp;ldquo;done&amp;rdquo; means and one check that would fail if the agent got it plausibly wrong. Then review the diff against that, not against the agent&amp;rsquo;s summary. If it doesn&amp;rsquo;t hold up, you&amp;rsquo;ve lost one session and gained a concrete reason; if it does, you have your first contract.&lt;/p&gt;
&lt;h2 id="resources-and-references"&gt;Resources and references&lt;/h2&gt;
&lt;h3 id="external-links"&gt;External links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.claude.com/en/docs/claude-code/skills"&gt;Agent Skills - Claude Code docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.claude.com/en/docs/claude-code/hooks"&gt;Hooks - Claude Code docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2507.09089"&gt;Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity (METR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secure_Coding_with_AI_Cheat_Sheet.html"&gt;OWASP Secure Coding with AI Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/stow/"&gt;GNU Stow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="from-this-website"&gt;From this website&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sglavoie.com/posts/systematic-reviews/"&gt;Systematic reviews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sglavoie.com/posts/git-worktrees-for-a-better-parallel-workflow/"&gt;Git worktrees for a better parallel workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sglavoie.com/posts/managing-dotfiles-with-git-bare-repository/"&gt;Managing dotfiles with a Git bare repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Sébastien Lavoie</dc:creator><pubDate>Fri, 09 Oct 2026 16:20:00 -0600</pubDate><guid isPermaLink="false">https://www.sglavoie.com/posts/from-plan-to-unattended-agent-runs/</guid><category>workflow</category><category>ai</category><category>agents</category><category>automation</category><category>claude-code</category><category>productivity</category><category>python</category></item></channel></rss>