<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ian Johnson</title>
    <description>The latest articles on DEV Community by Ian Johnson (@tacoda).</description>
    <link>https://dev.to/tacoda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F171498%2Fb1207a6e-f740-43c4-bb64-c675e3b3ce1d.jpeg</url>
      <title>DEV Community: Ian Johnson</title>
      <link>https://dev.to/tacoda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tacoda"/>
    <language>en</language>
    <item>
      <title>The middle path between a stock agent and a custom harness</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Mon, 27 Jul 2026 17:23:15 +0000</pubDate>
      <link>https://dev.to/tacoda/the-middle-path-between-a-stock-agent-and-a-custom-harness-4g8c</link>
      <guid>https://dev.to/tacoda/the-middle-path-between-a-stock-agent-and-a-custom-harness-4g8c</guid>
      <description>&lt;p&gt;Here’s a conversation I keep watching play out. An engineering lead wants their team on a coding agent. Compliance won’t sign off on the stock one — no audit log, no way to block writes to secrets, no way to make destructive shell commands ask twice. So the options on the table narrow to two: keep waving people off the tool they want, or build a custom agent from scratch on top of an SDK. Two engineers, a quarter, and now the team owns a product nobody wanted to own.&lt;/p&gt;

&lt;p&gt;The conversation is the same at every company I’ve seen it in. It’s the wrong choice on both ends. Stock coding agents are close enough that the gap feels small. A custom harness is far enough that you don’t want to cross the distance. There’s a middle path that seems to have gone unnoticed, and I want to make the case for it.&lt;/p&gt;

&lt;p&gt;The middle path is &lt;a href="https://pi.dev" rel="noopener noreferrer"&gt;pi&lt;/a&gt;, an OSS terminal coding agent that ships the boring parts of the harness and hands you the interesting parts as first-class extension points. Custom tools, tool-call gates, commands, skills, prompt templates, the system prompt itself. All of it lives in your repo as normal TypeScript and Markdown. No fork. No SDK-level plumbing. If you delete .pi/, you’re back to a stock coding agent.&lt;/p&gt;

&lt;p&gt;I spent a weekend testing this claim against five real workflows. Three coding cases, two non-coding cases. The non-coding ones matter, and I’ll get to why.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each end costs you
&lt;/h3&gt;

&lt;p&gt;Stock agents are the fastest thing to adopt. They also have a small surface. You get some combination of a system prompt override, MCP servers, and a permission list. That’s usually it. When you hit a real workflow constraint (an auditor who wants a log, a platform team with an opinionated deploy pipeline, a research group that wants a locked-down read-only reviewer) the shape of the tool doesn’t bend.&lt;/p&gt;

&lt;p&gt;The other end is building your own. Every SDK sample makes it look easy. The tool loop is fifty lines. And then you need streaming. And cancellation. And session storage that doesn’t corrupt on Ctrl-C. And a TUI that doesn’t jitter. And compaction that doesn’t drop the last user message. And a model router that handles four providers. And permission gates. And a way to update the model catalog without shipping a release. Six months later you have a product, and the product is worse than the OSS agent you started with, and the two people who understand it are the only two people who can touch it.&lt;/p&gt;

&lt;p&gt;The cost curve is below. Stock (red) is cheap up front and expensive the day a real constraint lands. Custom (black) is expensive up front and expensive forever. There ought to be a floor between them. Harness assembly (green) is that hypothesized floor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpcppw0mtsm0xn60kjo4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpcppw0mtsm0xn60kjo4.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What pi actually gives you
&lt;/h3&gt;

&lt;p&gt;Here’s the distinction the whole hypothesis rests on. Pi is nominally a coding agent. But only two things about it are coding-specific:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The four built-in tools: read, write, edit, bash.
&lt;/li&gt;
&lt;li&gt;The default system prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are replaceable. The built-ins go away with --no-builtin-tools. The system prompt is a flag or a file. Everything else pi ships is not coding-specific. It’s just what any competent terminal agent needs.&lt;/p&gt;

&lt;p&gt;That’s the observation. If those two coding-specific pieces are replaceable, then pi isn’t really a coding harness. It’s a &lt;strong&gt;general terminal-agent harness&lt;/strong&gt; &lt;em&gt;that happens to ship with a coding-agent profile enabled by default&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six strategies, laid out in a taxonomy
&lt;/h3&gt;

&lt;p&gt;Every “custom harness” a team actually wants is really some combination of six moves. Pi exposes all six as normal repo files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A. Repo context.&lt;/strong&gt; AGENTS.md or CLAUDE.md at the project root, or a .pi/SYSTEM.md if you want to replace the system prompt globally. Zero code. This is where house style and always-on rules live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B. Prompt templates.&lt;/strong&gt; Markdown files under .pi/prompts/, invoked as /name. For workflows a human kicks off, such as “do a security review of X.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C. Skills.&lt;/strong&gt; Markdown runbooks under .pi/skills//SKILL.md that the &lt;em&gt;model&lt;/em&gt; loads when a task matches. For playbooks: how we deploy, how we review a notebook, how we cite a source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D. Tool gates.&lt;/strong&gt; A TypeScript extension that hooks pi.on(“tool_call”, ...) and can block or modify any tool call before it runs. This is where compliance and blast-radius controls live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E. Custom tools and commands.&lt;/strong&gt; pi.registerTool() and pi.registerCommand() in an extension. For wrapping internal CLIs, deploy pipelines, ticketing systems, or anything else the agent should be able to do that isn’t a shell command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F. Launch profile.&lt;/strong&gt; A wrapper script that starts pi with a specific tool allowlist, a specific system prompt, and specific resources preloaded. This is how you ship a “mode.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost of each roughly goes &lt;strong&gt;A &amp;lt; B ≈ C &amp;lt; D ≈ E &amp;lt; F&lt;/strong&gt;. A team starts at A and adds strategies as pain shows up. None of them require touching pi’s internals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five workflows, put through the taxonomy
&lt;/h3&gt;

&lt;p&gt;I set up a repo, &lt;a href="https://github.com/tacoda/pi-harness-assembly" rel="noopener noreferrer"&gt;pi-harness-assembly&lt;/a&gt;, with five cases. Three coding, two non-coding. The point wasn’t to build production tools; it was to see whether the taxonomy holds up when you point it at real needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1: a regulated shop.&lt;/strong&gt; Every tool call logged to disk, writes to .env and prod-config paths hard-blocked, a confirmation before destructive shell commands run. This is strategy D plus A. One extension file, about 130 lines including comments. It reacts to tool_call events, appends a JSONL audit line, checks the path against a protected-paths list, and prompts the user for dangerous bash patterns. The auditor gets their log. The team keeps the agent they wanted. If someone hates it, they delete one file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2: an internal platform team.&lt;/strong&gt; They want a canonical deploy workflow (preflight, canary, verify, promote) that the agent can execute but can’t skip stages of. This is strategies E, C, and B together. An extension registers a deploy_service tool that shells out to the real deploy pipeline. A /deploy command kicks off the flow. A skill under .pi/skills/deploy-runbook/SKILL.md contains the actual playbook: run the stages in order, stop before prod promote, don’t batch services. The playbook lives in Markdown so the platform lead can edit it without touching TypeScript, which matters more than any technical detail on this list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 3: a research group.&lt;/strong&gt; They want a locked-down “explain and critique my notebook” mode — read, grep, list, nothing else. Strategies F, C, and B. A bin/pi-review launcher runs pi --tools read,grep,find,ls, expands a prompt template, and preloads a skill with a review rubric (correctness, reproducibility, statistical soundness, performance). No writes possible. The model describes changes instead of making them. Twenty lines of shell and two Markdown files.&lt;/p&gt;

&lt;p&gt;Those three are the interior of the hypothesis. Coding workflows, coding tools, coding-shaped constraints. If pi is a middle path, it should handle them, and it does.&lt;/p&gt;

&lt;p&gt;The two non-coding cases are where the hypothesis either generalizes or breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 4: a web scraping harness.&lt;/strong&gt; Not a coding agent. read, write, edit, bash are all gone. The extension registers fetch_url and save_record. A /scrape command starts a job. A skill contains the extraction playbook: probe with a headers request, fetch text-mode HTML, identify the repeating entity, save one JSONL record per entity, paginate carefully. The launcher passes --system-prompt “$(cat .pi/systems/scrape.md)” to reframe the agent’s identity for that invocation only, so it doesn’t leak into the coding cases in the same repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 5: a research assistant.&lt;/strong&gt; Also not a coding agent, and structurally different from scraping. Where scraping extracts rows from known URLs, research &lt;em&gt;finds&lt;/em&gt; sources, reads them, and synthesizes with citations. Four tools: web_search, fetch_source, add_note, list_notes. Every claim gets a citation to a URL the agent actually fetched because the skill mandates it, the extension can’t enforce it, and that’s fine because the skill is where the rule belongs. Notes accumulate as Markdown under research-notes/.md across sessions, and list_notes gives the agent a way to discover its own prior work. Long-term memory built out of the filesystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What surprised me
&lt;/h3&gt;

&lt;p&gt;The three coding cases were what I expected. Small extensions, plain Markdown, working workflows. Not novel; this is what pi is for.&lt;/p&gt;

&lt;p&gt;The two non-coding cases are what changed my mind about the scope of the claim.&lt;/p&gt;

&lt;p&gt;Case 4 and Case 5 use &lt;em&gt;the same six strategies&lt;/em&gt; as Cases 1–3. Same taxonomy, same file layout under .pi/, same shape of extension. The only new move is passing --system-prompt “$(cat …)” in the launcher instead of relying on the default. That’s it. Two unrelated domains — scraping and research — produced structurally identical harnesses.&lt;/p&gt;

&lt;p&gt;That symmetry is the finding. If the six strategies applied to coding but not to anything else, the hypothesis would be “pi is a good coding harness with room to customize,” which is a much smaller claim. What I actually saw is closer to: pi is a general terminal-agent harness with a coding profile as its default, and repointing it at a different domain costs one extra file (the replacement system prompt).&lt;/p&gt;

&lt;p&gt;The other thing worth naming: the same person can maintain all five cases. The extension files are 60 to 200 lines each. The skills are Markdown a domain expert can edit. There’s no shared framework code because pi &lt;em&gt;is&lt;/em&gt; the framework. The team doesn’t own a harness product; they own a small pile of configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the hypothesis is fragile
&lt;/h3&gt;

&lt;p&gt;I want to be careful here because I’ve seen “just use OSS X and extend it” arguments fall apart in production. A few honest caveats.&lt;/p&gt;

&lt;p&gt;The extension API is TypeScript. If your team doesn’t write TypeScript, every strategy above D costs more than I’m making it look. You’d still get A, B, and C for free, and that’s not nothing. The deploy runbook and the review rubric are Markdown, but the &lt;em&gt;enforcement layer needs code&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Extensions run with full system access. A malicious pi package can do &lt;em&gt;anything&lt;/em&gt; your shell can do. This is the same threat model as installing an npm dependency, and the same mitigation applies: read the code before installing anything from a stranger. Pi’s docs are direct about this. Teams with tight supply-chain controls need to account for it.&lt;/p&gt;

&lt;p&gt;Skills are &lt;em&gt;guidance&lt;/em&gt; to the model, not enforcement on the model. The scraping skill says “don’t spider off-domain unless the user asked.” A determined model can still do it if the tools allow it. If a constraint has to be true, put it in a tool gate (strategy D), not in a skill.&lt;/p&gt;

&lt;p&gt;And the whole argument rests on pi staying maintained. It’s an OSS project. The “expensive forever” costs of the build-it-yourself path come back if the project you’re depending on goes cold, you inherit the maintenance you were trying to dodge. That’s not a reason to avoid the middle path, but it’s a risk worthy of consideration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I’d do Monday morning
&lt;/h3&gt;

&lt;p&gt;If you’re on a team that wants a coding agent and can’t ship one because of a real constraint, here’s the smallest version of the experiment.&lt;/p&gt;

&lt;p&gt;Pick your single sharpest pain point. Not five. One. If it’s compliance, that’s a &lt;em&gt;tool-gate extension&lt;/em&gt;: sixty lines. If it’s a specific workflow you keep hand-holding the agent through, that’s a &lt;em&gt;skill&lt;/em&gt;: a Markdown file. If it’s “I need a locked-down mode for the intern,” that’s a &lt;em&gt;launcher script&lt;/em&gt; and a --tools flag.&lt;/p&gt;

&lt;p&gt;Write only that. Ship it to one person. See if it holds up for a week. If it does, add the next thing. If it doesn’t, delete it and you’re back to stock pi with no sunk cost.&lt;/p&gt;

&lt;p&gt;The reason to bother with the middle path isn’t that it’s technically clever. It’s that it lets you match the agent to the actual constraint without buying a product you don’t want to own. Every strategy in the taxonomy is reversible. You can start at A on a Tuesday and be back at stock pi by Friday if it wasn’t worth it. That property — cheap to try, cheap to unwind — is the whole point.&lt;/p&gt;

&lt;p&gt;The reason I care about this is that I keep meeting teams who’ve decided the choice is binary. It isn’t. The audit-and-guard extension in the test repo is 135 lines. The deploy workflow is 81. The scraping tools are 165. None of them are impressive engineering. That’s the point. The interesting work happens above them, in the skills and the workflows and the constraints that actually match the team. The taxonomy exists to keep the code layer small enough that the team can afford to focus on the layer that matters.&lt;/p&gt;

</description>
      <category>harnessengineering</category>
      <category>agenticengineering</category>
      <category>softwareengineering</category>
      <category>compliance</category>
    </item>
    <item>
      <title>The book I wrote because “harness” meant four different things</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:22:55 +0000</pubDate>
      <link>https://dev.to/tacoda/the-book-i-wrote-because-harness-meant-four-different-things-1jed</link>
      <guid>https://dev.to/tacoda/the-book-i-wrote-because-harness-meant-four-different-things-1jed</guid>
      <description>&lt;p&gt;You’ve probably felt this. You point the same agent at two of your repos with the same lazy prompt, and one gives you a tight change you can review over coffee while the other hands back a sprawling PR that quietly broke something three files away. Same model, same day. Different repo.&lt;/p&gt;

&lt;p&gt;That gap is the whole reason I wrote &lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;Harness Engineering: Building Reliable Systems Around Non-Deterministic Agents&lt;/a&gt;. It’s out, it’s long, and this is the pitch: what it covers, what it’ll teach you to do, and why it’s worth the shelf space.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem it’s about
&lt;/h3&gt;

&lt;p&gt;A coding agent is non-deterministic by design. It also writes faster than your team can socially enforce its standards. A human writes a hundred lines a day; an agent writes thousands, and review still moves at human speed. Most of what it writes lands before anyone reads it carefully. And it inherits whatever’s already in the repo and amplifies it; the agent doesn’t change your standards, it enforces them, so the lower the standards, the more enforcement you get.&lt;/p&gt;

&lt;p&gt;You can’t make the model deterministic. So make the work &lt;em&gt;around&lt;/em&gt; it deterministic. That’s the wager the whole book rests on. Reliability isn’t “the agent never varies” — it’s variance that stays inside a band you can &lt;strong&gt;read, debug, and ship&lt;/strong&gt;. You narrow that band three ways: narrow the &lt;strong&gt;input&lt;/strong&gt; (scope plus acceptance criteria that can actually fail), narrow the &lt;strong&gt;output&lt;/strong&gt; (gates that reject anything outside the band), and narrow the &lt;strong&gt;recovery&lt;/strong&gt; (decide the next move after a failure before the failure happens).&lt;/p&gt;

&lt;p&gt;Here’s the part that turns it from craft into a decision a business can sign off on: every bit of determinism you add moves the trust boundary. The question stops being “do I trust the model?” (you never fully will) and becomes “how much unreviewed agent output am I willing to ship, and behind which gates?” That’s not a vibe. That’s a number a team can own. The book’s job is to give you the levers to set it where you want it, on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shaping the charter
&lt;/h3&gt;

&lt;p&gt;The charter is everything you author to say how the agent should behave, and it’s the bulk of the book. This is where most teams have the most slack and the fastest wins.&lt;/p&gt;

&lt;p&gt;You’ll learn to write a lean CLAUDE.md — under two thousand words, because a bloated one is just token budget you’re setting on fire. You’ll tier your rules into iron laws, golden rules, and preferences so the agent knows what’s non-negotiable and what’s taste. You’ll build an ontology so one term means exactly one thing across the repo, because the fastest way to get inconsistent output is to let “job,” “task,” and “run” all mean the same thing in three files. And you’ll get honest about a trap I see constantly: aspirational rules teach the agent to lie. A CLAUDE.md that describes a codebase you wish you had is worse than none because it makes the agent confidently obey instructions the rest of the code contradicts.&lt;/p&gt;

&lt;p&gt;The book covers the Claude primitives directly — CLAUDE.md, rule files, skills, sub-agents, hooks, and MCP servers — and, more usefully, &lt;em&gt;when&lt;/em&gt; to reach for which. A skill and a sub-agent solve different problems; a hook and a gate live at different points in the loop. I keep the stock harness (what the tool actually gives you) separate from my own conventions (the rule tiers, the patterns I’ve found useful) so you can tell what’s load-bearing from what’s just my preference. Every tool you hand the agent is a capability grant: grant the smallest one that closes the gap. And a limit that only logs is not a limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the harness: loops, DAGs, RAG, and a coding agent from scratch
&lt;/h3&gt;

&lt;p&gt;When the stock agent can’t express your workflow, you build one. Part V is the engineering of that.&lt;/p&gt;

&lt;p&gt;The first fork is the shape of control flow: &lt;strong&gt;DAG or loop&lt;/strong&gt;. A DAG is a fixed graph of steps: predictable, testable, easy to reason about, and rigid. A loop lets the agent decide when it’s done: flexible, and dangerous if the exit condition is soft. The book teaches you to pick deliberately and to engineer the loop when you need one: real termination conditions, budgets, and the recovery move baked in, so “the agent decides when it’s done” doesn’t mean “the agent runs until your bill does.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG&lt;/strong&gt; gets treated as what it actually is — retrieval as a harness job, not magic. What you put in the context window, how you rank it, how you keep it from crowding out the instructions that matter. Feeding the agent more isn’t the goal; feeding it the &lt;em&gt;right&lt;/em&gt; thing at the right token cost is.&lt;/p&gt;

&lt;p&gt;Then the operational spine: sandboxing and permissions, prompt injection &lt;em&gt;inside&lt;/em&gt; a grant you’ve already given (the attack people forget), model selection and routing so you’re not paying frontier prices for a formatting pass, cost and resource caps, and evals so you can measure the harness instead of hoping. It ends with &lt;strong&gt;building a coding harness&lt;/strong&gt; end to end — there’s a from-scratch Go harness you can clone and read, because a harness you can’t read is one more thing you’re trusting blind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orchestrating multiple agents
&lt;/h3&gt;

&lt;p&gt;One agent is a harness problem. Many agents is orchestration, and it’s the last third of the book.&lt;/p&gt;

&lt;p&gt;This is where “vibe coding” breaks at team scale. The fix is &lt;strong&gt;intent contracts&lt;/strong&gt;  — a scoped statement of what a unit of work must do, with acceptance criteria that can fail. This can be a full spec, or one much smaller in scope. An acceptance criterion that cannot fail isn’t a criterion, it’s a wish. From there: the six phases a piece of work moves through and the &lt;strong&gt;interrupt protocol&lt;/strong&gt;  — when an agent hits ambiguity it doesn’t guess, it files an interrupt, the team-level version of raising an exception. You’ll cover graph engineering, a reconciler daemon that keeps the whole thing converging, and the two ends of the spectrum: &lt;strong&gt;dark factories&lt;/strong&gt; (fully automated, running unattended) and &lt;strong&gt;lights-on factories&lt;/strong&gt; (automated but observable, with an audit trail you can read after the fact and proactive approval processes). The rule that keeps a factory honest: one that can edit its own gates has no gates.&lt;/p&gt;

&lt;p&gt;It’s grounded in real tools, so it’s not architecture astronaut stuff. It’s patterns that run.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you walk away able to do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit a repo in 20 minutes&lt;/strong&gt; with a 17-item, 34-point instrument and get a gap score &lt;em&gt;before&lt;/em&gt; your next agent session — then close the cheapest gap first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author a charter your team actually follows&lt;/strong&gt; , from CLAUDE.md to rule tiers to ontology to bounded tool grants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a harness in code&lt;/strong&gt;  — loop or DAG, retrieval, sandboxing, routing, cost caps, evals — when you outgrow the stock agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stand up orchestration&lt;/strong&gt; for many agents without it becoming a factory you can’t see into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep all of it healthy over time&lt;/strong&gt; with flywheels, post-mortems, charter debt tracking, versioning, and evals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every chapter ends with one copyable artifact: a checklist, a hook script, a worksheet, a code module. There’s a 30-day starter plan in the appendices. The template is what survives the read.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest part
&lt;/h3&gt;

&lt;p&gt;The patterns aren’t mine. Tests, types, naming, bounded contexts, characterization tests, continuous delivery — they predate the agent and belong to Brooks, Parnas, Fowler, Evans, Beck, Feathers. What’s mine is the assembly: two named disciplines, a shared vocabulary that ends the “harness means four things” argument, a maintenance pattern, and a working tool.&lt;/p&gt;

&lt;p&gt;And the bet is built to last. If your reliability strategy depends on a specific model’s behavior, that strategy has an eight-month half-life. This one bets on the repo, not the model — so it doesn’t expire the next time a new model ships.&lt;/p&gt;

&lt;p&gt;It’s big: 62 chapters and a total of 866 pages. Not a weekend read. But it’s built to be used. The book is grounded in an engineering discipline: &lt;em&gt;no vibes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The curator doesn’t stop writing code — the curator stops writing code the agent can write. If that reframe lands for you, this is the long version.&lt;/p&gt;

&lt;p&gt;Read it on LeanPub: &lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;Harness Engineering: Building Reliable Systems Around Non-Deterministic Agents&lt;/a&gt;&lt;/p&gt;

</description>
      <category>harnessengineering</category>
      <category>agenticai</category>
      <category>softwareengineering</category>
      <category>books</category>
    </item>
    <item>
      <title>The Governance Gap Is a Boundary Problem</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:24:45 +0000</pubDate>
      <link>https://dev.to/tacoda/the-governance-gap-is-a-boundary-problem-gb</link>
      <guid>https://dev.to/tacoda/the-governance-gap-is-a-boundary-problem-gb</guid>
      <description>&lt;p&gt;One of my early factory prototypes had a clean audit trail. Every action an agent took, recorded. Who did it, what it touched, and when it ran. I was proud of it. Then I watched an agent remove a status enum from a database column. Not a value I wanted changed. It was a status that other records depended on. The audit trail caught it. Logged the delete, the actor, the timestamp. Caught it perfectly, and caught it after the status was already gone.&lt;/p&gt;

&lt;p&gt;That’s the moment the “governance gap” stopped being a phrase I’d read in other people’s posts and became something I’d built by accident. I had governance you could read. I didn’t have governance that could say no.&lt;/p&gt;

&lt;p&gt;This is what I learned about that gap by building a tool to close it. The tool is &lt;a href="https://www.tacoda.dev/open-refinery/" rel="noopener noreferrer"&gt;open-refinery&lt;/a&gt;, a control plane that governs how AI-driven work reaches repos, models, and tools. The lesson is simpler than the tool. The gap everyone’s naming isn’t a missing policy. The gap is that nothing makes an agent respect it. &lt;em&gt;And a policy nothing enforces is a suggestion.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The gap is between the policy and the work
&lt;/h3&gt;

&lt;p&gt;People use “governance gap” to name the space between the policy you wrote and the agent doing the work. You have a policy. The agent runs. The policy and the run never meet, so nothing makes the agent respect it.&lt;/p&gt;

&lt;p&gt;The common fix is to add something. A policy document, an approval step, or a content filter. Each of these is a thing you write down. &lt;em&gt;None&lt;/em&gt; of them is a thing that stops an action.&lt;/p&gt;

&lt;p&gt;Here’s the distinction that took me three prototypes to see. An audit log observes: it tells you what happened. Governance has to &lt;strong&gt;gate&lt;/strong&gt;. It must stop the thing &lt;em&gt;before&lt;/em&gt; it happens. My prototype with the clean audit trail was all observation. When the agent deleted that status, nothing stood between the intent and the delete. The log wrote the event down a moment too late to matter.&lt;/p&gt;

&lt;p&gt;Once you see governance as gating instead of recording, the question changes. It’s no longer “what policy should we write.” It’s “where does the policy get enforced before the action does.” That question has a location in it. Most governance talk doesn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  A gate has to live somewhere
&lt;/h3&gt;

&lt;p&gt;A gate is a control that runs before an action and can refuse it. To run before the action, it has to sit between the thing that wants to act and the thing being acted on. It needs a seam. In most agent setups, nobody drew the seam.&lt;/p&gt;

&lt;p&gt;So the first real decision in open-refinery was to split two things that usually run together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The harness does the work&lt;/strong&gt;. It’s the agents, the prompts, the memory, the tools, the orchestration. It lives in your application. It decides what to try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The platform governs how work reaches a target&lt;/strong&gt;  — the repos, the models, the tools the harness wants to touch. The platform does none of the work. open-refinery executes nothing. It sits on the seam and decides whether an action reaches its target.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehqbt46ih04f3ghgpi7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehqbt46ih04f3ghgpi7w.png" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The harness never touches the target directly. Every action passes a gate that can say no, and the platform owns that gate.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Draw that line and the gate has a home. The harness asks to do something. The platform checks the policy. The action reaches the target, or it doesn’t. Skip the line and your policy floats free of the work. That was the prototype I started with: the policy lived in one place, the actions happened in another, and nothing connected them.&lt;/p&gt;

&lt;p&gt;None of enforce-before-execute is new. RBAC, IAM, and Kubernetes admission controllers are all examples. What’s new is who’s on the other side of the gate. A service either has permission or it doesn’t. An agent reads your policy in prose and decides how to apply it. The boundary is what lets you stop trusting that interpretation: the policy gets enforced on the seam, not left to the agent to honor.&lt;/p&gt;

&lt;p&gt;The boundary is the whole idea. Everything else here is a consequence of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the loop boring on purpose
&lt;/h3&gt;

&lt;p&gt;I wanted the platform to run without a human watching. Lights-out. I also wanted every action fully auditable. Those two goals fight each other the moment the system makes a choice you can’t explain.&lt;/p&gt;

&lt;p&gt;You can’t audit “the model decided to.” You can audit a function that takes recorded inputs and returns a recorded output. So the core loops in open-refinery are plain code, not agent choices. The policy is encoded there, deterministically, where the agent can’t reinterpret it away. There are two loops: transition, which moves a work item from one stage to the next, and produce, which runs an action against a target. The agent fills in the inputs. The code decides what happens.&lt;/p&gt;

&lt;p&gt;This audits the mechanism, not the judgment. Which input the agent picks is still the agent’s call. Determinism doesn’t make the decision safe. It makes the decision inspectable, and it puts a gate between the decision and its effect.&lt;/p&gt;

&lt;p&gt;Every action runs the same sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorize -&amp;gt; enforce policy -&amp;gt; execute -&amp;gt; record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is important. Authorize before the run, or you can’t prevent it. Record after the run succeeds, or a crash mid-action loses the event.&lt;/p&gt;

&lt;p&gt;The check itself is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;enforce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;denied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PolicyDenied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A policy names who may do what to which resource. In strict mode, nothing’s allowed unless a policy allows it. In audit mode, everything’s allowed unless a policy denies it. Strict mode &lt;em&gt;fails closed&lt;/em&gt;: if nothing explicitly allows an action, it’s denied. Audit mode &lt;em&gt;fails open&lt;/em&gt;: an unknown action runs. Which one you pick is the whole difference between a gate and a log. Audit mode is where you learn what your agents do. Strict mode is where you decide what they may do. That enum from the top of the post? A denied action under a strict policy on the status resource. One line, and it never happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human oversight is a dial, not a switch
&lt;/h3&gt;

&lt;p&gt;One of the things I got wrong early was treating human-in-the-loop as on or off. Either a person approved every action, which didn’t scale, or nobody did, which was the prototype that deleted the status. Both are bad. The useful setting sits between them, and it should be a setting.&lt;/p&gt;

&lt;p&gt;open-refinery has five oversight levels, set per process. At L0, manual, every action needs a human to approve it. L1 and L2 loosen the grip: assisted, then supervised, where only the gated steps wait for a person. L3 is autonomous, no approval. L4 is dark, fully lights-out. You pick the level for a process based on how much you trust it — not for the whole system at once.&lt;/p&gt;

&lt;p&gt;What stays the same across all five is the machinery. The gates run the same way at every level, and the records share one schema. What changes is how many approval events the trail carries, and how many actions wait for a person before they execute. Because the loop is deterministic, moving the dial doesn’t change what gets checked or how it’s recorded; only who has to say yes, and how often.&lt;/p&gt;

&lt;p&gt;The boundary shows up again here. The dial sits on the platform, not in the harness. You can turn a process from supervised to dark without touching a line of agent code, because the approval step lives on the seam.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to tell whether a policy is real
&lt;/h3&gt;

&lt;p&gt;Once the platform could gate actions, I needed to know which policies were actually doing anything. A policy you wrote and a policy an agent respects are not the same policy.&lt;/p&gt;

&lt;p&gt;open-refinery models this with claims. A governance claim is a statement about a repo: agents may not delete production data, all migrations get reviewed. Each claim can be backed two ways. An instruction states the intent. A gate enforces it. A claim is covered &lt;em&gt;only&lt;/em&gt; when it has both.&lt;/p&gt;

&lt;p&gt;That gives three states, and the middle one is the whole problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction and gate:&lt;/strong&gt; covered. The policy is written, and something stops violations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction, no gate:&lt;/strong&gt; partial. You wrote the policy. Nothing enforces it. This is the governance gap, made countable. It was the most common state I found. It’s the one that feels safe and isn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither instruction nor gate:&lt;/strong&gt; an imitation surface. It reads as governed. It isn’t.&lt;/p&gt;

&lt;p&gt;open-refinery scores a repo on this. How many claims have a gate, and not just an instruction? I won’t pretend I have clean fleet-wide numbers to wave at you. What the model buys me is the ability to ask the question per repo instead of assuming the answer. Partial coverage isn’t a failure of intent. It’s a gap you can name, which means you can close it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A record you can’t quietly rewrite
&lt;/h3&gt;

&lt;p&gt;Gating decides what happens. Provenance decides whether you can trust the record of what happened. These are different jobs, and I discovered that after ran them together at first.&lt;/p&gt;

&lt;p&gt;An audit log you can edit isn’t evidence. If someone can delete the record of the delete, the record is worth nothing. So the records in open-refinery are immutable, and each one chains to the one before it with a hash. Change a past event and every hash after it breaks. One function walks the chain and checks it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# raises if any link's hash doesn't match
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn’t stop someone from doing something bad. It stops them from hiding that they did. I want to be honest about which one this is: &lt;em&gt;tamper-evidence&lt;/em&gt;, not tamper-proofing. It only holds if two things are true: someone actually checks the chain, and its head is anchored somewhere the writer can’t reach. Put the log and the verifier on the same box an attacker owns, and they can rewrite an event and recompute every hash after it. A local chain raises the cost of quiet tampering. It doesn’t make it impossible.&lt;/p&gt;

&lt;p&gt;The gate isn’t one thing in one place, either. It shows up at three seams. The transition gate checks a work item moving from one stage to the next. The invoke gate checks an executor about to run a command. And there’s a call the harness makes before it acts at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /authorize { role, action, resource, intent: "deploy to prod" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness asks before it deploys. It gets back an allow, or a PolicyDenied. The policy does nothing until it sits on one of these seams. A policy with no seam is the partial-coverage case from the last section, seen from the code side.&lt;/p&gt;

&lt;h3&gt;
  
  
  The policy itself needs governing
&lt;/h3&gt;

&lt;p&gt;This is the part the governance-gap conversation skips. If governance lives on the boundary, then changing the boundary is itself an action. So who governs that?&lt;/p&gt;

&lt;p&gt;In my early prototypes, the policy lived in a config file. Anyone who could edit the file could change it, and there was no record of who changed what, or why. I’d built a system that gated every agent action and left the policy itself wide open. That’s not a small hole. That’s the same gap, just moved up one level.&lt;/p&gt;

&lt;p&gt;In open-refinery, a policy change is a governed action like any other. It walks an approval chain. It lands in the same audit trail. Every version of a policy records its author, its timestamp, and what changed. Ask “who changed this policy, and when, and why,” and there’s an answer.&lt;/p&gt;

&lt;p&gt;A policy file anyone can edit with no record isn’t governance. It’s a suggestion with a version history, at best. Governance that can’t govern itself doesn’t hold. This is the second-order problem, and it’s where the boundary earns its keep: moving the seam is an action, so it passes a gate too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put one gate on one policy this week
&lt;/h3&gt;

&lt;p&gt;You don’t need a platform to start closing your own governance gap. You need to stop treating your audit log as a control.&lt;/p&gt;

&lt;p&gt;Pick one policy you’ve written for your agents. A real one: in a doc, a prompt, or a README. Find where it’s enforced. If the honest answer is “it isn’t,” you’ve found a partial-coverage surface on your own system. That’s the gap, and now you can see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add one gate:&lt;/strong&gt; a check that runs before the action and can refuse it. Start with the action you’d least want an agent to take without you watching. For me that was a destructive database change. Pick yours.&lt;/p&gt;

&lt;p&gt;Make the policy change leave a trace. Even a commit with a reason in the message is a start on second-order governance. The goal is that “who changed this policy” always has an answer.&lt;/p&gt;

&lt;p&gt;Do those three things and you’ve moved from governance you can read to governance that can say no. That’s the whole gap, and it closes one gate at a time.&lt;/p&gt;

</description>
      <category>aigovernance</category>
      <category>agenticai</category>
      <category>governance</category>
      <category>darkfactories</category>
    </item>
    <item>
      <title>Charter and Harness: What Each One Is</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Tue, 30 Jun 2026 23:47:29 +0000</pubDate>
      <link>https://dev.to/tacoda/charter-and-harness-what-each-one-is-3l60</link>
      <guid>https://dev.to/tacoda/charter-and-harness-what-each-one-is-3l60</guid>
      <description>&lt;p&gt;It seems that every time I &lt;em&gt;think&lt;/em&gt; I know the shape of the harness, a new fact presents itself that I have to account for. I recently wrote a book &lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;Harness Engineering&lt;/a&gt;. But new information has caused me to rethink my model a bit. I plan to leave the term &lt;em&gt;harness&lt;/em&gt; alone, and tighten up the definition. In addition, I add the term &lt;em&gt;charter&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What follows is a chapter I’m working on for the book to bridge the gap in my model. I’m offering it completely free because I think it’s the best definition I’ve had so far. Your feedback is very welcome.&lt;/p&gt;

&lt;p&gt;Clone a repository that someone has tuned for agentic coding and open the .claude folder at its root. What you find is mostly prose. A CLAUDE.md that explains the project. Rule files with names like testing.md and migrations.md. A few skills, each a short markdown file describing a workflow. Maybe a settings.json wiring up a hook. Almost none of it is code that runs on its own. It is a stack of instructions, written for a reader who happens to be a model.&lt;/p&gt;

&lt;p&gt;Now start the agent: Claude Code, or whatever the team standardized on. It reads that folder, holds it in mind, and goes to work on your repo. The folder did nothing. The agent did. There, in a single clone, sit the two things this book is about: the material you author, and the engine that acts on it. This chapter names both and says what each is made of. The next chapter draws the line between them. Here the job is to fill the two boxes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The charter: what you author
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;charter&lt;/strong&gt; is everything you author to specify how the agent should behave in your project. That .claude folder is most of it. The word is borrowed from its plainest sense: a charter is a founding document that says how a thing is to be governed. Your repo’s charter governs the agent that enters it. You write it, you review it in pull requests, you amend it when it turns out wrong, and it governs every agent that works the repo until you change it.&lt;/p&gt;

&lt;p&gt;A charter comes in four kinds of material, and naming them now saves arguments later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guides&lt;/strong&gt; instruct before the agent acts: the CLAUDE.md, the rule files, the slash commands, the prose half of a skill. A guide is a sentence the agent reads and weighs against the situation, like &lt;em&gt;prefer composition over inheritance, and justify a base class in the PR description&lt;/em&gt;. No graph expresses that. It is judgment, written down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool definitions&lt;/strong&gt; declare what the agent can reach for and how it is wired: the mcp.json entry, a tool’s schema, a skill’s script, or a plugin’s config. One artifact can span families: a skill’s prose is a guide, but its script a tool definition. The definition is yours and lives in the repo. The thing it points at, the installed server or the plugin binary, is external, and the next chapter sorts that seam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensors&lt;/strong&gt; tell the agent whether the work it just did is acceptable: the test files, the lint config, the type-checker settings. You authored them, so like a hook they are charter even though they run. A sensor reports; it does not refuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gates&lt;/strong&gt; refuse: the pre-commit hook, the branch-protection rule, the blocking reviewer. A gate is a sensor wired to a stop.&lt;/p&gt;

&lt;p&gt;Guides, tool definitions, sensors, gates. If something in your charter is none of those four, it is probably not earning its place.&lt;/p&gt;

&lt;p&gt;Most of a charter is prose, and that is the point. A rule written as a sentence covers cases no control-flow graph would, because the agent weighs it instead of branching on it. Some of a charter is code you author, a hook or a gate, and that code is still charter: you wrote it to state a standard, so it is a specification the engine runs, never the engine itself. A charter is also a living thing. It is versioned in the repo, reviewed like any change, and amended as the project teaches you where it was wrong. The rule you write today because an agent ignored a convention is a line in a document you will keep editing for as long as the repo lives.&lt;/p&gt;

&lt;p&gt;Here is the whole of it, as a definition you can hold: the charter is your repo’s .claude folder, all of it (the rules, the hooks, the agents, the skills, the slash commands, the settings), plus the dependencies you declare: plugins, policy packs, shared skills, tools such as MCP servers. Those are two questions about a thing, not one. The families above sort by &lt;em&gt;role&lt;/em&gt;: is it a guide, a tool definition, a sensor, a gate? &lt;em&gt;Origin&lt;/em&gt; is the second question: did you write it by hand in the folder, or adopt it as a declared dependency? A declared dependency keeps its declaration in your repo, which is charter, while its installed body lives outside, which is not. Declaration in, source out. Either way the test does not change: you authored or adopted it to specify behavior.&lt;/p&gt;

&lt;p&gt;If you have been in this field a while, you may have called this folder “the harness.” It is the charter. The word slipped for an understandable reason, and the next chapter traces it; for now, hold the line that the folder you author is the charter, and the engine that reads it is the harness.&lt;/p&gt;

&lt;h3&gt;
  
  
  The harness: the engine that applies it
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;harness&lt;/strong&gt; is the engine that applies the charter against the model. The field has a shorthand worth borrowing: an agent is a model plus a harness. The model predicts text and nothing more. The harness is everything wrapped around it to turn prediction into work, and the charter is what the harness consults while it does. So the relationship runs in a line. You author a charter; the harness applies it to drive the model; the model produces a result, a diff or a document or an answer. Charter, harness, model, result. The charter is the input you control. The harness is the only stage that acts. The next chapter takes that line apart; here it is enough to see the shape.&lt;/p&gt;

&lt;p&gt;An engine that does this has jobs, and Part B of this book takes them one at a time. It assembles the context the model sees each turn, choosing what to load and what to drop, and invokes the tools your charter defined. The permissions you set decide which files it may touch and which commands it may run. The gates fire, the sensors report, and the loop turns with bounds and a kill switch until the work is done or the budget is spent. Every one of those is the engine at work, not a standard you wrote down. The standard is the charter; the doing is the harness.&lt;/p&gt;

&lt;p&gt;You get most of a harness rather than build it. Claude Code is one, Cursor is another, and a LangGraph orchestration your platform team stood up is a third. The vendor ships the engine, you point it at your repo, and it reads your charter on the way in. For most teams that is the entire relationship with the harness: choose one, configure it, move on. The teams that build their own, a custom orchestrator or a queue that runs agents across a fleet, are doing harness engineering proper, and Part B of this book is written for them. Part A is for everyone, because everyone writes a charter.&lt;/p&gt;

&lt;p&gt;A harness can even ship its own charter-like pieces: Claude Code’s built-in slash commands, a default skill, baked-in rules. Those are harness, because the vendor authored them, not you, and they do not sit in your .claude folder. Your charter can override them. Drop a fix-bug skill into your folder and the agent runs your bug workflow instead of the vendor’s default; write a rule that bends a built-in command to your process, and your version wins. The built-in is harness, your override is charter, and the agent meets the charter.&lt;/p&gt;

&lt;p&gt;One part of the charter couples to the harness more tightly than the rest: &lt;em&gt;workflow&lt;/em&gt;. A rule or a hook is a primitive the harness simply reads, but how work gets defined, prioritized, and completed is a process the harness has to register and run, not merely read. That is one reason a team builds its own harness, when the workflow it needs is one a stock harness cannot express. Part A’s chapter on contracts is where you define the workflow; Part B’s on orchestration is where the harness runs it.&lt;/p&gt;

&lt;p&gt;Two words, two boxes: the charter you author, the harness that applies it. They are not the same kind of thing, and it is a mistake to treat them as one. Where exactly the line falls, why a hook you wrote is charter even though it runs, why the orchestrator is harness even though you might own it, is the next chapter. For now, hold the two boxes apart and know which one takes most of your time. For most of this book, it is the charter.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s in each box
&lt;/h3&gt;

&lt;p&gt;Run this against your own repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your charter&lt;/strong&gt;  — everything you authored or adopted to specify behavior. Most of it is the whole .claude folder; the rest is declared dependencies. By role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;guides: CLAUDE.md, rules, skills, slash commands (what instructs)&lt;/li&gt;
&lt;li&gt;tool and agent definitions: mcp.json, tool schemas, sub-agent and plugin config (what the agent reaches for)&lt;/li&gt;
&lt;li&gt;sensors: test files, lint and type-check config (what reports)&lt;/li&gt;
&lt;li&gt;gates: pre-commit hooks, branch protection, required reviews (what refuses)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By origin: written by hand in the folder, or a declared dependency (plugin, policy pack, shared skill, tool such as an MCP server) whose declaration is charter and whose installed body is external.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your harness&lt;/strong&gt;  — the engine you run, mostly not yours to write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the coding agent (Claude Code, Cursor, Aider)&lt;/li&gt;
&lt;li&gt;any framework or orchestrator above it (LangGraph, a custom runner)&lt;/li&gt;
&lt;li&gt;the loop that runs work on repeat&lt;/li&gt;
&lt;li&gt;the runtime that fires your hooks, invokes your tools, and enforces your permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A thing on the first list, you maintain, and it is where your reliability comes from. A thing on the second, you mostly choose. Telling which list something belongs to is the whole skill the next chapter sharpens into a single test.&lt;/p&gt;

&lt;p&gt;The charter is the half of the work with your name on it. The rest of this book is about writing it well.&lt;/p&gt;

&lt;h4&gt;
  
  
  Additional Resources
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnficjpsgjfgjvdz2rooz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnficjpsgjfgjvdz2rooz.png" width="569" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj86ipzaittyvg8tr5j67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj86ipzaittyvg8tr5j67.png" width="626" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4epobixnqb96v9xna5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4epobixnqb96v9xna5i.png" width="799" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pgztu7tr2ntu7p8ghzg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pgztu7tr2ntu7p8ghzg.png" width="586" height="702"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>harnessengineering</category>
      <category>charterengineering</category>
      <category>agenticai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Harness Engineering: Building Reliable Workflows Around Non-Deterministic Agents</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Fri, 26 Jun 2026 21:27:58 +0000</pubDate>
      <link>https://dev.to/tacoda/harness-engineering-building-reliable-workflows-around-non-deterministic-agents-4gh0</link>
      <guid>https://dev.to/tacoda/harness-engineering-building-reliable-workflows-around-non-deterministic-agents-4gh0</guid>
      <description>&lt;p&gt;Hello everyone. I just released a book about harness engineering. It’s on LeanPub. Pay what you want, starts at $10.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;https://leanpub.com/harness-engineering&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it cover?
&lt;/h3&gt;

&lt;p&gt;The discipline, the taxonomy, the behavioral system, the reliability layers, rules, guides, corpus, playbooks, rule cascades, architecture as communication, tools, skills, agents, plugins, MCP, evals, delivery orchestration, continuous delivery with agents, PR as checkpoints, policy libraries for team and org extension, patterns and anti-patterns, harness debt, post-mortem, harness curation, loop engineering. Everything I have learned over the past six months with hands-on experiments with a real production codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why so cheap?
&lt;/h3&gt;

&lt;p&gt;We have a problem right now with junior and mid level engineers feeling the pressure of AI. I think we have an obligation to help them — and I think that means providing a path to senior. How could they get to senior if the agent does everything for them?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We must give juniors the support they need to become seniors.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the goals with this book is to support these folks in their growth by showing the big picture, and then anchoring it with cited works of software engineering canon. This book gives both and has an appendix specifically for juniors with how to operate and grow in the world of agentic coding.&lt;/p&gt;

&lt;p&gt;If you are concerned about losing your job or not being able to find a job doing software engineering because of LLMs, then this book is for you!&lt;/p&gt;

&lt;h3&gt;
  
  
  There’s more
&lt;/h3&gt;

&lt;p&gt;The book also covers scopes from single contributor to enterprise and provides actionable advice whether you read a particular part or the whole thing. It offers benefits to anyone looking to learn how to get better results out of their coding agents, regardless of experience, role, project, team, company, or organization.&lt;/p&gt;

&lt;p&gt;I hope it serves you well.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>harnessengineering</category>
      <category>books</category>
      <category>agenticworkflow</category>
    </item>
    <item>
      <title>This just rearranged my view on harness engineering</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Thu, 25 Jun 2026 23:16:14 +0000</pubDate>
      <link>https://dev.to/tacoda/this-just-rearranged-my-view-on-harness-engineering-5ggh</link>
      <guid>https://dev.to/tacoda/this-just-rearranged-my-view-on-harness-engineering-5ggh</guid>
      <description>&lt;p&gt;I just read this article — I recommend you do as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/pulse/loop-engineering-just-software-we-have-name-mike-piccolo-yb73c/" rel="noopener noreferrer"&gt;Loop Engineering Is Just Software Engineering. We Have a Name for That.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve been doing a lot of work on harnesses, but I have been focusing at the project level. I’ve only recently started to delve into LangChain/LangGraph. I built my own daemon that implements this with a shell on my local machine. It works, but it could be better. I had always thought of these as two layers and they’ve become crystal clear to me now.&lt;/p&gt;

&lt;p&gt;When I read this, I immediately want to try it, but I do not have access to an API key yet. Also, I don’t really want to clobber my organization with API pricing. So I asked claude to build a prototype in Python given the description. I tested it, and it worked pretty well.&lt;/p&gt;

&lt;p&gt;But if you’re already at the API point (and you probably are if you are looping), here’s the tool: &lt;a href="https://iii.dev/" rel="noopener noreferrer"&gt;https://iii.dev/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  This idea is important
&lt;/h3&gt;

&lt;p&gt;The single most important takeaway from this article is that we’ve already solve the problems of loop engineering with distributed systems. Instead of getting stuck in loops or overpaying, you can take advantage of the queueing pattern — dead letter queues, circuit breakers, retries, and so on.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This idea is dynamite.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It had slipped past me because I was focused on the project level of the harness and loop engineering has been mainly focusing on creating loops with agents, not &lt;em&gt;how&lt;/em&gt; to do it. So diagrams and explanations tend to lead to synchronous or daemon sorts of solutions. In hindsight, it seems so obvious.&lt;/p&gt;

&lt;p&gt;It also removes the orchestrator problem: the fact that different subagents about bound to a main orchestrator thread replicates Brooks’ warning for organizations in the form of agents. But with the distributed system, there is no orchestrator — that’s been reduced to state. Brilliant!&lt;/p&gt;

&lt;h3&gt;
  
  
  Vocabulary
&lt;/h3&gt;

&lt;p&gt;It also shifts my vocabulary. I’m currently writing a book about harness engineering: &lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;https://leanpub.com/harness-engineering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was focusing on the repository level, but now the delivery harness (as I was calling it) builds one level up. This will probably cause me to have to re-arrange my definitions, but here’s where my head is at.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layers of the Harness
&lt;/h3&gt;

&lt;p&gt;I visualize the harness like an onion. But the harness I’ve been talking about has been &lt;em&gt;code&lt;/em&gt;, and the delivery harness is a &lt;em&gt;service&lt;/em&gt;. That means that personal still sits at the center, wrapped by the project (or repo) harness, and then wrapped with team and org harnesses that implement policies just like software libraries, and then wrapped in the delivery or orchestration harness. I’ve used some tools like this recently, like Hivemind (&lt;a href="https://cohen-liel.github.io/hivemind/" rel="noopener noreferrer"&gt;https://cohen-liel.github.io/hivemind/&lt;/a&gt;). It’s great, and there are plenty of others to try, but this has changed my entire outlook on the type of architecture the delivery harness should use. I think he’s right — it’s a distributed system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifld59p282yd9gttkl7j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifld59p282yd9gttkl7j.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Thank you Mike Piccolo!
&lt;/h3&gt;

</description>
      <category>agenticworkflow</category>
      <category>agenticai</category>
      <category>distributedsystems</category>
      <category>harnessengineering</category>
    </item>
    <item>
      <title>Reading the Agent Log Like a Detective</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Wed, 24 Jun 2026 16:21:53 +0000</pubDate>
      <link>https://dev.to/tacoda/reading-the-agent-log-like-a-detective-dgb</link>
      <guid>https://dev.to/tacoda/reading-the-agent-log-like-a-detective-dgb</guid>
      <description>&lt;p&gt;The agent shipped a broken migration last month. The PR was clean, the tests passed locally, the reviewer signed off in twelve minutes, and the deploy to staging green-lit on the first try. Production started erroring within four minutes of the merge because the migration assumed a column existed that had been renamed two weeks earlier.&lt;/p&gt;

&lt;p&gt;The first instinct was to blame the model. &lt;em&gt;Bad agent, didn’t read the docs.&lt;/em&gt; The second instinct was to add a rule. &lt;em&gt;Always check the current schema before writing migrations.&lt;/em&gt; Both instincts felt productive. Neither would have fixed it.&lt;/p&gt;

&lt;p&gt;What fixed it was reading the transcript. The agent had, in fact, looked at the schema file. The schema file in the repo was correct. It had been updated when the column was renamed. But the agent had also loaded a database/CLAUDE.md rule that described the column under its &lt;em&gt;old&lt;/em&gt; name as the canonical reference. The rule was stale. The agent followed the rule, but the rule was wrong.&lt;/p&gt;

&lt;p&gt;You could call this a model problem — the agent trusted a stale rule over the live schema file sitting right next to it. &lt;strong&gt;But that’s not the lever you can pull.&lt;/strong&gt; The fixable cause was in the harness (the rules, skills, hooks, and tools wrapped around the model) and here it was a rule that &lt;em&gt;lied&lt;/em&gt;. It was visible in the log if you read it forensically.&lt;/p&gt;

&lt;p&gt;That’s what this post is about. Reading agent transcripts as evidence, not as narrative. The five failure modes that show up over and over. And the habit of treating every wrong commit as a case to investigate before reaching for a fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replay, not blame
&lt;/h3&gt;

&lt;p&gt;The first move when an agent ships bad code is to resist the urge to fix and move on. The fix is downstream. The question worth asking is &lt;em&gt;how did the agent decide to do that&lt;/em&gt;. The answer lives in the transcript.&lt;/p&gt;

&lt;p&gt;Most modern agent harnesses save full transcripts somewhere. Claude Code stores them in ~/.claude/projects//, Cursor keeps recent ones in its UI, and most CI-driven agent runs log to a build artifact. The transcript is your scene of the incident. It records what the agent loaded, what it read, what it ran, what it produced, and in what order. Everything you need to reconstruct its reasoning is in that file.&lt;/p&gt;

&lt;p&gt;The discipline is to treat the transcript the same way an SRE treats a stack trace: not as a list of complaints, but as the most reliable evidence of what happened. The agent’s text answers in the transcript are sometimes hand-wavy. The tool calls are not. The tool calls are facts.&lt;/p&gt;

&lt;p&gt;Read the tool calls first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five failure modes
&lt;/h3&gt;

&lt;p&gt;Across the agent transcripts I’ve reviewed, the same five failure modes keep producing wrong-code incidents. Each one has a cause in the model but each also has a cause in the harness, and the harness is the half you can change. All five are visible in the log.&lt;/p&gt;

&lt;h4&gt;
  
  
  Missing context
&lt;/h4&gt;

&lt;p&gt;The agent never read the file that mattered. It wrote a migration without checking the schema. It wrote a test without checking the test conventions. It added a new module without checking how other modules in the same area were structured.&lt;/p&gt;

&lt;p&gt;How to spot it in the log: scroll back to the start of the task and look at the Read and Glob tool calls. Note which files the agent loaded. Compare to the files the change touched. The pattern is: &lt;em&gt;the agent edited file X, but never read file Y, where Y was the file that defined the convention X had to match&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fix isn’t in the model. The fix is to make sure the relevant context loads automatically. Either by giving the rule a sharper path scope (so it loads when the agent touches the area), by adding a skill that walks the agent through the right files in order, or by adding a hook that injects the relevant context at the start of tasks of that shape.&lt;/p&gt;

&lt;p&gt;The agent did what it was told to do. The instruction set didn’t include &lt;em&gt;read Y first&lt;/em&gt;. That’s a harness gap.&lt;/p&gt;

&lt;h4&gt;
  
  
  Contradicted rules
&lt;/h4&gt;

&lt;p&gt;The agent followed a rule that was wrong. The rule was outdated, or it referenced a pattern the codebase no longer uses, or two rules in the harness said different things and the agent picked the one that produced the bug.&lt;/p&gt;

&lt;p&gt;How to spot it in the log: look at the rules the agent cited in its reasoning. If the rule cited matches the code change exactly, but the change is still wrong, the rule is the bug. Cross-check the rule’s claims against the current code. &lt;em&gt;Rule says use UserRepository, code uses UserService&lt;/em&gt; is a smoking gun.&lt;/p&gt;

&lt;p&gt;The fix is the rule-rot audit. Rules drift faster than people think. A rule written six months ago can be confidently citing a class that was renamed in April. The agent trusts the harness; if the harness lies, the agent ships lies.&lt;/p&gt;

&lt;p&gt;The contradicted-rules failure mode is the one I find most embarrassing because the agent did exactly what the harness said to do. The agent isn’t wrong. The harness is wrong. And the harness was wrong because I didn’t audit it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hallucinated APIs
&lt;/h4&gt;

&lt;p&gt;The agent called a function that doesn’t exist. Used an option that isn’t supported. Imported from a module that doesn’t export what it imported.&lt;/p&gt;

&lt;p&gt;How to spot it in the log: search the agent’s output for function names and option names, then grep the actual codebase or library for them. The mismatches are obvious. The agent might have hallucinated findOrFailWithLock when the real method is findOrFail and there’s no lock option at all.&lt;/p&gt;

&lt;p&gt;The fix has two parts. Short-term, tighten the harness with documentation links or type signatures pointing at the real reference is the cheapest cure. Longer-term, give the agent tools instead of documentation. An MCP server that exposes the library’s actual API (so the agent can &lt;em&gt;call&lt;/em&gt; it instead of guessing) ends the hallucination class. A type checker run as a sensor (an automated check wired into the harness that fails the run when something’s wrong) catches the hallucinations that slip through.&lt;/p&gt;

&lt;p&gt;The recurrence pattern worth noting: hallucinations cluster around APIs the model is confidently familiar with from training but where the project has chosen to use the library differently than the common convention. The model isn’t making up the function from nothing; it’s substituting the public-API version when the project actually wraps it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scope creep
&lt;/h4&gt;

&lt;p&gt;The agent changed more than it was asked to. The task was &lt;em&gt;fix a bug in the login flow&lt;/em&gt;. The diff also reformatted three unrelated files, renamed a helper for clarity, and updated four imports because the rename touched them. The bug fix is correct. The reformatting is harmless. But the PR is now many times the size it should be, and the reviewer’s eye is going to slide past the actual fix.&lt;/p&gt;

&lt;p&gt;How to spot it in the log: look at the Edit and Write tool calls. Compare the files touched against the files the task description called out. The gap is the creep.&lt;/p&gt;

&lt;p&gt;The fix is a rule and a sensor. The rule says &lt;em&gt;touch only what you must, clean up only your own mess&lt;/em&gt;. The sensor is a pre-PR check that fails if the diff is wider than a budget; N files, M lines, whatever your numbers happen to be.&lt;/p&gt;

&lt;p&gt;Scope creep is one of the most common failure modes in my experience and the most invisible. It doesn’t break anything. It just makes every PR worse to review and every regression harder to track down.&lt;/p&gt;

&lt;h4&gt;
  
  
  Premature commit
&lt;/h4&gt;

&lt;p&gt;The agent committed before the work was done. The tests pass because the agent skipped writing the failing test first. The acceptance criteria look met because the agent declared them met without verifying. The PR description claims feature X is implemented when, under the hood, feature X is a stub.&lt;/p&gt;

&lt;p&gt;How to spot it in the log: look for the moment the agent decided the work was done. Compare against the actual evidence of doneness. Did the agent run the test it claimed proves the fix? Did the test exist before the change, or did the agent write it after seeing the change pass? Did the agent verify the acceptance criteria, or did it skip the verification step and write the PR description from memory of what the task asked?&lt;/p&gt;

&lt;p&gt;The fix is procedural. A skill called finishing-work that walks the agent through the verification steps before allowing a commit. Perhaps a pre-commit hook that fails if the changed files don’t have at least one paired test change. An agent that has to produce a &lt;em&gt;completion report&lt;/em&gt; (see my earlier post on &lt;a href="https://blog.tacoda.dev/post-mortems-for-agent-runs-2cceeaf13f2e" rel="noopener noreferrer"&gt;post-mortems for agent runs&lt;/a&gt;) before declaring done.&lt;/p&gt;

&lt;p&gt;Premature commit is the failure mode where the agent looks most productive and is least useful. The PR ships, the work isn’t finished, and the next person to touch the code finds out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the log: a worked example
&lt;/h3&gt;

&lt;p&gt;Here’s a transcript fragment from a real run I investigated. The task was to add a new field to a user profile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; User: Add a `pronouns` field to the user profile. Optional string, nullable.
[Read .claude/CLAUDE.md]
[Read app/Models/User.php]
[Glob database/migrations/*_users_*.php]
[Read database/migrations/2024_05_12_add_email_to_users.php]
[Write database/migrations/2026_06_18_add_pronouns_to_users.php]
[Edit app/Models/User.php] // added 'pronouns' to $fillable
[Bash: php artisan migrate - pretend]
[Edit app/Http/Resources/UserResource.php] // added pronouns to toArray
[Bash: vendor/bin/pest tests/Feature/UserResourceTest.php]
[Write app/Http/Controllers/UserController.php] // added validation rule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PR shipped. The reviewer signed off. Production threw an error the next morning, from the admin panel: Undefined array key 'pronouns'. The admin user view read $user['pronouns'] straight off the resource array, and the resource never put it there.&lt;/p&gt;

&lt;p&gt;Reading the transcript: the agent loaded the project’s CLAUDE.md, looked at the User model, looked at a recent migration as a template, and wrote a similar migration. It also added the field to the User resource and the controller validation. It did &lt;em&gt;not&lt;/em&gt; read app/Http/Resources/AdminUserResource.php, which the admin panel uses and which lists its fields explicitly, key by key. Unlike the model’s $fillable, the admin resource doesn’t auto-pick up new columns. A field exists there only if someone names it. The agent never opened the file, so pronouns was never added, and the admin view that assumed the key blew up.&lt;/p&gt;

&lt;p&gt;The failure mode: &lt;em&gt;missing context&lt;/em&gt;. The harness had no rule that said &lt;em&gt;user-profile fields appear in three places: User, UserResource, AdminUserResource&lt;/em&gt;. The agent followed the convention from the two places it knew about and shipped a partial change.&lt;/p&gt;

&lt;p&gt;The fix wasn’t &lt;em&gt;be smarter, agent&lt;/em&gt;. The fix was a path-scoped rule in app/Http/Resources/CLAUDE.md that named the resource files that needed to stay in sync. The rule went in. The sensor (a quick AST check that any new $fillable field appears in both resource files) went in alongside. The next pronouns-shaped task didn’t repeat the bug.&lt;/p&gt;

&lt;p&gt;That’s the loop. The transcript pointed at the gap and the harness change closed the gap. The model wasn’t the variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What forensic reading teaches you about your harness
&lt;/h3&gt;

&lt;p&gt;The payoff of reading transcripts isn’t fixing the specific bug. It’s the patterns that show up after twenty or thirty of them.&lt;/p&gt;

&lt;p&gt;You start noticing which files the agent rarely reads, and asking why. You notice which rules it cites a lot (they’re load-bearing) and which it never cites (they’re dead weight). You notice the directories where transcripts go wrong most often — usually the directories with the worst seams, the least context, or the most conventions that aren’t written down.&lt;/p&gt;

&lt;p&gt;There’s a cost the failure modes hide from each other. The fix for missing context is almost always a new rule and every rule you add is a future candidate for contradicted-rules rot. Fix enough gaps by piling on instructions and you build the exact stale-harness problem that produced failure mode two. So the audit cuts both ways: you read transcripts to find the rule you’re missing, and you read them to find the rule that’s now lying. A harness isn’t a pile of rules that only grows. The transcripts tell you which rules to add and which to retire, and the second list matters as much as the first. The transcript is the highest-resolution feedback signal the harness produces.&lt;/p&gt;

&lt;p&gt;Two practical reading habits help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read at least one failed transcript a week.&lt;/strong&gt; Not all of them. One. Pick the one that produced the most surprising failure. Read it end to end. Note the failure mode. Ask whether the harness has a gap there. The discipline isn’t to fix everything. Rather, it’s to develop the muscle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a transcript notebook.&lt;/strong&gt; A file in the repo. Each entry: link or paste of the transcript, the failure mode (pick from the five above), the gap in the harness, the change that fixed it. After ten entries, the notebook becomes a map of where your harness is weak.&lt;/p&gt;

&lt;p&gt;The notebook doesn’t have to be pretty. It has to exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The thing the model isn’t responsible for
&lt;/h3&gt;

&lt;p&gt;A theme runs through every failure mode above. You can argue each one back to &lt;em&gt;“the model should have known better”&lt;/em&gt;. But arguing about whose fault it is doesn’t change the rate. Changing the harness does. Missing context is a closable harness gap. Contradicted rules are stale rules. Hallucinated APIs are weak documentation or missing tools. Scope creep is an unset constraint. Premature commit is a missing procedure.&lt;/p&gt;

&lt;p&gt;This isn’t a defense of the model. The model is fallible, and the next generation of models will be more capable than this one. The point is that &lt;em&gt;the cheap improvements live in the harness, not in the model&lt;/em&gt;. You can’t retrain the model from your laptop. You can fix a rule, scope a rule, add a sensor, sharpen a tool, and write a procedure. All five are at your fingertips. All five reduce the rate of the same bug shipping again.&lt;/p&gt;

&lt;p&gt;Forensic reading of transcripts is the practice that tells you which one to fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read one transcript this week
&lt;/h3&gt;

&lt;p&gt;Find the last commit your agent made that you wish it hadn’t, or the last PR that came back with a “this isn’t quite right” review. Open the transcript that produced it.&lt;/p&gt;

&lt;p&gt;Read the tool calls first. Note which files the agent loaded and which it should have loaded. Find the rule it cited or skipped. Cross-check the agent’s claims against the actual code. Pick which of the five failure modes you’re looking at.&lt;/p&gt;

&lt;p&gt;Then write the harness change that would have stopped it. Whether a new rule, a path-scope, a sensor, a skill or a tool. Ship it the same day.&lt;/p&gt;

&lt;p&gt;After three or four passes, you’ll start seeing the same failure mode keep recurring. That’s the signal. The pattern is the rule waiting to be written. The transcript is just the place the pattern lives until you spot it.&lt;/p&gt;

&lt;p&gt;The model can’t read its own transcript. You can.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>agenticai</category>
      <category>postmortem</category>
      <category>investigation</category>
    </item>
    <item>
      <title>What Breaks When You Skip the Harness</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Tue, 23 Jun 2026 14:47:49 +0000</pubDate>
      <link>https://dev.to/tacoda/what-breaks-when-you-skip-the-harness-3237</link>
      <guid>https://dev.to/tacoda/what-breaks-when-you-skip-the-harness-3237</guid>
      <description>&lt;p&gt;The agent kept running the tests, watching them go red, scrolling up to find the failure, and then running the tests again because it had already lost the output. Third time in one session. The model wasn’t the problem. The session’s memory of its own output was. I had Claude tee the test command to a log file. After that it read the log instead of re-running.&lt;/p&gt;

&lt;p&gt;That was one fix on one project. The pattern repeats. If the model keeps producing bad code, what is actually broken?&lt;/p&gt;

&lt;h3&gt;
  
  
  The model isn’t the problem
&lt;/h3&gt;

&lt;p&gt;Output quality is the sum of two things: the model and the surroundings.&lt;/p&gt;

&lt;p&gt;In the teams I’ve worked with, the obsession is always the model. They upgrade. Switch tools. New IDE plugin every month. They read every benchmark. The defects keep landing in the same places.&lt;/p&gt;

&lt;p&gt;The surroundings get almost no attention. Those surroundings are the harness: the files, rules, tools, and feedback loops that wrap the model inside a real project. A CLAUDE.md with conventions. Skills — codified workflows the agent can run by name. MCP servers. Scoping rules. A feedback.md that records what the team has corrected. Pre-commit gates.&lt;/p&gt;

&lt;p&gt;Without those, the model is guessing.&lt;/p&gt;

&lt;p&gt;Five things teams blame on the model, each pointing at a missing piece of the harness, each with a first move you can ship this week. Some of the bigger fixes (codifying a team’s lore, writing a useful project-specific MCP, etc.) are quarter-of-work to do properly. The first move is week-sized. The first move gets you started.&lt;/p&gt;

&lt;h3&gt;
  
  
  APIs that don’t exist
&lt;/h3&gt;

&lt;p&gt;The model writes a call to client.users.list({ since: lastSyncAt }). The argument doesn’t exist. The real API takes a Unix timestamp on a different endpoint. Code compiles. Test fails. The engineer reads the docs. The bug gets filed as “the model hallucinated.”&lt;/p&gt;

&lt;p&gt;Except the model didn’t hallucinate from nothing. It wrote what it learned from training data and didn’t verify. Nothing in the session told it to.&lt;/p&gt;

&lt;p&gt;The fix is grounding. Give the model a way to read the real docs in the moment.&lt;/p&gt;

&lt;p&gt;Two grounding moves do the work.&lt;/p&gt;

&lt;p&gt;First: an MCP server for library docs — Context7 is one. The model fetches current docs and writes against what’s there. When it fails, it’s almost always because the rule telling the agent to fetch first wasn’t in CLAUDE.md.&lt;/p&gt;

&lt;p&gt;Second: a project-specific MCP for internal APIs. Every team I’ve worked with has one or two services nobody got around to documenting well. An MCP that serves the OpenAPI spec — or a search over the service’s source — puts real signatures in front of the model.&lt;/p&gt;

&lt;p&gt;A single rule in CLAUDE.md ties it together. “Fetch docs before writing library code.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smallest first move:&lt;/strong&gt; add Context7. Add the rule. Try it on the next library task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code that doesn’t match yours
&lt;/h3&gt;

&lt;p&gt;The model writes a new service. The file lands in the wrong directory. Errors throw instead of returning. The naming is camelCase in a snake_case codebase. The diff looks foreign.&lt;/p&gt;

&lt;p&gt;The reviewer sees it and rewrites it. The next PR has the same problems.&lt;/p&gt;

&lt;p&gt;The model has no reference for “how we do it here.” The rules live in three engineers’ heads and one out-of-date README.&lt;/p&gt;

&lt;p&gt;Part one is a CLAUDE.md that names the project’s conventions in plain words. Where files go. How errors propagate. Naming style. Testing framework. Logger. Time library. One or two sentences each. The bare minimum.&lt;/p&gt;

&lt;p&gt;Part two matters more: &lt;em&gt;worked examples.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When we moved a service to hexagonal architecture, the agent kept writing new code on the wrong side of the ports-and-adapters boundary. The old code was still everywhere and gave it permission to keep doing what it had always done.&lt;/p&gt;

&lt;p&gt;The model copies texture from examples better than from prose. A skill or a feature doc with one full worked example , such as a real PR, a real file, or a real test, gives it something to imitate. Three examples beat a thousand words of rules.&lt;/p&gt;

&lt;p&gt;The catch: if the codebase itself is inconsistent (legacy module is snake_case, new code is camelCase), the model picks up the contradiction and writes both. The worked example has to come from the side of the codebase you want the agent to copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smallest first move:&lt;/strong&gt; pick the workflow your team runs most often. Write one skill for it. Drop one full example into the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same bug, in every session
&lt;/h3&gt;

&lt;p&gt;You correct the agent on Monday. It used the wrong logger. You tell it to use the project’s logger. It does. Friday, new task, wrong logger again.&lt;/p&gt;

&lt;p&gt;The agent didn’t learn. The session ended. The correction went with it. The fix is a feedback loop the agent can read. Not a chat log, but a file.&lt;/p&gt;

&lt;p&gt;Create a feedback.md. When the team corrects the agent on something that’s a rule, not a typo, write it down in three lines. Context. Mistake. Rule going forward. Tell the agent to read it before every relevant task.&lt;/p&gt;

&lt;p&gt;The first entry in mine was about flaky E2E tests. The team’s rule was fix them or delete them. Never live with them. The agent kept patching around them instead. After the third time I corrected it I stopped repeating myself and wrote the rule down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who writes to it:&lt;/strong&gt; anyone on the team. The cost of adding an entry has to be smaller than the cost of fixing the same thing twice. Otherwise nobody writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The promotion rule:&lt;/strong&gt; when the same correction shows up in feedback.md three times, lift it into CLAUDE.md or into a skill. Three is arbitrary, pick a threshold and stick with it. I chose three because of the rule of three. The point is that nothing useful stays buried in the feedback file forever. Every promoted entry is one more rule the model reads at the start of the next session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smallest first move:&lt;/strong&gt; create the file. Write one entry today. Add one line to CLAUDE.md that says read it first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The knowledge stays in heads
&lt;/h3&gt;

&lt;p&gt;A senior engineer is on a call, explaining why the payment service has its own retry logic that is different from the rest of the system. The new engineer nods. The agent in everyone’s terminal doesn’t, because the agent wasn’t on the call.&lt;/p&gt;

&lt;p&gt;Two weeks later the agent writes a new payment integration. It calls the shared retry helper. It has no idea there’s a reason not to. The PR ships. The on-call gets paged.&lt;/p&gt;

&lt;p&gt;This is tribal knowledge. The lore of the system. It lives in Slack scrollback, in senior engineers’ heads, in a Notion or Confluence page.&lt;/p&gt;

&lt;p&gt;The fix is codification. Three kinds of files do the work.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;glossary&lt;/strong&gt; captures domain vocabulary. The word “session” can mean three different things in one system — auth, user, analytics. The glossary picks one definition per term and points to it. One paragraph each.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;feature doc&lt;/strong&gt; holds the why of a part of the system. Why payments has its own retry logic. Why we picked Postgres over MySQL. Why the queue uses Redis Streams and not Kafka. Short notes a new reader can scan in a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills&lt;/strong&gt; hold workflows. The steps to ship a database migration. How we add a feature flag. The onboarding checklist for a new service joining the monitoring stack.&lt;/p&gt;

&lt;p&gt;The hard part of this fix is not the format. The hard part is getting the senior engineer to write the feature doc instead of explaining it on calls. That’s a social problem, not a documentation problem. Pair on the first one. Make the doc the deliverable.&lt;/p&gt;

&lt;p&gt;A billing module on one of my projects had quiet invariants about how transaction state combined. The agent made an arithmetic change that looked clean in the diff. It would have produced wrong totals in production. The reason the change mattered wasn’t in the code. It was in the team’s heads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smallest first move:&lt;/strong&gt; pick the one piece of lore most often re-explained to new engineers. Write it down as a feature doc this week. Reference it in CLAUDE.md.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reviewers as the harness
&lt;/h3&gt;

&lt;p&gt;Every PR re-litigates the same things. Naming, test coverage, error handling, conventions, and so on. Same three reviewers, same six comments. PRs take two days to merge. The reviewers are tired.&lt;/p&gt;

&lt;p&gt;The reviewers have become the harness. Humans are doing what a script should do.&lt;/p&gt;

&lt;p&gt;The fix is gates the machine can run.&lt;/p&gt;

&lt;p&gt;A pre-commit hook catches the same six review comments before the diff exists. Match the gate to the team’s actual repeated comments. It could be naming-convention nits that needs a linter rule, missing-test-file nits that needs a coverage gate, or risky-file-changed nits that needs a CODEOWNERS-style script. The point is to read the team’s review history and turn the top three repeat offenders into machine checks.&lt;/p&gt;

&lt;p&gt;A review skill or review agent reads the diff and flags the things a linter can’t see.&lt;/p&gt;

&lt;p&gt;A code-health check (CodeScene is what I use) gives a numeric score for maintainability and blocks regressions. The score settles the argument over whether a diff makes the code worse in seconds.&lt;/p&gt;

&lt;p&gt;The worst case I’ve watched was naming inconsistencies during a migration. Two valid styles in the codebase, the model picking both, sometimes in the same PR. We added sensors that flagged the old style in new files and ran them in CI. The drift stopped showing up in review.&lt;/p&gt;

&lt;p&gt;The work reviewers should be doing (design, intent, what the PR is actually trying to do) gets crowded out by everything machines could catch and don’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smallest first move:&lt;/strong&gt; pick the review comment your team has left most often this month. Write one pre-commit hook for it. Push it in a PR this week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write three rules this week
&lt;/h3&gt;

&lt;p&gt;The model isn’t the problem. The surroundings are the work.&lt;/p&gt;

&lt;p&gt;Three things to do this week.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open CLAUDE.md. Write three rules: the three things you’ve corrected an agent on this week. That’s the start of a harness.
&lt;/li&gt;
&lt;li&gt;Create feedback.md. Write one entry. Tell the agent to read it. The feedback loop now exists.
&lt;/li&gt;
&lt;li&gt;Pick one workflow you’ve run through an agent more than three times this month. Make it a skill. The third time you reach for it, it’s already written down.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The harness rots too. CLAUDE.md drifts as the project changes. Skills go stale. feedback.md swells to four hundred entries and turns into noise. Two skills contradict each other and the agent picks one at random. Most teams who tried a harness and quit do so at the rot stage, not the setup. Treat it like the code it wraps: audit on a schedule, delete what no longer applies, promote what has earned its place.&lt;/p&gt;

&lt;p&gt;This isn’t the only thing that matters. A great model still beats a mediocre one on hard problems. I don’t have a clean benchmark for the bigger claim. What I have is a preference: given the choice between a great model in a bad harness and a mediocre model in a good one, I take the mediocre model every time. The model’s mistakes I can debug. The drift I cannot.&lt;/p&gt;

&lt;p&gt;Watch for two weeks. The defects don’t vanish. Instead they show up earlier at pre-commit, in the feedback file, or in the review skill instead of in CI or production. That’s the win.&lt;/p&gt;

</description>
      <category>harnessengineering</category>
      <category>agenticworkflow</category>
      <category>agenticai</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Tools Are Harness Too</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:30:29 +0000</pubDate>
      <link>https://dev.to/tacoda/tools-are-harness-too-3l9k</link>
      <guid>https://dev.to/tacoda/tools-are-harness-too-3l9k</guid>
      <description>&lt;p&gt;I had a rule in my project CLAUDE.md for months that said &lt;em&gt;always check Code Health before committing&lt;/em&gt;. The agent followed it about 70% of the time. The other 30%, it forgot; usually on the longer tasks, when its context window was filling up and the rules at the start of the prompt were losing weight against the code at the end. I added emphasis. The 70% became 80%. I added a section break. 85%. I rewrote the rule in capital letters with three exclamation points. Same 85%.&lt;/p&gt;

&lt;p&gt;Then I gave the agent a &lt;a href="https://codescene.com/product/code-health" rel="noopener noreferrer"&gt;Code Health&lt;/a&gt; tool. Not a rule. A tool — an MCP server entry point called code_health_safeguard that returned a structured analysis of the staged diff. The next day, the agent ran it before every commit. The day after that. The day after that. The compliance went to 100% and stayed there.&lt;/p&gt;

&lt;p&gt;The difference between a rule and a tool is the difference between &lt;em&gt;telling the agent what to do&lt;/em&gt; and &lt;em&gt;making the right thing easier than the wrong thing&lt;/em&gt;. Both belong in a working harness. Tools are the half nobody talks about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules tell, tools enforce
&lt;/h3&gt;

&lt;p&gt;A rule is a paragraph. The agent reads it, weighs it against the other things in its context, and produces output. If the rule wins the weighing, the agent follows it. If something else wins (the code’s existing pattern, a more recently mentioned constraint, the user’s apparent intent in the prompt) the rule loses.&lt;/p&gt;

&lt;p&gt;The probability the rule wins is not 100%. It is some number less than 100%, and the number drops as the context grows. Rules in long-running agent sessions are like introductory remarks at a long meeting: present, recorded, mostly forgotten by hour three.&lt;/p&gt;

&lt;p&gt;A tool is different. A tool is an &lt;em&gt;affordance:&lt;/em&gt; a thing the agent can do, exposed at the same level as everything else the agent does. The agent’s working surface is &lt;em&gt;I can read files, edit files, run shell commands, and call these specific functions&lt;/em&gt;. A tool slots into that list. The agent doesn’t have to weigh a paragraph against another paragraph; it sees a function and decides whether to call it.&lt;/p&gt;

&lt;p&gt;The decision shifts from &lt;em&gt;should I obey this rule&lt;/em&gt; to &lt;em&gt;should I use this capability&lt;/em&gt;. The shift matters because the decision happens at a different layer in the agent’s reasoning. Capability decisions are cheaper than rule-weighing decisions. They survive the long-context drift.&lt;/p&gt;

&lt;h3&gt;
  
  
  When a tool beats a rule
&lt;/h3&gt;

&lt;p&gt;Not every rule should become a tool. The translation has a cost. Tools take time to write, they introduce new surface area to maintain, and they only earn their place when they replace a rule that fails often enough to be worth replacing.&lt;/p&gt;

&lt;p&gt;A rule belongs as a tool when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The rule produces a check the agent can run.&lt;/strong&gt; &lt;em&gt;Always verify the schema before writing a migration&lt;/em&gt; is a checkable property. &lt;em&gt;Be thoughtful about naming&lt;/em&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check has a deterministic output.&lt;/strong&gt; &lt;em&gt;Run the linter and pass&lt;/em&gt; is a yes-or-no answer. &lt;em&gt;Match the team’s style&lt;/em&gt; is a vibes call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check fails silently when skipped.&lt;/strong&gt; A rule the agent can ignore without anyone noticing is a rule that begs to become a tool. The tool is the gate that closes the silent skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check is run often.&lt;/strong&gt; A check the agent should run on every commit is a tool. A check the agent should run on the rare cross-project migration is a skill or a rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everything else, rules are still the right unit. Tone, naming conventions, design preferences, when-to-use-X-vs-Y judgment calls: those are paragraphs, not function calls. Trying to encode them as tools is the path to a brittle harness with thirty checks and no taste.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Health example, end to end
&lt;/h3&gt;

&lt;p&gt;To make the trade-off concrete, here’s the rule-to-tool conversion that started this post.&lt;/p&gt;

&lt;p&gt;The rule, in CLAUDE.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Code Health is authoritative

Before suggesting a commit, run a Code Health check on the changed files.
If any file regresses below 9.0, refactor before committing. Do not commit
code with a Code Health below the project floor.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is fine. It’s specific, it names the threshold, it tells the agent what to do. It just doesn’t fire reliably under load.&lt;/p&gt;

&lt;p&gt;The tool is the CodeScene MCP server’s pre_commit_code_health_safeguard. It exposes itself to the agent as a callable function. The function takes the staged diff and returns a structured response — files checked, scores before and after, regressions flagged, suggested refactorings. The agent doesn’t have to remember to do the right thing; the right thing is one tool call away, and &lt;em&gt;not&lt;/em&gt; calling it now looks conspicuous next to the call to git diff --cached that the agent makes anyway.&lt;/p&gt;

&lt;p&gt;The rule still lives in CLAUDE.md, with one important change. It now says &lt;em&gt;call&lt;/em&gt; &lt;em&gt;pre_commit_code_health_safeguard before suggesting a commit&lt;/em&gt;. The rule names the tool. The tool does the work. The rule is shorter and the agent’s compliance is higher.&lt;/p&gt;

&lt;p&gt;That’s the conversion shape: a rule that names a tool, and a tool that does the check the rule used to describe. Both layers are present. Each is cheaper than it would be alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools as constraint design
&lt;/h3&gt;

&lt;p&gt;The other reason tools matter is that they let you design constraints that rules can’t express.&lt;/p&gt;

&lt;p&gt;Consider the constraint &lt;em&gt;the agent should never delete production data&lt;/em&gt;. As a rule, it’s a sentence: &lt;em&gt;don’t run destructive operations against production&lt;/em&gt;. The rule is correct and entirely advisory. As a tool, it’s the inverse. The absence of any production-database-write capability in the tool list at all. The agent literally cannot call what isn’t exposed.&lt;/p&gt;

&lt;p&gt;This is the difference between &lt;em&gt;advisory constraint&lt;/em&gt; and &lt;em&gt;constraint by construction&lt;/em&gt;. Rules are advisory. Tools are construction. The harness designer chooses where on that spectrum to put each constraint.&lt;/p&gt;

&lt;p&gt;The rule of thumb I use: constraints that are critical to safety belong as constraints by construction. Tools the agent shouldn’t use shouldn’t be in the tool list. Permissions the agent shouldn’t have shouldn’t be granted. The advisory layer (rules) is for guidance; the construction layer (tools, permissions, capabilities) is for safety.&lt;/p&gt;

&lt;p&gt;When I’m reading a harness, the first place I look isn’t the rules. It’s the tool list and the permission allowlist. The tool list tells me what the agent &lt;em&gt;can&lt;/em&gt; do. The rules tell me what it &lt;em&gt;should&lt;/em&gt; do. The safety properties live in the first answer, not the second.&lt;/p&gt;

&lt;h3&gt;
  
  
  What an MCP server gives you
&lt;/h3&gt;

&lt;p&gt;MCP (the Model Context Protocol) is the standard most modern agents use to expose tools. The mechanics are simple: a server runs somewhere (often as a subprocess of the editor), declares a set of capabilities to the agent, and handles requests when the agent calls them.&lt;/p&gt;

&lt;p&gt;The capabilities are arbitrary. They can wrap a CLI, an HTTP API, a database, a local file system, a third-party service, or pure computation. Whatever can be wrapped becomes a tool.&lt;/p&gt;

&lt;p&gt;A few of the MCP servers I find load-bearing in my own work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CodeScene.&lt;/strong&gt; Code Health checks, refactoring guidance, hotspot detection. Replaces a long rule about complexity with a tool that gives concrete numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context7.&lt;/strong&gt; Real-time library documentation. Replaces the failure mode where the agent hallucinates an API by giving it a way to look up the actual one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The project’s own MCP server.&lt;/strong&gt; Custom checks the team cares about. A “show me the current schema” tool. A “validate this against the API spec” tool. A “list the existing repositories” tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third one is the one most teams skip and shouldn’t. The project’s own MCP server is where the team’s domain constraints get encoded as capabilities. A validate_contract tool, a verify_acceptance_criteria tool, a check_naming_convention tool — each takes a rule that used to live in CLAUDE.md and turns it into something callable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tradeoff: tools cost more
&lt;/h3&gt;

&lt;p&gt;The honest cost of tools is that they’re harder to write than rules. A rule is a paragraph in a markdown file. A tool is a function with a typed schema, error handling, an installer, and a test surface. Writing a good tool takes an afternoon. Writing the rule it replaces takes ten minutes.&lt;/p&gt;

&lt;p&gt;The afternoon pays back, but only for the right tools. The rules to convert are the ones with high cost (the rule has been violated multiple times, each violation has a real cost) and high check-ability (the check is mechanical). The rules to leave alone are the ones with low cost or low check-ability. They’re cheaper to keep as rules and live with the occasional miss.&lt;/p&gt;

&lt;p&gt;There’s a second tradeoff. Tools accumulate. Every tool added to the agent’s surface is another option it considers. A tool list that grows to fifty capabilities slows the agent’s decision-making and pushes the relevant tools further out of immediate reach. Tools, like rules, deserve their own audit cadence in the same way rule-rot eats at rules, tool sprawl eats at agents.&lt;/p&gt;

&lt;p&gt;Keep the load-bearing tools. Delete the ones the agent never calls. Watch the same pattern as the rules: which tools earn their slot, which sit unused.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools as the third layer of the harness
&lt;/h3&gt;

&lt;p&gt;If rules are the first layer and sensors are the second, tools are the third. Each layer does work the others can’t.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt; communicate intent and shape the agent’s reasoning. They’re cheap. They’re advisory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensors&lt;/strong&gt; detect when intent isn’t followed. They’re checks at the workflow’s edges. They’re load-bearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; make right things easy and wrong things impossible. They’re capabilities. They’re construction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A working harness uses all three. The rule says &lt;em&gt;do X&lt;/em&gt;. The tool makes &lt;em&gt;X&lt;/em&gt; a callable capability. The sensor verifies &lt;em&gt;X&lt;/em&gt; happened. Each layer has a different cost and a different power. The mistake is to load everything into one layer because that layer is comfortable.&lt;/p&gt;

&lt;p&gt;The teams I see with rules-only harnesses ship the same bugs over and over with great-sounding documentation. The teams I see with tools-only harnesses ship code that compiles but doesn’t match the team’s conventions. The teams I see thriving have a balance — rules for the guidance, tools for the capability, sensors for the gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The seams between rules and tools
&lt;/h3&gt;

&lt;p&gt;The most powerful arrangement is when a rule references a tool by name. The rule loads into context, communicates the intent, and tells the agent which tool to reach for. The tool, when called, produces the deterministic answer the rule was hand-waving toward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Verifying schema changes

Before writing a migration that adds, drops, or alters a column, call
`db_schema_dump` to get the current schema. Confirm the columns you reference
exist and are typed as you expect. Cite the schema dump in your reasoning so
the reviewer can see you verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a rule paired with a tool. The rule does what rules do: it explains the intent, names the situations it applies to, and tells the agent how to handle them. The tool does what tools do: it provides the actual capability — fetching the current schema, in a structured form, deterministically.&lt;/p&gt;

&lt;p&gt;The pair is stronger than either piece alone. The rule alone would be ignored on long contexts. The tool alone wouldn’t know when to be called. Together, they cover the case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convert one rule to a tool this week
&lt;/h3&gt;

&lt;p&gt;Pick a rule from your CLAUDE.md that names a check. &lt;em&gt;Always run the linter. Verify the schema first. Check the test coverage.&lt;/em&gt; Anything that says &lt;em&gt;always X&lt;/em&gt; where &lt;em&gt;X&lt;/em&gt; is mechanical.&lt;/p&gt;

&lt;p&gt;Look at whether there’s an MCP server that already does &lt;em&gt;X&lt;/em&gt;. If so, install it, expose the tool to the agent, and rewrite the rule to name the tool. If not, look at the size of &lt;em&gt;X&lt;/em&gt;. If it’s small (a shell command, a script), write a one-tool MCP server that exposes the script. Most agent harnesses make this surprisingly easy now.&lt;/p&gt;

&lt;p&gt;Ship the tool. Update the rule to name the tool. Watch the next ten agent runs. The rule that was complied with 80% of the time becomes 100% almost overnight, because the agent isn’t being asked to remember anymore. The capability is right there.&lt;/p&gt;

&lt;p&gt;Rules tell. Tools enforce. The harness with both is the harness the agent doesn’t drift out of.&lt;/p&gt;

</description>
      <category>agenticai</category>
      <category>aitools</category>
      <category>harnessengineering</category>
      <category>agenticworkflow</category>
    </item>
    <item>
      <title>Harness Engineering — Building Reliable Workflows Around Non-Deterministic Agents</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Sun, 21 Jun 2026 22:55:28 +0000</pubDate>
      <link>https://dev.to/tacoda/harness-engineering-building-reliable-workflows-around-non-deterministic-agents-3f5m</link>
      <guid>https://dev.to/tacoda/harness-engineering-building-reliable-workflows-around-non-deterministic-agents-3f5m</guid>
      <description>&lt;p&gt;Six months ago this was a blog post. Then a second blog post. Then Bridle, Sellier, intent-driven-delivery, and eventually Keystone.&lt;/p&gt;

&lt;p&gt;Now it’s a book: &lt;em&gt;Harness Engineering — Building Reliable Workflows Around Non-Deterministic Agents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What surprised me writing it: the experiments earned their place. Bridle taught me the flywheel. Sellier taught me defaults beat flexibility. IDD taught me the team layer can be reified. Keystone is the synthesis they pointed at.&lt;/p&gt;

&lt;p&gt;It’s on LeanPub now. Pay what you want, starts at $10.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://leanpub.com/harness-engineering" rel="noopener noreferrer"&gt;https://leanpub.com/harness-engineering&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agenticworkflow</category>
      <category>books</category>
      <category>softwareengineering</category>
      <category>harnessengineering</category>
    </item>
    <item>
      <title>Sensors: The Other Half of the Harness</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Sat, 20 Jun 2026 15:15:50 +0000</pubDate>
      <link>https://dev.to/tacoda/sensors-the-other-half-of-the-harness-3mml</link>
      <guid>https://dev.to/tacoda/sensors-the-other-half-of-the-harness-3mml</guid>
      <description>&lt;p&gt;The pre-commit hook caught a migration last Tuesday that would have shipped to staging green and broken production on the next deploy. The migration dropped a foreign key constraint without naming a backfill plan, and the rule that said &lt;em&gt;every constraint change needs a backfill plan&lt;/em&gt; had been sitting in migrations.md for four months. The rule didn’t stop the agent. The rule didn’t stop the human reviewer either. The diff was 38 files and the constraint drop was a single line. What stopped it was a four-line shell script wired to pre-commit that grepped the staged migration for DROP CONSTRAINT and exited non-zero if no--backfill: comment followed it.&lt;/p&gt;

&lt;p&gt;That four-line script is what I want to talk about. Most of what gets written about agent harnesses is about rules — what CLAUDE.md should say, where to put it, how to scope it, when to version it. Rules are half the harness. The other half is the set of checks that fire when a rule gets broken. I call them sensors, and they get talked about a lot less than they should.&lt;/p&gt;

&lt;h3&gt;
  
  
  The asymmetry in the conversation
&lt;/h3&gt;

&lt;p&gt;Read any of the well-circulated posts on agent harnesses and count the words spent on each side. Rules are the centerpiece. Sensors are glanced over, as if they’re so obvious they don’t deserve their own treatment. They aren’t obvious, and the assumption that they’re already there is the most expensive assumption in the field.&lt;/p&gt;

&lt;p&gt;The asymmetry has a reason. Rules are easier to write. A rule is a paragraph. A sensor is a script, a hook, a CI step, a pre-commit config, a custom check. Rules sit at the cognitive level a writer naturally operates at; sensors sit at the level of plumbing. The first one is a lot more fun to write than the second.&lt;/p&gt;

&lt;p&gt;The asymmetry also has a cost. &lt;em&gt;Rules without sensors are vibes.&lt;/em&gt; The agent reads them, claims to follow them, and gets graded on whether the resulting code &lt;em&gt;looks&lt;/em&gt; like it followed them. &lt;em&gt;Looks like&lt;/em&gt; is the failure mode. The agent is a probabilistic system. It will obey a rule most of the time and skip it some of the time, and the times it skips are the times the rule mattered most: the awkward branch, the migration nobody likes to think about, or the edge case the rule was written to catch.&lt;/p&gt;

&lt;p&gt;A sensor changes the contract. The rule is no longer a hope. It’s a falsifiable check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a sensor actually is
&lt;/h3&gt;

&lt;p&gt;A sensor is anything that can detect whether a rule was followed and signal that detection in a way the workflow respects. The shape is narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs deterministically.&lt;/li&gt;
&lt;li&gt;It returns a clear pass or fail.&lt;/li&gt;
&lt;li&gt;It fires at a point in the workflow where its result still matters.&lt;/li&gt;
&lt;li&gt;It’s cheap enough that nobody is tempted to skip it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the sensors in a working harness aren’t fancy. A grep with set -e. A line in a linter config. A pytest fixture that asserts the database fixture hasn’t drifted. A pre-commit hook that runs mypy on changed files. A GitHub Actions job that fails the PR if the migration directory has a file without a paired rollback. None of these is impressive on its own. The point isn’t the individual sensor; &lt;em&gt;it’s the discipline of having one for every rule that matters.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Five places a sensor fires
&lt;/h3&gt;

&lt;p&gt;There are five workflow positions where sensors earn their pay. Each catches a different class of failure. A working harness has sensors at most of them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pre-edit
&lt;/h4&gt;

&lt;p&gt;The earliest sensor is the one the agent encounters before it writes code. The rule is loaded into context; the sensor is something the agent can run to check its own work before producing output. A type stub generator, a schema dump, a --dry-run command that simulates the change. The agent doesn’t need approval; it just needs to see what its proposed change &lt;em&gt;would&lt;/em&gt; do.&lt;/p&gt;

&lt;p&gt;Pre-edit sensors are the rarest of the five, because they require giving the agent tools, not just rules. The pay-off is that they catch errors before any bytes hit disk. The agent that can run prisma validate on a proposed schema change is going to make fewer broken commits than the agent that can only read the rule that says &lt;em&gt;make sure your schema is valid&lt;/em&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pre-commit
&lt;/h4&gt;

&lt;p&gt;Pre-commit is where most teams put their first sensor, because Git makes it easy. A .pre-commit.config.yaml or a husky hook fires the moment the agent (or human) tries to land a change. The check has the staged diff to look at, the rest of the repo for context, and a hard exit code that the workflow respects.&lt;/p&gt;

&lt;p&gt;This is the sensor layer for rules that constrain the &lt;em&gt;artifact&lt;/em&gt; — what the code looks like, not what it does. Format, lint, type, dead code, banned imports, naming patterns, the migration backfill check above. Cheap, fast, local. The agent that breaks a pre-commit hook either fixes the violation or doesn’t commit. Either outcome beats &lt;em&gt;the agent commits and nobody notices&lt;/em&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pre-PR
&lt;/h4&gt;

&lt;p&gt;A pre-PR sensor is one that runs after the commits exist but before the diff goes up for review. The check has more to work with: a full branch, a base ref, a set of changed files that’s no longer one commit at a time. It’s the right layer for cross-file checks. &lt;em&gt;Did you change this API without updating its consumers? Did you add a new migration without bumping the schema version? Does the test coverage diff drop more than two points?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pre-PR runs cheap on CI and slow locally. Most teams run it on the branch push, which catches the agent’s work right as it’s offering the work for human attention. That timing matters. A check that fires after the human has started reviewing is a check that’s been skipped.&lt;/p&gt;

&lt;h4&gt;
  
  
  Post-merge
&lt;/h4&gt;

&lt;p&gt;Post-merge sensors are the ones that run against main. They aren’t strictly part of the agent’s loop, but they’re part of the harness because they detect when the loop produced something that broke once integrated. Smoke tests, end-to-end suites, schema-diff jobs that compare staging to production, query-plan monitors that fire when a slow query joins the rotation.&lt;/p&gt;

&lt;p&gt;The instinct is to think of post-merge as &lt;em&gt;CI&lt;/em&gt; and not as part of the harness. That’s a mistake. The agent’s behavior is shaped by what fails &lt;em&gt;after&lt;/em&gt; it shipped. A post-merge sensor that catches a regression and gets traced back to a missing rule is the most valuable kind of sensor in the whole stack. It tells you something the inner sensors missed, and it tells you what to add upstream.&lt;/p&gt;

&lt;h4&gt;
  
  
  Drift
&lt;/h4&gt;

&lt;p&gt;The fifth and most-skipped sensor is the one that checks whether the rules themselves are still true. The codebase moves. The framework version bumps. The pattern the rule used to describe gets replaced by a new pattern. The rule is now wrong, and nobody notices because nothing fires.&lt;/p&gt;

&lt;p&gt;A drift sensor is the check that wakes up periodically and asks: does this rule still match reality? Sometimes that’s a literal grep for the pattern the rule references. Sometimes it’s a count of how many files in the codebase still match the rule’s example. Sometimes it’s a last-modified audit that flags any rule older than six months for review. The cadence is monthly or quarterly; the goal is to keep the harness honest as the code beneath it changes.&lt;/p&gt;

&lt;p&gt;I’ll write a whole post about rule-rot soon. The short version: &lt;em&gt;a drift sensor is the difference between a harness that’s six months old and helpful and a harness that’s six months old and lying.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The pairing rule
&lt;/h3&gt;

&lt;p&gt;If a rule matters, it has a sensor. That’s the principle. It sounds rigid, and it is, on purpose.&lt;/p&gt;

&lt;p&gt;The discipline works because it forces a real question every time a rule gets written: &lt;em&gt;how would I know if this got violated?&lt;/em&gt; If the answer is &lt;em&gt;I’d see it in code review&lt;/em&gt;, the rule is on probation. Code review is slow, expensive, and inconsistent. It is also the wrong layer for rules that produce mechanical violations.&lt;/p&gt;

&lt;p&gt;If the answer is &lt;em&gt;the linter would catch it&lt;/em&gt;, the rule probably doesn’t need to be a rule. &lt;strong&gt;The linter is the rule. Just configure the linter and skip the prose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interesting cases are the rules where the answer is &lt;em&gt;nothing currently checks for this, but I could write a check&lt;/em&gt;. Those are the rules that earn a sensor. The sensor doesn’t have to ship the same day the rule ships, but it should be on the list and have a date.&lt;/p&gt;

&lt;p&gt;The rules that aren’t worth a sensor are the rules that aren’t worth writing down. &lt;em&gt;Be thoughtful&lt;/em&gt; is not a rule. &lt;em&gt;Bullet points should be sentence fragments unless they form a list of full sentences&lt;/em&gt; is also not a rule, because nothing catches a violation and the cost of writing it down is more than the cost of fixing the rare slip in editing.&lt;/p&gt;

&lt;p&gt;A rule with no sensor is either too vague to matter or important enough that someone should pair it within the month. &lt;em&gt;Either way, the absence of a sensor is information.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What good sensors share
&lt;/h3&gt;

&lt;p&gt;The sensors that survive in production look alike. They share five traits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They run in seconds.&lt;/strong&gt; A pre-commit hook that takes 90 seconds gets bypassed within two weeks. A pre-PR check that takes 12 minutes gets the --skip-checks shortcut added to the team Slack. The check has to be fast enough that running it is cheaper than figuring out how to avoid running it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They produce clear messages.&lt;/strong&gt; A failed sensor that prints EXIT 1 is a failed sensor that’s about to get ignored. A failed sensor that prints migrations/20260620_drop_users_index.sql dropped an index without a paired backfill comment. Add a comment starting with --backfill: explaining the plan, or annotate the migrations with --no-backfill-needed:  is a sensor that teaches the agent (or the human) how to fix the violation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They run locally.&lt;/strong&gt; A check that only exists in CI is a check that the agent finds out about ten minutes after it pushed. &lt;em&gt;Running the same check locally before push closes the loop.&lt;/em&gt; The pre-commit framework and npm run check-style scripts both make this easy; the discipline is to make sure CI runs the &lt;em&gt;same&lt;/em&gt; scripts the developer can run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They cite the rule they enforce.&lt;/strong&gt; Every sensor message that earns its weight names the rule. *Failed: rule MIGRATIONS-04 (foreign-key changes require a backfill plan).* The agent reads the message; if the rule is wrong, the agent can find it and propose a change. The sensor is the gate, but the rule is the explanation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They fail loud and pass quiet.&lt;/strong&gt; A sensor that prints OK checked 47 migration files in 1.2s every commit is a sensor whose output everyone learns to ignore. Pass silently. Fail with a paragraph. Save the human’s attention for the cases that need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where sensors live in the project
&lt;/h3&gt;

&lt;p&gt;A working sensor layout has four physical homes. Each holds the sensors for a different workflow position.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.husky/ # or .pre-commit-config.yaml
  pre-commit # fast, staged-files only
  commit-msg # commit message linting
.github/workflows/
  pr-checks.yml # pre-PR, runs on push
  post-merge.yml # post-merge, runs on main
.claude/
  hooks/ # agent-facing pre-edit checks
  sensors/ # one shell script per drift check
scripts/check-*.sh # the actual checks, invoked from above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape isn’t sacred. &lt;em&gt;What matters is that each sensor has a single home and the workflow knows where to call it.&lt;/em&gt; The anti-pattern is the same check running in three places with slightly different logic and the team unable to remember which one is authoritative.&lt;/p&gt;

&lt;p&gt;The sensors themselves go in scripts/ (or wherever your project keeps tooling) and get called by whichever runner needs them. That separation means the same check can fire from pre-commit, from CI, and from a drift-audit cron without three copies. One script, three invocations.&lt;/p&gt;

&lt;p&gt;Personally, I prefer to wrap these scripts in make tasks. The benefit that I receive is that the agent always knows to use make. And that makes everything easier to find and more consistent. I use make in CI too. Usually, I work with docker and so this is a natural pair for me. &lt;em&gt;Isolate the environment with docker; isolate the commands with make.&lt;/em&gt; That makes it easier for the human &lt;em&gt;and&lt;/em&gt; the agent to work with the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two sensors worth writing this month
&lt;/h3&gt;

&lt;p&gt;The two highest-value sensors for a team starting from zero are both small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The migration safeguard.&lt;/strong&gt; Whatever your migration tool is, the team’s hardest-to-catch bugs ship through it. A pre-commit script that knows the patterns your migrations are supposed to avoid — DROP COLUMN without paired backfill, ADD COLUMN NOT NULL without a default, schema-breaking renames without a two-phase plan — catches more production issues than any rule on its own. The agent will write migrations that look right and miss the constraint. The sensor sees what the human reviewer doesn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The diff-budget gate.&lt;/strong&gt; A pre-PR check that fails if the diff exceeds N files or M lines. The number is yours to pick. I use 15 files and 400 lines, which roughly maps to a PR a human can review in one sitting. The rule that says &lt;em&gt;keep PRs small&lt;/em&gt; is exactly the kind of rule that gets nodded at and then drowned in the next month. The sensor that fails the build at file 16 is the rule with teeth.&lt;/p&gt;

&lt;p&gt;Both are under fifty lines of shell. Both pay for themselves the first week.&lt;/p&gt;

&lt;h3&gt;
  
  
  The anti-patterns to watch
&lt;/h3&gt;

&lt;p&gt;A few sensor failure modes recur often enough to name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unrunnable sensor.&lt;/strong&gt; It lives in CI, takes 14 minutes, and depends on a secret only the deploy bot has. Nobody on the team can run it locally. By the time it fires, the human reviewer has already mentally signed off. The fix: factor out the local-runnable piece and run &lt;em&gt;that&lt;/em&gt; on every commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sensor that always passes.&lt;/strong&gt; Someone wrote it months ago, the inputs changed, the assertions still hold, the failure case is no longer reachable. The sensor is a green light glued to the dashboard. The fix: every sensor needs at least one &lt;em&gt;known-bad&lt;/em&gt; fixture that the test suite runs against it to confirm it still detects the violation it was written for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The flaky sensor.&lt;/strong&gt; It fails sometimes and passes the rest. The team learns to rerun. Within a month, the rerun habit has spread to the other checks, and a real failure gets bypassed because it &lt;em&gt;looked&lt;/em&gt; flaky. The fix: &lt;strong&gt;a flaky sensor is broken.&lt;/strong&gt; Pull it from the gate, fix it, or delete it. Do not leave it firing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule with no sensor.&lt;/strong&gt; The whole point of this post. A rule that lives in CLAUDE.md and has no check is a rule that gets followed when the agent feels like it. The fix is the pairing rule above: if the rule matters, pair it within a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sensor with no rule.&lt;/strong&gt; Less common but worth naming. A check fires, a developer doesn’t know why, the rule it’s enforcing isn’t written down. The check is correct on its own terms but unteachable. The fix: cite the rule in the failure message. If you can’t, write the rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pair one rule to a sensor this week
&lt;/h3&gt;

&lt;p&gt;Pick one rule from your CLAUDE.md — the one you find yourself reminding the agent about most. Open a file. Write a check. Wire it to pre-commit. Ship it.&lt;/p&gt;

&lt;p&gt;The right rule for the exercise is one with a clear violation pattern. &lt;em&gt;Always use the&lt;/em&gt; &lt;em&gt;Result type for fallible operations&lt;/em&gt; is a fine candidate; you can grep for throw in the changed files and warn. &lt;em&gt;Match the existing code style&lt;/em&gt; is a poor candidate; the violation pattern is too diffuse for a small sensor to catch.&lt;/p&gt;

&lt;p&gt;When the sensor catches its first violation, two things happen. The rule becomes load-bearing. The agent now has to satisfy it instead of just acknowledge it. Additionally, you find out whether the rule was right. Sensors that fail a lot on changes that look reasonable in review are sensors enforcing rules that need to change. &lt;em&gt;The sensor is the feedback loop; the rule is the hypothesis.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Add a second one next month. By the end of the quarter your harness will feel different. Not because the rules are smarter, but because the rules can be checked.&lt;/p&gt;

&lt;p&gt;The rules are the part everyone writes. The sensors are the part that makes them true.&lt;/p&gt;

</description>
      <category>agenticai</category>
      <category>softwareengineering</category>
      <category>harnessengineering</category>
    </item>
    <item>
      <title>The Accidental Framework</title>
      <dc:creator>Ian Johnson</dc:creator>
      <pubDate>Fri, 19 Jun 2026 15:23:25 +0000</pubDate>
      <link>https://dev.to/tacoda/the-accidental-framework-40l5</link>
      <guid>https://dev.to/tacoda/the-accidental-framework-40l5</guid>
      <description>&lt;p&gt;The thing I built was a Go binary that copied markdown files into a repository. I called it an installer. The README called it an installer. The CLI verb was init. For the several months in early 2026, that framing held. Then I sat down to plan the 1.0 release, started typing, and could not write the next paragraph without using the word &lt;em&gt;framework&lt;/em&gt;. Not in the marketing-blurb sense. In the load-bearing sense: a small runtime, a set of named extension points, conventional file layouts, a resolver that picked a winner among competing sources, a lockfile, a migration runner, per-backend adapters. The thing I had been calling an installer for four months had been doing framework work the whole time, and I just had not noticed.&lt;/p&gt;

&lt;p&gt;This post is about that moment of recognition and what came after. It is about the refactor that turned an installer-shaped framework into a framework-shaped framework. The most surprising part was not the rewrite. It was how little of the &lt;em&gt;concepts&lt;/em&gt; had to change. The names were already right. The directories needed to move. The runtime needed a physical line down the middle between framework code and content. But the abstractions I had been using (guides, corpus, sensors, actions, playbooks, adapters) held up under the new framing without revision. The work I had done naming things over the previous months turned out to be the work that made the refactor cheap.&lt;/p&gt;

&lt;p&gt;I want to walk through how that happened, what tipped me off, and the one wrong turn the refactor almost made before backing out of it. The story has a tidy ending; the lesson does not, and I will get to that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shape was there before the word
&lt;/h3&gt;

&lt;p&gt;Keystone started as keystone init — a binary you ran in a fresh repo and it laid down a harness/ directory full of markdown. Rules for the agent. A small lifecycle. A few sensors that ran your existing lint and test commands. Nothing especially framework-ish. The 0.1 README said &lt;em&gt;installer&lt;/em&gt; four times.&lt;/p&gt;

&lt;p&gt;Then I started adding things. Each addition felt like a feature, not an architectural shift. The commit log reads like a feature log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.2: install-time options. Pick your agent. Add a target.&lt;/li&gt;
&lt;li&gt;0.3: restructured the harness into corpus, guides, sensors, flywheels. Named taxonomy for the first time.&lt;/li&gt;
&lt;li&gt;0.4: a kind taxonomy on guides and sensors. Different kinds, different load behavior.&lt;/li&gt;
&lt;li&gt;0.5: a migrate command for forward-compatible upgrades. The harness had a schema now, and the schema had a version.&lt;/li&gt;
&lt;li&gt;0.10: policy plugins. Org-level rules that projects pulled in.&lt;/li&gt;
&lt;li&gt;0.11: the policy cascade. Org → Team → Project. Strict and non-strict layers.&lt;/li&gt;
&lt;li&gt;0.12: playbooks. Ordered chains of actions.&lt;/li&gt;
&lt;li&gt;0.13: sensors as a tier-aware policy kind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that list once and it looks like product growth. Read it twice and the shape gets harder to miss. By 0.13 there was a runtime with conventions, plugins, a cascade, a lockfile, migrations, and per-agent rendering. The README still said &lt;em&gt;installer&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The thing about a shape forming under a name is that the name is the last thing to change. Each individual commit looks like a small feature; the architectural mass piles up underneath and nobody is watching the mass. I was not, anyway. I was writing the next feature.&lt;/p&gt;

&lt;p&gt;When I opened a fresh document called PLAN-10.md and tried to summarize where things stood, the first sentence I wrote was: &lt;em&gt;Convert Keystone from a harness installer with org policy plugins into a harness framework.&lt;/em&gt; I stared at that for a while. The conversion was not a conversion. &lt;strong&gt;It was an admission.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What an installer was pretending not to be
&lt;/h3&gt;

&lt;p&gt;Here is what makes the installer label dishonest in retrospect. An installer drops files. It is done. The user takes it from there. The framework that Keystone had become did all of this on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It defined named extension points.&lt;/strong&gt; Guides, corpus, sensors, actions, playbooks, adapters per agent. Each one had a path convention, a frontmatter contract, and a load behavior. That is an API, not a directory listing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It resolved conflicts across sources.&lt;/strong&gt; The same /.md could exist in the project, in a team plugin, in an org plugin. Exactly one won. The resolution order mattered, and the ordering rules were stable across versions. That is a runtime, not a copy step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It tracked drift.&lt;/strong&gt; A lockfile pinned per-source SHAs and per-file hashes. The binary would notice when plugin files had been edited under it. That is integrity enforcement, not file-laying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It migrated old layouts forward.&lt;/strong&gt; keystone migrate would walk an installed harness, apply numbered transforms, and bring it to the current schema. That is a schema migrator, with all the implications of having a schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It rendered the same harness into multiple targets.&lt;/strong&gt; Claude Code’s CLAUDE.md shape, Codex’s AGENTS.md, Cursor’s .cursor/rules/. The adapter layer translated one source into many. That is a code generator, not a paste-once-and-done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take any one of those in isolation and you can argue it was a feature an installer happened to include. Take them together and the argument falls over. Installers do not carry adapter layers. They do not have lockfiles. They do not have migration runners. They certainly do not have a cascade resolver with strict and non-strict overrides.&lt;/p&gt;

&lt;p&gt;The give-away, when I went looking, was the layout of the source tree. Go files at the repo root next to a harness/ directory full of markdown that the binary embedded. No physical boundary. A single Go module mixing the runtime with the content the runtime shipped. If you asked me to point at “the framework” and “the content” in the 0.x tree, I would have had to do it with words, not directories. &lt;em&gt;That is a tell.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The vocabulary did the heavy lifting
&lt;/h3&gt;

&lt;p&gt;Here is the part of the story that surprised me most. I was bracing for a rewrite — the kind where you sit down with a blank internal/framework/ directory and start naming concepts from scratch, then translate the old code into them, then translate again because the first names were wrong. That is the usual cost of recognizing a shape late.&lt;/p&gt;

&lt;p&gt;It did not happen. The abstractions held.&lt;/p&gt;

&lt;p&gt;When I sat down to write the ports-and-adapters layer of the 1.0 plan, I had a table to draft. One row per port. For each port, a name, a path convention, an activation rule. I wrote it expecting to discover gaps. Here is what I found instead.&lt;/p&gt;

&lt;p&gt;Every port already had a name. Every name was already in use. Every path was already conventional. &lt;em&gt;Guide&lt;/em&gt; meant a rule loaded on every turn at guides//.md. &lt;em&gt;Corpus&lt;/em&gt; meant reasoning loaded on demand at corpus//.md. &lt;em&gt;Sensor&lt;/em&gt; meant an automated check at sensors/.md. &lt;em&gt;Action&lt;/em&gt; meant a single unit of lifecycle work. &lt;em&gt;Playbook&lt;/em&gt; meant an ordered chain of actions. &lt;em&gt;Adapter&lt;/em&gt; meant the per-agent binding at adapters//…. The table I sat down to invent had been the truth of the code since half-way to 1.0. I was writing it down, not designing it.&lt;/p&gt;

&lt;p&gt;Writing it down still mattered. The contract was implicit until that table existed. New ports would have slipped in by accident the same way old ones had. But the &lt;em&gt;design&lt;/em&gt; work had already happened, one commit at a time, while I thought I was just shipping features.&lt;/p&gt;

&lt;p&gt;That is the angle worth chewing on. The names I had given things while building features carried more architectural weight than any single commit they appeared in. &lt;em&gt;Guide&lt;/em&gt; and &lt;em&gt;corpus&lt;/em&gt; drew the line between rules-loaded-every-turn and reasoning-loaded-on-demand. &lt;em&gt;Sensor&lt;/em&gt; drew the line between an automated check and an aspirational rule. &lt;em&gt;Action&lt;/em&gt; and &lt;em&gt;playbook&lt;/em&gt; drew the line between an atomic unit of work and an ordered chain of them. Every one of those lines was a load-bearing distinction in the runtime, and I had named them all before I knew they were load-bearing.&lt;/p&gt;

&lt;p&gt;The corollary is the part to hold onto: when the refactor came, &lt;strong&gt;I did not have to invent a single new concept&lt;/strong&gt;. I had to relocate the code that &lt;em&gt;implemented&lt;/em&gt; the concepts. The concepts themselves stayed put. Naming had front-loaded the design work. The refactor was a directory move on top of a vocabulary that was already correct.&lt;/p&gt;

&lt;p&gt;This is the underrated payoff of taking names seriously while building. If you have named the abstractions well in version 0.3, you can refactor the runtime in version 1.0 without renaming anything. If you have named them badly, version 1.0 starts with a six-month tax to fix the words before you can fix the code. The cheap version of a hard refactor is the one where the user-facing vocabulary is already correct and only the implementation moves.&lt;/p&gt;

&lt;p&gt;It also changes the social cost of the refactor. A user who learned &lt;em&gt;guide&lt;/em&gt; and &lt;em&gt;corpus&lt;/em&gt; in 0.3 still knows what those words mean in 1.0. The blog post they wrote about the harness in 0.5 is still accurate. The wiki page in their team’s Notion has not gone stale. Refactor-without-renames is the kind a user does not feel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing it down made it real
&lt;/h3&gt;

&lt;p&gt;Once I admitted what was happening, the next move was to make the framing physical. Not “we say it is a framework now.” That is marketing. The framework had to &lt;em&gt;look&lt;/em&gt; like a framework when you opened the source tree, and behave like one when you read the code.&lt;/p&gt;

&lt;p&gt;The plan went through six phases. Each had a small handful of commits, none too clever, each one a small structural improvement. Order mattered, because some moves enabled others.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn1bd0kdy7phslii0puq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn1bd0kdy7phslii0puq.png" width="800" height="45"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The phased refactor, in order. Each arrow is a phase the next one depends on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few things about the order are worth pulling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 0 was the cheapest and the most important.&lt;/strong&gt; Before any code moved, I wrote eight architecture decision records and one port contract per abstraction. Each ADR was a single page: context, decision, consequences, alternatives considered. Each port contract was a single page: path convention, required frontmatter, cascade behavior, an example, the command that scaffolds it. The total page count was around twenty. Writing them took two days. They turned out to be the most useful pages in the whole project, because every subsequent phase referenced them. When a phase started, I re-read the relevant ADR; when a phase finished, I re-read it again to check we had stayed inside the lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 was the physical line down the middle.&lt;/strong&gt; Every Go file under the repo root got moved into internal/framework/. The CLI entrypoint moved to cmd/keystone/. The template tree relocated to internal/framework/scaffold/templates/. After Phase 1, you could point at the framework in the directory tree without using words. That is a small thing that turns out to be a big thing. Two months later, when a contributor asks “is this framework behavior or content?”, the answer is a path, not a paragraph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phases 2 through 5 were small structural improvements stacking on top of that line.&lt;/strong&gt; JSON-only config (Phase 2), because YAML had drifted across half a dozen schemas during 0.x. Vendored read-only plugins (Phase 3), because the plugin model had been “edit the markdown in harness/policies/” and that turned out to be the wrong default. Projects would silently diverge from upstream and nobody would notice. Conventions, generators, and a doctor command (Phase 4), because once you have named ports you can write generators that scaffold an adapter for each port. Per-port token budgets (Phase 5), because once you have named ports you can also count tokens per port and tell the user when one is bloated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 6 was the documentation phase.&lt;/strong&gt; An upgrade guide for 0.x users. A compatibility doc spelling out what 1.x promised to keep stable. Nothing structural changed in Phase 6. The point was to make a contract that the next year’s worth of changes had to respect.&lt;/p&gt;

&lt;p&gt;Each phase landed in a separate set of commits. Each commit was small enough to read in one sitting. None of them were a “big refactor commit.” That is the texture of a refactor that respects the work already there: many small surgical moves, each one preserving behavior, with the architecture emerging from the sum.&lt;/p&gt;

&lt;h3&gt;
  
  
  The plugin draft we threw away
&lt;/h3&gt;

&lt;p&gt;I want to talk about a wrong turn, because every refactor has one, and the ones that do not get talked about tend to be the ones that bite later.&lt;/p&gt;

&lt;p&gt;The first draft of the 1.0 plan made the built-in defaults (universal guides, lifecycle playbook, default sensors, per-agent adapters) into &lt;em&gt;first-class plugins shipped embedded in the binary&lt;/em&gt;. The plan read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The universal engineering corpus/guides, the lifecycle actions, the task playbook, and the default sensors all become first-class policy plugins — same shape as user-installed plugins, loaded by the same engine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The promise was symmetry. Built-ins and external policy would travel the same pipeline. One loader, one cascade, no special cases. It was clean. It was elegant. It also had a giant cost buried in the elegance: the moment defaults are loaded as plugins, &lt;em&gt;editing a default&lt;/em&gt; stops being “edit a markdown file in your repo” and becomes “fork the embedded plugin or override it from a higher layer.”&lt;/p&gt;

&lt;p&gt;That breaks the Rails-style ergonomics I wanted at the project layer. The whole point of conventions over configuration is that the conventional file is just sitting there in the repo, your repo, your git, ready to be edited like any other file. If the default lives inside the binary and only appears in the consumer’s repo as a &lt;em&gt;shadow&lt;/em&gt; that overrides it, you have turned a one-line edit into a four-step debugging session. &lt;em&gt;Where is this rule actually coming from? Is the override winning? Why is my edit not taking effect?&lt;/em&gt; I have seen that pattern in tools that load defaults from inside their distribution, and the answer is always the same: it is a constant low-level tax on every user, paid forever.&lt;/p&gt;

&lt;p&gt;I caught it because I sat with the plan for a day before starting any code. Two of the ADRs (number five, &lt;em&gt;Conventions, not plugins&lt;/em&gt;, and number two, &lt;em&gt;Framework / client boundary&lt;/em&gt;) ended up being the place where the wrong turn got walked back. The decision was: defaults are &lt;em&gt;scaffolded&lt;/em&gt; into the consumer’s harness// on keystone init, from embedded templates. From that moment on, defaults are project content. The user edits them as markdown files in their own git. There is no override mechanism for defaults, because there is nothing to override. There is just one file sitting on their disk.&lt;/p&gt;

&lt;p&gt;Plugins still exist. They do one job: share policy across projects. Read-only, vendored, hash-verified, drift-reset on the next run. They are not the mechanism for shipping defaults, and the mechanism for shipping defaults is not the mechanism for sharing policy. Two concerns, two mechanisms. The earlier symmetry was buying elegance with the user’s debugging time.&lt;/p&gt;

&lt;p&gt;The lesson I want to draw from this is narrower than “avoid premature symmetry.” It is: when two mechanisms produce a surface that looks identical to the user, you should still ask whether the &lt;em&gt;editing model&lt;/em&gt; is identical. If the user edits both the same way, symmetry is paying its rent. If the user edits one of them and merely &lt;em&gt;reads&lt;/em&gt; the other, you have two concerns that happen to share a shape, and unifying them costs you the editing UX of the one the user actually edits.&lt;/p&gt;

&lt;p&gt;I had to throw away two days of plan-writing to walk that back. It was worth it. &lt;em&gt;The walkback is what made the 1.0 surface usable.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code moved, concepts did not
&lt;/h3&gt;

&lt;p&gt;When 1.0 shipped, I went back through the changelog to count. The framework had moved every Go file into a new location. It had dropped YAML. It had rewritten the cascade resolver. It had added a vendored plugin model, a doctor command, a budget command, a port-level scaffold generator, and per-agent adapter regeneration. The repo layout looked nothing like 0.x.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The user-facing vocabulary had not changed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A 0.x user who never read the 1.0 plan could open the new docs and find every word they already knew. &lt;em&gt;Guide&lt;/em&gt; still meant rules loaded every turn. &lt;em&gt;Corpus&lt;/em&gt; still meant reasoning loaded on demand. &lt;em&gt;Sensor&lt;/em&gt; still meant an automated check. &lt;em&gt;Action&lt;/em&gt; and &lt;em&gt;playbook&lt;/em&gt; still meant what they meant. The only new word at 1.0 was &lt;em&gt;port&lt;/em&gt;, and &lt;em&gt;port&lt;/em&gt; was a name for something the user already understood without the name. Namely, the named slot a piece of content lives in.&lt;/p&gt;

&lt;p&gt;This is the payoff I underestimated when I was naming things in 0.2 and 0.3. The cost of naming a thing well at the start is that you have to actually sit with it for a few minutes and ask whether the name says what the thing does. The benefit is that later, when you refactor the runtime, you do not have to renegotiate the contract with every user who learned the old word. &lt;em&gt;The user’s&lt;/em&gt; mental model does not change. Only the implementation does.&lt;/p&gt;

&lt;p&gt;A refactor that does not rename anything is the cheapest kind for a user. They keep their docs, their wiki, their training onboarding. They keep the words they say out loud when describing the product to a teammate. The internals can move freely as long as the names stay fixed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How shapes form under names
&lt;/h3&gt;

&lt;p&gt;Step back from Keystone for a second. The pattern is broader than one project.&lt;/p&gt;

&lt;p&gt;Tools grow into frameworks. They almost always do, if they live long enough. The path looks like this: a small utility solves a small problem. It picks up a configuration file. The config file grows extension points. The extension points need naming conventions. The naming conventions need a resolver. The resolver needs ordering rules. The ordering rules need a way to opt out, then a way to opt back in. Suddenly there is a runtime, and the runtime has a contract with everything plugged into it, &lt;em&gt;and the contract is a framework whether you call it one or not&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The interesting question is when to notice. Too early and you are over-designing: naming abstractions before you have three concrete uses for them, building cascade rules for a single tier, writing port contracts for one port. Too late and the runtime has accumulated implicit contracts that nobody wrote down, and the cost of writing them down is paid in surprise regressions when you try to clean up.&lt;/p&gt;

&lt;p&gt;The way I think about it now: the moment you find yourself adding a &lt;em&gt;resolver&lt;/em&gt; (anything that picks a winner among multiple sources of the same concept) you are in framework territory. Lockfiles and migrations are also strong signals. Per-target rendering (the adapter layer) is a hard signal. Any one of those, taken alone, is not enough. Two of them together is. Three of them together and the question is not &lt;em&gt;if&lt;/em&gt; you are building a framework, it is whether the framework is going to be &lt;em&gt;honest&lt;/em&gt; about itself in its README.&lt;/p&gt;

&lt;p&gt;If you only catch it at three, the way I did, you are not in trouble. You are in the spot where naming the shape costs you a few days of writing and a few weeks of refactoring, and the user-facing surface comes out untouched on the other side. If you catch it at six or seven signals deep, the cost is higher, because by then there are real users with real assumptions and the cleanup runs into compatibility costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three things to do this week
&lt;/h3&gt;

&lt;p&gt;If you are reading this and recognizing your own tool in the description, here is what I would do this week, in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the port table.&lt;/strong&gt; Open a new document. List every named extension point in the tool. For each one, write the path convention, the activation rule, and the shape of the file (or config) that goes there. Do not stop until the table is exhaustive. The table is the easiest possible draft of the framework contract. If it takes an hour, you were already a framework and your vocabulary was holding the design up; if it takes a week and you keep discovering anonymous concepts, you have found the work that needs naming first. Either way, this is the highest-value hour you can spend on the project this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw the physical boundary.&lt;/strong&gt; Look at your source tree. Can you point at “framework code” and “content the framework happens to ship” using a path, or do you have to use words? If it is words, move directories until it is a path. This is the single most clarifying refactor for a maturing tool, and the tests will catch you if you break anything along the way. One day’s work for a small project, a week for a medium one. From the moment it lands, every conversation about “where does this go?” gets shorter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write ADRs for the decisions you have already made.&lt;/strong&gt; Not the future decisions, but the ones already locked in by the code. Three to ten ADRs of one page each. Context, decision, consequences, alternatives considered. Future you will thank present you the next time someone asks why X is the way it is. The act of writing them often surfaces a wrong turn you can still back out of cheaply, exactly like the plugin draft I walked back. Better to find that during ADR-writing than during a refactor PR.&lt;/p&gt;

&lt;p&gt;After those three, you will have a clear picture of what you have, where it lives, and why. The work that comes next is the work that actually changes things — the renaming, if any, the directory moves, the new scaffolders, the migration path for existing users. That work is real and it takes time. But you can scope it, plan it, and ship it in phases that do not break users, because the contract you wrote down in the first three steps is the contract every subsequent phase has to honor.&lt;/p&gt;

&lt;p&gt;The thing I keep coming back to from the Keystone refactor is how much of the work had been done already by past versions of me when I named &lt;em&gt;guide&lt;/em&gt; and &lt;em&gt;corpus&lt;/em&gt; and &lt;em&gt;sensor&lt;/em&gt; and &lt;em&gt;action&lt;/em&gt; in different commits weeks apart. They were not designing a framework. They were each adding one feature and giving it a name. The framework emerged from the sum of those names. The 1.0 refactor was the moment I looked up and noticed.&lt;/p&gt;

&lt;p&gt;If you have been adding features and naming them well, your framework is already there. It is just waiting for you to write it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keystone
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.tacoda.dev/keystone/" rel="noopener noreferrer"&gt;Keytsone&lt;/a&gt; is now 2.1.1! It is open source and MIT-licensed. I am very open to feedback, so if you have any, please create a discussion or an issue on &lt;a href="https://github.com/tacoda/keystone" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It has a &lt;em&gt;real&lt;/em&gt; chance of landing.&lt;/p&gt;

&lt;p&gt;A lot landed between the 1.0 cut and 2.1.1, and all of it sits on top of the framework/client split 1.0 introduced. The shape of the project didn’t change. The surfaces around it did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.0&lt;/strong&gt; was the big one, shipped June 17. A new primitive taxonomy ran through the harness end to end. An in-binary &lt;strong&gt;MCP server&lt;/strong&gt; moved keystone onto the same wire its agents already speak, so there’s no separate process to babysit. A &lt;strong&gt;localhost dashboard&lt;/strong&gt; surfaced live state for the first time. An &lt;strong&gt;eval suite&lt;/strong&gt; came with it. The disk layout moved alongside all of that, and the upgrade was still one command: keystone migrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1.0&lt;/strong&gt; followed the next day. The old patches mechanism retired in favor of a versioned migrations subsystem with paired Up/Down transforms under migrations//, run with keystone migrate up | down | status. The plugin→policy rename finished across Go source, JSON schemas, and docs in the same release. Installs that hadn’t migrated yet started warning and continuing instead of breaking, so the upgrade path stays soft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1.1&lt;/strong&gt; reshaped the dashboard. It’s now an HTMX SPA with a single &lt;/p&gt; swap target, real back/forward navigation, and fragment responses keyed off HX-Request. The 14 single-purpose nav links collapsed into five sections: &lt;strong&gt;Observability, Harness, Sources, Flywheels, Quality.&lt;/strong&gt; SSE topic narrowing means a widget only refreshes when its own path changes. A per-session audit log lands at .keystone/state/audit/session--.jsonl, opened with O_CREATE|O_EXCL so nothing ever overwrites a prior run. &lt;em&gt;Cmd+K&lt;/em&gt; (&lt;em&gt;Ctrl+K&lt;/em&gt; on Windows and Linux) opens a search popover from any page.

&lt;p&gt;If you’re still on 1.x, the upgrade is the same as the install (except Brew, which has an upgrade subcommand. It carries you the whole way to 2.1.1. To bring a project harness up-to-date with the new core framework structure, run keystone migrate up. This will patch core files only and will never touch user-edited files.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>harnessengineering</category>
      <category>agenticai</category>
      <category>agentharness</category>
    </item>
  </channel>
</rss>
