A level 3 agentic harness built by following Agentic Programming by Jerod W. Wilkerson. The harness is a story execution system: stories enter with an approved plan, move through implementation, testing, documentation, and verification, retry when verification fails, and end completed or escalated — or pause, when capacity runs out, and continue where they stopped.
About the name. The name is aspirational. What you'll find here is a level 3 harness, but level five is where the ladder leads, and the repository is built to grow in that direction.
This repository tracks Agentic Programming through level 3. Part 3 (Chapters 12–19) explains how an agentic harness works, and Appendix A, "Building a Sample Level 3 Harness," builds this one from an empty directory to a working system, including the real escalations that happened along the way.
The book is at agenticprogrammingbook.com.
The appendix is the starting point, not the finish line. It stops at a deliberately small harness so the essential structure stays readable, and then hands you a roadmap: harden first, following Chapter 18, then scale, following Chapter 19. This repository is walking that roadmap. Every improvement arrives the way the book says it should — as a story the harness plans, executes, verifies, and documents itself.
The exact harness Appendix A describes is tagged appendix-a. If you are reading the appendix and want to check your own build against it, or want to start where the appendix stops, use that tag:
git clone https://github.com/jerodw/level-five.git
cd level-five
git checkout appendix-a
main has moved past it. The appendix's code excerpts — the workflow definition, the implementer prompt, the coordinator's routing loop — match the tag, and the differences on main are the point rather than drift.
Each story below is a step the book's roadmap calls for, or a failure the build hit that the roadmap did not anticipate. The story artifacts are committed in .harness/stories/. Run directories are execution state rather than source, so .harness/runs/ is gitignored and does not travel with a clone; the runs worth keeping — four escalations, and three runs preserved for what they showed — are copied into .harness/runs-archive/.
| Story | Change | Where the book argues for it |
|---|---|---|
| 001–002 | l5-status; per-stage changed-files records |
Appendix A (appendix-a) |
| 003–004 | One shared harness layer; machine-readable artifact schemas | Ch. 16, prompt layering; Ch. 14, artifact contracts |
| 005–007 | Schema-directed story parser and pre-flight validation; one reader of a story artifact; coordinator-enforced stage output ownership | Ch. 15, governance boundaries; Ch. 18, hardening |
| 008–009 | The story schema and the workflow's stage rules injected into the planner prompt | Ch. 16, injection over restatement |
| 010–012 | attempts/attempt-N/ archives, execution-history.json, retry-history.json |
Ch. 17–18, retry evidence |
| 013–017 | Verification hardening: the suite re-run in a clean clone, assertions that can be shown to fail, the schema inventory moved out of tests/, the coordinator's output contract asserted directly, an implementer's test edits decided by reverting them |
Failures this build hit; Ch. 18 in spirit |
| 018–020 | Story artifacts validated at plan time; the revert check reverting to what the stage found rather than to HEAD; escalated runs made resumable, committing their work when they stop |
Ch. 18, hardening; Ch. 17, retry evidence |
| 021–024 | A run commits only what it produced; required outputs must be written by the attempt that ran; l5-plan commits the artifact it caused; escalation-summary.md carries the finding rather than a pointer to it |
Ch. 18, hardening |
| 025–027 | Plan time validates the artifact it just wrote; one resolution of a story's own commit range; a re-run onto a branch already holding finished work refused | Ch. 18, hardening |
| 028–031 | Retries routed to the stage that owns the defect; loading code retired out of git history and the rule enforced mechanically; a story branched from a declared base; mutation controls that mutate the working tree, never a pinned revision | Ch. 17, retry routing; Ch. 18 |
| 032–035 | A plan refused when it assigns work a stage cannot own; cloning over the normal transport instead of copying a live object store; a resume guard that works when the harness is its own target; stages granted the read-only tools they need, with mutation denied at the door by a hook | Ch. 15, governance boundaries; Ch. 18 |
| 036–038 | A stage that failed mechanically runs again in place, on its own budget; a stage's baseline is what that stage first found; a test module named for what it checks | Ch. 18, hardening |
| 039–043 | Every configurable value proven configurable; no target-stack literal in harness source; the verification runner no longer assumed to be Python; a plan may assign an existing file to the implementer; an undeclared config key refused | Ch. 15, governance; portability the appendix assumes |
| 044–046 | The documenter records what it changed; the documenter runs before verification, so its output is judged; the test location comes from configuration | Ch. 18, hardening |
| 047–051 | The tester writes fixture-based tests, asking whether a shipped artifact is an assertion's subject or its input; every test that needed a workflow as an input now builds one, leaving only the modules the shipped definition is genuinely about; a verifier verdict can say that retrying cannot finish the work, ending the run without spending the budget; retry guidance declares what would satisfy each instruction, so guidance that sanctions the outcome it then fails is caught and rewritten rather than charged to the stage; and a documented claim about a story with no merged work is reported, because the run directories and request files a documenter writes from are untracked and reach no clone | Ch. 18, hardening |
| 052–053 | A new test may not resolve this repository's own git history, so a test's result stops depending on what has been committed since; the modules that still did are declared under a ceiling that only a conversion lowers, and the conversion took that ceiling to zero — the texts those tests needed are committed fixtures now, read from the tree rather than out of the commit graph | Ch. 18, hardening |
| 054–058 | A documented quantity counts whether it is written in digits or in words; a finding too small to fail a run has somewhere to go, re-entering the workflow as a correction pass that spends no retry budget and may change words but never behaviour, beside a shared prose layer reaching every stage that writes for a reader; a story's commit range ends where the story ended rather than where it escalated; a plan assigning a stage a path it is restricted from is refused when the session ends, naming the grant that would make it legal; and a check that re-runs the suite says so before it starts, so a silent console no longer reads as a hang | Ch. 18, hardening; Ch. 15, governance boundaries |
| 059–063 | Budgets, resumes, and what a stopped run keeps: l5-plan offers to run the story it just committed and skips without reading when nothing can answer; every stage declares a self-route budget, so a mechanical failure runs again in place rather than ending the run; a crashed run's resume archives the interrupted attempt before re-running the stage, and refuses rather than overwrites; a resume restores the attempt allowance so a run that escalated at its ceiling does not resume with nothing to spend; and a run has a cost ceiling — one for the run and one per stage execution, declared with the reasoning behind each figure, with every invocation recorded in cost.json |
Ch. 17, retry evidence; Ch. 18, hardening; Ch. 13, budgets |
| 064–068 | The coordinator runs the test suite, not the agents — an eleven-minute command stops being something a ten-minute agent turn has to fit around: the implementer runs only the tests its change touches, since the revert check, the coordinator and the clean-clone check each run after it; a story that escalated and resumed leaves a completion its merge cannot drop; the coordinator runs the target's configured suite and the tester only authors, so the verdict is an exit code rather than a stage's account of one and the full output is kept; a correction pass costs a correction rather than a re-run; and a plan may declare that a change forces an existing test to adapt, so plan-time validation refuses only what a run would actually refuse | Ch. 18, hardening |
| 069–072 | A second workflow, and the work choosing it: a story artifact selects the workflow definition its run loads, so the choice belongs to the work rather than to the target's configuration; refactor-workflow.json drops the create restriction and the revert check — which assume every legitimate test edit is forced by a change elsewhere — and guards the implementer with a suite census instead, because a refactor's threat is weakening the validation that already exists rather than authoring the validation that judges it; a prompt's filename says which workflow owns it; and the planner proposes the workflow it plans for, every definition declaring an applies_when, with a headless invocation that can name no workflow and ask nobody refused before anything is invoked |
Ch. 18, hardening |
| 073–076 | Enforcement where a prompt paragraph used to stand: a stage that runs no suite has the invocation denied at the door by a deny-only Bash guard that reduces a command to its program and targets; the denial is recorded as a filter over the spellings agents reach for rather than a boundary an invocation cannot cross, with the measurement that makes it worth having; a superseded attempt keeps its evidence, the archive collecting each check's result off the shape of the declaration that carries it and following a record's own output_path; and the workflow proposal delivers its answer to a path it is permitted to write, keeping the transcript of a classifying turn a developer needs to read |
Ch. 18, hardening |
| 077–080 | A cheaper check, a suite that behaves on real hardware, and a run that waits rather than ends: a writing stage may nominate the test that fails without its change and the revert check decides on that test alone — passing where the stage left the tree and failing with the governed edits reverted — because one failing test proves a failing suite while no number of passing tests proves a passing one; a pty teardown stops overwriting the exit status its test asserts; the suite runs in parallel with no fixed worker count, from dependencies declared in a tracked file; and a capacity stop pauses the run rather than ending it, bounded by a wait the harness was told rather than one it guessed, because a budget ceiling is a reason to stop while capacity exhaustion is only a reason to wait | Ch. 18, hardening; Ch. 13, budgets |
| 081–085 | Evidence that outlives the run directory it was written in: .harness/history/*.jsonl are versioned append-only records declared in one schema, summaries rather than copies, that no routing decision reads; every terminal path appends its record before the commit that path leaves, so the only writer's own output is never left untracked; every coordinator suite run records the scope it was narrowed by, so a whole-suite green and a one-test green stop being indistinguishable; each run keeps a result-and-output pair keyed by stage, attempt and try, so a rerun no longer writes over the evidence the self-route that caused it cited; and a later pass supersedes an earlier failure only where its scope is a subset, so a rerun narrowed until the answer is convenient clears nothing |
Ch. 19, scaling; Ch. 15 and Ch. 18, evidence |
| 086–088 | Who authorized the work, and who may edit what judges it: .harness/stories/ is blocked to every stage, so no stage can rewrite the artifact its own work is judged against; a story artifact carries a required mandate and the coordinator refuses at pre-flight — above the run directory, the branch and every invocation — to run work whose mandate does not resolve to a human, permitting only on positive evidence and reporting each failure distinctly; and the approval behind it is observed rather than inferred, l5-plan asking the developer directly and stamping only what the answer gave it, where before the evidence was that a terminal was attached and a file appeared |
Ch. 16, governance; Ch. 18, evidence |
| 089–093 | Filing to an issue tracker without letting the tracker break a run — the outbox: anything bound for a system outside the harness goes into a durable local queue first, written through a temporary file replaced into place and keyed by a digest of the identity alone, so no failure on the far side becomes a failure of the run that produced the item; enqueue is total over whatever it is handed, since a value json could not render would otherwise have stopped a story, and a refused item is reported rather than coerced; one configured command files an entry, through which the harness knows nothing else about the tracker — stdin, a key, a reference never parsed, and an exit code saying landed, retry or stop; the sweep drains at three points chosen so failure is free and alone among the pre-flights may not refuse; and the same shape run backwards asks what is already filed, carrying nothing known and nothing filed as different answers |
Ch. 17, external systems; Ch. 18, hardening |
| 094–095 | The Inspector reads a target's own code and files story briefs — pre-planning artifacts carrying intent, evidence and a severity, which nothing executes and which become stories only through l5-plan's interview, so there is nothing to refuse; as the outbox's first producer it pays what a producer owes, its dedupe identity carrying the mechanically stable parts and never the prose a model rephrases between runs; and that dedupe gains its free tier, the local queue read as a record of what this harness already filed, both sources asked every time with neither a fallback for the other — a landed entry suppressing, a pending one reported as queued rather than filed, and a failed one suppressing nothing, because it is terminal and the finding it carries reached nobody |
Ch. 18, inspection |
| 096–099 | A brief becomes something a person uses end to end — written in conversation, filed, and planned from: what the planning entry point takes in, what it tells you it is doing, what a brief is allowed to be about, and where one a developer asked for goes. The link that makes the chain a loop: l5-plan --brief <key> fetches a filed brief through the same configured command the dedupe question is asked of — asked this time by key, so one brief comes back in full rather than a bounded summary of a set — and hands the planner the brief's own prose instead of a paraphrase of it, with the brief's workflow standing in for the selector's proposal. Planning stays interactive: the interview happens, the developer approves, and the mandate is still stamped from an observed answer, because "plan from a brief" and "plan headlessly" sound like the same feature and are not. The brief is a plan-time input alone, leaving one trace — its key in the story's description, as prose — and the filing side is widened so the round trip is lossless, since a title and a body throw away the fields a fetched brief is held to. And every message a developer decides on now names the story's title beside its id — the approval prompt, which named no story at all where "approve this plan?" omitted the act the whole mandate mechanism rests on, the run offer, each skip line, and the fresh run's workflow-started announcement; the title is bounded where it is printed rather than trusted, because it is prose an agent wrote and nothing in the story contract constrains its length or forbids it a newline. And a brief stops being only a defect the Inspector found: severity is redefined as how much the work matters and confidence as how sure the writer is of the judgement the brief makes, each given a defect reading and a work-to-be-done reading at every level, and category gains feature and refactor — appended, because the identity a brief is filed under carries the category as a string, so the ten already filed keep their keys. The Inspector still files defects and nothing else, since everything it can find is one; the widening is for the producer that is a person, for whom leaving a feature nobody built alone has no consequence to weigh and a feature just asked for cannot be imagined. And the harness's second producer of briefs finally has somewhere to put one. It wrote them already and stopped there — the brief existed as text in a terminal and reached nothing. It now goes into the same outbox the Inspector files into, deliberately rather than into a directory of files: a second producer writing files would rebuild the identity, the key, the transport and the landed/pending/failed record beside the queue that already has them, and dedupe would never see those briefs, so the Inspector would go on filing findings a developer had already filed by hand. What a brief is filed under moves into a module of its own, because an identity derived twice is a duplicate filed on every inspection. The seam is a skill rather than a command, since the filing is part of a conversation: the harness ships a plugin directory, l5-assist loads it for the session, and an assist session in any target has it with nothing installed into that target. The judgement half is the skill's — the slug rule, the bare paths, showing the developer the whole brief and filing on their word — and the deterministic half is a module the suite drives. Nothing here plans, confers a mandate, or changes anything already filed; a brief is inert, and a brief nobody wants costs a human reading it and deciding no |
Ch. 18, inspection; Ch. 17, external systems; Ch. 16, governance |
| 100–103 | Inspection becomes standing, and pays for itself: when a run completes the coordinator inspects what that story touched and files briefs for what it finds, so nobody has to remember to ask — its scope the changed files plus what git tracks beside them, because duplication and parity gaps live in the file the story did not change. It may never block or delay a run, expressed as a function with nothing to return and no way to refuse. What an inspection costs is then recorded, since every other ceiling here was set from a record and a number guessed and never tightened stays where it was guessed. And a run stands on its branch before it writes anything: writing the tracked history first meant git refused the checkout that followed, killing every resume from the base — repaired, then repaired again where the resume guard was reading a branch rather than the tree it would actually run | Ch. 18, hardening; Ch. 18, evidence |
| 104–110 | The developer, the tracker, and what a stage is allowed to do: questions and explanations move to the level of consequence, because a question nobody can evaluate without opening three files gets a guess for an answer. The outbox reaches a real project board, filing every brief with its category as a label and its classification as fields that sort and group. A tester gains the restriction the code stage already carried, and a refused edit is undone rather than stopping the run. The queue is split from the record of what it filed, so what grows without bound is an index rather than a backlog. A forgotten file stops costing what a red suite costs. And a test may not bound how slow the machine is allowed to be — the assertion that had ended a run three times | Ch. 15, governance; Ch. 17, external systems; Ch. 18, hardening |
| 111–116 | What a stage owes, and what a brief says: a stage that changed a file and left it out of its own record is caught, by signing the tree before its turn and comparing after — those records being what the whole governance layer reads. A stage may ask, mid-turn, whether it has written what it owes, with a hook catching what the question would have; the check is one derivation, because two spellings of written freshly and valid drift silently in the worst direction. A refile over a terminal failure is reported rather than silently erasing why filing keeps failing. The Inspector gains a severity floor, set from output already read rather than predicted. And every brief is titled for the behaviour it produces, so a tracker column stops reading as a list of complaints | Ch. 18, hardening; Ch. 18, inspection |
| 117–154 | Planning and running each get a tree of their own: a plan written during a run had been landing its commit in the middle of that run's branch, so both now happen in a worktree cut for them, and the developer's checkout is left where it was found. Everything that assumed one tree then had to be told: a fresh worktree holds only tracked files, so the interpreter a target configures inside its own tree was absent and a run could not invoke its own suite; the outbox and its receipts had become per-tree, so an entry nothing could file sat where nothing later looked; and a run started inside a worktree nested the next one beneath it rather than beside the repository's others. A planned story is now published onto the brief it came from and carries that brief's key, so three moments — planned, running, complete — move the item a developer is watching, where before a brief sat exactly as filed while its story was written, run and finished. The board is read by an item's own id rather than by listing a project that outgrows its own bound, and a field's name is matched the same way when it is read and when it is written. That last repair had to be made twice, in two of the three scripts answering this target's tracker, which shared most of what they knew and duplicated the rest — so the three become one file with three entry points, the project settings, the markers, the field-matching rule and the id resolution declared once and the three jobs branches below it. The harness side does not move: it still invokes three commands and still interprets three sets of exit codes. What changes on this deployment falls out of the merge rather than being its object — one project constant now serves all three branches, so status moves stop failing where the item script had never been given one. And the dedupe that whole marker scheme exists to serve turns out never to have run: the query searched the tracker once per path, and an inspection scoped to sixty files spent close to a minute against a thirty-second bound, so it was killed partway through and answered nothing — on every story from 101 to 130, said each time as a clause at the end of an event line and read by nobody as the standing failure it was. The searches are batched now, twenty markers to a query, with any batch whose page filled to the limit or whose search failed re-asked path by path, because a truncated page read as complete is a duplicate filed. Whether dedupe ran is recorded on every inspection rather than only where it did not, so the question is one read of a tracked file rather than a scroll through thirty runs. And the two halves of publishing a story onto its item stop being one thing: the status was resolved before the document was written and a target configuring no project was refused outright, so a target with no board never got a projection at all — the half that has nothing to do with a board lost to the half that does. The document is published first now, a status that cannot be honoured leaves it there and exits non-zero, and no configured project skips the board work the way the filing branch already did. What that costs is said rather than hidden, in the module, in the schema and in the line the developer reads: a failure now means not all of the question was answered rather than none of it, so the projection may already be on the item. And a run that moves its own tracker commands says so: a run reads its target's configuration once, before the run directory exists, and threads that one dictionary through to the end — which is deliberate and is kept, since a run that re-read the file could quietly exercise something other than what it was launched under. The consequence had been that a story whose own work moves a tracker command spends the rest of its run calling the path it just removed, and the entry left pending, the status not sent and the empty dedupe answer all read as the mechanism being broken. What is added is noticing rather than re-reading: the two values are compared and reported, each of the three post-story sites saying what a stale command means where it stands, and nothing routes on the answer — a configuration that will not parse reports nothing moved, because a report about a run whose work is already committed may not become the thing that fails it. And the suite stops holding a second copy of what the target configured: four of this deployment's board values were written into it as literals, so renaming the column a filing lands in — one constant in an installed script — turned the suite red on a defect that was not there. They are read out of the script now, with the claim the writing-down was really for kept by two that name no value: a declared constant must be non-empty, so a target that configured no board fails loudly rather than passing every board assertion vacuously, and the installed copy is driven with nothing in its environment, so the board it reaches is the one it declares. A standing guard scans for a board value written down again and is shown to catch one planted, and a copy declaring an invented board is driven onto that board instead, so the suite is proven to follow the configuration rather than to agree with it by coincidence. And the step after the harness's last tracker moment is taken, on this deployment and nowhere else: a run's final word is ready-to-merge, sent when it completes, and the merge happens afterwards where the harness is not watching — so a merge here closes the brief its story was planned from, and the board's own automation carries the item to Done. It is this repository's glue and is installed into no target, because closing an issue on a merge assumes a tracker, a pull-request model and a deployment that wants it, and the harness assumes none of the three: it gained no fourth status token and no configuration key. The workflow definition is only a trigger; what decides lives in a shell script beside the tracker script, where the suite drives the shipped text rather than an imitation of it, and the close carries no comment — because this board moves an item from the state of a linked pull request, and a link is what dragged a finished brief backwards once already And a red suite stops being attributed to whoever ran last: the coordinator had been re-entering the stage whose turn had just ended, which for the tester is backwards by construction — its job is to write assertions that can fail against somebody else's code and it may only change files under the tests directory, so the one stage a broken tree was handed to was the one stage forbidden to repair it, and story-136 spent two re-entries and $29 establishing that twice before it stopped. Whose failure it is was never the coordinator's judgement to make and is not moved into it: a non-zero exit is recorded exactly as it was recorded before, a line saying so is appended, and the run advances — so the failure reaches the verifier, which already reads the workflow's routing table and already writes back a category the coordinator routes on without interpreting a word of it. What the verifier needed was one paragraph saying a red suite is a failing verdict rather than a finding, since the rest of that prompt would otherwise have entitled it to record the failures and pass; what makes the whole thing safe was already there, in the refusal that will not let a run complete carrying a suite failure nothing superseded, so a verifier that ignored one wastes its turn and stops the run anyway. The tester's second self-route goes with it, the red suite having been the cause it was bought for And a brief says which part of the system it concerns, so a backlog can be sorted by feature and not only by kind and severity. What makes that affordable is what it refuses to be: which areas a system has is target knowledge, so the vocabulary is a document in the target's own standards directory, reached by the glob that already hands the standards over whole and looks for no file by name — a target declaring none is read as declaring none and is untouched by every part of it. The harness gains a slot rather than a taxonomy, one optional free-text field carried on the terms the paths are already carried on, and the brief's identity does not take it: a growing vocabulary re-sorts areas, and an identity carrying one would refile every brief whose area moved. Both producers are told the same rule and told which way to fail — name exactly one from the document, name none where none fits, never coin a name, because a missing area is reported and gets a name added while a wrong one is silent and misfiles the work for everyone who filters afterwards. A filing that named none is said in a line of its own before the summary rather than as a clause at the end of it, which is the shape the dedupe failure had when it stayed true for thirty stories and was read by nobody — and the gate that keeps that honest is the one a target with no vocabulary depends on, since without it every brief from everyone who never opted in would carry a complaint And a slip in the one field that reaches nothing stops costing everything beside it: an envelope whose area suggestions carried a single unusable entry — a concerns misspelled, omitted or left null, which is the ordinary slip of a model asked to write one per brief it could name no area for — failed validation as a whole document, and every well-formed brief that invocation had written was discarded as a scope that produced no usable artifact, paid for and thrown away. The findings beside it had been deliberately typed as objects and nothing more so that exactly this could not happen, and the field added next to them answered the same question the opposite way, strictly, on behalf of data that reaches no brief, no payload, no outbox entry and no tracker. The tolerance written for the case could never run, and said so in its own first line. The envelope now declares that field by description alone and holds it to none of it, so the filter beneath is the only thing that judges an entry and a field reaching nothing may not become the thing that stops an inspection And a filed brief stops landing where every other filed brief landed: its category and the paths it is about already say enough to choose between a column somebody reads and a column that waits, so the sync chooses, and a person who disagrees drags the card. It is routing rather than a verdict and is written to fail toward being seen — the Status is written only where the board reports none, a path override is consulted before the category so that drift in an agent-facing file stays reviewable while the same category elsewhere waits, and a category named in no list lands in review, which costs a fuller column rather than a buried finding. The script holds no vocabulary of categories and no vocabulary of paths and checks a member of neither against anything, exactly as it already held none of the field names beside it; the lists are whitespace-separated data and the path match is a prefix rather than a substring, with a near-miss path in the suite to keep it one. The template ships all four values empty and the deferred column is the switch, so a target that opts into none of it files exactly as it filed before any of this existed, and what this deployment actually defers lives in its installed copy alone — because a template carrying those would hand one deployment's triage to every other And a run stops going quiet at the one moment it has told the developer to stop watching: the post-story inspection began after "story completed" and said nothing for about as long as a stage, which reads as a hang rather than as work, and then said everything at once — the counts, every drop reason, the dedupe verdict and every trimmed path on one line, some hundred and thirty names of them. It now says it has started before it starts, and says what the wait is worth beside it, which is the clause that stops a silence being read as a fault; the convention gets one home rather than each site imitating a neighbour, and that home refuses to render a line whose cost clause is empty, since a step announcing itself without saying what the wait is worth is the failure the announcement exists against. The inspection turned out not to be the only silent step once somebody checked instead of assuming — the outbox sweep was one too. What the inspection did is now a line a person takes in at a glance, and the names that made it unreadable go to the run's own log, named from the summary by their count and their file: the property being protected was that a trimmed name stays recoverable, and a count beside a location keeps it, while a terminal stops having to be where the whole record fits. A log rather than anywhere else because the watcher this roadmap still owes is a thing that watches a log being appended to while work happens And the prompts catch up with it: a change to what follows a red suite had been made in the coordinator and written into one of the prompts that describe it, leaving two asserting the behaviour it removed and one without the paragraph that makes the new behaviour work — and these are instructions to an agent rather than commentary for a reader, so a stale one costs wrong work and not confusion. The tester had been told a red suite would bring it back in place to repair what failed, which is the opposite of what now happens and the opposite of the argument for the change, since that stage may only write tests and is the wrong place to send a broken tree; it is now told the run advances and another stage judges it. The refactor verifier, whose workflow sits in the same blast radius because the coordinator's suite block names no stage and no workflow, gains the paragraph its sibling already had, in its own terms: a suite failing in the tree the change left is the tree being broken, which is the one thing a behaviour-preserving change may not do. Without it that workflow would have paid the whole cost of the change and taken none of its benefit. A race in an unrelated test surfaced by chance on the way and was fixed rather than retried past — a marker file existed before anything was written into it, so a reader could see the path appear and the content missing And the third filing of one defect is answered by holding the rule rather than repairing another instance of it: twice already a prompt had been found describing a coordinator behaviour the coordinator no longer had, and twice the instances were fixed while nothing was left asserting the rule, so the next stage to declare a suite run would have drifted the same way and been filed a fourth time. What holds it now is computed from the shipped definitions — which stages declare the suite run is read off workflows/, and each of their prompts is held to saying both halves, where a red suite goes and what it costs the run — so no prompt is named in the assertion and a workflow added later is covered by nobody remembering to add it. Four controls keep that from being a spelling: a prompt silent about a red suite is reported, one saying where it goes but not what it costs is reported, one whose stage declares no suite run is outside the rule, and a stage that stops declaring the suite run stops being held — each building its own prompts and definitions under a temporary directory, so the shipped tree is never edited to manufacture a failure. The cost is what both earlier rounds had left out, and it is the half that changes behaviour: a stage told only where a red suite goes can read the carry-forward as free, where one told it buys an archived attempt and a line in the retry history knows that the targeted run before its turn ends is the cheapest place a failure can be found And the rule that holds them is made honest as well as general: it had asked whether a prompt contained the required phrases anywhere at all, so the right words scattered through passages about something else satisfied it — the standing weakness of an assertion over prose, and one that matters here more than most, since this rule exists because the prompts kept drifting and a rule satisfiable by accident fails silently in exactly the case it was built for. Both file-wise rules now decide over a single paragraph, read by a splitter that collapses wrapping, so a claim made across a hard-wrapped sentence still counts as one passage while a claim split across two does not. Three controls for each rule carry it: phrases met in unrelated prose are reported, a rule split across paragraphs is reported, and a rule stated in one paragraph is not. That the four shipped prompts pass unchanged is the premise the story was planned on rather than a result it arrived at — editing a prompt to make an assertion pass was forbidden by the story's own scope, which is also why its implementer changed nothing and said so: every file the work touches belongs to a later stage, and doing it anyway would have put the whole change under the prefix that stage's revert check governs And the guard that stops a story branch being cut from a stale base stops being blind to one: it compared the local base against its local remote-tracking ref and nothing refreshed that ref first, so a checkout that had not fetched since the base moved held both of them behind, in agreement, and passed a check whose whole purpose was that case — a refusal already written, which simply never fired. What it cost is on the record: a merge, a plan begun sixteen minutes later from an unfetched checkout, a branch cut five commits back, and a story planned against a tree its own branch would never hold, whose run delivered nothing and whose repair was a developer resetting the branch by hand. The ref is brought up to date before the comparison now, so the check answers about the base as the remote holds it rather than as the last fetch left it; what it decides is unchanged and a stale base earns the wording it always would have. The care is in the failure: this is network work in a pre-flight, and being offline may not refuse a run for a condition that has nothing to do with the work, so a refresh that cannot be made leaves the decision exactly where it stood and says so in the run's record, while one that succeeded says nothing and having nothing to refresh is not a failure at all. The fetch cannot sit waiting for credentials nobody will type And the fetch that check makes cannot hang: putting a fetch in a pre-flight had stopped it waiting forever for credentials nobody would type, and left it free to wait forever on a remote that simply never answers — before any work has started, which is where a stall costs most, since a run that refuses is recoverable and a run that hangs is a developer wondering whether to wait. One bound covers every fetch the module spawns rather than each site carrying its own, at twenty seconds, chosen so a slow remote reads as slow rather than as hung and deliberately not zero, which would mean no bound at all. It is a constant rather than a configured key, unlike the tracker timeouts beside it, because a target's tracker is a target's business and this is a knob nobody is expected to turn — a configured key would be permanent surface for it. The kill reaches the process group and not the process: the fetch is spawned into its own session, because killing it alone would leave the child git spawned to talk to the remote running past the bound, which is the failure a naive timeout leaves in place. And the timing assertion this needs earns its place under the standing rule that no test may bound how slow the machine is allowed to be — the stalled fetch's stub is told to sit far longer than the ceiling, so what is asserted is that the fetch was killed rather than that it was fast And the answer that bound guarantees is made the right answer: a clone that could not reach its remote had refused with the sentence it uses for a story that was never planned, which sends a developer hunting through the repository for a missing artifact when what broke was the network. There are two refusals now — the artifact is absent from the branch, or this clone could not ask — and where it could not ask the sentence carries why, in the three shapes the base refresh already uses: a spawn the platform refused, an expiry naming the bound it was killed at, or what git actually wrote, with a fetch that said nothing still refusing with a reason rather than an empty clause. Both return the same status and create nothing, so what changed is only what a developer is told. One wording now serves both fetches the module spawns, because giving the second one the first's three shapes would otherwise have meant a second copy free to drift. A clone that can reach its remote prints exactly what it printed before, pinned by an assertion rather than left to inspection, and a clone with no remote is not told the branch is absent from one — it never had a remote for the branch to be absent from And the backlog stops being fed by the work that is meant to reduce it: across sixteen stories the post-story inspection filed forty-four briefs and dropped none, against the one each story closes, and eleven of the twelve most recent open ones named a file the story that filed them had just edited — one story adding a function and three briefs about that function existing within the hour. The Inspector was reviewing a fresh diff and filing the review comments as stories, which is a category error rather than a volume problem: a comment on a change belongs to the change that produced it, and only a finding about code that was already there is a backlog item. The inspection now runs before the stage that judges, and what it finds is partitioned by what it names — a finding naming a file the story changed is written where the verifier reads it, a finding naming only the files beside the change is filed as a brief exactly as before, and the partition is asserted on its edges so nothing falls between the two. It is evidence and never a verdict, said in the declaration and again in the prompt: no coordinator branch reads it, no budget is spent by it, the verifier triages it on the terms it already uses, and thin evidence is worded as thin evidence rather than as a clean bill of health. The same diff is not read twice — the post-story inspection now runs only where no stage declares one of its own — and both shipped workflows declare it, so neither takes the cost of the change without its benefit And the mechanism that carries them can see the code they are about: the scope kept a changed path only where git already had it in the index, which was true of the caller that ran below the completion commit and false of the one now running inside the stage loop, so every file a story created was dropped — with the reason that the repository no longer tracked it, a sentence false in both directions for a file written ten minutes earlier. The newest code in a change was therefore the code the inspection could not read, and it said so nowhere: the verifier was handed a record reading that the Inspector had looked and found nothing. The scope is now what the tree holds rather than what the index has, so a created file counts whether or not anything staged it and a path the tree no longer holds still does not, which is one derivation answering for both callers instead of for whichever ran after a commit. The run that made this change is the first under the rule it completes: six findings about its own diff, none of them filed, both correction passes spent answering them, and no post-story inspection at all And a control that needed the machine to be quick stops needing it: the escaped-descendant case asserted that a fetch had answered rather than been killed, under a one-second bound, in a suite that runs six thousand tests at full parallelism against a subject that spawns a shell that spawns an interpreter — so under load the bound fired and the assertion reported the opposite of what it was written to establish, which had already cost one run a whole attempt in its clean-clone check. It now rests on the subject's own exit status, a field a fetch killed at the bound does not carry, and on the escaped descendant being seen still running, with no elapsed interval compared against anything: load makes both more true rather than less, which is the inversion that distinguishes a repair from a larger number. The subject is written once and driven by both the case and its control, so the control drives the same thing rather than something resembling it, and the module's place under the rule that no test may bound how slow the machine is allowed to be is now argued for this pair rather than inherited from the neighbours it never covered And a test filed as flaky turns out to have been catching a defect intermittently: the interrupt guard in the planning script covered the session and nothing after it, so an interrupt arriving while the script was committing, stamping the mandate, validating, publishing or waiting at one of its own prompts left the program through an uncaught exception and the process died by the signal instead of exiting. A shell reports a process killed that way as 128 plus the signal, which is why a developer's terminal showed the same number either way and nobody noticed; anything reading the return code rather than watching a terminal is told two different things, and under load the interrupt lands in that window often enough to redden a suite. The post-session work now carries its own guard and the status is named once for both. Nothing is undone, retried or completed by it — whatever had already been committed, stamped, pushed or published stands, and the steps below the interrupt did not happen, which is what dying by the signal already meant. What remains outside both guards is said rather than left to be found: everything above the session, where an interrupt still dies by the signal because nothing has been committed there to report on And the suite stops getting slower on its own: the fixture that copies this repository for the tests that mutate a coordinator was excluding the virtualenv and nothing else, so it carried the logs and run directories the harness's own runs leave behind — nine hundred megabytes and eight hundred of them, against eleven of history it actually needed — and it carried a second virtualenv the pattern missed by naming the first. About one and three quarter gigabytes copied per module, nineteen and a half seconds of setup, the most expensive fixture in the suite and above every test in it but four; and both of those directories grow with every story that runs, so it got slower without a line of test code changing. What is excluded is now asked of git rather than written down, which is the property that matters: the list that was there had already drifted once, and a directory added to the ignore file later is left behind here with nobody editing the module. The history still comes along, because every assertion the copy is made for bounds itself at its own story's commit range and cannot resolve one without it And the finding that reaches a story rather than the backlog stops being able to reach neither: routing it to the verifier was right where the verifier acted on it, and where the verifier passed one over — which its own prompt permits, and should — the finding had nowhere left to go, written to a run-directory artifact that nothing reads after the verdict and filed by nothing. One story lost two that way, one of them a real defect about the very change it was making, and recovering them meant reading two files by hand. A verifier now names a finding's slug where it raises anything from it, and after the verdict whatever no entry named is filed as a brief; a finding acted on without its slug named is filed too, because a duplicate somebody closes is the cheaper of the two mistakes and over-filing is the direction this should fail in. The line written when the inspection ends stops saying the story answered anything, since at that moment nothing has — it says what was routed, and what became of it is written afterwards — and a correction pass raised from those findings now says how many of them it carries, which before could only be learned by opening the artifact and the verdict together And the copy a test makes of this repository stops being the repository: in a worktree .git is not a directory but a one-line file naming the real one, and the copy carried that file verbatim — so a module whose whole safety argument is that it mutates a copy, because the repository the suite runs from is the one being validated, was mutating source beside a git directory that pointed straight back at the original. Every story runs in a worktree, so the suite met that case on every run the harness made of it, and it passed in a developer's own checkout because there .git really is a directory, which is why nobody had seen it. What replaces the copy is a clone made without a checkout, with the working tree laid over it: a clone resolves the pointer itself, so nothing in the test spells git's worktree layout, and the two consequences of cloning rather than copying are handled rather than found — the tree a nested run must see is the working tree and not HEAD's, and a clone inherits none of its source's identity, so a commit made inside it is given one And the last stage writing outside its subject is asked about it: the implementer declares what it may not create and the tester what it may only change, each with a revert check behind it, and the documenter — which runs after the last suite the coordinator ran and before the verdict that judges the tree — declared neither, so nothing asked what it wrote and nothing recorded it. It had been writing production code on three consecutive runs, and a reader could not tell that work from the implementer's, because only one of the two leaves a governed record. Its confinement is the architecture documents the target's own configuration already names, which is a shape neither of its neighbours could lend it: bounding what may be added is wrong where the risk is modification, and a directory prefix is wrong where the subject is a set of documents. An edit outside it is not forbidden but asked about — the revert check decides what it always decides, so a forced adaptation still stands and what changes is that the question is put at all |
Ch. 19, scaling; Ch. 17, external systems; Ch. 18, inspection |
Still ahead, in the order Chapters 18 and 19 recommend: per-agent logs and a watcher, a fuller hook-based tool policy in place of the static allowed_tools allowlist, an adjudicator, git worktrees and parallel story execution, and a real initialization library.
The harness stays at level 3. Epics and products (Chapters 20–22) are a different unit of coordination, and the book is explicit that the story workflow earns that step through a track record rather than a feature list.
- Claude Code CLI (
claude) with an active subscription - Python 3 (3.10+)
- Git
The harness itself has no third-party runtime dependency — it uses only the Python standard library. Running its test suite needs what requirements-dev.txt declares; see Tests.
All harness capabilities are invoked through l5- scripts in scripts/:
| Script | Purpose |
|---|---|
l5-init |
Initialize a .harness/ structure in a target repository |
l5-plan |
Plan a story interactively with the planner agent, from request text or from a filed brief named with --brief; commits and pushes the story artifact the session produced, then offers to run it |
l5-run |
Execute an approved story through the story workflow |
l5-status |
Show a snapshot of story runs (status, current stage, retries), or one run's detail |
l5-assist |
Launch the interactive assist agent with harness context, and with the skills the harness ships — among them filing a story brief into the outbox |
l5-check |
Report whether a stage's required outputs are present in the run directory, were written by the invocation running now, and satisfy their declared schemas — the check the coordinator makes after a stage's turn, reachable while the turn is still open. Every stage is invited to run it on itself; it decides nothing |
l5-sync |
Drain the outbox — the durable local queue of work to file externally — reporting what landed, what is still pending, and what no sweep will clear on its own |
Example:
scripts/l5-plan "Add a --dry-run flag to l5-run"
scripts/l5-plan --brief <the key the tracker reported for a filed brief>
scripts/l5-run story-001
scripts/l5-status
workflows/ workflow definitions (stages, artifact routes, retry rules):
story-workflow.json, and since story-070
refactor-workflow.json for behaviour-preserving work
schemas/ JSON Schemas for the structured artifacts, plus their manifest
prompts/ reusable agent prompt templates ({{placeholder}} injection)
plugin/ the skills the harness loads into the sessions it starts,
for the session rather than installed into a target
orchestration/ the Story Coordinator and its supporting modules
rules/ execution rules enforced by the coordinator
scripts/ thin l5- entry points
hooks/ the deny-only tool guard each stage invocation carries,
and the turn-end check that asks a stage for the outputs
it owes before letting the turn end
templates/ starter files l5-init copies into a new target repository
tests/ the coordinator's test suite, run without model calls
.harness/ target-repository state: config, standards, stories, and
docs/ARCHITECTURE.md; plus runs, logs, requests and the
outbox queue, which are gitignored execution state
The harness pieces (workflows/, schemas/, prompts/, plugin/, orchestration/, rules/, scripts/, hooks/, templates/) are reusable across target repositories. The .harness/ directory is target-repository state; run l5-init to create it in any other repository you want the harness to work on.
This repository is both the harness repository and its own first target repository. Every demo story is a real harness feature, so the harness participates in building itself from the start.
Looking for the architecture document? It is .harness/docs/ARCHITECTURE.md, not a top-level docs/. Its location is a configuration value rather than a fixed part of the layout: architecture_docs in .harness/config.yaml names it, and the coordinator injects whatever that key names into the implementer's context on every run. It sits beside .harness/standards/ because both are agent context, and a target repository is free to keep them elsewhere.
l5-planruns an interactive planning session and writes an approved story artifact to.harness/stories/, then validates, commits and pushes it and offers to run it: Enter startsl5-runfor the story just planned, anything else skips and prints the command that would have started it. When stdin is not a terminal the offer is not made at all and the command is printed, so a scripted invocation cannot hang on a prompt nothing can answer. Unless--workflownames one, the session runs in two phases: a first classifying turn proposes the workflow the request should run under and shows its reasoning, and the interview begins only once that proposal is confirmed or overridden — because a workflow's stage list is injected before the interview, so the choice cannot be made partway through it. With no terminal and no--workflowthere is nobody to confirm a proposal, and the invocation is refused before anything is invoked, written or committed. Since story-096 the request may be a filed brief rather than text:--brief <key>fetches it through the configured filed-query command, renders it as the request the session is given, and takes the brief's workflow as the proposal, so the classifying turn is not made at all. A brief and request text are mutually exclusive, and every way the fetch can fail refuses above the session, the snapshot and the commit, saying what happened and that the request can be passed as text instead.l5-runhands the story to the Story Coordinator, which creates a story branch and a run directory under.harness/runs/<story-id>/.- The coordinator advances the workflow stage by stage — implement → test → document → verify under
story-workflow.json, or implement → document → verify underrefactor-workflow.json, whose correctness claim is that behaviour is unchanged and whose implementer is guarded by a suite census rather than by the create and revert checks. A second workflow is not a second unit of work: the story is still the unit, and which definition its run loads is a field on the story artifact. The coordinator assembles each stage's context, injects it into the stage prompt, and invokes the agent headlessly (claude -p). A stage that runs no suite cannot invoke one: the deny-only Bash guard each invocation carries reduces a command to its program and the targets it was pointed at, and refuses it when that matches the same reduction of the target's configured test command. Every stage is invited to runl5-checkon itself, and a stage that tries to end its turn with a required output missing, stale or invalid is blocked once by a Stop hook naming what is unwritten — so the artifact is written in the turn that earned it rather than in a re-entry that costs the run an invocation. Both are reports: they are optional, they block at most once, and the coordinator's own check after the turn is what decides, unchanged. The documenter runs before verification so that what it writes is judged rather than taken on trust. - The verifier writes
verification-result.json. The coordinator routes from that artifact: advance, retry, or escalate. A retry goes to the stage that owns the defect — named by the verifier as a category the workflow defines, with no default route — carrying structured guidance inretry-guidance.json. A verdict may also report that retrying cannot finish the work at all, which escalates immediately and leaves the retry budget unspent. On a passing verdict the coordinator re-runs the suite in a fresh clone with the story committed, because the working tree is the one place that commit does not yet exist. - A stage that fails mechanically — rather than being judged wrong — runs again in place, on a separate per-stage budget that retries do not share.
- A stage invocation stopped because capacity ran out — a provider rate limit, an exhausted plan quota — is not a failure of the work, so the run pauses rather than escalating: status
paused, exit code 3 (completion is 0 and escalation is 2), no escalation summary, and every counter left where it stood. The pause commits what the run left in the working tree and then continues at the same stage — waiting in place when the signal named a reset time withinmax_pause_wait_seconds, and otherwise exiting forl5-run <story-id>to resume, which continues at the stage it paused on with nothing reset and nothing archived. The state and the commit land before any waiting begins, so a process killed while it waits loses nothing. - A run that completes is then inspected, where
inspect_after_story_max_filesis configured: the Inspector reads what the story changed plus the files git tracks directly beside them — one level, because duplication and parity gaps live in the file the story did not change — and files what it finds as story briefs into the outbox. It happens after the completion commit, so a slow inspection cannot delay the durability of the work, and before the completion sweep, so the briefs it enqueues are filed by that same sweep. It may never block, delay or refuse a run: it returns nothing, raises nothing, and a value it cannot obey is named in its record rather than refused. A completed run therefore leaves a second commit where an inspection was made, carrying its record in.harness/history/inspection-log.jsonland staging that path by name and nothing else. Delete the key and no run inspects anything. That record says what the inspection cost: the figure the invocation itself reported, beside which mode it was, how many files were in scope, how many invocations the figure covers and the three finding counts — and a broadl5-inspectinvocation writes and commits a line of its own shape into the same file, so one read of one file answers what inspection has cost across both modes. The cost of a completed run's inspection is also appended to that run'scost.jsonbeside the stage invocations, and is deliberately never charged to the allowancemax_run_cost_usdis compared against: the inspection happens after the completion commit, and charging it there could put a completed run over a cap it had already honoured. An invocation that reported no cost records that it reported none rather than a zero, and a record that cannot be written costs the record and nothing else. - Every run leaves its state (
state.json), the same events in two renderings (events.logandexecution-history.json), a record of any retry (retry-history.json), and the artifacts each stage produced.
See .harness/docs/ARCHITECTURE.md for the full architecture.
The Story Coordinator is deterministic and fully unit-tested without any model calls (a fake runner plays back scripted stage artifacts). Run the suite with:
.venv/bin/python -m pytest tests/ -q -n auto
That is test_command in .harness/config.yaml verbatim, and running it verbatim is the point: the harness's own gates — the revert check, the coordinator's suite run in the tree, and the clean-clone check — execute the configured command, so what a developer runs and what the gates run cannot drift apart. -n auto reads the core count of whatever machine it lands on, so no core count is written down anywhere.
The revert check reaches that command second. A writing stage may nominate the test that fails without its change, and the check then runs test_selection_command — the same configuration's selector, with the nominated test substituted at {test} — on the tree the stage left, where it must pass, and again with the governed edits reverted, where it must fail. Pass-then-fail decides the check on that one test; anything else falls through to the configured suite command above, so a bad nomination costs one selector run and changes no verdict.
The dependencies that command needs are declared in requirements-dev.txt, and it has to be installed into both interpreters this repository configures — the one you run the suite in and the one verification_runner names:
.venv/bin/pip install -r requirements-dev.txt
.venv310/bin/pip install -r requirements-dev.txt
Install it into only the first and your local suite is green while the clean-clone check dies on an unrecognized argument, because that check runs the same command under the second interpreter. That is the failure this instruction exists to prevent.
This repository tracks the book through level 3, so its scope is what Part 3 and Appendix A describe. Small fixes — genuine bugs, or errors in the code and its docs — are welcome via pull request. Improvements the book's roadmap calls for are welcome as issues; they are best planned and executed through the harness itself, which is the whole point of it. Changes that would take the harness past level 3, or in a direction the book does not argue for, are out of scope here.
Found a bug in the harness code? Open a GitHub issue. For anything about the book's content — typos, unclear passages, errata — please use the feedback form at agenticprogrammingbook.com/feedback rather than GitHub Issues.
MIT — see LICENSE. Copyright © 2026 Jerod W. Wilkerson.