Skip to content

OpenCode roles

When a single agent plans a change, writes it and then checks it, it is checking its own work. If it misunderstood the task at the start, it will still misunderstand it at the end, because nothing in between looked at the task again.

This feature splits the work across three agents. The planner turns your task into a plan file and writes no code. The implementer follows that plan and is not allowed to change it. The reviewer compares the result against the plan without having seen the implementation happen. The plan file is what they all work from, and you can read it.

Because the three are separate agents, each can run on its own model. Planning and reviewing need a model that reasons well. Implementing is mostly writing code against a plan that already names the files and functions, which a smaller model can do.

Catalog name opencode-roles. Requires the opencode feature in the same workbench.

Terminal window
monoceros add-feature acme opencode-roles

The roles come without a model of their own, so each one runs on the model you configured for the opencode feature. That already works: you get the plan file and the separate review even when all three run on the same model.

You get more out of the split by giving each role its own model. Here is what that looks like in the config, and the command that writes it:

features:
- ref: ghcr.io/getmonoceros/monoceros-features/opencode-roles:1
options:
plannerModel: 'openrouter/moonshotai/kimi-k3'
implementModel: 'openrouter/moonshotai/kimi-k2.7-code'
reviewModel: 'openrouter/moonshotai/kimi-k3'

The yml is the source of truth, so both tabs describe the same workbench: the command writes exactly that block. To change a model later, edit it there and apply again.

Give the planner and the reviewer your best model. The planner has to understand your task and your code before it can write a usable plan, and the reviewer has to spot the step that is missing. The implementer follows a plan that already names the files, the functions and the command that decides pass or fail, so a smaller model is usually enough.

Either way, apply once and the agents and commands are in the container:

Terminal window
monoceros apply acme

Then start OpenCode in the app you want to work on, so the commands find the plans filed for it. Use --auto while you are at it: it approves everything that is not explicitly denied, and the roles deny what matters, so the chain runs without stopping at every tool call and the planner still cannot write source.

For an app that does not exist yet, name its directory anyway. run asks whether to create it, which answers the question of where to start when the project is the thing you are about to build. Starting one level up instead splits your session history and leaves the language servers without the markers they look for.

Terminal window
monoceros run acme --in=projects/<app> -- opencode --auto

Three commands. Whichever one you call runs the rest of the chain, so start at the top for a task that needs thinking through, and further down when the earlier work is already done.

/monoceros-plan <your task, an issue number, or a backlog entry>

The planner loads the task, looks at the code, and asks before it assumes. One question at a time, each with the answer it will work from if you say nothing, so “yes, yes, no” is a complete conversation. It asks nothing when your task already says what should be true afterwards and how to check it.

The questions are about what you want, not about how it gets built. Not which database, but whether the data should still be there after a restart. Not whether you need authentication, but whether anyone else uses this. One answer often opens the next question, and that is the point. Turning those answers into technical decisions is the planner’s own job, and it writes them into the plan where you can read them back.

Then it writes the plan, shows you the summary and the host steps, and waits. It implements nothing until you tell it to. After that it runs the implementer, checks that the acceptance command came back green before the reviewer looks, and repairs findings for at most two rounds. If the same finding comes back after a repair, it stops and tells you.

/monoceros-ship <slug>

The implementer reads the whole plan, works the steps in order, runs the acceptance command the plan names, and reports what changed, the real output of that command, its deviations, and the steps only you can do on the host. Called directly, it leads: once its command is green and the app is back up, it runs the review itself.

The slug is enough: plans are filed per app, and the command looks yours up in the app you are working in. A full path works too, and so does a ~/... one.

/monoceros-review <slug>

The reviewer runs the acceptance command itself instead of trusting the report, and reads the diff. Then it answers PASS or CHANGES_REQUIRED with a numbered list, each item pinned to file:line.

It checks what the tests cannot: a step that was skipped, something in the diff no step asked for, an acceptance criterion met in the letter but not in substance, code that does not fit how the rest of the project is built. It reports security problems as defects, which block a PASS. It leaves style alone unless it can say what breaks and when.

When the app serves a page a browser loads, all three roles follow the page’s references rather than its status code. A dev server answers / with the page shell whether the app works or not, so a check there can never fail: the plan’s acceptance command fetches the served HTML and then every script and stylesheet it names. That is the check that catches a white page.

OptionDefaultDescription
plannerModel(empty)Model for the planning role as provider/model-id.
implementModel(empty)Model for the implementing role.
reviewModel(empty)Model for the reviewing role.
plannerEffort(empty)Model variant (reasoning effort) for the planning role.
implementEffort(empty)Model variant for the implementing role.
reviewEffort(empty)Model variant for the reviewing role.

An empty option falls back to the model you set on the opencode feature. If that is empty too, the role uses whatever model the session is running on. There are no built-in defaults, because a model id written into a release goes out of date faster than the release does.

All three roles use the provider you configured on the opencode feature, so mixing model families means going through a gateway that carries all of them. Both OpenRouter and OpenCode Zen do, and every model below is available on either. The prefix differs: on OpenRouter a model is openrouter/anthropic/claude-opus-5, on Zen it is opencode/claude-opus-5. The tabs use the OpenRouter form.

plannerModel: 'openrouter/anthropic/claude-opus-5'
implementModel: 'openrouter/anthropic/claude-sonnet-5'
reviewModel: 'openrouter/anthropic/claude-opus-5'

The first two stay within one family, which is the safe choice: the plan is written in the same style the implementer expects. The last two mix on purpose. A reviewer from a different family than the planner does not share its blind spots, so it questions decisions the planner considered obvious.

These are starting points, not measured results. The combination in the example further up is the one we measured. Judge your own by the first run: count how many tool calls come back rejected, and swap the implementing model if that number is not close to zero.

OpenCode calls this a model variant, and it is the second dial next to the model. An empty value leaves the model to decide on its own.

options:
plannerEffort: 'high'
implementEffort: 'low'
reviewEffort: 'high'

The accepted values come from the model, not from Monoceros. Kimi K3 takes low, high and max; other models offer different sets, and some offer none at all. A value the model does not know is ignored rather than rejected, so check that it arrived instead of assuming: the reasoning-token count per session is the evidence.

Terminal window
sqlite3 ~/.monoceros/container/acme/home/.local/share/opencode/opencode.db \
"select title, tokens_reasoning from session order by time_created desc limit 5"

The split follows the same logic as the models. Planning and reviewing are thinking work; the implementer follows a plan that already names the files and the acceptance command, so it is the cheapest place to turn the dial down.

Plans are stored in OpenCode’s own data directory, ~/.local/share/opencode/plans/, not in your project. That directory survives monoceros apply, and it also survives deleting everything under projects/, which is what you tend to do between runs.

The implementer commits when the project is a git repository: one commit per round, so the reviewer reads a real diff instead of guessing which untracked files belong to the change. It does not push. git push, gh pr create and the package publishers are denied for the implementer and the reviewer.

When the plan scaffolds a new project, the first step is git init plus a .gitignore, because version control belongs to creating a project. A directory that was already there is left alone: whether it becomes a repository is your decision, and the planner only mentions it in its report.

If the container has no git identity, the commit fails with “Please tell me who you are”. The implementer reports that line as it stands and finishes its report. It does not invent a name to get past it, because that name would end up in the history of your project.

You decide what leaves the machine. Everything the roles produce stays in your container: the plan file, the commits and the test output, all of which you can check yourself. How you name branches, which branch you merge into and how you review are decisions for your project, so the chain hands you a committed, reviewed change and stops there.

You talk to the session in whatever language you use. The planner records that language in the plan, in a field called Reply to the user in, and the implementer and the reviewer read it from there. They never see your messages, so without that field they would answer a German question in English.

The plan file itself stays English, along with code, comments and commit messages. It outlives the conversation and the two roles that execute it work from English prompts.

monoceros apply rewrites all six files, so any change you make to them is lost on the next apply. To keep a change, put your own version in the project: .opencode/agents/monoceros-planner.md takes precedence over the global one and is never overwritten.

The monoceros- prefix on every agent and command has a reason. OpenCode uses one namespace per container, and a custom command replaces a built-in one without warning. A role called /plan or implement could silently take over a command you defined yourself.

Terminal window
monoceros remove-feature acme opencode-roles

This is also how you change the options from the command line. Adding the feature a second time with different options is an error on purpose, so the yml can never disagree with what you asked for.

The next apply takes the agents, the commands and the role entries in opencode.json out of the container. Whatever else lives in those directories stays, and so do the plans you already wrote.