feat(opencode-plugin): add user-managed workflow mode (#667)

Registers submit_plan tool and slash commands without modifying prompts
or agent permissions. Fills the gap between manual (commands only) and
plan-agent (full automation) for users who want to manage prompts and
permissions themselves.
This commit is contained in:
Kirill Saksin
2026-05-06 03:05:19 +04:00
committed by GitHub
parent 3b88415aeb
commit 5ecf5be51e
5 changed files with 61 additions and 9 deletions
@@ -22,10 +22,11 @@ The OpenCode plugin (`@plannotator/opencode`) hooks into OpenCode's plugin syste
## Workflow modes
OpenCode support has three explicit modes:
OpenCode support has four explicit modes:
- **`plan-agent`** (default): `submit_plan` is available to OpenCode's built-in `plan` agent plus any extra agents listed in `planningAgents`.
- **`manual`**: `submit_plan` is not registered. Use `/plannotator-last`, `/plannotator-annotate`, `/plannotator-review`, and `/plannotator-archive` when you want Plannotator.
- **`user-managed`**: `submit_plan` is registered but no prompts or agent permissions are modified. You configure which agents can call `submit_plan` via OpenCode's agent configuration.
- **`all-agents`**: legacy broad behavior. Primary agents can see and call `submit_plan`.
Default config:
@@ -87,6 +88,19 @@ If you want commands only:
}
```
If you want the tool registered but want to manage prompts and permissions yourself:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "user-managed"
}]
]
}
```
## Custom planning agents
OpenCode's built-in `plan` agent is always included in `plan-agent` mode. If you use another planning agent, add its OpenCode agent name to `planningAgents`:
+15 -1
View File
@@ -38,10 +38,11 @@ Restart OpenCode. By default, the `submit_plan` tool is available to OpenCode's
## Workflow Modes
Plannotator supports three OpenCode workflows:
Plannotator supports four OpenCode workflows:
- **`plan-agent`** (default): `submit_plan` is available to OpenCode's built-in `plan` agent plus any extra agents listed in `planningAgents`. This keeps Plannotator integrated with OpenCode plan mode without nudging `build` to call it.
- **`manual`**: `submit_plan` is not registered. Use `/plannotator-last`, `/plannotator-annotate`, `/plannotator-review`, and `/plannotator-archive` when you want Plannotator.
- **`user-managed`**: `submit_plan` is registered but no prompts or agent permissions are modified. You manage which agents can call `submit_plan` via OpenCode's native agent configuration.
- **`all-agents`**: legacy broad behavior. Primary agents can see and call `submit_plan`.
Default config:
@@ -103,6 +104,19 @@ Use commands only:
}
```
Register the tool but manage prompts and permissions yourself:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "user-managed"
}]
]
}
```
## How It Works
1. The configured planning agent calls `submit_plan` → Plannotator opens in your browser
+3 -2
View File
@@ -74,6 +74,7 @@ import {
shouldApplyToolDefinitionRewrites,
shouldInjectFullPlanningPrompt,
shouldInjectGenericPlanReminder,
shouldModifyPrompts,
shouldRegisterSubmitPlan,
shouldRejectSubmitPlanForAgent,
type PlannotatorOpenCodeOptions,
@@ -259,7 +260,7 @@ export const PlannotatorPlugin: Plugin = async (ctx, rawOptions?: PlannotatorOpe
// that allows markdown file writing. OpenCode's original blocks ALL file edits,
// but we need the agent to write plans, specs, docs, etc.
"experimental.chat.messages.transform": async (input, output) => {
if (workflowOptions.workflow === "manual") return;
if (!shouldModifyPrompts(workflowOptions)) return;
const lastUserAgent = getLastUserAgentFromMessages(output.messages);
if (
@@ -313,7 +314,7 @@ tools (except writing markdown files), or otherwise make changes to the system.
// Inject planning instructions into system prompt
"experimental.chat.system.transform": async (input, output) => {
if (workflowOptions.workflow === "manual") return;
if (!shouldModifyPrompts(workflowOptions)) return;
const systemText = output.system.join("\n");
if (systemText.toLowerCase().includes("title generator") || systemText.toLowerCase().includes("generate a title")) {
+19
View File
@@ -5,6 +5,7 @@ import {
shouldApplyToolDefinitionRewrites,
shouldInjectFullPlanningPrompt,
shouldInjectGenericPlanReminder,
shouldModifyPrompts,
shouldRegisterSubmitPlan,
shouldRejectSubmitPlanForAgent,
} from "./workflow";
@@ -53,6 +54,16 @@ describe("workflow gates", () => {
expect(shouldRejectSubmitPlanForAgent("build", options)).toBe(false);
});
test("user-managed mode registers tool but skips prompt/config modifications", () => {
const options = normalizeWorkflowOptions({ workflow: "user-managed" });
expect(shouldRegisterSubmitPlan(options)).toBe(true);
expect(shouldModifyPrompts(options)).toBe(false);
expect(shouldApplyToolDefinitionRewrites(options)).toBe(false);
expect(shouldInjectFullPlanningPrompt("plan", options)).toBe(false);
expect(shouldRejectSubmitPlanForAgent("build", options)).toBe(false);
});
test("plan-agent mode injects only for configured planning agents", () => {
const options = normalizeWorkflowOptions({
workflow: "plan-agent",
@@ -96,6 +107,14 @@ describe("applyWorkflowConfig", () => {
expect(config).toEqual({});
});
test("user-managed mode leaves OpenCode config untouched", () => {
const config: any = {};
applyWorkflowConfig(config, normalizeWorkflowOptions({ workflow: "user-managed" }), false);
expect(config).toEqual({});
});
test("plan-agent mode exposes submit_plan to plan and denies build", () => {
const config: any = {
experimental: {
+9 -5
View File
@@ -1,6 +1,6 @@
import { normalizeEditPermission } from "./plan-mode";
export type WorkflowMode = "manual" | "plan-agent" | "all-agents";
export type WorkflowMode = "manual" | "user-managed" | "plan-agent" | "all-agents";
export interface PlannotatorOpenCodeOptions {
workflow?: unknown;
@@ -13,7 +13,7 @@ export interface NormalizedWorkflowOptions {
planningAgentSet: Set<string>;
}
const WORKFLOWS = new Set<WorkflowMode>(["manual", "plan-agent", "all-agents"]);
const WORKFLOWS = new Set<WorkflowMode>(["manual", "user-managed", "plan-agent", "all-agents"]);
const DEFAULT_WORKFLOW: WorkflowMode = "plan-agent";
const DEFAULT_PLANNING_AGENTS = ["plan"];
const BUILTIN_PLAN_AGENT = "plan";
@@ -80,15 +80,19 @@ export function shouldRegisterSubmitPlan(options: NormalizedWorkflowOptions): bo
return options.workflow !== "manual";
}
export function shouldModifyPrompts(options: NormalizedWorkflowOptions): boolean {
return options.workflow !== "manual" && options.workflow !== "user-managed";
}
export function shouldApplyToolDefinitionRewrites(options: NormalizedWorkflowOptions): boolean {
return options.workflow !== "manual";
return options.workflow !== "manual" && options.workflow !== "user-managed";
}
export function shouldInjectFullPlanningPrompt(
agentName: string | undefined,
options: NormalizedWorkflowOptions,
): boolean {
return options.workflow !== "manual" && isPlanningAgent(agentName, options);
return shouldModifyPrompts(options) && isPlanningAgent(agentName, options);
}
export function shouldInjectGenericPlanReminder(
@@ -114,7 +118,7 @@ export function applyWorkflowConfig(
options: NormalizedWorkflowOptions,
allowSubagents: boolean,
): void {
if (options.workflow === "manual") return;
if (options.workflow === "manual" || options.workflow === "user-managed") return;
if (!allowSubagents) {
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];