Model real teamwork.
Agent frameworks give you one clever session. When it dies, it starts over. Workflow engines give you durable steps and a server to run them on. Neither gives you a team: named roles, a review that returns work to whoever owns it, a vote when one opinion is not enough, and a person to answer to.
That is the layer Obversa owns. You describe the work the way you would describe it to people, and the runtime runs it one bounded engine call at a time.
Every step appends events to a file on disk, with its artifacts beside them. That record is the whole story: no server, no database.
@obversa/runner supervises a run in its own worker. After a crash it starts
a fresh worker that reads the record and carries on. Steps that finished are
never repeated. A step that was mid-flight when the worker died runs again
only if its binding declares it safe to retry; otherwise the run pauses and
asks a person to reconcile it before it continues, so uncertain work is never
repeated silently. That is a separate layer with its own call, not something
a plain run() does by itself, and
the runner's page has it.
npm install @obversa/runtime # Node >= 22.12Install the teams package and two engine plugins, and this file delivers a change with a team of models: one seat researches the brief and writes the requirements and the plan, each reviewed; another writes the tests before any code, then implements until the tests pass and a reviewer from a different model family accepts; then the change is put to a person; and the evidence is written from the record. A red test or a rejected review sends the work back to the stage that owns it, with the findings. The file is complete; copy it, put your brief in, run it with Node.
npm install @obversa/runtime @obversa/teams @obversa/engine-claude-cli @obversa/engine-codeximport { claude } from '@obversa/engine-claude-cli';
import { codex } from '@obversa/engine-codex';
import { run } from '@obversa/runtime';
import { fromFile, person, stage, workflow } from '@obversa/teams';
/**
* A feature, delivered the way a team delivers one. The roles are named once;
* every stage is a small block of nouns: who does it, what it writes, who
* reads it, where a red result goes back to. Inference happens only where a
* role is named; every other stage is a command or a person.
*/
const team = workflow('feature-delivery', {
brief: fromFile('briefs/triple.md'),
options: { timeout: '10m' },
roles: {
analyse: claude('claude-sonnet-4-5'),
implement: codex('gpt-5.6-luna'),
'research-review': [codex('gpt-5.6-luna')],
'code-review': [claude('claude-sonnet-4-5')],
approve: person('Ship this change?'),
},
stages: [
stage('research-context', {
agent: 'analyse',
writes: 'team-output/research-context.md',
desc: 'Read the workspace and write down what the change touches.',
gate: 'The context note is in the workspace and a reviewer has accepted it.',
reviewedBy: 'research-review',
retry: 3,
}),
stage('research-requirements', {
agent: 'analyse',
writes: 'team-output/research-requirements.md',
desc: 'Turn the brief and the context note into requirements, one REQ-n per line.',
gate: 'The requirements note is in the workspace and a reviewer has accepted it.',
reviewedBy: 'research-review',
retry: 3,
}),
stage('plan', {
agent: 'analyse',
writes: 'team-output/plan.md',
desc: 'Write an executable plan from the requirements, one check per REQ-n.',
gate: 'Every requirement has a check in the plan.',
reviewedBy: 'research-review',
retry: 3,
}),
stage('tests-first', {
agent: 'implement',
writes: 'test/triple.test.mjs',
desc: 'Write the declared test files from the accepted plan before any implementation exists.',
gate: 'Every declared test file exists and covers the plan.',
reviewedBy: 'code-review',
retry: 3,
}),
stage('implement', {
agent: 'implement',
writes: 'src/triple.mjs',
desc: 'Write the code to the plan and the tests.',
gate: 'The source file exists.',
retry: 3,
}),
stage('test', {
run: ['node', '--test', 'test/triple.test.mjs'],
desc: 'Run the tests; a red run goes back to implement with the output.',
gate: 'The test command exits 0.',
sendsBackTo: 'implement',
}),
stage('review', {
panel: 'code-review',
agree: 1,
desc: 'Read the change and the test result against the plan.',
gate: 'At least one reviewer has accepted the change.',
sendsBackTo: 'implement',
}),
stage('approve', {
input: 'approve',
desc: 'Put the verified change in front of a person.',
gate: 'A person has said yes.',
}),
stage('close', {
agent: 'analyse',
writes: ['team-output/evidence.md', 'team-output/learning.md'],
desc: 'Write the evidence of the run and what was learned, from the record alone.',
gate: 'Both notes are in the workspace.',
}),
],
});
const result = await run(team);
console.log(JSON.stringify(result.outcome, null, 2));The roles are named once, from the seat helpers the engine plugins export, and every stage refers to a role by name. The implementer and every reviewer must be different model families, and the package refuses the team before any model runs if they are not.
The team is a graph of nine named stages. Every step carries a sentence
saying what it does and a sentence saying what must be true for it to
count, and both reach the reviewers and the run record. Each step that
repeats carries its own retry.
| stage | done when |
|---|---|
| research-context | The context note is in the workspace and a reviewer has accepted it. |
| research-requirements | The requirements note is in the workspace and a reviewer has accepted it. |
| plan | Every requirement has a check in the plan. |
| tests-first | Every declared test file exists and covers the plan. |
| implement | The source file exists. |
| test | The test command exits 0. |
| review | At least one reviewer has accepted the change. |
| approve | A person has said yes. |
| close | Both notes are in the workspace. |
A step that promises a file fails by name when the file is missing. The test step passes on the command's exit code, never on a model's report, and a reviewer's decision is the file it writes. Feature delivery shows what a real run of this file printed and the files the models wrote; a writer and reviewer and a review panel are the two smaller teams in the same package, and the shape of a real process is the full-size one.
An engine binding names the adapter, the provider, the model family and the model. A review seat can be required to differ from the writer, so the model that wrote the work is not the model that grades it.
| package | drives | needs |
|---|---|---|
@obversa/engine-claude-cli |
the Claude CLI, one process per attempt | Claude CLI, host auth |
@obversa/engine-codex |
the Codex CLI | Codex CLI, host auth |
@obversa/engine-grok-cli |
the Grok CLI | Grok CLI 1.0.5 |
@obversa/engine-opencode-cli |
the OpenCode CLI | OpenCode CLI 1.18.23 |
@obversa/engine-anthropic-api |
the Anthropic API | an API key |
@obversa/engine-agent-sdk |
the Claude Agent SDK | host Claude auth |
Write your own against the engine contract; it must pass the conformance kit.
- Site: obversa.ai
- Docs: docs.obversa.ai
- Your first run: get started
- Contributing: AGENTS.md
16 publishable packages. packages/ holds the eight that define the
product: @obversa/runtime is the runtime and its public contract,
@obversa/teams is three ready-made teams built on it, @obversa/runner
supervises stored runs, @obversa/engine and @obversa/memory are the
engine and memory contracts, @obversa/process runs a child process to a
deadline, and @obversa/surfacer and @obversa/source are the local
review surface. plugins/ holds the eight adapters: the six engines above
and two memories, one in process and one in private Git references.
hosts/ holds the terminal host, which is not published.
- Node.js 22.12 or later
- pnpm 10.15.1
From an Obversa checkout, run:
corepack enable
pnpm install --frozen-lockfile
pnpm buildThe install activates this repository's commit hooks for the checkout. See AGENTS.md for what the hooks need before your first commit.
The first example uses deterministic function jobs. It does not use a model or network service.
pnpm example:offlineThe full form, if the shortcut is not available:
pnpm --filter @obversa/runtime exec tsx ../../examples/offline-review.tsExpected result:
{
"status": "pass",
"attempts": 2,
"summary": "config is complete"
}The graph contract lets a package define a pure graph type without importing runtime implementation modules. It validates a graph definition, reduces recorded events, makes graph commands, and describes a stable plan for a host.
Run the checked-in example from the workspace root:
pnpm example:graphThe example is examples/custom-graph.ts. It defines a graph type,
checks it with the public conformance kit, and prints its resolved plan bounds.
validateGraphDescription(unknown) returns a frozen valid description or
throws GraphValidationError. The conformance kit checks repeatable graph
behavior from independent compiled instances.
Read the graph contract and plan admission before a host uses a graph package.
The runtime storage ports keep small JSON events separate from larger byte content. Run the offline local-storage example from the workspace root:
pnpm example:storageThe example writes one large synthetic artifact, appends one small reference, reopens the stores through a fresh binding, folds the same state, and runs the event-store and artifact-store conformance kits.
Read Events and artifacts for the storage contract, limits, secret handling, conflict behavior, and integrity checks. This storage layer does not execute graph work or recover a stopped run.
The runtime adapters run one fresh CLI process for one bounded node attempt. The
offline example uses scripted Grok and OpenCode executables, validates both
structured results, keeps missing usage as unknown, and removes its temporary
fixture files.
pnpm example:attemptRead Safe node attempts for result parts, declared capabilities, workspace access, fallback, and cleanup limits.
The public documentation is in docs/public. It includes the
first-run guide, the memory contract, the guides to writing a workflow shape
and to what a run records, the examples,
cmux host setup, and
reviewing a diff in a host pane.
Validate the documentation from the workspace root:
pnpm docs:validatepnpm typecheck
pnpm typecheck:ts6
pnpm test
pnpm buildObversa uses the MIT License. Report security problems as described in the security policy.