No description
  • TypeScript 100%
Find a file
Magnus Sandell 1da1903e73 First
2026-10-27 17:59:07 +01:00
.opencode/agents First 2026-10-27 17:59:07 +01:00
src First 2026-10-27 17:59:07 +01:00
.gitignore First 2026-10-27 17:59:07 +01:00
bun.lock First 2026-10-27 17:59:07 +01:00
index.ts First 2026-10-27 17:59:07 +01:00
package.json First 2026-10-27 17:59:07 +01:00
README.md First 2026-10-27 17:59:07 +01:00
tsconfig.json First 2026-10-27 17:59:07 +01:00

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 (parentID links 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/shell denied 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 opencode CLI setup (the embedded host uses your global ~/.config/opencode provider 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>.md file, then add a controller phase that calls runAgent(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.