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.
Add it
Section titled “Add it”monoceros add-feature acme opencode-rolesThe 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'monoceros add-feature acme opencode-roles -- \ plannerModel=openrouter/moonshotai/kimi-k3 \ implementModel=openrouter/moonshotai/kimi-k2.7-code \ reviewModel=openrouter/moonshotai/kimi-k3The 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:
monoceros apply acmeThen 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.
monoceros run acme --in=projects/<app> -- opencode --autoUse it
Section titled “Use it”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.
Options
Section titled “Options”| Option | Default | Description |
|---|---|---|
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.
More combinations
Section titled “More combinations”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'plannerModel: 'openrouter/openai/gpt-5.6-sol'implementModel: 'openrouter/openai/gpt-5.3-codex'reviewModel: 'openrouter/openai/gpt-5.6-sol'plannerModel: 'openrouter/openai/gpt-5.6-sol'implementModel: 'openrouter/anthropic/claude-sonnet-5'reviewModel: 'openrouter/anthropic/claude-opus-5'plannerModel: 'openrouter/moonshotai/kimi-k3'implementModel: 'openrouter/deepseek/deepseek-v4-flash'reviewModel: 'openrouter/deepseek/deepseek-v4-pro'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.
How hard each role thinks
Section titled “How hard each role thinks”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.
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.
Where the plans live
Section titled “Where the plans live”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.
Committing and pushing
Section titled “Committing and pushing”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.
Which language you get answers in
Section titled “Which language you get answers in”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.
Customising a role
Section titled “Customising a role”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.
Remove it
Section titled “Remove it”monoceros remove-feature acme opencode-rolesThis 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.