- TypeScript 100%
| .opencode/agents | ||
| src | ||
| .gitignore | ||
| bun.lock | ||
| index.ts | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
opencode-harness
A coding-task orchestrator built on the OpenCode SDK. A deterministic controller (plain TypeScript) spawns role agents — planner, worker, reviewer — each as a fresh child OpenCode session, so every context stays as small as possible.
Architecture
main.ts (CLI)
└─ controller.runTask(host, task)
├─ root session "Harness: <task>" (tree anchor, no messages)
├─ planner session agent=planner → JSON plan {steps:[{id,instruction,files}]}
├─ worker session agent=worker × N one per plan step, fresh session each
├─ reviewer session agent=reviewer → JSON verdict {verdict,issues}
└─ fix sessions agent=worker × N only when reviewer requests changes
Why contexts stay compact:
- The controller is code, not a model. It never holds transcripts — it only reads each session's final message and forwards short summaries.
- Every role run is a fresh child session (
parentIDlinks it in the TUI tree). A worker only ever sees its one step plus a few lines of prior-step summaries. - The planner and reviewer have
edit/shelldenied via agent permissions, so they cannot mutate the workspace. - Parse failures degrade gracefully: an unparseable plan becomes one big step; an unparseable review becomes a change request with the raw reply.
Files
| Path | Purpose |
|---|---|
src/main.ts |
CLI entrypoint: parse args, start host, run task, print summary |
src/controller.ts |
Orchestration: plan → build → review → fix loop, prompt builders, reply parsing |
src/agent.ts |
Session runner primitive: create → prompt → wait → read reply/tokens |
src/util.ts |
Loose JSON extraction, truncation, HarnessError |
.opencode/agents/*.md |
The three role agents (system prompts + permissions) |
Requirements
- Bun and an authenticated
opencodeCLI setup (the embedded host uses your global~/.config/opencodeprovider config). - Install deps:
bun install.
Usage
# run a task in the current directory
bun run src/main.ts "Add a retry helper with tests to src/"
# run against another repository, with an explicit model (see note below)
bun run src/main.ts "Migrate the config loader to zod" --dir /path/to/repo --model openrouter/anthropic/claude-sonnet-5.5
# tune the loop
bun run src/main.ts "Fix the flaky test in tests/api.test.ts" --steps 4 --rounds 2
The run prints a summary: status, root session id, session count, token and cost totals.
Transcript persistence. The embedded host keeps sessions in memory for the
lifetime of the process. When it exits, the session tree is gone — the work in
the target repo remains, but you cannot reopen transcripts in the TUI. To keep
an auditable session tree, either keep the process alive (e.g. add a REPL or
serve mode) or switch createHost() to connect to the shared background
service via @opencode/client/service (Service.ensure()), which persists
sessions in ~/.local/share/opencode.
Models and keys. Without --model, sessions use OpenCode's default model.
The embedded host does not read ~/.local/share/opencode/auth.json (the shared
service does), so the harness copies each stored provider key into its
conventional <PROVIDER>_API_KEY env var at startup unless one is already set.
If a session fails, the harness prints the provider error from the assistant
message (for example a free-limit 429), not just a bare "failed".
Note on target directories: the role agents are discovered from
.opencode/agents/ in the target directory's ancestry. Running from this repo
works for tasks on this repo, and for any project nested under it. To target
an unrelated repository, either copy .opencode/agents/ into it, or install
the agents globally in ~/.config/opencode/agents/.
Extending
- Add a role: drop a new
.opencode/agents/<role>.mdfile, then add a controller phase that callsrunAgent(host, { agent: "<role>", … }). - Different models per role: add
model: <provider>/<model>to the agent frontmatter (e.g. a cheap model for planner, a stronger one for worker). - Compact long sessions: call
host.sessions.compact({ sessionID })before follow-up prompts on any session you reuse. - Parallel workers: steps with disjoint file sets can run concurrently with
Promise.all— the controller is plain code, so orchestration strategies are easy to add.