STAGE 1 / MILESTONE 1
Harness Platform M1: closing the operator loop
How Stage 1, Milestone 1 turned the Harness Platform foundation into an operator loop with durable events, offline evals, policy compilation, a terminal viewer, and a CI exit gate.
01 / MILESTONE CONTRACT
The foundation could judge a run. M1 made the judgment operable.
Stage 1, Milestone 1 closes the first local operator loop: define a task, run its gate, retain typed evidence, inspect the result, and repeat the same proof in continuous integration.
M0 ended with useful contracts but a mostly momentary result. Sessions lived in memory, the TUI was a placeholder, evaluations were a directory, and no hosted workflow ran the harness against itself. M1 did not add a live coding model or distributed services. It made the existing kernel and exit gate observable enough to dogfood.
M1 — Operator loop
├── CI: test + typecheck + evals + harness exit gate
├── read-only terminal session/event viewer
├── first deterministic golden-kernel scenario
├── SQLite persistence for session evidence
├── CI-provided deliverables.pullRequest value
└── process.exec rule compiler
implementation shape
├── 5 task manifests (CI and PR evidence share one task)
├── 5 task commits + 1 documentation commit
└── generated reports and SQLite stay outside Git history02 / FROM FOUNDATION TO LOOP
The shared stage plan separates kernel, operator plane, protocols, and isolation.
The planning model used Pi, OpenCode, Goose, and OpenHands as architectural references, not literal nested dependencies. M0 established the Pi-like center: model protocol, kernel loop, tools, events, task schema, policy decisions, and a local exit gate. This M1 moves into the operator plane—sessions, policy compilation, evaluation, a terminal view, and CI.
| Layer in the plan | Public state at M1 | Later proof |
|---|---|---|
| Minimal kernel | Typed local loop, FakeModel, tools, budgets, events. | Live provider adapter after sandbox and eval credibility. |
| Operator plane | SQLite sessions, compiled policy, eval runner, viewer, CI. | Web task board, telemetry, approvals, restore. |
| Protocol membrane | MCP and ACP TypeScript shapes only. | Live MCP client and ACP server. |
| Execution substrate | Local CLI; service packages still report not ready. | Per-run container, scoped mounts, default-deny network. |
This ordering preserves the bootstrapping rule from the plan: the current version builds the next version on a task branch, produces reviewable evidence, and leaves the previous version intact until the change is accepted. M1 implements a local approximation of that cycle; verified pull-request delivery and an isolated sandbox are still ahead.
03 / FIVE DOGFOODED TASKS
Each capability was built through the contract it was improving.
The Pi session created a manifest and task branch for each workstream, ran tests and the exit gate, generated a report, then continued from that branch. The five implementation branches formed one linear chain and were fast-forwarded into main. A final documentation commit marked M1 complete.
88ef2f4 M0 public baseline
│
261cc88 m1-sessions-sqlite
│
6d86d0a m1-exec-rules
│
5ec9d93 m1-eval-scenarios
│
be8b298 m1-ci-gate + PR delivery input
│
702bfd7 m1-tui-viewer
│
a596fc5 ROADMAP + AGENTS documentation261cc88SQLite sessions, CLI event persistence, and report session identifiers.6d86d0aCompiled process rules and one decision table for CLI enforcement.5ec9d93Scenario DSL, deterministic runner, and the first golden-kernel case.be8b298GitHub Actions exit gate and caller-supplied pull-request evidence.702bfd7Terminal list, show, and report views over the new evidence.a596fc5Public roadmap and repository map updated for the completed milestone.
From the M0 baseline to the verified M1 commit, the chain changes 41 files and adds 40 tests. The generated tasks/runs/*.json reports and SQLite databases are intentionally ignored, so the public source proves the implementation and manifests—not the five private local run artifacts described by the development transcript.
04 / DURABLE EVENT EVIDENCE
SQLite turns a run’s event stream into something the next process can inspect.
packages/sessions now uses Node’s built-in node:sqlite. A session row owns identity and lifecycle state; an events table stores an ordered sequence of wire payloads. The API validates a frame through the existing event decoder before insertion and after retrieval, so malformed JSON, unknown versions, and unknown event types remain typed failures.
CREATE TABLE sessions (
session_id TEXT PRIMARY KEY,
task_id TEXT,
status TEXT NOT NULL CHECK (
status IN ('active', 'closed', 'archived')
),
created_at TEXT NOT NULL,
closed_at TEXT
);
CREATE TABLE events (
session_id TEXT NOT NULL REFERENCES sessions (session_id),
seq INTEGER NOT NULL,
event_id TEXT NOT NULL,
at TEXT NOT NULL,
actor TEXT,
type TEXT NOT NULL,
payload TEXT NOT NULL,
PRIMARY KEY (session_id, seq)
);const wire = serializeEvent(event)
deserializeEvent(wire) // validate before disk
const obj = JSON.parse(wire)
const row = this.db.prepare(
'SELECT COALESCE(MAX(seq), -1) + 1 AS next ' +
'FROM events WHERE session_id = ?'
).get(this.sessionId)
this.db.prepare(
'INSERT INTO events ' +
'(session_id, seq, event_id, at, actor, type, payload) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?)'
).run(
this.sessionId,
row.next,
obj.eventId,
obj.at,
obj.actor ?? null,
obj.type,
wire,
)
// Reads pass payload through deserializeEvent again.The exit gate emits task.updated and run.recorded, persists those same frames, then places the generated session identifier and database path in run-report/v1. The JSON report also carries the wire strings, so it remains inspectable even when SQLite persistence fails.
05 / OPERATOR VIEW
The “TUI” is intentionally a small terminal evidence viewer.
apps/tui replaces its placeholder with harness-view. The command lists stored sessions, shows ordered events with stable columns, or opens a JSON report and renders its metadata, deliverables, and decoded event stream. ANSI color is enabled only for an appropriate terminal and can be disabled; the formatting layer itself is pure and golden-testable.
# List newest sessions and their event counts.
node apps/tui/bin/view.js list
# Render one stored event stream, or emit raw JSON frames.
node apps/tui/bin/view.js show --session sess-…
node apps/tui/bin/view.js show --session sess-… --raw
# Render report metadata, deliverables, and decoded events.
node apps/tui/bin/view.js report tasks/runs/<report>.json- list
- Session ID, task, status, event count, and creation time.
- show
- One stored stream, with range and raw-frame options.
- report
- Validated run metadata, delivery fields, and embedded event evidence.
“Read-only” describes the command surface: it offers no mutation or execution command. It is not a full-screen interactive terminal application, and the show path currently opens SQLite through the schema-initializing read/write helper. A strict read-only database connection would make the implementation match the product claim more closely.
06 / FIRST GOLDEN EVALUATION
The first eval checks observable behavior, not private kernel structure.
The new runner loads a task manifest and a YAML scenario, scripts FakeModel, fixes timestamps and identifiers, then calls the real runAgent kernel. Event assertions use the documented data.<path> shape and match an ordered subsequence. Unknown event types and malformed invariant keys fail during scenario decoding.
id: kernel-0001-golden
uses_tasks:
- kernel-0001
script:
- content: >
Serialization of every kernel event is implemented
and round-trips cleanly.
expect:
run:
status: completed
steps: 1
toolCalls: 0
textContains: round-trips
emittedBudgetWarning: false
events:
- type: session.created
- type: agent.started
data.taskId: kernel-0001
data.model: fake-model/v1
- type: model.request
- type: model.response
data.finishReason: stop
- type: agent.stopped
data.status: completed$ pnpm evals
✓ kernel-0001-golden
task: kernel-0001
status: completed
steps: 1 · tool calls: 0
events: 5 · tokens: 37
1/1 scenarios passed07 / POLICY RULE COMPILER
Manifest patterns now become one reusable decision table.
M0 rescanned permission strings at each decision. M1 adds compileGlob and compileRules. Each pattern produces an anchored regular-expression source and lazily caches its matcher on first use. The longest matching pattern wins; equal lengths prefer deny over ask over allow. A configured subject map without a match or fallback is closed by default.
const execDecision = compileRules(manifest.permissions).decide(
'process.exec',
args.testCommand ?? DEFAULT_TEST_COMMAND,
)
// Matching contract
// 1. most-specific matching pattern wins
// 2. equal specificity: deny > ask > allow
// 3. configured subject map, no match or fallback: deny
// 4. action with no rule: ask
if (execDecision.effect === 'deny') {
outcome = 'blocked'
push(createEvent(
'policy.decision',
{
action: 'process.exec',
subject: args.testCommand ?? DEFAULT_TEST_COMMAND,
effect: 'deny',
reason: execDecision.reason,
},
eventOpts(),
))
}
// Current M1 caveat: headless "ask" also reaches execution.The CLI now asks this compiled table whether its test command may execute. That resolves the policy-layer design question; it does not create a process, filesystem, or network sandbox. The future sandbox runner is still responsible for enforcing those operating system boundaries.
08 / CONTINUOUS INTEGRATION
GitHub Actions now runs the same evidence path on Node 22.
One gate job installs from the frozen lockfile, typechecks the workspace, runs all tests, runs the first eval, executes kernel-0001 through the harness, and uploads tasks/runs even after a failed step. Because the harness uses pnpm test as its default quality command, the 80-test suite runs once as a direct workflow step and once inside the exit gate.
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm test
- run: pnpm evals
- run: >-
pnpm harness run tasks/kernel-0001.yaml
--branch "${{ github.head_ref }}"
- uses: actions/upload-artifact@v4
if: always()
with:
name: gate-evidence-${{ github.run_id }}
path: tasks/runs/The hosted CI run at the verified commit is green on Node 22. Its evidence artifact contains a JSON report and SQLite database. The workflow log proves 80 passing tests; the JSON report omits those numeric totals because ANSI output defeated its summary parser.
There is a second boundary: the CLI’s changedPaths() reads only git status --porcelain. On a clean CI checkout it sees no dirty paths, even though the pull request may contain committed changes. Tests and evals exercise the PR code, but allowed_paths does not yet compare the PR diff to a base commit. This is a regression gate—not a complete per-PR scope gate.
09 / DELIVERY EVIDENCE
The report records a supplied pull-request value and refuses to invent one.
A pull-request workflow constructs its own GitHub URL and passes it through HARNESS_PULL_REQUEST_URL. Local callers can use --pr-url. The CLI trims and records that non-empty string; when none exists, a passing run records a branch label instead.
const supplied = (
args.prUrl ?? process.env.HARNESS_PULL_REQUEST_URL ?? ''
).trim()
deliverables: {
pullRequest:
supplied.length > 0
? supplied
: outcome === 'passed'
? `branch: ${branch}`
: undefined,
reportPath,
sessionId,
artifacts: sessionId ? ['tasks/runs/sessions.sqlite'] : [],
}This is honest about absence, but it is not proof that the string names a real pull request. The field accepts any non-empty value, and the recorded development URLs such as /pull/123 were simulations. The public repository had no pull requests at verification time. The schema field is camelCase deliverables.pullRequest, despite the earlier roadmap shorthand pull_request.
10 / THE COMPLETED LOOP
The milestone’s value is composition, not any one package.
A manifest now drives the local gate. The gate makes a compiled command decision, checks the current working tree, runs the quality command, serializes a report, and attempts to persist the same event evidence in SQLite. The terminal viewer reads that result. The eval runner independently checks the kernel’s observable contract. GitHub Actions composes all of those pieces and retains the generated artifacts.
task YAML
↓
harness validate / run
├── branch label
├── dirty working-tree path check
├── compiled process.exec decision
└── pnpm test
↓
run-report/v1 JSON + SQLite session
↓
harness-view + CI artifact
parallel: scenario YAML + FakeModel → kernel → event invariantsM0 made the harness capable of saying “passed,” “failed,” or “blocked.” M1 gives that answer a memory, a viewer, a calibration case, and a hosted place to run.
11 / WHAT BROKE
The most useful failures tested the boundaries being built.
Generated evidence correctly blocked its own task.
Reusing a temporary repository left run artifacts outside the next manifest’s scope. A later CI simulation redirected out.txt inside the repository and was blocked again. Fresh fixtures and output outside the worktree fixed the tests; the policy gate was doing its job.
Human-readable YAML was not automatically valid YAML.
Acceptance bullets beginning with Markdown backticks failed parsing, and a dotted scenario identifier violated the declared kebab-case schema. The manifests were rewritten and the scenario became kernel-0001-golden.
A pretty shell pipeline hid a failed check.
An early TUI verification printed success because the pipeline returned tail’s status. The session caught the false positive and reran using pipeline status inspection, proving typecheck exit zero and 80 passing tests.
Deterministic rendering exposed small interface mistakes.
The viewer initially had an invalid terminal import, unchecked arguments, spacing mismatches, and a fixture that expected an event it had never created. Pure render functions made each correction narrow and testable.
12 / VERIFIED RESULT
The public commit passes locally and in hosted CI.
$ pnpm install --frozen-lockfile
# 18 workspaces · lockfile accepted
$ pnpm typecheck
# exit 0
$ pnpm test
Test Files 10 passed (10)
Tests 80 passed (80)
$ pnpm evals
1/1 scenarios passed
$ for task in tasks/*.yaml; do pnpm harness validate "$task"; done
# 6/6 manifests valid: kernel-0001 + five M1 tasks
$ pnpm harness run tasks/kernel-0001.yaml --branch tasks/kernel-0001
# passed · JSON report + SQLite session · 2 events
$ node apps/tui/bin/view.js list
$ node apps/tui/bin/view.js show --session <session-id>
$ node apps/tui/bin/view.js report <report>
# every command exited 0 against the generated evidenceI reran the locked repository checks from a fresh clone at a596fc5. The host globally signs commits, which initially broke seven CLI test fixtures that create temporary repositories; disabling inherited signing for that verification command—not changing project code—produced the 80/80 result. The canonical GitHub Actions run passed in a clean Node 22 environment. A CodeQL run is also green, and the API audit found zero open CodeQL alerts.
The independent harness run produced one valid JSON report and a SQLite session with two events; harness-view list, show, and report all returned zero against that evidence. The public CI artifact independently exposes the same two-file shape.
13 / CURRENT TRUTH
M1 is an inspectable local operator loop, not a production agent platform.
| Boundary | What exists now | Next proof |
|---|---|---|
| Required CI | The workflow is green, but main currently has no branch protection or ruleset. | Enable a rule that requires the gate job before merge. |
| PR path scope | changedPaths reads Git status. A clean CI checkout reports zero changed paths. | Compare the pull-request head against an explicit trusted base commit. |
| Task branches | Branch names are created or recorded; checkout and one-PR-per-task are not fully enforced. | Verify the checked-out ref and delivery relationship instead of trusting a label. |
| Session durability | SQLite assumes one writer; passed sessions remain active; persistence failure does not fail the gate. | Use transactional sequence allocation, close sessions, and define whether evidence is mandatory. |
| Viewer safety | Commands expose no writes, but show opens the database through a schema-initializing read/write helper. | Open an existing database in strict read-only mode for every viewer path. |
| Permission ask | The compiler returns ask safely, but the current headless CLI only blocks deny and executes ask. | Make headless ask block unless an explicit approval artifact exists. |
| Evaluation depth | One scripted FakeModel turn checks five events; no provider, tools, or golden repository is exercised. | Land the M2 calibration repository, SDK-owned DSL, and representative scenarios. |
| Platform scope | Web, agent server, sandbox runner, and control plane remain placeholders; MCP is types only. | Keep M1 described as a local operator loop, not a production agent platform. |
14 / FILE GUIDE
Where to follow the operator loop.
CONTRACT + GATE
Start with the task boundary
EVIDENCE
Follow a stored run
EVALUATION
Follow the golden kernel
POLICY
Follow a command decision
15 / WHAT IS NEXT
M2 should make the evidence representative—and tighten what M1 exposed.
The harness can now leave evidence about itself. The next milestone is to make that evidence difficult to fool, broad enough to compare, and required where it matters.
PRIMARY SOURCES
Evidence used for this note.
- Harness Platform at verified M1 commit a596fc5
- Public M1 contract and M2 roadmap
- Hosted Node 22 CI run
- Uploaded gate evidence artifact
- Hosted CodeQL run
- Node 22 SQLite API documentation
- GitHub protected-branch and required-check documentation
The user-supplied Pi transcript and shared planning conversation establish development chronology, intent, and the recorded local experiments. Public source at the pinned commit, hosted workflow output, and an independent fresh-clone audit establish the implementation claims. Simulated pull-request URLs and gitignored local reports are not presented as public delivery evidence.