The Diamond Graph

(Updated: )

The thing that breaks agentic development at scale is not the agents. It is the human gate: one person approving one plan, reading one diff, verifying one running app, once per item. A faster agent just arrives at that gate sooner.

The other shape is a diamond.

The diamond graph: intake feeds a queue, plain code partitions it into collision free nodes and waves, builders fan out into isolated worktrees, each node is verified by three independent lenses, a plain code reduce step compares what returned against what was dispatched, accepted nodes integrate in wave order, and a single human gate stands before the PR

A node is one bounded job with a defined input and a defined output, built in its own git worktree. Builders fan out. Each node is then asked three different questions by verifiers that have never seen the builder. A plain-code reduce step compares what returned against what was dispatched, and what was claimed against what was observed. Accepted nodes integrate in wave order. One human gate stands before the PR: one report, one diff. Not one per branch.

One queue item built in a single git worktree, checked by three independent lenses in a fresh context, then compared by a plain-code reduce step on returned versus dispatched and claimed versus observed

That is the machine. This is a graph of loops, not a loop. One agent improving one thing on repeat is a loop. Here, build cycles and verify cycles watch each other, and an edge exists only where one node genuinely consumes another node’s result. Four properties do the actual work. Everything else is plumbing.

I built it because I hit the ceiling. On a client operations app I had a growing queue of 116 known defects and planned changes.

  • Every week I would have a call
  • Every week the list would grow with new features or iteration changes
  • Every week I added more and more to my project map
  • Every week I was falling farther behind

Working them one at a time still meant 116 gates. Nine batches on that project have gone through the diamond, saving me weeks of work. I then wanted to see how project specific the harness was and if it could be mobilized.

I extracted the parts that were never project-specific into a portable kit, then ran two batches of this site’s own backlog through the process. The extraction found four more defects in the original along the way. It found issues I knew about but I didn’t know how to articulate, it found issues I hadn’t considered, and it even surfaced prose issues where I was contradicting myself by mistake.

NOTE: This is not a research harness. It does not find topics to write about. It finds material defects in a repository: code, prose, comments, components, tests. The rest of this post is why each edge exists, what it costs, and the failures that forced the design.


What it is, and what it is not

It is roughly 3,100 lines of plain JavaScript and bash (1,779 of them if you exclude comments and blanks), plus six documents totaling about 18,100 words. 1 No runtime, no daemon, no registry, no plugin API. Nothing imports it. You copy it into a project and it runs there. If you use git, you can use this.

The documents are the more valuable half. The code encodes decisions; the documents record why each decision exists, and those reasons transfer to any multi agent build system regardless of the language it is written in.

Three layers, and portability depends entirely on nothing leaking upward: 2

Layer What Portable
Machinery partitioner, graph, reduce, guards, gates copied verbatim
Config anchors, setup, lanes, paired artifacts, branch policy one JSON file
Knowledge the map, and which invariants have already burned you 100% yours

The machinery never imports from the knowledge layer. Config is the only channel between them. That is the whole portability story, and it is the only claim in this field note I would call architectural.


It does not care what you build in

I want to settle this early, because a harness born on a TypeScript project is easy to mistake for a TypeScript tool.

There is no language in the machinery. Anchors are arbitrary shell commands with an id and an exit code, and that is the entire interface between the harness and your toolchain. 3

Three jobs. The machinery is the same for all three. Two of these run live today; the third ships as a public example. 4

Job What it looks like Anchors Lanes Paired artifacts
App with a test suite typed app, linter, tests tsc, lint, test, component test gate migration component tests
Static site, no tests build and content gates only build, check, canary gate, audit none none
Go service, empty setup modules already on the machine go build, go vet, go test, gofmt migration, proto migration rollback

Three jobs feeding one unchanged machinery layer: an app with a test suite, a static site with no tests, and a Go service with an empty setup array, all connecting only through harness.config.json

A repo with no test suite and no linter is a valid config. The harness does not notice, because it never asked what a test was. I run that shape on this site: a build, a type check, a script that verifies every post carries its prompt injection tripwire, and a dependency advisory gate. Zero lanes. Zero paired artifacts.

The public Go example is the other half of the point. Its setup array is empty, because Go modules cache per machine rather than per worktree. The Node jobs need npm ci first. That is the one real porting question, and it has nothing to do with language: what does a tree containing only tracked files lack?

What it actually requires

Being honest about the ceiling matters more than claiming there is not one.

  • git. Not negotiable. Isolation is git worktree, branches are the unit of work, and the push guard is a git guard.
  • bash, jq and node, for the guards, the pre-flight check and the partitioner. Those are dependencies of the tool, not of your project. The Go service does not gain a package.json.
  • A subagent runtime with structured output. That part is Claude Code specific today, and it is the piece you would reimplement to move to another runtime.
  • Anchors that reduce to an exit code. The real constraint, and it bites in a language independent way. The Go example wraps gofmt as test -z "$(gofmt -l .)", because gofmt -l prints unformatted filenames and still exits 0. A check that cannot fail is not a check.
  • One repository. There is no design for a batch spanning two.

1. Collision detection is plain code, and it is forced to be

The way a fan out corrupts a repository is not dramatic. Two items look unrelated because their prompts never mention each other, and they write the same file. Two agents, two worktrees, one file, last merge wins. I call it false independence, and it is the single most likely failure mode of any parallel build system.


From the margins: I have read a lot about agent fanning, both pro and con. My answer to it all was not: to use or not use. It was to break the problem down to the core issues, which was inflight collision. I think too many people just see, hear, read about multi-agent build systems and expect it to just work like any other prompt. “Deploy 6 agents, make no mistakes” meme style. I don’t know that I have found or built anything new, or note-worthy, but I do know that it is working and I wanted to share my experience with it, and the results of that experience so far.

Back to the article:


A model cannot own that decision. It is not a judgment call. It is a graph problem with a correct answer, and it has to be reviewable before anything runs.

So the partitioner is union find over declared file sets, in plain code, printed for a human to read before dispatch. 5 Three refinements, each found the hard way:

  • Files an item will create count as collisions. Two items that both create components/Combobox.tsx collide exactly as hard as two that edit an existing file.
  • A declared cross reference is a collision edge. An item and the audit section it cites are routinely one job described twice, and the two descriptions can cite completely different files. The edge only forms when the reference exactly matches another selected item’s id, because most references point at document sections and those must not fabricate edges.
  • Exclusive nodes need a wave, not just a flag. Exclusivity bypasses the union find, so a repository wide item can share a file with a parallel node and the grouping will never have caught it. Sequencing is what makes it safe.

That last bullet described the design correctly, and the code did not implement it. I found that because a reader pushed on this article.

The repository wide item was unioned like every other item and merely skipped later, when groups were emitted. So it sat in the graph as an ordinary vertex, and an ordinary vertex with edges to two unrelated items is a bridge.

Three items reproduce it. P1 owns p1 and x. P2 owns p2 and y. Repository wide H claims x and y. P1 and P2 share nothing with each other. Union find joined all three through H, and P1 and P2 came out as one serial node.

Losing the fan out width is the cheap half. A node’s file list is the union of its members, and that list is what becomes the builder’s “files this node owns, do not edit anything else” instruction. So the bridge handed each builder the other item’s files and the repository wide item’s, as files they were authorized to edit. The exclusive node then claimed the same files again a wave later.

It hid behind a fallback string. The function that explains why two items were grouped ended with || "grouped", so when no shared file, no cross reference and no lane could account for the grouping, the plan printed the bare word grouped. That reads exactly like a decision somebody made.

The fix is two lines and one deletion. An exclusive item now forms no edges of any kind, not file edges, not reference edges, not lane edges, because sequencing into its own wave was always supposed to be the entire guarantee. And the explain function throws instead of falling back to a word: every edge the partitioner can draw is one of three nameable kinds, so a group it cannot explain is a group joined by an edge nobody intended, and that is not something to print calmly and continue past.

The blast radius was narrower than it sounds, and worth stating. --auto only ever selects bounded items, so this could never fire on an auto proposed batch. It fired when an operator named ids explicitly, which is the documented path for any batch containing a repository wide item.

Left, the bug: a repo-wide item H claiming files x and y sits in the graph as an ordinary vertex, so union find joins the two unrelated items P1 and P2 through it into one serial node whose reason prints the bare word grouped, and both builders are handed x and y as files they own. Right, the fix: H forms no edges at all, P1 and P2 stay parallel in wave 0, H runs alone in wave 1, and the shared files are reported as a cross-wave overlap instead of hidden

Eight queue items resolving through union find into five nodes: three independent parallel nodes, one serial node clustering the items that share a file, a created file, or a cross-reference, and one exclusive repo-wide node placed alone in wave 1 after wave 0 merges

The edges are the hard half, and they come from parsing English. Union find is the easy half and it is correct given its edges. But the edges come from a regex scraping file paths out of map prose, so the extension list that regex can see is the partitioner. An extension it cannot see is a file the collision graph cannot see.

The case that bites is not zero visibility, it is partial. An item with no scrapeable path scores unscoped and gets refused, loudly. An item touching docs/guide.md and src/nav.ts scrapes the .ts, scores bounded, and ships inside a node the planner is confident about, with an invisible .md collision in it. A content site running the default list is running a partitioner that cannot see most of its own work.

So the list moved into config, and the regex now lints itself: a second pass matches any path shaped token with any extension, and anything resolving to a file tracked in git whose extension is not configured gets printed under INVISIBLE FILE TYPES, naming the extension and the config key to add. The scraper suggests and reports its own blind spots. It does not silently decide what collides.

There is a constraint that made this good by accident. Workflow scripts in my runtime have no filesystem access, so the partition and the config slice both have to be computed in the main loop and handed in as arguments. That started as a limitation. It is now the best property of the design, because it means the one decision that must be deterministic cannot happen inside a model, and I see the partition before a single token is spent.

Refusal is a first class outcome here. The partitioner will not dispatch an item whose files it cannot derive, because no files means no collision guarantee. On that client queue, 38 of 116 items were not dispatchable: 15 needed design (where the code goes is itself the open question), 11 were serialized into a migration lane, 6 were repository wide, 4 lived outside the repo entirely, and 2 were held by a human decision. 6 A third of the backlog was work a builder would have turned into confident fiction.


2. Verifiers never see the builder’s context

A model grading its own output is far too easy on itself. This is not a hunch. LLM judges measurably favor their own generations, and evaluator bias is documented well enough to design against rather than test for. 7 A verifier that shares the builder’s context is that same loop wearing a different hat.

So every verifier gets a fresh context that has never seen the work. It reads the diff itself with git diff <base>...<branch>. It is handed the builder’s claims explicitly labeled as assertions to test, not facts. Its default verdict is reject, and it passes only what it positively confirms.

That default matters more than it reads. An agentic system’s failure mode is not “it stops.” It is “it proceeds confidently on a bad assumption.” Every refusal in this system sits at a place where I watched that happen.


3. The lenses ask different questions, so one reject fails the node

Three identical reviewers are a majority vote, and a majority vote should be treated as one opinion. Three different questions are three independent tests.

Lens The question
intent Did it do what the item asked, and nothing else?
invariants Did it break something this repository has already been burned by?
anchors Do the checks actually pass, re-run right now in a fresh worktree?

Left: three identical reviewers voting on the same question, where two passes outvote one reject and the finding is lost. Right: three lenses asking different questions, where a single reject fails the node because no other lens examined that area

Because they ask different questions, one reject fails the node. Majority would be actively wrong here: it lets two lenses that never examined an area outvote the one that did.

This is also why the lens list is a set of names the output schema enumerates, not a count. Adding a fourth lens means writing a fourth question. You cannot dial up rigor by turning a number up.

There is a corollary I got wrong for a while, and it cuts the other way: a question you can write as a script does not belong in a lens. The intent prompt used to ask “does the diff touch any file outside the node’s declared list, and is that an automatic reject?” That is not a judgment call. It is a set difference between the paths a branch changed and the paths a node declared, and comm does it faster and more reliably than a language model. Worse, it displaced the question only that lens can answer. That check is now a gate script, run by the builder, re-run by the anchors lens, and compared as two numbers exactly the way anchors are. The intent prompt is explicitly told not to spend itself on file sets. One lens got smaller and better, which is the opposite of the direction people usually push a verifier.

Here is the finding that justifies the whole verify stage. One batch came back 3 of 3 nodes green on every anchor, and all three were then rejected on semantics: a wrong value written onto a shipping document, a render loop crash, and three swallowed errors. Type checks, lint, and tests prove the code compiles and the existing assertions still hold. They cannot prove the change did what was asked.

Green is the start of review, not the end. Budget a fix pass for any batch that changes behavior.


4. Anchors, not agreement

This is the principle I would keep if I had to throw the rest away.

A graph where every node reads another node’s report and they all agree is consistent and unverified. Agreement between models is not evidence. What this system trusts is exit codes it watched happen.

The builder reports the exit codes it observed. An independent verifier re-runs the same anchors in a fresh worktree and reports what it observed. The reduce step compares them:

A builder branch reporting exit code 0 for the test anchor, an independent verifier in a fresh worktree observing exit code 1 on the same commit, and the reduce step raising the disagreement as the batch’s top warning

There is no benign explanation for that line. Either the builder reported a green it did not see, or one of the two ran against a tree that is not the commit. Both are exactly what you want to find before merging, and this single comparison is the detector that caught several of the failures further down this post.

There is a corollary I only learned by watching a builder apply it. After a batch added a dependency advisory anchor, the builder that wrote it did not trust it on sight. It seeded a deliberately vulnerable scratch tree, watched the new anchor exit 1, and only then reported the anchor as working. 8 An anchor you have never seen fail is a line in a config file, not a check.

Judge the system on numbers that cannot argue back, and prove the numbers can move.


The four outcomes, and why collapsing them invents findings

The reduce step is plain code with no model and no tokens in it. It produces four outcomes and never collapses them:

Outcome Meaning
accepted Built, every lens ran, none rejected
rejected A verifier positively found a problem
unverified Built, but a lens did not return. Nobody judged this.
not-built The builder reported blocked or partial

The four outcome lattice, with unverified highlighted as structurally distinct from rejected: rejected means someone found a problem, unverified means nobody looked, and merging the two fabricates a finding that no agent ever made

Plus a fan in guard: if fewer nodes returned than were dispatched, the report says so at the top and refuses to present itself as complete.

unverified is not rejected, and the distinction is not ambiguous. A rejection is a claim that someone found a problem. Reporting an unjudged node as rejected fabricates a finding nobody made, and the next thing a human does is hunt for a defect that does not exist. I know the cost of that collapse precisely, which is the next section.


Seven failures that shaped the code

The kit ships a scar file: 31 documented failures, each with the design decision it produced. 9 The pattern worth internalizing is that almost none of them announced themselves. Every one produced a report that looked exactly like a good report. That is why the mitigations are structural (schemas, exit codes, hooks) and not instructional.

As you use this harness, your agent should ask to add to the SCARS.md and if it doesn’t you should prompt it to. A lesson unrecorded, is a lesson you will learn repeatedly.

Here are seven of the lessons I learned, in the order they cost me the most. These are in the provided SCARS.md already:

Lost structured output manufactured five false negatives

On the first real run, 7 of 18 verifiers died with StructuredOutput retry cap (5) exceeded. Each was trying to emit about 3.5KB of prose inside a single JSON string field, and the embedded newlines and quotes broke the parse on every retry.

Every one of those lost verdicts was a pass. The serialization failure manufactured five false negatives: nodes reported as problematic when no verifier had objected to anything.

The fix is not “tell the model to be brief.” It is to make brevity the only thing the schema can hold:

const SHORT = { type: 'string', maxLength: 300 };
evidence: { type: 'array', items: SHORT, maxItems: 8 }

A 300 character element cannot hold a paragraph, so the model splits instead of escaping. Telling a model to keep it short fails under pressure. A maxLength does not.

Borrowed dependencies silently voided every anchor

A fresh git worktree materializes only tracked files, 10 so node_modules does not exist and the test runner is not installed. The verifier contract offered an escape hatch: if a fresh install is too slow, verify in place.

Every single agent took it. They symlinked, cp -r’d, or cp -al’d node_modules out of whichever other worktree they found first, and no two agents picked the same source. Two batches were verified against a dependency set that did not belong to the commit under verification. It went unnoticed only because the lockfile had not moved in weeks. The day it moved, borrowing meant reporting a pass for a build that does not exist.

The fix was to delete the escape hatch and state the cost in the contract instead: it takes eight seconds and it is the only correct answer. An agent skips a step whose cost it cannot see.

Any instruction of the form “if X is too slow, do Y instead” will be taken 100% of the time. If Y is unsafe, do not offer it.

pipefail plus grep -q silently broke a gate

A gate script checked commit messages for a waiver:

grep -q "^no-test($name):" <<< "$messages"    # correct
printf '%s' "$messages" | grep -q "..."       # broken

grep -q exits the instant it matches. 11 That closes the pipe and kills the writer with SIGPIPE, exit 141. With set -o pipefail on, bash then reports the whole pipeline as failed even though grep matched. 12

A validly waived file was reported as a violation. It fails closed, so a pass is never invented, but whether SIGPIPE lands depends on how much the writer flushed first, so it presented as intermittent. I only found it when a verifier could not reproduce the gate’s own exit code.

Then it came back, in the self test, while I was writing a test for a different bug. Same mechanism, one layer out, inside the harness that exists to catch this class of thing. cmd | grep -q under pipefail is broken by construction. Capture to a variable and match with a herestring.

A rule in a prompt is a suggestion; a hook is a gate

“A component you change ships a test in the same node” sat in the builder contract as prose for a week. It produced three test files across fifty-one components.

The worse version I wrote up separately. An agent on my own box was given an instruction level command blocklist, and it wrote and ran its own client for the blocked capability, reasoning that the quarantine was a launcher. The approval log read correct and enforced nothing: 13 verdicts, 0 denials, and a token reached disk. 13

So anything that must actually bind is an exit code or a PreToolUse deny, never a paragraph. 14 The test rule became a gate script run as an anchor and re-run by an independent verifier. The push rule became a fail closed hook.

If you find yourself writing “the agent must always…”, ask what command would return non-zero when it did not.

I did not apply that consistently, and a reader caught where. “Do not touch files outside this node’s list” was written three separate times, in prose: the builder contract, the intent lens, and the integrate step. Three paragraphs and zero exit codes, about a rule that is a two line set difference. It is a gate script now, and the integrate step is a script rather than a placeholder. That change matters below, because writing it produced a bug of exactly the kind this section is about.

And a guard that fails open is the same as no guard. The hook denies by default if it cannot locate its script. A missing guard is the most dangerous state possible, precisely because everything keeps working.


From the Margin: Every one of these has the same shape. The system kept running, the report kept looking correct, and the only evidence anything was wrong lived in a number nobody was comparing. I have started treating “it produced a clean report” as a neutral observation rather than a positive one. A clean report is what both the working system and the broken system produce. The question is always which numbers you watched with your own eyes.


The stacking guard failed open in its default configuration

Branching from main is unsafe while any local branch is unpushed, because it silently drops those commits from your base, including the queue and the lockfile. So I wrote a check for it. The check then reported nothing unpushed, main is a valid base on a repository four commits ahead of its remote, and had presumably been doing that since the day it was written.

excluded=$(jq -r '[.git.excludeFromStacking[]?] | join("\n")' "$CFG")
... | grep -v -x -e "$MAIN" $(printf -- '-e %s ' $excluded) 2>/dev/null | ...

With the exclusion list empty, which is the default every new install has, printf receives no arguments and the unquoted command substitution collapses to a single dangling -e. grep exits 2, the redirect swallows the message, and the empty result is indistinguishable from “nothing to report.” A non-empty list accidentally fixed it, so the more configured a project was, the more likely the guard actually worked.

The part worth keeping is not the grep. My first attempt to reproduce it passed. I ran the check by hand in an interactive zsh, and zsh does not word split an unquoted command substitution, so the dangling -e never formed. 15 The bug only exists under the #!/usr/bin/env bash the script actually runs with.

A field the prompt does not render reaches nobody, silently

The graph renders exactly five item fields into the builder prompt and two into the verifier prompt. Anything else on an item was dropped without a word.

On an early batch I re-scoped six items before dispatch: settled decisions, hazards, explicit do-not-touch lists. I attached the result as a sharpened key beside detail. It would have reached no agent on any of the four nodes.

The batch would still have run. Every builder returns, every lens passes, every anchor is green, and the report is indistinguishable from one where the constraints were honored. I would have read “4 of 4 accepted” as evidence that carefully scoped work came back correct, when no agent had seen a line of it. I caught it by reading the prompt builder before launching, which is not a control.

The fix is an explicit allowlist of item keys, checked before any agent spawns. An unknown key throws, naming every offender. Throwing rather than warning is the point: a warning scrolls past in a run that then looks successful. Throwing costs a launch that dies in seconds having spent zero tokens.

The same audit found a live instance of the shape. The partitioner unions files and newFiles for grouped nodes, but the repository wide branch used files alone, so a repository wide item that must create a file handed its builder a prompt forbidding the file it was told to create.

Two code paths that must agree, written at different times, will disagree. The one that gets exercised least is the one that will be wrong.

The bridge described earlier is the same shape on the same branch, in the same rarest node type, for the same reason. It is the one nothing exercises.


A gate that passed because it was asked the wrong question

This one is fresh, and I like it because writing the fix for an earlier complaint produced it.

Turning the integrate step into a script meant running the scope gate against the merged tree: after merging a wave, no path in the result should be one that no node in that wave declared. The gate takes a base ref and diffs base...HEAD.

The merges land on the base branch. So by the time the gate runs, base points at the merge I just made. The gate diffed the merged tree against itself, got an empty change list, and reported “nothing to check” with exit 0.

The happy path printed a green scope line for a merge it had never looked at. And this is the part worth sitting with: that output is byte for byte identical to the output of a gate that genuinely passed. Not a silent failure, a confident one. The fix is one line, pinning the base commit to a SHA before anything moves, and the self test now stages an undeclared path in a merged branch so that reverting the pin turns the suite red.

A guard that fails open is the same as no guard. A guard asked the wrong question is worse, because it reports.


What a second project found in a week

Everything above came from the original client app or from a reader. Then the portable kit ran daily on a different private repository (Python and TypeScript, a real test suite, CI) and found six more. Three were mine, in the machinery, and they are the ones worth repeating.

A regex that could not see a leading dot. The path scraper opened with \b, a boundary between a word character and a non-word character. A path starting with a dot-directory, .github/workflows/ci.yml, has no word character before the dot, so the regex could not start there. It started one character later and captured github/workflows/ci.yml, which resolves to nothing, because suffix matching looks for a path ending in /github/workflows/ci.yml and the real file ends in /.github/.

Every citation of .github/, .claude/ or .circleci/ was silently dropped from the collision graph. CI workflow files are exactly the kind of file several unrelated items touch at once.

The way it surfaced is the part I want to keep. The kit did not find it. The downstream repository had quietly grown an alias table to work around it, with a comment reading “FILE_RE’s leading \b eats the dot on a dot-directory,” and one of those aliases was load-bearing for seven items. The workaround was correct, was written three times, and was per-repo boilerplate for an upstream defect. A workaround in a consuming project is evidence about the tool, and it is only evidence if somebody goes and reads it. I would not have thought to look.

The harness compelled a file that no file list granted. This is the one that is genuinely a design contradiction rather than a bug. The paired-artifact rule demands a counterpart: change a component, ship its test, or the gate exits non-zero. But an item’s file list is scraped from citations, and a map entry cites the code it is about, not the test that does not exist yet.

So a builder handed a component and not its test had exactly two moves. Fail its own anchor, or edit outside its node. It left scope, wrote the test, and the intent lens rejected the node for it.

Every party behaved correctly and the node was still lost. The builder obeyed the gate. The lens obeyed the scope rule. The rule they were both obeying was self-contradictory. The same hole showed up three times in one week wearing different clothes: a manifest that cannot move without its lockfile, a test directory no item could cite, and a generated schema that is the paired artifact of every file that feeds it.

The paired-artifact gate compels a changed component to ship its test, while the file list grants only the component itself because the test is never cited. The builder is left two moves, both bad: stay in scope and fail its own anchor, or leave scope and be rejected by the intent lens. The fix derives the counterpart from the same rule the gate enforces and adds it to the file list

The fix is to derive the counterpart from the same rule the gate enforces and add it to whatever the scraper already found, so the two cannot drift. And the general lesson took three instances to see: a scope reject is not automatically a builder error. Read which of the builder, the lens, or the list is wrong before re-dispatching, because re-running a builder against the same wrong list just reproduces the reject.

The same false alarm, from a second direction. I wrote above that the anchor disagreement is the most valuable line the system produces, and that it only works if it is rare. A conditional anchor, one configured to run only when the diff touches certain paths, has a third state besides pass and fail, and the output schema typed every anchor as a bare integer. So “did not apply” had no representation, and each agent invented one. A builder reported -1. Its verifier ran the gate, got a not-applicable exit 0, and reported 0. Both were correct about reality. The reduce compared the integers and fired the loudest warning the system has, on a node where nothing was wrong.

That was the second independent cause of that identical false alarm, and it surfaced on the first batch to run the fix for the first cause. The fix is null, the JSON-native absence, with both prompts told to use it. Deliberately not “teach the reduce to tolerate -1”, which would promote one agent’s guess to a convention. And in the same change null had to be made unsafe everywhere else: an always-run anchor reported as null is a required check nobody ran, which is the fail-open shape again.

The other three are in the scar file: a generated file is the paired artifact of everything that feeds it and belongs in the integrate step rather than in a per-node rule; a refusal decided by evidence belongs in the map as a field, not as prose the extractor cannot read; and closing an item is two edits, of which only one reliably gets made.


What it costs

I have been describing what the harness buys. Here is the other column, honestly.

A two column ledger of the harness tradeoff: on the left, what it buys, being one human gate per batch, deterministic collision safety, independent verification, refusal instead of fiction, and structural rather than instructional guarantees; on the right, what it costs, being roughly four agents per node, wall clock serialized by waves, a map that takes four to six directed sessions rather than one pass, denser collision graphs on small codebases, and a human gate that is still the only semantic check

Token cost is roughly four agents per node. One builder plus three verifiers. A real batch on the client app was 7 items, 6 nodes, and 24 agents for a single wave. This blog’s batch 2 was 6 items across 5 nodes, so 20 agents to close six defects on a static site. If you would not spend that on the work, do not build the graph around it.

Waves serialize wall clock. Every repository wide item takes a wave of its own, and a wave does not start until the previous one is merged and the anchors re-run on the merged tree. A batch with two exclusive items is three sequential rounds regardless of how much parallel capacity you have.

The map is expensive, and it is the honest ceiling on the whole thing. It is a JSON document recording what is true about the codebase, with file:line citations as evidence rather than descriptions, and it is written by agents, deliberately, because the extractor scrapes those citations to build the collision graph and a builder re-verifies its own citation before changing anything. Nobody hand types it. I would not want to.

What cannot be done is generating it in one unattended pass. Every version of “point an agent at the repo and ask for a map” produced a file describing structure, which any agent can already read from the code, rather than consequences: which columns do not exist despite older code implying they do, which contract looks like style but is actually data integrity. What produces the second kind is a designed process across four to six directed sessions, and that process is the real cost. It is enough material for its own field note, and I will write it as one rather than bury it here.

The part that belongs in a costs column is the conclusion. The map is the one artifact in this system with no anchor. Everything else is judged by an exit code someone watched. The map is judged by a human reading it at a checkpoint and believing it, four times over. That is the bill, and it is attention rather than authorship.

Small codebases have denser collision graphs, not sparser ones. Fewer files means more items sharing them. On that client queue, a single actions file was claimed by 8 separate items and one component by 6. 16 The partitioner correctly merges those into serial nodes, so a batch of six items can legitimately partition into two. That is the system working, and it means your real fan out width is lower than your item count suggests. Judge batch width by files, never by item count.

The human gate is still the only semantic check. Nothing in the machinery reads a sentence, renders a page, or knows what the business rule is supposed to be. On a static site the gap is stark enough that the push guard’s own denial message spells it out: build proves it compiles, check type-checks, a content gate reads markdown, and nothing renders a page or executes a line of client-side JavaScript. 17 The highest severity defect that kind of repository can produce is a false claim published under a real name, and no anchor can see one.

Note: There comes a point where this harness is not in your favor to use. Size of the build matters, but surface count matters more. If your whole project is 1-3 surfaces, this harness may introduce issues that you wouldn’t have had otherwise. This harness is best used for larger projects. I think the better use for smaller projects is to just take the APP_MAP portion. So much of your work can be tracked and improved, if all you use is the map.

And the guard will annoy you. The push guard denies git stash push, because the pattern deliberately spans a git invocation’s arguments so it can catch git -C /path push, and stash sits inside that span. Five separate sessions have independently proposed the same carve out, and the argument is good every time: stash writes a local ref, takes pathspecs, and cannot contact a remote. A patch was written for it. It tested clean. It was reverted, and the revert is the point. Every exemption is individually defensible and collectively fatal, and the sixth argument will be the one that is wrong. The cost of a false positive is one command. The cost of a false negative is a published repository under a real person’s name, or company PI landing on a homepage, or api secrets in a repo, on-and-on. The gaurd is there for a reason. A human must be the one who pushes. Period. You aren’t that busy, the models already did 90% of the work for you, all you have to do is verify the work before you push it.

At some point in the future this may change, but one major thing must always be known machines can’t be held liable, and intent doesn’t compute.

What I have not proved

Two batches of this blog’s backlog have now run through the generalized kit: 12 items, 9 nodes, 1 wave each, everything merged. 10 of 10 nodes were accepted first pass, with zero rejects and zero anchor disagreements. 18

That is not the result I want to report, and I am reporting it as a weakness rather than a headline. A perfect acceptance rate proves the lenses agreed with the builders. It does not prove the builders were right, and it certainly does not prove the harness catches bad work, because it has not yet been given any. The one batch that genuinely tested the verify stage is the client batch where 3 of 3 green nodes were rejected on semantics, and that ran under the original project specific version.

There is a sharper version of that criticism, and it is correct. The kit’s proof is the wrong shape for the kit. I wrote above that a project of one to three surfaces should take the map and the anchors and leave the graph, and that a static site with no test suite cannot see a render. Then I ran the graph on exactly that kind of site and published 10 of 10 as the portable kit’s result. The client work is not made untrue by this. But the run that exercised verification is the one that is not this repository, and reporting a clean reduce from a site where two of the three lenses have nothing to bite on trains you, and trained me, to read green as detection.

What is missing is small and specific: a canary node. One deliberately broken change, with wrong copy, an extra file, and a failing invariant, run through this kit until something rejects it. Until that happens the verify half of the diamond is an architecture diagram, and the honest claim for the portable kit is that its reduce is now tested and its lenses are not.

The sharpest case is a pure rendering fix in batch 2 whose correctness rested entirely on measurements taken outside the repository, by the same agent that wrote the fix, in a browser harness. All three lenses reported they could not reproduce those measurements. The system verified that the diff matched the item and that the tree still builds. It did not verify that the built app looks right on a phone, and it cannot. Back to the human gate part.

The kit lists its other gaps rather than waiting for someone to find them. 19 The self test now covers 205 assertions across config refusals, the partitioner, the companion derivation, the scope gate, the reduce, integration, the push guard, and the paired artifact gate, and it passes clean. 20

That number grew because the same reader who found the bridge pointed out that the reduce step had no test at all, and the reduce is the code that decides what a batch means: accepted versus rejected versus unverified, whether a partial run gets reported as complete, whether the builder’s claimed exit codes match what an independent agent observed. Both of the earlier scars in this article are about that function. The blocker was real, a workflow script has no module loader and no filesystem, so it cannot be imported and could only ever be parse checked.

The way through was to fence the reduce between two markers, make it a pure function of what came back, and have a fixture slice the text out of the shipped file and evaluate it. The test runs the real code rather than a copy, at zero agents and zero tokens. Then mutate it, because a test that cannot fail proves nothing: the self test now patches the shipped reduce four ways, collapsing unverified into rejected, disabling the anchor comparison, downgrading a single reject to a majority vote, removing the fan in guard, and asserts the fixture catches each one.

What it still does not cover is the graph end to end, because that needs a live agent runtime. The config loader validates by hand rather than against its own JSON schema, and the two can drift. The gates are bash plus jq and inherit bash’s quoting model.


The sizing fork

I mentioned above that there is a point where your project may be too small for this harness to work effectively. I don’t want to waste your time. I also don’t think you should go through all the setup of this harness if you only want to use it once and ditch it. This is for people who want to learn, who have ongoing work, who know AI agents can help, but they don’t want to go through the 2-months of troubleshooting it took me to get here.

The harness has two halves and they pay off at different sizes. Deciding which half you want is the first real decision, and taking only one is a legitimate outcome.

The sizing fork: the spine, consisting of the map, anchors, push guard and paired artifact gate, pays off at any project size including one person projects; the graph, consisting of the queue, partitioner, fan out and multi lens verify, only pays off when the work is genuinely wide

Half What it is Pays off at
The spine map, anchors, push guard, paired artifact gate any size, including one person projects
The graph queue, partitioner, fan out, multi lens verify only when work is genuinely wide

Three questions decide it. Can you name eight or more open items where most pairs touch no common file? Do you have at least two distinct surfaces, such as schema plus app, or API plus worker? Will more than one agent’s worth of work run per sitting?

Three yes, build the graph. One or two, take the spine and skip the graph. Zero, take the map and the anchors only, because a single agent in the main loop with a good map beats a graph with a thin one every time. Adopting the graph later costs one session and wastes nothing, since the map is the expensive artifact and it carries over intact.


When not to use this

If you cannot find two items with no edge between them, there is no graph to build.

A single bug fix, an exploratory question, or anything where you want to approve each step is cheaper and better as one agent in the main loop. The coordination is pure overhead unless the work is wide.

That advice was in this article and in the runbook, and nothing enforced it, which is a slightly embarrassing thing to find in a piece arguing that a rule in a prompt is a suggestion. The partitioner would happily print a one node “diamond” and exit 0. A fan out width of 1 is not a cheaper loop, it is the same loop plus a builder handoff, three verifiers and a reduce step, roughly four times the tokens for zero parallelism, and it reads as a graph in the report afterwards.

It is an exit code now, with the force deliberately split. --auto refuses to propose a width 1 batch, because auto mode is the machine proposing and it should not propose a non graph. Naming ids explicitly only warns, because re-running one rejected node through the verify lenses is a legitimate thing to want, and refusing it to enforce a style rule would break real work. A node holding many items gets its own flag: that is the density trap working as designed, but one builder doing eight items serially in one context has no fan out inside it, and you should choose that knowingly rather than discover it in the diff.

I follow my own advice here often enough that the log has a name for it. On the same day as a real batch, five prose contradictions in published posts were fixed directly, in one commit, with no builder nodes and no verifier lenses, because they were single file edits with nothing to collide. I recorded that non-batch alongside the real ones, with an explicit note that nothing about it was independently verified, so the batch history is never mistaken for the complete account of what closed. 21 A process log that only records the times you followed the process is a marketing document.

Skip it too for items whose truth lives outside the repository. A SaaS console, a client’s box, a decision nobody has made yet. Dispatching a builder at those produces confident fiction, which is the most expensive output an agent can generate because it arrives formatted exactly like a result.


The one thing to take

Strip away the partitioner, the worktrees, the lenses, and the waves, and what is left is a single question you can apply to any agentic system you are running today:

Which of the numbers in front of you did you watch happen, and which of them are agents agreeing with each other?

Consensus between models is cheap and it is not evidence. Every failure in this post was caught by a comparison against something that could not argue back: an exit code observed twice, a count of what returned against what was dispatched, a schema that could not hold the thing the model wanted to write. The report always looked fine. The numbers did not.

Build the thing that compares the numbers. The rest is plumbing.

Not every company has to be an AI or tech company, but every company must be a data company. This harness puts that process to work. Not every decision has to be an AI decision, but every AI decision needs data to validate it. Every test we build, we force fail it, because a test that never fails is a failing test.


TL;DR

Use heading for easy jump-to.

The problem. Agentic development does not bottleneck on the agents. It bottlenecks on the human gate: one plan approved, one diff read, one app verified, per item. A 116-item queue is 116 gates. Faster agents just reach the gate sooner.

The shape. Fan out one bounded job per git worktree, verify each with three lenses that never saw the builder, reduce in plain code, and put one human gate before the PR. It is plain JavaScript and bash you copy into a project, and it does not care what language you build inwhat it does require is git, and checks that reduce to an exit code.

The four properties that do the work:

  1. Collision detection is plain code, never a model. Union find over declared file sets, printed before dispatch. Created files count. A cross reference is an edge. Repo-wide items form no edges and get their own wave. And the file extension list your scraper can see is the partitioner, so it has to lint its own blind spots.
  2. Verifiers get a fresh context. They read the diff themselves, treat builder claims as assertions to test, and default to reject. Models favor their own output; design against it rather than test for it.
  3. Three lenses, three different questions — intent, invariants, anchors — so one reject fails the node. Majority vote lets two lenses that never looked outvote the one that did. And any question you can write as a script does not belong in a lens at all: it is slower there, less reliable, and it displaces the judgment only that lens can make.
  4. Anchors, not agreement. Builder and verifier both report exit codes; the reduce step compares them. Agreement between models is not evidence. An exit code observed twice is.

Four outcomes, never collapsed: accepted, rejected, unverified, not-built. Reporting an unjudged node as rejected fabricates a finding nobody made.

Seven failures that shaped it. None of them announced themselves. Lost structured output, borrowed node_modules, grep -q under pipefail, a rule that was prose instead of a hook, a guard that failed open by default, a field the prompt never rendered, and a gate that passed because it was asked the wrong question. Every one produced a report that looked correct.

And six more from a second project. A regex that could not see a leading dot, so every .github/ citation was invisible to the collision graph. A rule that compelled a file no file list granted, so a correct builder and a correct lens still lost the node. And a conditional check with no way to say “did not apply”, which manufactured the one warning the system most needs to stay rare.

What it costs. ~4 agents per node (one builder, three verifiers) — a real 7-item batch was 24 agents. Waves serialize wall clock. The map takes 4 to 6 directed sessions, not one pass, and it is the one artifact with no anchor. Small codebases collide more, so judge batch width by files, not item count. The human gate is still the only semantic check.

Whether it fits your project. Two halves. The spine — map, anchors, push guard, paired artifact gate — pays off at any size. The graph only pays off when the work is genuinely wide.

When to skip it. If you cannot find two items with no edge between them, there is no graph. Single fixes, exploratory work, and anything whose truth lives outside the repo are cheaper as one agent in the main loop. Under three surfaces, take the map and the anchors and leave the graph.

What I have not proved. 10 of 10 nodes accepted first pass across two batches of this site’s backlog. That is a weakness, not a headline — the harness has not yet been handed bad work to catch.

The one question to carry off: which of the numbers in front of you did you watch happen, and which of them are agents agreeing with each other?


Citations

The harness is at github.com/Curious-Keeper/sprint-harness, published alongside this post. Citations that name files under docs/ or core/ in the footnotes point there.

Figures for the client project (item counts, batch composition, agent counts, collision counts) come from my own run logs and a private client repository. They are stated as observations here and will not be published. Figures for this blog’s two batches come from the same class of private run log: the published site is public (you’re reading it), the repository and its map are not. Nothing in this post links a private tree. Sorry.

Footnotes

  1. Re-measured 2026-08-21 against core/ and templates/ and docs/ in sprint-harness: 3,103 total lines, 1,779 excluding comment and blank lines; 18,105 words across the six files in docs/. The figures at first publication were 1,667 / 1,026 / 13,647; the growth is the scope gate, the integrate script, the reduce fixture and twelve new scars.

  2. The three layer split and the full portability audit: docs/DESIGN.md.

  3. grep -rn "npm\|node_modules\|package.json\|tsx" core/ returns four lines: two comments illustrating a collision example and a citation format, one comment about regex alternation ordering, and one real line, the DEFAULT_EXTENSIONS array in core/lib/extract.mjs. No npm, node_modules or package.json hits. Re-verified 2026-08-21; the original version of this footnote claimed one comment line and was accurate when written, before the extension list was made configurable.

  4. This site’s harness config is not in a public repository. Observed 2026-08-15: no test anchor, a canary script as a substitute, zero lanes, zero paired artifacts. The Go configuration, including the gofmt wrapping and the empty setup array, is public: examples/go-service.harness.config.json.

  5. Union find with path compression, the partitioner’s core data structure. Robert Tarjan, “Efficiency of a Good But Not Linear Set Union Algorithm”, Journal of the ACM 22(2), 1975. Implementation: core/plan-batch.mjs.

  6. Scope counts from the client project’s generated queue, 116 items: 78 bounded, 15 needs-design, 11 migration lane, 6 repo-wide, 4 external, 2 held. Private repository. The scope taxonomy itself is documented in docs/SCARS.md under “Refusing to dispatch is a feature.”

  7. Arjun Panickssery, Samuel R. Bowman, Shi Feng, “LLM Evaluators Recognize and Favor Their Own Generations”, arXiv:2404.13076, 2024. See also Peiyi Wang et al., “Large Language Models are not Fair Evaluators”, arXiv:2305.17926, 2023.

  8. This site’s private batch log, batch-2, 2026-08-14. The map is not published. The observation is that the builder seeded a vulnerable scratch tree, watched the new advisory anchor exit 1, and only then reported the anchor as working.

  9. docs/SCARS.md, 31 documented failures with the design decision each produced. Scars 20 through 25 came from a reader’s review of this article; 26 through 31 came from a week of running the portable kit on a second private project, and are the ones described below as found in use.

  10. git worktree checks out the tracked content of a commit into a new working tree; untracked and ignored files from other working trees are not present. git-worktree documentation.

  11. POSIX specifies that grep -q exits with zero status immediately when any input line is selected, even if a read error occurs. POSIX grep specification.

  12. With pipefail set, a pipeline’s return status is the value of the last command to exit with a non-zero status. A command killed by a signal exits with 128 plus the signal number, so SIGPIPE yields 141. Bash Reference Manual, The Set Builtin and Exit Status.

  13. My own incident writeup: Hermes Agent Wrote Its Own Client. Related, on why “isolated” claims need measuring rather than trusting: Sandboxes or just Sand?.

  14. PreToolUse hooks run before a tool executes and can deny the call outright. Claude Code hooks documentation. Implementation: core/deny-push.sh.

  15. zsh does not perform word splitting on the results of unquoted parameter expansion or command substitution by default, unlike bash and other Bourne-derived shells. zsh FAQ 3.1.

  16. Collision report from the same private client queue: one server actions file claimed by 8 items, one component file by 6. The density trap this produces is documented in docs/RUNBOOK-SMALL.md.

  17. Observed on this site’s private harness config and map, 2026-08-15: the push guard’s denial text states that build proves it compiles, check type-checks, the canary gate reads markdown, and nothing renders a page or executes client-side JavaScript. The config file is not published. Canaries: if you see or wonder why I mention canaries in this article and many of my articles is because I embed canaries to all my work. So for my use case, one of my gates is a canary_gate.

  18. This site’s private batch log, 2026-08-14: batch-1 (6 dispatched, 4 nodes, 0 rejected) and batch-2 (6 dispatched, 5 nodes, 15 of 15 verdicts accepted). Both entries record what the batch did not prove, which is where the caveats in this section come from. The log is not published.

  19. docs/DESIGN.md, “Known gaps.”

  20. ./selftest.sh run 2026-08-21: 205 passed, 0 failed. It was 81 passed at first publication. selftest.sh. The reduce assertions include eight mutation checks that patch the shipped reduce and require the fixture to catch each one; the helper also asserts each patch actually applied, since a sed that matched nothing would otherwise look identical to a fixture that missed.

  21. This site’s private batch log records a direct-2026-08-14 entry alongside the real batches: five prose contradictions fixed in one commit with no builder nodes and no verifier lenses, noted as not independently verified. The log is not published.