{"slug": "building-a-harness-from-zero", "title": "Building a Harness From Zero", "summary": "A developer built a repository harness from zero that gives AI coding agents the files, conventions, and rules needed to operate without chat-fed context. The harness centers on a CLAUDE.md entry point under 100 lines, an ARCHITECTURE.md file, a startup script, a feature_list.json, and a claude-progress.md session log, demonstrated on a minimal ledger-cli project. The developer argues that moving the definition of \"done\" out of chat and into the repository makes agent work reproducible across sessions.", "body_md": "We're going to build a harness from zero, explaining step by step how to do it and why.\n\nA harness is the set of files, conventions, and rules that give an AI agent everything it needs to operate in a repository without depending on someone feeding it context in a chat. It defines how the agent starts, what it can and can't do, how it knows a task is done, and how it leaves a record of what it did so the next session (another agent, or you) can continue without rebuilding everything from scratch.\n\nThe underlying idea is simple: if the criterion for \"done\" only lives in your head, or in a chat message that's going to disappear, any agent (or any person) who continues the work is going to make different decisions than you would. A harness takes those decisions out of the chat and puts them in the repository, as the single source of truth.\n\nWe're going to build a minimal sandbox project, since it's not the important part here — what matters is what we build around it.\n\n[https://github.com/crisbonomodev/ledger-cli](https://github.com/crisbonomodev/ledger-cli)\n\nThe project is a simple ledger that lets you record credit and debit operations, search by date, and get an account's balance, built on top of a LinkedList and a Map.\n\nWe'll also need a verification mechanism: check and test scripts the harness can run to validate its own changes.\n\nOnce we have the base of our ledger, let's set up the initial structure of our harness.\n\nWe need an entry point for our harness — an AGENTS.md file (or CLAUDE.md, in Claude Code's case). We'll use `/init` in Claude to generate an initial CLAUDE.md, and then modify it.\n\nReference commit with the generated base file: [https://github.com/crisbonomodev/ledger-cli/commit/e0cb08c81b1d92d3c7fc831bdc05f931b683908f#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7](https://github.com/crisbonomodev/ledger-cli/commit/e0cb08c81b1d92d3c7fc831bdc05f931b683908f#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7)\n\nCLAUDE.md should contain only the instructions the harness needs to operate, plus references to other files the agent can reach if it needs them. It shouldn't go over 100 lines.\n\nOur CLAUDE.md had architecture details in it — let's move those to a new ARCHITECTURE.md file.\n\nYou can see the state after that refactor in this commit:\n\n[https://github.com/crisbonomodev/ledger-cli/commit/7ef78c14c334748a29a7b22e1aaf67d77a993e54](https://github.com/crisbonomodev/ledger-cli/commit/7ef78c14c334748a29a7b22e1aaf67d77a993e54)\n\nNow we need to add our startup script, which takes care of initializing everything related to the project: installing dependencies, running verification scripts, and the start command, all in a single step — so the Operating Loop has something real to run.\n\nYou can see it in this commit:\n\n[https://github.com/crisbonomodev/ledger-cli/commit/bed716002edb4678412f85a4544e17fbc912018e](https://github.com/crisbonomodev/ledger-cli/commit/bed716002edb4678412f85a4544e17fbc912018e)\n\nOnce this script is validated, we can move on to structuring feature_list.json. But first, we need to define what our ledger-cli's features will be. For this example, they'll be:\n\n`history(account)`\nNow let's define the structure of our feature_list.json. Each feature needs:\n\nYou can see the feature_list.json in this commit:\n\n[https://github.com/crisbonomodev/ledger-cli/commit/50fdc573689144e858cba83226cdeeca059d2f3b](https://github.com/crisbonomodev/ledger-cli/commit/50fdc573689144e858cba83226cdeeca059d2f3b)\n\nNow we need a file where we can log every session and task we work through. For this, we use claude-progress.md (or just PROGRESS.md).\n\nYou can see it here: [https://github.com/crisbonomodev/ledger-cli/commit/19cf16927c6fcfa83511010a61fc37945182021e](https://github.com/crisbonomodev/ledger-cli/commit/19cf16927c6fcfa83511010a61fc37945182021e)\n\nWith this, we now have the base of our harness. Let's run our first feature. The history feature needs to:\n\nFor now, we'll just leave it the information it has in feature_list.json and see what it does. We open Claude in a terminal and give it the following prompt:\n\nFollow the operating loop from CLAUDE.md and work on the top priority feature pending on @harness/feature_list.json\n\nCommit with the result of that run:\n\n[https://github.com/crisbonomodev/ledger-cli/commit/9e7fbe53907d0a0227de1ec54084b01b2cbb6632](https://github.com/crisbonomodev/ledger-cli/commit/9e7fbe53907d0a0227de1ec54084b01b2cbb6632)\n\nLooking at the result, we can see the harness completed the task, but took quite a few liberties with how the code was written. For example:\n\n```\nhistory(account: string): { transaction: Transaction, balance: number }[] {\n    const transactions = this.load().toArray()\n        .filter((tx) => tx.account === account)\n        .sort((a, b) => a.date.localeCompare(b.date))\n\n    let balance = 0\n    return transactions.map((transaction) => {\n        balance += transaction.type === TransactionType.CREDIT ? transaction.amount : -transaction.amount\n        return { transaction, balance }\n    })\n}\n```\n\nNotice it used an anonymous type, when by convention it would've been better to declare a new `AccountHistoryEntry { transaction: Transaction; runningBalance: number }` interface in src/types.ts, following the convention we'd been building toward. But we never wrote that convention down anywhere, and since the repository is the single source of truth, it implemented it however it saw fit.\n\nTo improve this, we have a few options:\n\nYou can see the files in this commit: [https://github.com/crisbonomodev/ledger-cli/commit/261dd0fb7a3c2dc596fcd06693770ef525d9173d](https://github.com/crisbonomodev/ledger-cli/commit/261dd0fb7a3c2dc596fcd06693770ef525d9173d)\n\nNow let's implement the next task and see the result.\n\nBefore dispatching the implementer, we wrote this task's sprint contract (void). What's interesting isn't just the Scope section, but the Exclusions one:\n\n## Exclusions (explicitly out of scope for this sprint)\n\n- Behavior when\n`id` does not exist — undefined, not required, not tested.- Preventing or detecting double-voiding the same transaction — undefined, not required, not tested.\n\nExplicitly saying what *not* to solve is just as important as saying what to solve — without this, an agent might \"improve\" the task by adding error handling nobody asked for, or worse, leave it half-done.\n\nUsing our Claude terminal in the project, we run `/clear` and then:\n\nuse the implementer subagent for ledger-002\n\nCommit with the implementation: [https://github.com/crisbonomodev/ledger-cli/commit/d8d3d187a0f93a8944886ead3ae2802b0e37f04c](https://github.com/crisbonomodev/ledger-cli/commit/d8d3d187a0f93a8944886ead3ae2802b0e37f04c)\n\nAnd then, to have it evaluated:\n\nuse the evaluator subagent for ledger-002\n\nThe evaluator doesn't give a free-form verdict — it fills out evaluator-rubric.md, which scores 6 dimensions from 0 to 2: correctness (does it do what feature_list.json asks for?), contract fidelity (did it respect the names and locations from the sprint contract, not just \"does it work\"?), verification (did it re-run the checks itself, or trust the implementer's notes?), scope discipline (did it stay within what the contract allowed?), reliability (does it survive a `rm -rf dist && ./init.sh` from clean?), and handoff readiness (was progress properly recorded?). Only if everything scores 2 (or there's a documented exception) is the verdict Accept.\n\nNow we can see that different agents were used, conventions were respected, the sprint contract was implemented, and the rubric was completed. But the progress file didn't record the commits, and we still aren't really validating that the app stays in a consistent state and runs correctly, beyond the tests.\n\nTo fix that, we need to add a file listing the checks the evaluator agent has to run as part of the passing requirement.\n\nThe underlying problem is that \"tests pass\" isn't the same as \"the repository is left in a consistent state.\" An agent can leave the build broken, progress badly recorded, or stray work files behind, and the tests for that specific feature will still pass just fine. The clean state checklist is a short list of non-negotiable conditions (build compiles, tests pass, recorded progress matches what actually happened, no leftover temporary artifacts) that the evaluator has to confirm one by one before considering the session closed — it's not enough for just the feature at hand to work.\n\nOne detail worth calling out: the check that recorded progress is accurate has to happen *after* committing, not before — the commit hash depends on the entire tree, including the progress file itself, so you can't verify a reference that doesn't exist yet.\n\nWe implemented the clean-state-checklist.md file in the harness. And while we're at it, remember we'd blocked the evaluator agent from committing — that has to change, because if the evaluator is the one running the final checklist, it also has to be the one closing the session with the commit.\n\n[https://github.com/crisbonomodev/ledger-cli/commit/bdf3b8a1122dd843109dbd1db78b9e8db55348bf](https://github.com/crisbonomodev/ledger-cli/commit/bdf3b8a1122dd843109dbd1db78b9e8db55348bf)\n\nWith these changes in place, we can now implement the third task (ledger-003, transfers). Since we already wrote the sprint contract before dispatching it, the prompt is straightforward after a `/clear`:\n\nimplement the next task by priority\n\n[https://github.com/crisbonomodev/ledger-cli/commit/51fa2cbadc51773ec72d167d83fb58347b6a6d7b](https://github.com/crisbonomodev/ledger-cli/commit/51fa2cbadc51773ec72d167d83fb58347b6a6d7b)\n\nThis time the whole cycle (contract → implementer → evaluator → checklist) held up without any manual adjustment on my part. The difference from the first task isn't that the agent got better — it's that this time the design decisions were already in the repository before it started writing code, instead of living in my head.\n\nWe now have our three features implemented, but our agents left loose fixes and validations scattered along the way. We're going to tackle error handling and validation across the flows. But to do that, we need to split the implementation into subtasks — and that's where we need a safe mechanism for agents to record what they did, so each agent can pick up exactly where the last one left off.\n\nWe add a new session-handoff file to handle this, update our CLAUDE.md and agent definitions, add the task to the feature list, and write our sprint contract.\n\n[https://github.com/crisbonomodev/ledger-cli/commit/555b93016eec16974985ee0d1b16cfdd6e9a8e0e](https://github.com/crisbonomodev/ledger-cli/commit/555b93016eec16974985ee0d1b16cfdd6e9a8e0e)\n\nNow we can start implementing our task. Session A builds the shared mechanism (the error hierarchy) and proves it out on a single flow:\n\nYou are the implementer. Work on ledger-004 following harness/sprint-contracts/ledger-004-validation-errors.md —\n\nbut ONLY the \"Session A\" part of the Delivery Plan: create a complete src/errors.ts (the 4-exception hierarchy)\n\nand implement/test only record()'s guard. Don't touch transfer() or void(). When you're done, write\n\nharness/session-handoff.md instead of harness/claude-progress.md, and leave the feature in_progress.\n\n[https://github.com/crisbonomodev/ledger-cli/commit/e854260551a37c401e9121154bc89550c3211b40](https://github.com/crisbonomodev/ledger-cli/commit/e854260551a37c401e9121154bc89550c3211b40)\n\nNow we move on to Session B. This is the key point of the whole experiment: it starts with a `/clear`, with nothing from Session A's chat, and the only context it receives is session-handoff.md:\n\nYou are the implementer. Start by reading harness/session-handoff.md — don't assume anything else about what the\n\nprevious session did beyond what that file says. Follow the rest of CLAUDE.md's Operating Loop and finish\n\nledger-004 according to the \"Session B\" part of harness/sprint-contracts/ledger-004-validation-errors.md: transfer(),\n\nvoid(), and the CLI's try/catch. Write harness/claude-progress.md as usual.\n\n(Note: between Session A, Session B, and the evaluator, the intermediate commits were made by me, by hand, in the terminal, orchestrating each dispatch. The implementer is explicitly forbidden from committing — so if you spot a commit \"mid-task\" anywhere, that's the human operating the harness, not the agent breaking its own rule.)\n\nFinally, we run the evaluator, which is the one that actually closes out the task with a single commit:\n\nYou are the evaluator. Evaluate ledger-004 against harness/sprint-contracts/ledger-004-validation-errors.md and\n\nharness/evaluator-rubric.md, reviewing both sessions' work together (A and B) — including whether Session B's code\n\nis genuinely consistent with the classes Session A defined, without redefining them or drifting from them.\n\n[https://github.com/crisbonomodev/ledger-cli/commit/35ffd50101e43dc736be0bd7869e1436c0f9a6a3](https://github.com/crisbonomodev/ledger-cli/commit/35ffd50101e43dc736be0bd7869e1436c0f9a6a3)\n\nWhat matters here isn't that Session B finished the work — it's that it did so without having seen a single line of the chat where Session A was designed. Everything it needed — which error classes already existed, where they lived, what they were called — was in session-handoff.md. And I'm not taking the evaluator's word for it: I checked the code myself afterward — zero redefinitions, zero naming drift. Session B used exactly the classes Session A left behind.\n\nThat's the point of the whole exercise: a harness isn't a tidy set of files, it's the answer to a concrete question — *what does the next session need to know so it doesn't repeat or contradict what's already been decided?* When that answer lives in the repo instead of in your memory or in a chat that's about to close, the work becomes pickable-up by anyone: another agent, another session of yours, or someone else on the team. And that habit of not trusting an agent's declared state without checking the evidence yourself is, to me, the one that holds up all the rest.\n\nAll this talk of contracts, roles, and checklists can sound abstract if you never see the result actually working. After building (`npm run build`), here's a real terminal session using the four features we built:\n\n``` bash\n$ node dist/index.js record checking 1 500 \"Paycheck\"\n$ node dist/index.js record checking debit 120 \"Groceries\"\n$ node dist/index.js balance checking\nbalance of checking: 380\n\n$ node dist/index.js history checking\n2026-09-13 #1 1 500 - Paycheck | balance: 500\n2026-09-13 #2 0 120 - Groceries | balance: 380\n\n$ node dist/index.js void 2\n#3 voids #2 - checking 1 120\n\n$ node dist/index.js transfer checking savings 100\n#4 checking -> #5 savings - 100 (transferId 4)\n\n$ node dist/index.js balance checking\nbalance of checking: 400\n$ node dist/index.js balance savings\nbalance of savings: 100\n```\n\n(Side note: `1` as the second argument to `record` means CREDIT — a known, never-fixed bug means comparing it against the string `\"credit\"` doesn't actually work; anything else falls back to DEBIT by default. It's exactly the kind of minor technical debt a sprint contract deliberately leaves out of scope, and that stays documented instead of quietly fixed by nobody's request.)\n\nThe numbers check out: 500 − 120 + 120 (reversed) − 100 (transferred) = 400 in checking, 100 in savings. That's the result of all four features, built across separate agent sessions, working together without friction.\n\n`ledger-cli` is a toy project on purpose — the idea was to see the problem and the solution in a full cycle in a couple of hours, not to build something production-grade. Depending on the complexity of the project it's applied to, these files will need to be more or less detailed — and in fact, building and maintaining a harness turns out to be an iterative, ongoing process.\n\nThe full code — CLAUDE.md, sprint contracts, the implementer/evaluator split, all of it — is on GitHub: ledger-cli.\n\nCurious how others handle this: when two agent sessions can't share chat context, what do you pin outside the conversation so the second one doesn't quietly redefine what the first one built?", "url": "https://wpnews.pro/news/building-a-harness-from-zero", "canonical_source": "https://dev.to/cristianbonomo/building-a-harness-from-zero-17if", "published_at": "2026-09-14 13:08:21+00:00", "updated_at": "2026-09-14 13:19:05.933043+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Claude", "Claude Code", "ledger-cli"], "alternates": {"html": "https://wpnews.pro/news/building-a-harness-from-zero", "markdown": "https://wpnews.pro/news/building-a-harness-from-zero.md", "text": "https://wpnews.pro/news/building-a-harness-from-zero.txt", "jsonld": "https://wpnews.pro/news/building-a-harness-from-zero.jsonld"}}