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.

Stage 1 / Milestone 1 scopetext · source
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 history

02 / 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 planPublic state at M1Later proof
Minimal kernelTyped local loop, FakeModel, tools, budgets, events.Live provider adapter after sandbox and eval credibility.
Operator planeSQLite sessions, compiled policy, eval runner, viewer, CI.Web task board, telemetry, approvals, restore.
Protocol membraneMCP and ACP TypeScript shapes only.Live MCP client and ACP server.
Execution substrateLocal 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.

Public M0 → M1 commit chaintext
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 documentation
  1. 261cc88SQLite sessions, CLI event persistence, and report session identifiers.
  2. 6d86d0aCompiled process rules and one decision table for CLI enforcement.
  3. 5ec9d93Scenario DSL, deterministic runner, and the first golden-kernel case.
  4. be8b298GitHub Actions exit gate and caller-supplied pull-request evidence.
  5. 702bfd7Terminal list, show, and report views over the new evidence.
  6. 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.

Harness Platform M1 operator loopA task manifest leads to implementation on a task branch, then to the exit gate. The gate writes a run report and SQLite session. A terminal viewer and continuous integration expose that evidence to the operator, who starts the next task.ONE TASK · ONE CONTRACT · ONE EVIDENCE TRAIL01TASK MANIFESTgoal · paths · policy02TASK BRANCHimplementation + tests03EXIT GATEscope · exec · test04REPORT + SESSIONJSON + SQLite events05OPERATOR VIEWviewer · CI artifact06NEXT DECISIONaccept · fix · continuelocal task loop: five manifests · five branches · five generated reportspublic CI: one regression workflow + uploaded gate evidencenot yet: committed PR diff → allowed_paths comparison
The operator loop closes around evidenceEach milestone task starts as a manifest, changes a task branch, passes the exit gate, and leaves a report plus session evidence for an operator or CI to inspect. The loop is real; pull-request diff scoping is still future work.

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.

Session evidence schema (condensed)sql · source
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)
);
Validate → sequence → appendtypescript · source
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.

SQLite session evidence flowThe exit-gate CLI emits task-updated and run-recorded events. Wire validation protects a SQLite sessions table and ordered events table. The JSON report stores the same event strings and the session identifier. The terminal viewer reads reports or stored sessions.EXIT-GATE CLIevent stringsWIRE BOUNDARYvalidate on write + readSQLITEone writer per filesessionsid · task · statuscurrent CLI leaves activeeventssession + sequenceserialized payloadRUN REPORTevents[] + sessionIdbranch / supplied PR stringHARNESS-VIEWlist · show · reportread-only commandsgenerated database and reports are CI artifacts / local files, not tracked source
One event stream, two inspectable artifactsThe CLI validates every serialized event before SQLite write and again after read. A report references the session, while the terminal viewer can render either source. The current store assumes one writer and leaves successful sessions active.

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.

Operator commandsshell · source
# 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.

evals/scenarios/kernel-0001-golden.yamlyaml · source
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
Golden kernel evaluation pipelineScenario YAML and the kernel task manifest enter a deterministic runner. A scripted fake model, fixed timestamps, and counting identifiers drive the real kernel. Five events and a run summary are checked against public invariants.INPUTSSCENARIO YAMLscript + observable invariantsTASK MANIFESTgoal + budgetGOLDEN RUNNERFakeModel · one scripted turnfixed clock · counting IDsREAL KERNELrunAgent(options)EVENT STREAMfive typed eventsRUN SUMMARY1 step · 0 tools · 37 tokensINVARIANT CHECKERrun status + exact counts + ordered event subsequence1 / 1 scenario passed · no provider · no network · no golden repository yet
The first eval measures the kernel contractThe first scenario loads kernel-0001, scripts one FakeModel response, fixes clocks and IDs, runs the real kernel, and checks the observable event stream as an ordered subsequence. It is a deterministic calibration seed—not broad model evaluation.
Independent evaluation resulttext
$ pnpm evals
✓ kernel-0001-golden
  task: kernel-0001
  status: completed
  steps: 1 · tool calls: 0
  events: 5 · tokens: 37

1/1 scenarios passed

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

Compiled decision contracttypescript · source
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.
Compiled Harness policy decisionsPermission patterns from a task manifest are translated into anchored matchers. A command is matched by specificity, with deny winning a tie. The resulting allow, ask, or deny decision feeds the current CLI and a future sandbox runner.MANIFEST RULESpnpm test* → allowgit push* → deny* → denyCOMPILE RULESanchored pattern matchermost specific pattern winsties: deny › ask › allowDECISIONallowaskdenyCLI · M1blocks deny · runs allowcurrently also runs askSANDBOX RUNNER · M3process + filesystem + networkenforcement boundaryunknown action → ask · unmatched subject without fallback → denycompilation decides; it does not create an operating-system sandbox
Policy is compiled once, then enforced elsewhereManifest rules become reusable matchers with most-specific-wins and deny-over-ask-over-allow tie-breaking. The CLI consumes the decision table today; process and network isolation remain a future sandbox-runner responsibility.

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.

.github/workflows/ci.yaml (condensed)yaml · source
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.

Report delivery selection (condensed)typescript · source
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.

M1 implemented data flowtext
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 invariants

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

01

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.

02

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.

03

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.

04

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.

5M1 task manifests
80 / 80tests passing
1 / 1golden scenarios
2gate events persisted
Independent repository verificationtext
$ 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 evidence

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

BoundaryWhat exists nowNext proof
Required CIThe workflow is green, but main currently has no branch protection or ruleset.Enable a rule that requires the gate job before merge.
PR path scopechangedPaths reads Git status. A clean CI checkout reports zero changed paths.Compare the pull-request head against an explicit trusted base commit.
Task branchesBranch 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 durabilitySQLite 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 safetyCommands 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 askThe 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 depthOne 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 scopeWeb, 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.

15 / WHAT IS NEXT

M2 should make the evidence representative—and tighten what M1 exposed.

  1. 01Require the hosted gate with a branch ruleset and compare the real PR diff.
  2. 02Block headless ask, close sessions, and decide whether persistence is mandatory.
  3. 03Calibrate against the hello-service golden repository with the DSL in the SDK.
  4. 04Add the minimal web task board and end-to-end OpenTelemetry wiring.
  5. 05Exercise one live MCP client in a separately network-gated CI job.

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.

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.

CONTINUE EXPLORING

Inspect the operator loop and its evidence boundaries.

The pinned public commit contains the five task manifests, SQLite store, terminal viewer, eval runner, rule compiler, and CI workflow described here.