project 10 — ~70 min
Build a mini agent harness, then use a real one
How a coding agent's loop runs, from the inside
by the end you can:
- Run the agentic coding loop and name what each step of it is made of: plan, edit, run, verify, commit → taught in 2.06
- Make the plan artifact the human gate, and verify from a fact produced outside the model → taught in 2.06
- Put a boundary in code where an instruction would only request it → taught in 2.03
- Define a tool as a contract the model routes on by its description → taught in 1.03
- Explain the runner: the model calls tools until it stops calling them → taught in 2.01
you need: Node 22+ and the course repo · Gemini key (free)
A coding agent exposes a set of tools to a language model, executes each tool call emitted by the network, feeds the execution output back as text, and repeats the cycle. That loop forms the computational core of automated software development. Its phases follow an established sequence: planning, editing, execution, verification, and commitment. Yet naming those stages leaves out the enclosing harness itself: the deterministic code that provides tool contracts, fences what those tools may touch, halts execution until an engineer approves a plan, terminates runaway loops through step budgets, and executes validation checks outside the model context. In this project, you write that harness in under two hundred lines of TypeScript, using the course engine, and run it against the course repository to complete a functional change.
You build this harness to observe three defensive behaviors directly in your terminal: halting on an unapproved plan, refusing to edit an unread file, and returning a test runner exit code into the context loop. Once you observe these fences in operation, commercial harnesses cease to look like magic. They reveal themselves as specific configurations of these same mechanisms, allowing you to select among production tools based on concrete architectural trade-offs.
the loop, and the five fences around it
An autoregressive language model generates plausible completions without inspecting external system state. When asked to refactor code, the model will write an articulate four-step plan for scripts/direct_call.ts without reading the file first. It will report that a test suite passed when the test never ran. It will edit .env simply because the file path was plausible in the conversation context. What has to sit in code, outside the model, so that each of those actions returns an error string instead of taking effect on disk?
The core execution engine is the runner. The runner passes tool definitions to the model, dispatches each tool call emitted by the network, feeds the string result back into the prompt context, and repeats until the model outputs a text message instead of a tool call. A tool is an explicit software contract: a name and a description that guide model routing, a schema that validates arguments, and an execution function that runs outside the model context.
One statement about this page’s figure and numbers, made once. The figure draws one change going through cleanly, and a refused plan, an exhausted budget, and a rejected edit each return an error string into the same loop. Every count later on the page (twenty-four calls, eleven calls, 6,500 to 26,000 tokens, fifteen calls, twenty-seven lines, sixty tests) is measured from the course’s runs of 2026-09-03 on the course repo.
The answer to each failure mode is a fence: a check written in code that the model cannot talk its way past. Five fences enforce the execution limits of the harness:
- The root jail. Every path resolved by a tool must reside within the repository root and outside forbidden subdirectories. Invocations targeting
../../.sshor.envreturn immediate error strings into the context. - The step budget. The runner terminates upon reaching forty tool invocations, stopping runaway execution loops regardless of subsequent model requests.
- The plan gate. The runner locks all mutation tools until a markdown artifact under
plans/contains an explicit approval line written by you. - The read-before-edit check. The runner requires that a file be read this run (
readThisRun) before accepting mutations to it, rejecting edits to uninspected files. - The allowlist. The execution tool
run_checkaccepts onlynpm test, preventing arbitrary shell execution on the host machine.
What passes through these fences represents the remaining risk: semantic errors inside properly read and approved files.
step one: put the file in the repo and read its fences
Copy scripts/mini_harness.ts from the kit into the scripts/ directory of your clone of the course repo. It imports three components from the engine: loadEnv to parse .env, registerAvailableProviders so the model string can resolve any configured provider, and defineTool alongside toFunctionTool from the contract layer used in the financial analyst project. The remaining implementation relies entirely on Node’s standard filesystem APIs and one ADK agent instance.
The fences appear at the top of the file:
const ROOT = process.cwd();
const MODEL = process.env.HARNESS_MODEL ?? 'gemini-3.8-flash';
const STEP_BUDGET = 40; // tool calls per run; the loop ends here, whatever the next call would have been
const DENY = ['node_modules', '.git', '.env', 'dist'];
const CHECKS: Record<string, string> = { test: 'npm test' }; // the only commands run_check may run
/** Every path the model names is resolved inside ROOT and outside DENY, or refused. */
function jail(rel: string): string | Error {
const abs = resolve(ROOT, rel);
const inside = relative(ROOT, abs);
if (inside.startsWith('..')) return new Error(`Error: "${rel}" is outside the repository.`);
if (inside.split(sep).some((seg) => DENY.includes(seg))) return new Error(`Error: "${rel}" is off limits.`);
return abs;
}
The variable ROOT establishes the execution working directory. The jail function converts every relative path supplied by the model into an absolute path, rejecting any target that traverses above the working tree or touches a segment listed in DENY. Invocations attempting to inspect ../../.ssh or .env yield deterministic error strings returned directly to the agent context. The step budget tallies each tool invocation; every tool inspects this counter before running, terminating execution on the fortieth call regardless of subsequent model requests. The CHECKS dictionary defines the complete set of permitted shell commands, indexed by symbolic names the model selects. Restricting commands to this dictionary converts an open command-execution vulnerability into a closed validation check. Setting HARNESS_MODEL allows you to route execution to claude-sonnet-4-6 or ollama/qwen3:8b without modifying code, as the provider registry resolves models by prefix.
step two: the read tools hand the model subsets, never the repo
A codebase does not fit in a single context window, making context management the primary discipline of coding agents. The harness equips the model with three read-only tools that return bounded payloads: a directory listing capped at two hundred entries, a file search capped at sixty matching lines, and a file reader that accepts specific line ranges.
const readFile = defineTool({
name: 'read_file',
description: 'Read a file with line numbers. Pass start_line and end_line to read a subset; a whole large file is cut at the cap. Read before you edit.',
schema: z.object({
path: z.string(),
start_line: z.number().int().min(1).optional(),
end_line: z.number().int().min(1).optional(),
}),
execute: async ({ path, start_line, end_line }) => {
const over = spend(); if (over) return over;
const abs = jail(path); if (abs instanceof Error) return abs.message;
if (!existsSync(abs)) return `Error: ${path} does not exist.`;
readThisRun.add(relative(ROOT, abs));
const lines = readFileSync(abs, 'utf-8').split('\n');
const from = (start_line ?? 1) - 1;
const to = end_line ?? lines.length;
const body = lines.slice(from, to).map((l, i) => `${from + i + 1}\t${l}`).join('\n');
return body.length > READ_CAP ? body.slice(0, READ_CAP) + `\n[cut at ${READ_CAP} chars; read a narrower range]` : body;
},
});
The tool description provides the operational contract evaluated by the model during routing. It specifies when to invoke the tool and how to parameterize arguments. In the execution body, the function increments the step budget, applies path jailing, prepends line numbers for subsequent citation in replacement operations, and caps the output at twelve thousand characters with an explicit notice instructing the model to request a narrower window. Crucially, the execution logic appends the resolved path to the readThisRun set, establishing an audit trail that mutation tools evaluate later. Every failure state returns a formatted error string rather than throwing an uncaught exception, keeping the execution loop alive and informing the model of the exact failure condition.
step three: plan mode writes one artifact and stops
In plan mode, the agent receives the three inspection tools and a single mutation tool: write_plan. This tool writes a markdown file into the plans/ directory and performs no repository edits. The plan artifact gates the rest of the lifecycle. The harness enforces this boundary by injecting approved: no into the document frontmatter:
execute: async (p) => {
const over = spend(); if (over) return over;
const unread = p.files.filter((f) => existsSync(resolve(ROOT, f)) && !readThisRun.has(relative(ROOT, resolve(ROOT, f))));
if (unread.length) return `Error: read these files with read_file before planning changes to them: ${unread.join(', ')}`;
mkdirSync(join(ROOT, 'plans'), { recursive: true });
const file = `plans/${new Date().toISOString().slice(0, 10)}-${p.slug}.md`;
// ... writes title, approved: no, verify, summary, files, steps, risks ...
return `Plan written to ${file}. Waiting for approval: the human edits the file and sets "approved: yes". Nothing else happens in this run.`;
},
The tool requires structured arguments: a slug, a summary, affected file paths, sequenced steps, potential risks, and the command required for verification. It actively rejects the submission if any listed target file was omitted from the readThisRun registry.
This programmatic constraint addresses a concrete failure mode observed during harness development. An earlier system prompt explicitly instructed the model: “never propose a change to a file you did not read.” During initial testing on the course repository, the model executed twenty-four tool calls, searched for the flag across tests/, lib/, and scripts/, inspected telemetry_admin.ts and syndicate_chat.ts, and generated an articulate four-step plan for scripts/direct_call.ts without ever reading that file. The output was stylistically convincing, yet technically ungrounded. The prompt instruction functioned only as a loose probabilistic hint. Enforcing the constraint in deterministic software eliminated the defect: with the four-line check implemented above, the subsequent run inspected the target file immediately and completed planning in eleven calls.
Execute the command in your terminal using that same task:
node --disable-warning=DEP0040 --experimental-strip-types scripts/mini_harness.ts plan "In scripts/direct_call.ts, add a --json flag: when present, print the final reply as a JSON object {model, prompt, reply} instead of plain text. Keep the existing behavior when the flag is absent."
The terminal displays a trace of each tool invocation alongside telemetry spans recording token consumption and response latency. In the second run, eleven tool calls and twelve model invocations expanded the active context window from 6,500 tokens to 26,000 tokens as the history accumulated. Once the process halts, inspect the generated file under plans/. The artifact from that benchmark run demonstrates the structure:
---
title: Add --json flag to direct_call script
approved: no
verify: node --disable-warning=DEP0040 --experimental-strip-types --check scripts/direct_call.ts && npm test
---
## files
- scripts/direct_call.ts
## steps
1. In scripts/direct_call.ts, update the docblock usage comments and parse the `--json` flag in the CLI argument parsing loop, setting a `jsonOutput` boolean flag and omitting `--json` from the prompt words.
2. In scripts/direct_call.ts, conditionally guard the initial console.log banners so they only print when `!jsonOutput`.
3. In scripts/direct_call.ts, initialize a `reply` string accumulator before the stream loop; if `jsonOutput` is true, accumulate `part.text` into `reply`, otherwise stream to `process.stdout.write`.
4. In scripts/direct_call.ts, after stream completion, output `JSON.stringify({ model, prompt, reply }, null, 2)` when `jsonOutput` is true.
## risks
- Stdout pollution: banner logs or intermediate token chunks printed in --json mode would break JSON consumers.
Every step specifies the target path and the exact structural modification, while the risks section highlights failure modes the test suite must exercise. Notice that the generated verify command contains arguments outside our CHECKS allowlist. Because the harness restricts execution strictly to npm test, you must strike unapproved commands during review. Examine the plan artifact sequentially, as shown in the Grok Bot project: verify each step, remove invalid proposals, and manually update approved: no to approved: yes. That explicit file mutation opens the gate.
step four: execute mode edits by exact replacement and checks from outside
Execute mode parses the plan artifact at startup and halts immediately if the approval marker is missing:
node --disable-warning=DEP0040 --experimental-strip-types scripts/mini_harness.ts execute plans/2026-09-03-direct-call-json-flag.md
Execute the command once before granting approval to verify the failure behavior. Then update the approval field and run the command again. Two modification tools govern changes across the repository, both deliberately constrained:
const strReplace = defineTool({
name: 'str_replace',
description: 'Replace ONE exact occurrence of old_text with new_text in a file. Fails if old_text is absent or appears more than once, so read the file first and quote it exactly.',
schema: z.object({ path: z.string(), old_text: z.string().min(1), new_text: z.string() }),
execute: async ({ path, old_text, new_text }) => {
const over = spend(); if (over) return over;
const abs = jail(path); if (abs instanceof Error) return abs.message;
if (!existsSync(abs)) return `Error: ${path} does not exist; use write_file for a new file.`;
if (!readThisRun.has(relative(ROOT, abs))) return `Error: read ${path} with read_file before editing it.`;
const text = readFileSync(abs, 'utf-8');
const count = text.split(old_text).length - 1;
if (count !== 1) return `Error: old_text occurs ${count} times in ${path}; it must occur exactly once.`;
writeFileSync(abs, text.replace(old_text, new_text));
return `Replaced one occurrence in ${path}.`;
},
});
The str_replace tool performs a single exact string substitution. It halts if the target text is absent or matches multiple locations in the file. This rule obligates the model to inspect the source lines beforehand and quote them precisely. The read-before-edit boundary operates within the tool code: an uninspected file cannot be modified, regardless of what the approved plan describes. Complementing this, write_file exclusively provisions new files and rejects overwrite attempts, eliminating accidental file truncations. Finally, run_check exposes verification through a single string argument restricted to allowlisted commands, returning the exit code and the terminal two thousand characters of stdout.
The trace below records the tool calls from the execution benchmark:
[tool] read_file scripts/direct_call.ts 1-100
[tool] read_file scripts/direct_call.ts 100-125
[tool] str_replace scripts/direct_call.ts (the usage docblock)
[tool] read_file scripts/direct_call.ts 80-115
[tool] str_replace scripts/direct_call.ts (the banner, guarded by !jsonOutput)
[tool] str_replace scripts/direct_call.ts (the stream loop, accumulating reply)
[tool] str_replace scripts/direct_call.ts (the JSON print after the loop)
[tool] read_file scripts/direct_call.ts 1-130
[tool] run_check test
Step 1: DONE Step 2: DONE Step 3: DONE Step 4: DONE
check: test exit: 0
[harness] 15 tool calls
[harness] git diff --stat
scripts/direct_call.ts | 27 ++++++++++++++++++++++-----
The agent performed four replacements, preceding each mutation with an inspection of the surrounding lines, ran the test suite, and generated the status summary. Notably, the prompt requested a concluding confirmation stating “Nothing outside the plan was changed”, which the model omitted. The harness design anticipates this limitation: rather than relying on ungrounded text generation, the harness executes git diff --stat directly upon loop exit. That deterministic output, confirming twenty-seven modified lines across a single file, provides verifiable proof. A textual assertion from the model remains an unverified claim.
step five: verify with your own hands, once
The harness completed the test run with exit code zero. Verification relies on facts generated outside the language model, and a process exit status is one such metric. You establish a second concrete verification point by invoking the modified feature directly in your shell:
node --disable-warning=DEP0040 --experimental-strip-types scripts/direct_call.ts --json "In five words, what is an agent?"
During testing, this invocation emitted a structured JSON payload containing the model identifier, the input prompt, and the resulting five-word completion, maintaining a clean stdout stream free of banner text, addressing the primary risk outlined in the plan artifact. Next, run npm test directly in your shell: sixty tests pass, with one test skipped, confirming the values reported by the agent tool. Once verified, run git checkout scripts/direct_call.ts to restore the repository working tree, retaining the plan document in plans/ as an audit record.
what the mini harness leaves out, which is the product
Two hundred lines of TypeScript establish the execution loop and its fences. Commercial coding harnesses expand upon this baseline across eight distinct architectural dimensions:
- Context management. The mini harness re-reads the full conversation history on every model invocation, allowing the context window to expand from 6,500 to 26,000 tokens across a single run. Production harnesses implement message compaction, rolling window eviction, and structured summarization to prevent context saturation.
- Permissions and operational modes. The mini harness implements two static modes separated by a manual frontmatter edit. Production harnesses introduce granular per-tool permission matrices, autonomous modes governed by external classification models, and pre-execution hooks that intercept unauthorized operations before dispatch.
- Filesystem isolation. The mini harness modifies the active working tree directly. Production harnesses isolate mutations inside ephemeral git worktrees, containerized sandboxes, or remote virtual environments, preventing destructive local side effects.
- Diff inspection. The mini harness outputs a summary diff statistic upon completion. Production environments integrate interactive visual diff reviewers that support line-by-line and hunk-by-hunk approvals.
- Tool integration reach. The mini harness implements six local tools. Production architectures dynamically expose hundreds of capabilities via the Model Context Protocol (MCP), augmenting them with project instruction files and custom capability scripts.
- Session memory. The mini harness discards all state upon process exit. Production harnesses maintain durable context through persistent project documentation, local memory stores, and cross-session configuration files maintained in the host repository.
- Parallelism and subagent delegation. Production harnesses dispatch subordinate agent processes across distinct context windows, orchestrating concurrent investigation tracks and consolidating findings into a primary plan.
- Model routing. The mini harness binds to a single model identifier passed through an environment variable. Production architectures dynamically route discrete subtasks across different models based on complexity, context requirements, and cost targets.
Evaluate production tools by analyzing how they implement these eight core mechanisms.
use a real harness, and choose it by the fences you need
Four production harnesses merit engineering consideration in 2026. They diverge primarily in execution runtime, supported model providers, and which architectural boundaries they prioritize. Product specifications reflect official documentation and release reporting as of early 2026:
Claude Code operates in the terminal, as a desktop application, in web browsers, and within extensions for VS Code and JetBrains IDEs. Its architectural control relies on four core elements detailed in the Claude Code project: a CLAUDE.md repository rules file, custom skills triggered by tool descriptions, deterministic hooks that enforce boundaries in code, and MCP servers configured via .mcp.json. It also includes subagent delegation and dedicated planning modes. It connects to Claude models using an Anthropic subscription or Console API key (quickstart). Select this system when you require scriptable terminal automation, code-enforced boundary hooks, and parity with this course’s repository infrastructure.
Cursor is an integrated development environment built as a fork of VS Code with embedded agent capabilities. Its Agent Mode performs edits directly inside the file editor with inline diff visualizers, Background Agents execute asynchronous tasks in isolated cloud sandboxes, BugBot handles pull request reviews, and MCP server access can be scoped individually per agent (a 2026 overview; the Cursor 3 review). Select Cursor when your primary workflow centers on the editor interface, when inline hunk-by-hunk diff review matches your governance needs, and when dynamic model selection across providers is essential.
Google Antigravity is an agentic development environment built upon VS Code architecture, separating workflows into an agent dispatch Manager and a code-review Editor. Antigravity structures agent outputs into Artifacts, which expand upon the planning pattern used in this project: structured task lists, file implementation plans, visual interface captures, and browser interaction logs. An integrated browser sub-agent executes end-to-end integration verifications autonomously (Google’s announcement; the getting-started codelab). It runs the Gemini 3 model family alongside third-party models. Select Antigravity when you require native browser verification loops and visual tracking of multi-agent task hierarchies.
OpenCode is an open-source development harness distributed under the MIT license, functioning across terminal, desktop, and IDE interfaces. It provides provider-agnostic model routing: using the Models.dev registry, it interfaces with more than seventy inference providers, including local instances via Ollama, incurring only direct token costs. Its runtime features specialized planning and execution agents, MCP extensibility, custom agent configurations, and plugin support (the OpenCode guide; a terminal-first overview). Choose OpenCode when you require full auditability of the harness source code, require local execution of open-weight models for data privacy, or require strict infrastructure independence.
| The fence in the mini harness | Claude Code | Cursor | Antigravity | OpenCode |
|---|---|---|---|---|
| the plan gate | plan mode, plan artifacts | Agent Mode with inline review | Artifacts: task lists, implementation plans | planning and execution agents |
| the boundary in code | hooks, permission modes | per-agent MCP scoping, rules | agent manager, verification sub-agent | agent config, plugins |
| verify from outside | build and test in the loop, a browser via tools | tests, BugBot on pull requests | browser sub-agent, recordings | shell and tests in the loop |
| reach | MCP, skills, subagents | MCP, background agents | MCP, browser | MCP, custom agents |
| where it runs, whose models | terminal, app, IDE; Claude | editor; multi-model | editor; Gemini 3 and others | terminal; any provider or local |
equip yourself with the materials
- The harness, complete, with the plan template, the two runs’ commands, and the cleanup line: mini-agent-harness.md.
- The setup for the harness you will use for real: the Claude Code project, whose four instruments are the same fences in configuration.
- The modules: the loop and the plan artifact, tools as contracts and boundaries in code, the runner.
apply it to your own ecosystem
The mini harness operates on any codebase because its tools interface directly with the standard filesystem. Execute it against your own repository before introducing a full commercial harness:
- Copy the script into your target project and configure a concrete verification command in the
CHECKStable:npm test,pytest,cargo test, or any command yielding a reliable exit code. - Invoke plan mode on a scoped change whose implementation details you already understand. Review the generated plan artifact against your knowledge of the codebase, noting how many files the model inspected before finalizing its proposal.
- Grant approval, trigger execution mode, and inspect the resulting state directly in your terminal. Audit
git diffrather than relying on the agent summary text. - Implement an additional fence required by your codebase: an extra entry in the
DENYarray, a reduced step budget, or a path boundary inwrite_filerestricting writes to a specific directory. - Select a production harness from the comparison matrix based on the fences your codebase demands, configuring it with equivalent rules files, validation hooks, and check commands.
Evaluate your productivity by measuring the number of interaction cycles required to move from an initial plan to a verified production commit. The reduction in developer overhead marks the exact value provided by a production harness, and you now understand every mechanism powering that loop.