DAY 001 / HARNESS FROM SCRATCH
Harness from Scratch: building the system that checks its own work
How I used Pi and a local Qwen model to bootstrap a TypeScript agent harness whose own task contract can block unsafe changes and produce evidence.
01 / THE QUESTION
Why build a harness when good coding agents already exist?
I had just learned to see a coding agent as more than a chat window. The model is only one component. A harness supplies the loop, context, tools, permissions, workspace, event history, budgets, and delivery rules that turn model output into controlled work.
Pi, OpenCode, Goose, and OpenHands each made a different boundary visible. My goal was not to clone all four. I wanted to build a small vertical slice so I could understand where a harness earns trust: at the contracts between intention, execution, and evidence.
The learning method came from Linux From Scratch. Instead of beginning with a finished platform, I would assemble one layer at a time, keep every boundary inspectable, and make the project operate on itself as early as possible. The working name became Harness from Scratch.
02 / THE MENTAL MODEL
Four projects helped me separate the layers.
I used the projects below as design references, not dependencies. The repository produced on this day does not import their code or claim compatibility with them.
Pi
The smallest useful center: an interactive coding agent, provider-neutral model API, tool calls, state, and a terminal loop. Pi also ran this bootstrap session.
OpenCode
A reference for named agents, permission modes, subagent work, session-oriented operation, and product surfaces around the loop.
Goose
A reference for extensible capabilities and open protocols, especially tools arriving through MCP and clients connecting through ACP.
OpenHands
A reference for the lower operational boundary: agent servers, workspaces, isolated execution, automations, and interfaces that can switch between backends.
That first five-level sketch was an exploration map. The repository’s final architecture contract simplifies it to contracts, execution, services, and interfaces. That is the more useful boundary because it describes code that can be tested.
03 / THE FIRST CUT
I reduced the platform to one M0 exit gate.
The initial idea included a web app, TUI, control plane, agent server, sandbox runner, SQLite, Postgres, object storage, observability, MCP, ACP, Docker, and eventually Kubernetes. Building all of that first would have produced a wide scaffold with no proof that the central contracts worked.
So I imposed three constraints:
- 01
One language until measurement says otherwise
TypeScript, Node 22 or newer, and pnpm workspaces across every layer.
- 02
Offline determinism before a live model adapter
A scripted FakeModel would make the kernel testable without network or credentials.
- 03
A manifest in and a report out
The first milestone would accept a task contract, evaluate an existing working tree, run verification, and emit machine-readable evidence.
04 / BOOTSTRAPPING WITH PI
Pi was the harness; Qwen was the model.
I launched Pi through Ollama and selected the model label shown in the session. Pi supplied the interactive harness—context, read/write/edit/bash tools, and the turn loop. Qwen supplied model output. The files and commits were their result, but the new repository does not embed either Pi or Qwen.
# Start Pi with Ollama as its model provider
ollama launch pi
# The interactive picker showed this model for the session
qwen3.8:27bThe long bootstrap prompt described the monorepo, task schema, events, fake model, policy engine, tests, documentation, and infrastructure direction. I also gave it an explicit finish line: do not stop at a file tree; demonstrate the task manifest and report pipeline.
The session itself ran on Node 24.18.0 and pnpm 11.15.1. The repository deliberately targets Node 22 or newer. Ollama now documents the same ollama launch pi integration and Pi’s core coding tools.
05 / REPOSITORY SHAPE
The monorepo separates contracts from products and processes.
I wanted the kernel to remain a local library rather than become a microservice. Apps can present the work; services can schedule and isolate it; packages own the portable contracts. That keeps the part most likely to be evaluated—goal, model, tools, budget, events—small enough to reason about.
harness-platform/
├── apps/
│ ├── cli/ # working validate/run exit gate
│ ├── tui/ # typed placeholder
│ └── web/ # typed placeholder
├── services/
│ ├── agent-server/ # roadmap placeholder
│ ├── control-plane/ # roadmap placeholder
│ └── sandbox-runner/ # roadmap placeholder
├── packages/
│ ├── events/ # schemas + wire serialization
│ ├── kernel/ # local model/tool loop
│ ├── models/ # interface + FakeModel
│ ├── tools/ # validated tool registry
│ ├── policy/ # pure permission decisions
│ ├── sdk/ # manifest in + report out
│ ├── sessions/ # in-memory append-only log
│ ├── workspace/ # lexical path scoping
│ ├── mcp/ # contract types
│ └── acp/ # contract types
├── tasks/ # YAML contracts + ignored reports
├── evals/ # future scenarios and golden repos
├── skills/ # platform-builder operating guide
├── infra/ # Docker + MinIO development files
└── {ARCHITECTURE,EVENTS,ROADMAP,SECURITY}.mdAt the verified public commit, the repository contains 73 tracked files and 3,847 lines excluding the lockfile. The breadth is real, but maturity is uneven by design: the CLI and core packages work; the TUI, web app, and three services are placeholders that declare future boundaries.
pnpm install
pnpm test
pnpm typecheck
pnpm harness validate tasks/kernel-0001.yaml
pnpm harness run tasks/kernel-0001.yaml06 / TASK CONTRACT
The manifest became the spine of the design.
A natural-language request is useful to a model, but it is a weak contract for a runner. I wanted the same input to express scope, proof, permissions, resource ceilings, and the expected delivery form. That became tasks/kernel-0001.yaml.
id: kernel-0001
title: Add agent event serialization
goal: >
Implement JSON serialization and deserialization for all kernel events.
acceptance:
- All event variants round-trip without data loss
- Unknown event versions return a typed error
- Unit and integration tests pass
allowed_paths:
- packages/events/**
- packages/kernel/**
- evals/**
permissions:
fs.read: allow
fs.write: ask
process.exec:
"pnpm test*": allow
"pnpm lint*": allow
"*": deny
network: deny
git.push: deny
budget:
max_model_tokens: 100000
max_tool_calls: 100
delivery:
type: pull_request- Goal + acceptance
- Human intent and observable completion criteria.
- Allowed paths
- The portion of the working tree this task is permitted to change.
- Permissions
- Declared allow, ask, and deny decisions for files, commands, network, and Git.
- Budget
- Maximum model tokens and tool calls for a future integrated run.
- Delivery
- The intended artifact—in this case, a pull request.
07 / EVENTS
Events are the common language between replaceable parts.
If the kernel can run locally, in a sandbox, or inside an evaluation, its observable output cannot depend on a particular UI or database. I used a fixed envelope and twelve discriminated payload schemas.
{
"v": 1,
"type": "agent.started",
"eventId": "evt-…",
"at": "2026-08-30T12:00:00.000Z",
"actor": "kernel",
"data": {
"agentId": "agent-…",
"sessionId": "sess-…",
"model": "fake-model/v1"
}
}session.createdagent.startedagent.stoppedmodel.requestmodel.responsetool.calltool.resulttask.updatedbudget.warningpolicy.decisionrun.recordederror
The decoder is intentionally staged. Invalid JSON, an unsupported envelope version, an unknown event type, and an invalid payload are different operational failures. A caller might retry one, migrate another, quarantine a third, and treat the last as a producer bug.
// The decoder fails at a precise boundary.
JSON.parse(raw) // EventParseError
supportedVersions.includes(event.v) // EventVersionError
isEventType(event.type) // UnknownEventTypeError
eventSchemas[event.type].safeParse(...) // EventSchemaError08 / DETERMINISTIC CORE
A fake model made the real loop testable.
The first model implementation is deliberately not Ollama. FakeModel replays a queue of scripted turns, records requests, estimates tokens deterministically, and returns a fixed acknowledgement when the queue is empty. That makes tool calls and budget edges reproducible offline.
const turn =
this.queue.shift() ??
{ content: `[fake-model] ack #${seq}` }
const promptTokens = estimateTokens(prompt)
const completionTokens = estimateTokens(scriptedCompletion)
return {
content: turn.content ?? "",
toolCalls: turn.toolCalls ?? [],
usage: {
promptTokens,
completionTokens,
totalTokens: promptTokens + completionTokens,
},
}The kernel contract then stays narrow:
goal + model + tools + budget
│
▼
runAgent(options)
│
┌────────┴────────┐
▼ ▼
typed event stream final textOn each turn, the kernel emits a model request, calls the model, records usage, and emits a response. With no tool calls it stops successfully. With tool calls it validates arguments against the tool’s Zod schema, executes the registered tool, emits a typed result, adds that result to context, and continues. Token and tool-call ceilings can warn and stop the run.
09 / POLICY + EVIDENCE
The harness’s best moment was refusing its own work.
The policy package is pure: given a permission map, action, and optional subject, it returnsallow, ask, or deny. More-specific patterns win. For paths, * stays within one segment while ** can cross directories.
The CLI combines that decision logic with Git status, tests, and report generation. Its pipeline is schema → branch → dirty paths → command policy → tests → evidence.
const changed = changedPaths(cwd)
const violations = changed.filter(
(path) =>
path !== relManifest &&
!pathAllowed(manifest.allowed_paths, path),
)
const policyCheck = {
ok: violations.length === 0,
violations,
}The first real run was blocked. The new scaffold touched root, app, service, documentation, and infrastructure files while the task allowed only packages/events/**, packages/kernel/**, and evals/**. That was not a nuisance. It was the first evidence that the contract could contradict the builder and win.
After the base scaffold was committed, the task branch was cleaned up, type and event bugs were fixed, and the report parser was corrected, a later run passed with no dirty paths and no policy violations.
{
"schema": "run-report/v1",
"status": "passed",
"branch": "tasks/kernel-0001",
"policy": {
"changedPathsOk": true,
"changedPaths": [],
"violations": []
},
"tests": {
"command": "pnpm test",
"exitCode": 0,
"ok": true,
"total": 40,
"passed": 40,
"failed": 0
}
}10 / WHAT BROKE
The failures improved the design more than the initial scaffold did.
The policy result lied
Violations were calculated correctly, but ok was hard-coded to true. A rogue Dockerfile reproduction exposed it. The fix tied truth to the data: ok: violations.length === 0.
The model’s usage total was zero
FakeModel originally failed to add prompt and completion estimates. That undermined budget tests, so accounting became an explicit part of every deterministic turn.
A successful agent stopped twice
The kernel emitted agent.stopped inside the successful branch and again after the loop. A done state made the terminal event singular.
The task contract looked like an illegal change
The manifest itself appeared in Git status but was not inside its own allowed paths. The runner now exempts that input while still checking every output path.
The report counted files, not tests
The first green report extracted “Test Files 6” and called that the test total. The parser now prefers Vitest’s “Tests 40” line. Evidence is only useful when it names the right unit.
The package manager enforced its own boundary
pnpm 11 blocked the esbuild install script until the workspace explicitly approved it with allowBuilds. That accidental lesson matched the project: defaults should make execution visible, not magical.
More fixes from the session
Other iterations corrected an invalid z.record().passthrough() call, literal types written as false as const inside interfaces, a queue field colliding with a method, event generics resolving to never, a mistaken glob expectation, relative manifest resolution, test Git repositories with no initial file, and a commit that landed on the wrong branch.
These are not all equally important. Together they show why an agent harness needs deterministic feedback and explicit evidence instead of relying on a convincing final message.
11 / VERIFIED RESULT
What I can substantiate at the public commit.
Test Files 6 passed (6)
Tests 40 passed (40)
$ pnpm typecheck
$ tsc -p tsconfig.json
# exit 0
$ pnpm harness validate tasks/kernel-0001.yaml
valid task manifest: kernel-0001 "Add agent event serialization"I re-cloned the public repository at 88ef2f4, installed its locked dependencies, and reran the checks. The test fixtures inherit a developer’s global Git signing setting, so I disabled signing for their temporary repositories; with that environment isolation, all 40 tests passed. Type checking and manifest validation passed directly.
1c17c71M0 foundation: events, kernel, policy, SDK, CLI, packages, docs, and infrastructure scaffold.d2783c4Exit-gate fixes: path policy, event types, FakeModel accounting, kernel stop behavior, and pnpm build approval.c831d57Report accuracy: parse Vitest’s Tests line instead of mistaking six test files for six tests.88ef2f4Public README and quick-start documentation.
12 / CURRENT TRUTH
M0 is a foundation, not the finished platform.
It would be easy to turn the directory names into a claim that the platform already has a distributed control plane, secure sandboxes, durable sessions, live protocols, and a web console. It does not. The table keeps working code and roadmap work separate.
| Area | Working now | Next contract |
|---|---|---|
| Events | 12 validated schemas and a typed wire decoder | Durable storage and cross-process transport |
| Kernel | Local model/tool loop with event and budget handling | Provider adapter and sandbox execution |
| Model | Deterministic offline FakeModel | Ollama/OpenAI-compatible adapter |
| Policy | Pure allow/ask/deny decisions and dirty-path checks | Runtime enforcement and approval flow |
| State | In-memory append-only session log | SQLite, then Postgres |
| Interfaces | Working CLI exit gate | Read-only TUI, task board, then interactive clients |
| Protocols | MCP and ACP TypeScript shapes | Live MCP client and ACP server |
| Operations | Docker/MinIO development files | OTel, isolated runners, S3 and later Kubernetes |
The public roadmap names the sequence honestly: CI and SQLite first, then evaluation credibility and a web task board, followed by a real agent server, sandbox runner, provider adapter, control plane, Postgres, object storage, and only then a Kubernetes decision.
13 / FILE GUIDE
Where to read the implementation.
CONTRACTS
Start with the boundaries
CORE LOOP
Then follow one run
GATES
Inspect how work is judged
14 / NEXT DEVELOPMENT LOG
The next useful milestone is not more scaffolding.
The project now needs to make the two working slices meet. The operator loop should run in CI against a real base-branch diff, treat headless ask as blocked, persist events in SQLite, and execute the first evaluation scenario. After that, a real provider adapter can connect the manifest, kernel, policy decisions, and report into one auditable run.
SOURCES + PROVENANCE
Primary material used for this note.
- Harness Platform at verified commit 88ef2f4
- Ollama’s Pi integration documentation
- Pi Agent Harness repository
- OpenCode repository
- Goose repository
- OpenHands repository
Build-session chronology comes from my attached local Pi transcript. Repository claims were checked against the public commit above and rerun locally on 30 August 2026.