mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
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:
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
Reference in New Issue
Block a user