For system-level changes, start with docs/design/system.md
and docs/design/roadmap.md, and state which agent
decision gets cheaper or more accurate, which layer owns the change, and which
durable artifact lets the next worker continue. Read
docs/design/ before changing a public surface or
revisiting a design decision, and docs/spec/ when a change
touches the record-stream contract (record shapes, attribution, the session
graph, the cursor). When working on an adapter, read its
runtime page (native API inputs and results, resume
semantics, current OAR mapping, evidence gaps) and update it when the mapping
changes.
Detail lives next to the code:
packages/oar/src/README.md (source layout,
import rules, ownership), sea-trial/README.md
(behavior-suite layout and harness),
experiments/README.md (live probes and their
conclusions).
Run first, test second: while shaping a change, run the real thing and look.
Drive the CLI (pnpm oar ... builds and runs the in-repo oar):
oar run <runtime> "<prompt>" shows a turn's progress live, --record <file>
keeps the run as an oar-voyage/3 JSONL log, and oar list,
oar installation <id>, oar usage <id> and oar models <id> cover the
observation surfaces. Or point a scratch pnpm tsx script at the public
Session API, or reuse an experiment. This is how you find out what correct
looks like before pinning it.
When the check you need has no tool (a CLI flag, a script that replays a recorded run), build it or extend the oar CLI as part of the change: a command someone else can rerun turns "trust me, I checked" into "run this and see".
- Ad-hoc evidence never closes a change. Once the behavior is understood, encode it at the cheapest test layer that can catch a regression (below); the commit gate stays the arbiter.
- Leave the lever behind. A scratch script that earned its keep becomes a test, or an experiment with its conclusion recorded, not a loose file. A tool you built or extended for the run is polished and committed with the change.
- Leave a handoff. For work that spans turns or owners, record the runtime identity, cursor or artifact, current conclusion, and next action. A status message without a continuation is not an accumulated result.
- Real logins cost quota. Repeatable ad-hoc runs belong on the mock or
aimock backends; use a real installation deliberately, as with
OAR_TEST=<real id>.
Setup: Node.js 24+, pnpm install.
The tests let whoever makes a change, human or coding agent, verify it end to end without a person as the bottleneck: a change is done when the tests prove it, not when someone has eyeballed it. The suite is also the proof that the contracts hold on every runtime, so never weaken an assertion to get past a failure: a red test means the work is not done.
Each layer below validates a different part of a change at a different cost. Run the cheapest one that can catch your likely mistake, escalate as the change gets riskier, and finish with the commit gate.
tests/**/*.test.ts (per-runtime subdirectories included), run by pnpm test
(vitest). Seconds, no vendor binaries. The inner loop for shared mechanisms:
folds, resolvers, process plumbing. Use virtual time via vi.useFakeTimers
(mock Date too when clocks matter).
One suite in sea-trial/cases/, run against interchangeable backends that
OAR_TEST selects (sea-trial/harness/backends.ts; unset means mock, an
unavailable runtime skips). Write a case for a contract promise every runtime
must honor: it asserts only through the public Session API and must pass on
the mock, the aimock backends and real logins alike, race-honest where
runtimes may legitimately differ.
pnpm sea-trial: the in-process mock fixture. Fast, deterministic, no binaries or network; the default validation for any change to contracts or session behavior.OAR_TEST=<runtime>-aimock pnpm sea-trial(claude-aimock,codex-aimock,pi-aimock,pi-durable-aimock): the real vendor binary and adapter with only the model provider scripted, no login. Use it when you touched a runtime's adapter: it catches real-process integration mistakes the mock cannot.OAR_TEST=scripted pnpm sea-trial: the publicscriptedRuntime(@botiverse/oar/testing) held to the same contract.pnpm tsx sea-trial/all.ts [backend...]: several backends concurrently, by default mock plus the four aimock backends. Use it before pushing a change to shared runtime machinery.OAR_TEST=<real id>(antigravity,claude,codex,cursor,grok,kimi,opencode,pi): your local installation and login. The final word when vendor reality is in doubt; it costs quota, so never run it by default.
sea-trial/vendor/*.vendor.test.ts, run by
OAR_TEST=<runtime>-aimock pnpm vitest run sea-trial/vendor. The ACP
runtimes' vendor backends (opencode-aimock, kimi-aimock, grok-aimock,
antigravity-aimock, sea-trial/harness/aimock-acp.ts) need the installed
CLI and run only locally; CI runs the four behavior backends. Vendor tests see
the scripted provider's side: request contents, error edges, scripted tool
rounds. Run them when a change affects what a runtime sends upstream or how it
handles vendor-specific behavior; write one for anything that needs the
provider's view or a vendor-specific trigger. Each is gated with
skipIf(process.env.OAR_TEST !== ...); helpers live in
sea-trial/vendor/support/.
experiments/, run by pnpm tsx experiments/<name>.ts. A live probe that
answers one question about a real runtime, kept as evidence with its
conclusion indexed in the experiments README. Not run in CI and not part of
validating a change; reach for one when the vendor's actual behavior is the
open question.
The shared cases assert the minimum every backend must honor. The strong
claims (a steer lands in the same turn, a queued input runs as its own turn,
an abort ends with the runtime's own report, a killed process shows up as an
exited response, sub-agent frames attribute) need a real login:
experiments/live-contract.ts <runtime>
writes one voyage log per scenario plus a report.json of observed facts
under oar-trial-run/live-<runtime>-*/. It burns tokens; run it when an
adapter changes what the stream carries, and record the observation in the
runtime's page and the experiments README.
pnpm run check must be green before every commit: typecheck, lint,
pnpm test and the mock behavior suite. Vendor tests are collected by
pnpm test but skip unless OAR_TEST names their backend, so run them for
the backend you touched. Rebase on origin/main before pushing.
CI (.github/workflows/ci.yml) skips a push
or pull request whose changes are all Markdown files or under docs/ or
assets/. Otherwise it runs these jobs:
check:pnpm run checkandpnpm run buildon Linux, macOS and Windows. The browser-entry unit test bundles the complete portable entry for a browser and refuses Node imports or unresolved externals.behavior: per aimock backend on the same three systems,pnpm run sea-trialplus that backend's vendor tests.browser-demo:pnpm durable-demo:smokein headless Chromium on Linux. The real Pi Durable Harness and native fetch provider run in the browser; provider requests go to local scripted responses using a synthetic key.clean-install:pnpm run clean-install(tests/clean-install.ts) on Linux. Both packages are packed andnpm installed into an empty project: without the optional peers, the root entry and thebrowser,forms,observe,kernel,brands,testing,agentsandagents/reportentries must load and type-check, OAR must probe every built-in runtime, and the line that adds cursor must fail the host's compile; with the SDK added, cursor loads. With the Pi Durable peers added, a pi-durable session opens and its entry bundles for a browser. The CLI, installed after the host's SDK is removed again, lists cursor's models through the SDK it depends on itself. It needs the npm registry.
Windows Codex behavior jobs forward child stderr to the job log with
OAR_CHILD_STDERR=inherit, including failures before a session trace exists
(a login's or logout's process is never forwarded: its output may carry secrets).
Artifact upload warns when no trace directory was created; the failed test
or startup step still fails the job. Codex RPC exit errors also retain a
bounded stderr tail without this setting, so use that evidence before
changing a timeout or assertion to address an intermittent startup failure.
Installation readiness and --version execution failures retain the native
error, signal, timeout, exit code and stderr tail in their exception too.
-
Probe reality first. Write an experiment that answers how the vendor actually behaves (session lifecycle, event stream, error shapes) and index its conclusion in the experiments README. Build the adapter on that evidence, not on the vendor's docs, and against the runtime's own interface, never another host's adapter for it (decision).
-
Implement it in
packages/oar/src/runtimes/<id>/.index.tsdeclares the runtime withdefineRuntime({ id, ... }), listing only the capabilities the runtime honestly supports: an absent capability is correct, a faked one is not. Thatindex.tsis also the public@botiverse/oar/runtimes/<id>entry, so it imports no SDK statically: load SDK-bound code on first call (onFirstCall,shared/lazy.ts) and add the id totests/runtime-entries.test.ts. The session adapter feeds acreateSessionKernel():- every native frame becomes exactly one
Framerecord viakernel.frame()(type,nativeverbatim,eventsholding theRuntimeEventBodys oar reads from it), never gated on turn state, never dropped, never synthesized; the sharedeventsOfderives the flatSession.events()surface from those records, so an adapter never builds it; - control calls go through
kernel.control()so request and response are records; - the process exit is an
exitedresponse; - a whole operation the runtime cannot do is a member the session lacks:
no
steeron a runtime that cannot inject, never asteerthat always rejects;capabilitiesdeclares queue durability, the attribution tier and image input as the runtime actually exposes them; - an option the runtime cannot honor is refused at open with an
UnsupportedOptionError, never dropped; one it refuses whenever given is declared inrefusedSessionOptionsand checked byrefuseSessionOptions, so the declaration and the refusal agree.
Keep the projection a pure fold (frame → commands) so replay tests (
tests/replay/, fixtures captured bypnpm sea-trial:record) can pin it. Keep parsing, compatibility policy and protocol details in that directory; reuseshared/mechanisms freely but never add runtime identity toshared/(source layout has the import rules). - every native frame becomes exactly one
-
Register it in
src/index.ts, the only composition root: import, add to the built-indefaultRuntimesregistry, re-export. Give it aruntimeBrandsentry insrc/brands.tswith its SVG inpackages/oar/assets/brands/(attribution inNOTICE.md). A runtime that needs something the host installs (cursor's SDK) is not built in: export its constructor instead, and add it toallRuntimesinsea-trial/harness/runtimes.tsand to the CLI's registry inpackages/cli/src/runtimes.ts(capabilities). A runtime with host-owned execution/storage, such as pi-durable, stays out of the default and CLI registries; test it with its configured fixture. -
Make the behavior suite pass unchanged.
OAR_TEST=<id> pnpm sea-trialagainst your real local installation. The cases insea-trial/cases/are the contract: make the runtime pass them, don't loosen them to fit. A case that needs a capability skips on its absence (a session withoutsteer), never on a runtime name. -
Add an aimock backend if feasible (
sea-trial/harness/aimock.ts,backends.ts, and thebehaviormatrix in.github/workflows/ci.yml) so the contract stays verifiable without a login, and add vendor tests for the provider-side specifics worth pinning. -
Update the docs in the same commit: the runtime lists in the root and
packages/oarREADMEs, a runtime page and its index entry, and the experiments README for any probes you added. The runtime page starts with the vendor's programming interface and concepts, then maps each capability to OAR or names the missing support and evidence.
- Locate the layer; that picks the test. Wrong behavior visible through the public Session API: a behavior case (if every runtime must honor the promise). A wrong request sent to the vendor, or a mishandled vendor-specific edge: a vendor test. A pure-logic mistake in a fold or resolver: a unit test.
- If the vendor's actual behavior is the open question, probe it first with an experiment and record the conclusion; don't derive the fix from documentation guesses.
- Write the failing test before the fix, at the cheapest layer that can
express it.
OAR_TEST=<runtime>-aimockreproduces most integration bugs without a login. - Fix at the right level. Runtime-specific policy belongs in
runtimes/<id>/; touchshared/only when the mistake is genuinely runtime-independent, and then runpnpm tsx sea-trial/all.ts, because every backend is affected. - Validate up the layers and finish with
pnpm run check. The reproducing test stays in the suite as the regression proof. - Update the runtime page when a bug changes the documented native behavior, OAR mapping or verification status. Distinguish the vendor's behavior from loss or policy introduced by OAR.
- Assert, don't
if + throw. Checks usenode:assert/strict(or vitestexpect), inside behavior cases and vendor tests too. Shared narrowing helpers (promptTurn,expectAvailable) live insea-trial/vendor/support/asserts.ts. - Snapshot value shapes. When asserting what a value looks like (an
outcome object, an event sequence, a fold result), pin the whole thing with
toMatchInlineSnapshot; no substring checks likereason.includes("400"). Snapshots update on local runs and are enforced whenCIis set (vitest.config.ts: updatenone). Keep plain assertions for logic and invariants (ordering, ranges, idempotence). - Test layer = assertion channel. What the public Session API shows goes in a behavior case that runs on every backend; anything that needs the scripted provider's view (request contents, error edges, vendor fingerprints) or a vendor-specific trigger goes in a vendor test.
- Failures must self-diagnose. Include the actual value in the assertion
message; long-running suites write traces and artifacts under
oar-trial-run/.
Both packages carry one version. A release is a pull request
(chore: release vX.Y.Z) that only sets version in
packages/oar/package.json and packages/cli/package.json; its body is the
release notes. Squash merge it, then start
.github/workflows/release.yml on main:
stamp workflow run release.yml --repo botiverse/oar -f version=X.Y.ZDispatch right after the release pull request merges: the workflow releases
the head of main only after ci.yml succeeded on that exact commit (a
docs-only head has no run and fails the gate). It refuses a version that
already has a tag or differs from either package version, runs
pnpm run check and pnpm run build, and publishes both packages with
pnpm -r publish, which rewrites the CLI's workspace:* dependency to the
real version. A separate release job, which runs no project code and is
the only one with write access, then creates the annotated vX.Y.Z tag and
the GitHub release, whose notes are the release pull request's body. If that
job fails after the publish, "Re-run failed jobs" finishes it. If ci.yml
flaked on the release commit, start a fresh run with
stamp workflow run ci.yml --repo botiverse/oar and dispatch again once it
passes. npm trusted
publishing (OIDC) authenticates the publish job; the repo holds no npm
token. Pushing a vX.Y.Z tag by hand still publishes that commit, without
the CI gate or the GitHub release.