opencode-plan-review
v0.3.1
Published
opencode plugin: review plans in $EDITOR with per-agent model tracking
Maintainers
Readme
opencode-plan-review
Plan review plugin for opencode. Opens plans in $EDITOR, returns a unified diff of the user's edits as feedback for the model.
Origin
Adapted from the Claude Code planning plugin in umputun/cc-thingz (MIT). The original targets Claude Code via PreToolUse hooks; this project targets opencode via a custom tool + system-prompt injection.
What it does
- Registers a
plan_reviewtool the model calls when the plan is ready - Rewrites
plan_exit/ExitPlanModereferences in system prompts →plan_reviewso the model always calls the right tool - Opens the plan in
$EDITOR(cascade: agterm → tmux → zellij → kitty → wezterm → ghostty →code -w/cursor -w→ blocking spawn) - Computes a unified diff (Python
difflib); the diff becomes the model's next user message - When the user closes the editor without changes: auto-switches from plan agent to build agent on a per-session build model
- Slash commands:
/plan-review <file>,/set-build-model [provider/model | N],/plan-diag [reset]
Requirements
- Python 3.x — stdlib only
- Terminal overlay (optional):
agtermctl,tmux,zellij,kitty,wezterm, orghostty. Falls back to plain$EDITORon bare ssh.
Install
Add to ~/.config/opencode/opencode.jsonc:
{
"plugin": ["opencode-plan-review"]
}Restart opencode. The plugin self-installs on first load:
commands/*.md— symlinked into~/.config/opencode/commands/- TUI plugin — auto-registered into
~/.config/opencode/tui.jsonc(tracks the fork's native per-session selection state and adds anAgent modelssidebar block) bin/plan-review.py— Python helper, resolved from the package directory.chmod +xapplied if needed.
Build-model resolution
When a plan is approved, the session switches to the build agent. The build model is resolved from per-session sources only (never global model.json, never plan's model):
planReviewModels.buildrecord — single per-session metadata key written by every pick path throughplugin/model-store.ts(chat.messagecapture,tui.model.selected, home→session flush,/set-build-model). Precedence is decided at write time: explicit picks overwrite freely;/set-build-modelsetspinned: trueso future implicit captures leave it alone; home flush (mergeHomeFlush) only fills absent per-agent records. LegacyplanReviewDeferredPickskey is read as a one-shot fallback; the next write migrates.- Session history — model of the last build-agent user message
agent.build.model— from opencode configconfig.model— global default
If none resolve, the plugin refuses the auto-switch and prints manual instructions — it never falls back to the plan agent's model. With write-time precedence on the single per-session record (planReviewModels), the sticky-model bug is structurally gone; a synthetic-prompt guard is kept as defense-in-depth so diagnostics stay clean. Use /plan-diag to inspect.
How model tracking works
The opencode fork exposes api.state.selection() for the sidebar, advertises api.state.modelSelectionEvents, and emits tui.model.selected with the session, agent, and selected model. Each model event updates only that agent through a serialized per-session metadata read-modify-write; startup snapshots are never persisted. The TUI plugin also shows a compact Agent models sidebar block with status-dot highlighting.
Published plugin types do not yet include this additive API, so the plugin uses feature detection. On stock opencode it logs a safe fallback and relies on the server-side chat.message hook; it never reads global model.json or guesses from Tab presses. This prevents cross-session contamination.
Editor cascade
| Priority | Condition | How |
|---|---|---|
| 1 | $AGTERM_SESSION_ID set, agtermctl on PATH | agtermctl session overlay open (blocks natively) |
| 2 | $TMUX set, tmux on PATH | tmux display-popup -E -w 90% -h 90% |
| 3 | $ZELLIJ set, zellij on PATH | zellij run --floating + sentinel file |
| 4 | $KITTY_LISTEN_ON set, kitty on PATH | kitty @ launch --type=overlay + sentinel file |
| 5 | $WEZTERM_PANE set, wezterm on PATH | wezterm cli split-pane + sentinel file |
| 6 | ghostty on PATH | blocking spawn with --command |
| 7 | $EDITOR is code / cursor / subl | spawn with -w (blocks until GUI closes) |
| 8 | otherwise | subprocess.run([$EDITOR, file]) (blocks, works on ssh) |
If $EDITOR is unset: $VISUAL → micro → nano → vi.
For kitty: enable allow_remote_control yes and listen_on unix:/tmp/kitty-$KITTY_PID in kitty.conf.
Color behavior
| Condition | Colored output |
|---|---|
| stdout is a TTY | yes (unless --no-color or NO_COLOR set) |
| stdout is a pipe / redirect | no (auto-detected via isatty()) |
| --no-color flag | no |
| NO_COLOR env var (any non-empty) | no (no-color.org) |
Development setup
cd plugin
npm install
python3 bin/plan-review.py --test
cd ..
EDITOR=true bun ../tests/plugin-smoke.tsOverride the helper path with PLAN_REVIEW_SCRIPT=<absolute> if not running from a clone.
Layout
opencode-planning/
├── plugin/ # npm package root
│ ├── index.ts # server plugin (tool + hooks, thin wiring)
│ ├── tui-plugin.tsx # Native selection tracking + sidebar block
│ ├── model-store.ts # shared RMW + per-session record (single source of truth)
│ ├── resolution.ts # exitPlanMode + resolveBuildModel
│ ├── system-prompt.ts # system.transform + messages.transform
│ ├── commands.ts # slash-command handlers
│ ├── install.ts # self-install + tui.jsonc registration
│ ├── helpers.ts # logged / visibleErr / withTimeoutSafe
│ ├── package.json
│ ├── bin/plan-review.py # Python helper (stdlib only)
│ └── commands/ # slash commands (auto-symlinked)
├── tests/plugin-smoke.ts # end-to-end smoke (~60 checks incl. P1–P4 regressions)
└── .github/workflows/publish.yml # npm Trusted Publishing (OIDC)License
MIT.
