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.

Five-layer harness inspiration stackFive horizontal layers run from the execution boundary at level zero to the product surface at level four.L4 · Product surfaceWeb · TUI · APIL3 · OrchestrationAgents · permissions · sessionsL2 · Agent kernelLoop · models · tools · streamingL1 · Capability adaptersMCP · ACP · external toolsL0 · Execution boundaryFilesystem · terminal · sandbox
The original inspiration stackThe stack was a planning lens informed by four reference projects. It does not mean those projects were imported into the repository.

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:

  1. 01

    One language until measurement says otherwise

    TypeScript, Node 22 or newer, and pnpm workspaces across every layer.

  2. 02

    Offline determinism before a live model adapter

    A scripted FakeModel would make the kernel testable without network or credentials.

  3. 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.

Session launchshell
# Start Pi with Ollama as its model provider
ollama launch pi

# The interactive picker showed this model for the session
qwen3.8:27b

The 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.

M0 repository maptext
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}.md

At 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.

Current quick startshell · source
pnpm install
pnpm test
pnpm typecheck
pnpm harness validate tasks/kernel-0001.yaml
pnpm harness run tasks/kernel-0001.yaml

06 / 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.

tasks/kernel-0001.yamlyaml · source
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.
Task manifest fan-outA central task manifest connects to policy, scheduler, CLI and UI, and audit and evaluation consumers.POLICYSCHEDULERCLI / UIAUDIT / EVALSTASK MANIFESTgoal · acceptance · pathspermissions · budget · delivery
One contract, several future consumersThe manifest is intended to become the shared input to policy, scheduling, operator interfaces, and evidence. In M0, the CLI and policy slice are the working part.

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.

Canonical event envelopejson · source
{
  "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.created
  • agent.started
  • agent.stopped
  • model.request
  • model.response
  • tool.call
  • tool.result
  • task.updated
  • budget.warning
  • policy.decision
  • run.recorded
  • error

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.

Ordered deserialization gatesTypeScript · source
// The decoder fails at a precise boundary.
JSON.parse(raw)                         // EventParseError
supportedVersions.includes(event.v)    // EventVersionError
isEventType(event.type)                 // UnknownEventTypeError
eventSchemas[event.type].safeParse(...) // EventSchemaError

08 / 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.

Condensed FakeModel behaviorTypeScript · source
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:

Kernel input and outputtext
goal + model + tools + budget
                │
                ▼
        runAgent(options)
                │
       ┌────────┴────────┐
       ▼                 ▼
typed event stream   final text

On 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.

Kernel model and tool sequenceThe operator gives the kernel a goal. The kernel calls the model, optionally executes a tool, returns the tool result to the model, and delivers a final answer with events.OPERATORKERNELMODELTOOLgoal + budgetmodel.requestmodel.response + tool callvalidated tool.calltool.resultresult added to contextfinal text + event stream
The local kernel loopThe model can return a final answer or request a tool. Every request, response, call, result, warning, and stop becomes a typed event.

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.

Dirty-path scope gateTypeScript · source
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.

Dogfooding timelineThe first run was blocked by path policy, defects were fixed and the base committed, and a later clean run passed forty tests with no policy violations.BLOCKEDscaffold escapedallowed_pathsCORRECTEDpolicy truth · typesevents · test parserPASSED0 violations40 / 40 testsSTRUCTURED EVIDENCErun-report/v1 · exit code 0
Blocked, corrected, then passedThe most valuable result was not the green check. It was the policy gate refusing a working tree that exceeded its declared scope.
Condensed passing reportjson
{
  "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.

01

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.

02

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.

03

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.

04

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.

05

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.

06

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.

73tracked files
3,847lines excluding lockfile
6test files
40 / 40tests passing
Independent verificationtext
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.

  1. 1c17c71M0 foundation: events, kernel, policy, SDK, CLI, packages, docs, and infrastructure scaffold.
  2. d2783c4Exit-gate fixes: path policy, event types, FakeModel accounting, kernel stop behavior, and pnpm build approval.
  3. c831d57Report accuracy: parse Vitest’s Tests line instead of mistaking six test files for six tests.
  4. 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.

AreaWorking nowNext contract
Events12 validated schemas and a typed wire decoderDurable storage and cross-process transport
KernelLocal model/tool loop with event and budget handlingProvider adapter and sandbox execution
ModelDeterministic offline FakeModelOllama/OpenAI-compatible adapter
PolicyPure allow/ask/deny decisions and dirty-path checksRuntime enforcement and approval flow
StateIn-memory append-only session logSQLite, then Postgres
InterfacesWorking CLI exit gateRead-only TUI, task board, then interactive clients
ProtocolsMCP and ACP TypeScript shapesLive MCP client and ACP server
OperationsDocker/MinIO development filesOTel, 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.

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.

  1. 01Make the exit gate a required CI check.
  2. 02Compare the task branch with its base, not only Git status.
  3. 03Persist the typed event stream in SQLite.
  4. 04Run one golden-repository evaluation end to end.
  5. 05Wire a real model only after those controls are observable.

SOURCES + PROVENANCE

Primary material used for this note.

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.

CONTINUE EXPLORING

See the system, then watch it change.

The repository is public, and the next milestone will become the next build note.

Open Harness Platform