Skip to content

Commit ba90f02

Browse files
committed
chore(release): publish 0.1.1-beta.6
1 parent 343976a commit ba90f02

6 files changed

Lines changed: 189 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.1.1-beta.6
4+
5+
- let users replace the plugin's base `agent.plan.prompt` instead of appending to it
6+
- add a `plan_prompt` tool so the `plan` agent can reveal the plugin prompt basis for customization
7+
38
## 0.1.1-beta.5
49

510
- gate `plan_exit` instructions on the OpenCode experimental plan-mode runtime flags

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,43 @@ If you want reproducible installs instead of automatic plugin refreshes, pin an
3434
- adds a `plan` agent intended for design and implementation planning
3535
- constrains that agent to read-only tools plus markdown plan editing
3636
- injects a system reminder that keeps the planning workflow explicit
37+
- lets users replace the plugin's base `plan` prompt with their own `agent.plan.prompt`
38+
- exposes a `plan_prompt` tool so the `plan` agent can show the plugin's prompt basis for customization
3739
- uses `submit_plan` for review when available, otherwise falls back to manual chat review
3840
- can leave planner mode with `plan_exit` after approval when experimental plan mode is enabled in the CLI runtime
3941

42+
## Customize the plan prompt
43+
44+
If you set `agent.plan.prompt`, it replaces the plugin's base prompt instead of being appended to it.
45+
46+
```json
47+
{
48+
"agent": {
49+
"plan": {
50+
"prompt": "You are my planning agent. Focus on migration risk, rollout steps, and testing strategy."
51+
}
52+
}
53+
}
54+
```
55+
56+
The runtime planner reminder still applies, so the agent stays in planner mode and continues to use the review handoff flow. That reminder is injected by the plugin at runtime and is not customized through `agent.plan.prompt`.
57+
58+
## Reveal the plugin prompt basis
59+
60+
The plugin also adds a read-only `plan_prompt` tool. Ask the `plan` agent to use it when you want the plugin's own prompt text as a starting point for customization.
61+
62+
Example:
63+
64+
```text
65+
Use the plan_prompt tool and show me the plugin prompt so I can customize it.
66+
```
67+
68+
The tool returns:
69+
70+
- the plugin base prompt
71+
- the injected planner reminder, which is plugin-controlled runtime guidance and is not customized via `agent.plan.prompt`
72+
- a short note explaining that the final runtime prompt can still differ because of user config, other plugins, or runtime tool availability like `plan_exit`
73+
4074
## Auto-updates
4175

4276
OpenCode installs npm plugins automatically. During the prerelease phase, `opencode-planner@beta` gives the smoothest update path for most users.
@@ -57,7 +91,7 @@ npm run debug:plan
5791
npm run opencode:no-plannotator -- debug config
5892
```
5993

60-
`npm run debug:plan` checks the active OpenCode runtime and reports whether the local repo plugin is loaded, whether `submit_plan` and `plan_exit` are allowed by the `plan` agent, and whether they are actually registered as runtime tools.
94+
`npm run debug:plan` checks the active OpenCode runtime and reports whether the local repo plugin is loaded, whether `plan_prompt`, `submit_plan`, and `plan_exit` are allowed by the `plan` agent, and whether they are actually registered as runtime tools.
6195

6296
This is the fastest way to distinguish:
6397

index.js

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from "path"
22

33
const agent = "plan"
44
const root = ".opencode/plans"
5+
const defaultPlanTarget = file("<session-id>")
56

67
function truthy(key) {
78
const value = process.env[key]?.toLowerCase()
@@ -25,6 +26,50 @@ function reviewInstruction(target) {
2526
].join(" ")
2627
}
2728

29+
function agentPrompt(target = defaultPlanTarget) {
30+
const planExit = hasPlanExit()
31+
32+
return [
33+
"Use this agent when the user wants a design, implementation plan, or scoped investigation before coding.",
34+
"Stay in planning mode: inspect the codebase, ask targeted questions when needed, and write a concise execution plan before implementation.",
35+
`Default plan path: ${target}.`,
36+
"Prefer the task tool with the explore and general subagents for deeper research.",
37+
reviewInstruction(target),
38+
...(planExit
39+
? [
40+
"After approval, if the user or Plannotator says something like 'Proceed with implementation', call plan_exit to hand off back to implementation mode.",
41+
]
42+
: []),
43+
].join("\n\n")
44+
}
45+
46+
function promptDisclosure(target = defaultPlanTarget) {
47+
return [
48+
"# opencode-planner prompt basis",
49+
"This tool shows the prompt text and planner reminder supplied by the opencode-planner plugin itself.",
50+
"The final runtime prompt can still differ if the user overrides `agent.plan.prompt`, another plugin edits `agent.plan`, or runtime tool availability changes.",
51+
"## Base prompt",
52+
agentPrompt(defaultPlanTarget),
53+
"## Planner reminder",
54+
"This reminder is injected by the plugin at runtime to keep the `plan` agent in planner mode and enforce the review handoff workflow. It is plugin-controlled and is not customized through `agent.plan.prompt`.",
55+
note(target.replace(`${root}/`, "").replace(/\.md$/, "")),
56+
"## How to customize it",
57+
"Only the Base prompt above is replaced by `agent.plan.prompt`. Add this to `opencode.json` to replace that base prompt:",
58+
[
59+
"```json",
60+
"{",
61+
' "agent": {',
62+
' "plan": {',
63+
' "prompt": "You are my planning agent. Focus on migration risk, rollout steps, and testing strategy."',
64+
" }",
65+
" }",
66+
"}",
67+
"```",
68+
].join("\n"),
69+
"Ask the `plan` agent to call `plan_prompt` when you want a fresh copy of the plugin prompt as a starting point.",
70+
].join("\n\n")
71+
}
72+
2873
function note(id) {
2974
const out = [
3075
"<system-reminder>",
@@ -74,23 +119,11 @@ function merge(a, b) {
74119
}
75120

76121
function mode(input = {}) {
77-
const planExit = hasPlanExit()
78122
const base = {
79123
mode: "primary",
80124
color: "info",
81125
description: "Researches the codebase and writes execution plans without editing source files.",
82-
prompt: [
83-
"Use this agent when the user wants a design, implementation plan, or scoped investigation before coding.",
84-
"Stay in planning mode: inspect the codebase, ask targeted questions when needed, and write a concise execution plan before implementation.",
85-
"Default plan path: .opencode/plans/<session-id>.md.",
86-
"Prefer the task tool with the explore and general subagents for deeper research.",
87-
reviewInstruction(".opencode/plans/<session-id>.md"),
88-
...(planExit
89-
? [
90-
"After approval, if the user or Plannotator says something like 'Proceed with implementation', call plan_exit to hand off back to implementation mode.",
91-
]
92-
: []),
93-
].join("\n\n"),
126+
prompt: agentPrompt(),
94127
permission: {
95128
"*": "deny",
96129
read: {
@@ -111,8 +144,9 @@ function mode(input = {}) {
111144
websearch: "allow",
112145
codesearch: "allow",
113146
batch: "allow",
147+
plan_prompt: "allow",
114148
submit_plan: "allow",
115-
...(planExit ? { plan_exit: "allow" } : {}),
149+
...(hasPlanExit() ? { plan_exit: "allow" } : {}),
116150
edit: {
117151
"*": "deny",
118152
[path.posix.join(root, "*.md")]: "allow",
@@ -130,7 +164,7 @@ function mode(input = {}) {
130164
return {
131165
...base,
132166
...input,
133-
prompt: [base.prompt, input?.prompt].filter(Boolean).join("\n\n"),
167+
prompt: input && Object.hasOwn(input, "prompt") ? input.prompt : base.prompt,
134168
permission: merge(base.permission, input?.permission),
135169
}
136170
}
@@ -139,6 +173,15 @@ export default async function plannerPlugin() {
139173
const seen = new Set()
140174

141175
return {
176+
tool: {
177+
plan_prompt: {
178+
description: "Reveal the planner plugin prompt basis",
179+
args: {},
180+
async execute(_, context) {
181+
return promptDisclosure(context.sessionID ? file(context.sessionID) : defaultPlanTarget)
182+
},
183+
},
184+
},
142185
async config(cfg) {
143186
cfg.agent ??= {}
144187
cfg.agent[agent] = mode(cfg.agent[agent])

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-planner",
3-
"version": "0.1.1-beta.5",
3+
"version": "0.1.1-beta.6",
44
"description": "Experimental OpenCode plugin that adds a dedicated planning agent with read-only planning constraints.",
55
"type": "module",
66
"author": "Tim Richardson",

scripts/debug-plan-runtime.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ const permissions = plan.permission ?? []
4040
const tools = plan.tools ?? {}
4141
const prompt = plan.prompt ?? ""
4242

43+
const planPromptAllowed = hasAllowedPermission("plan_prompt", permissions)
4344
const planExitAllowed = hasAllowedPermission("plan_exit", permissions)
4445
const submitPlanAllowed = hasAllowedPermission("submit_plan", permissions)
46+
const planPromptTool = Boolean(tools.plan_prompt)
4547
const planExitTool = Boolean(tools.plan_exit)
4648
const submitPlanTool = Boolean(tools.submit_plan)
4749
const usingLocalPlugin = plugins.includes(localPlugin)
@@ -50,6 +52,8 @@ const promptMentionsPlanExit = prompt.includes("plan_exit")
5052
console.log("OpenCode plan runtime check")
5153
console.log("")
5254
line("Repo plugin loaded", usingLocalPlugin ? "yes" : "no")
55+
line("plan_prompt allowed", planPromptAllowed ? "yes" : "no")
56+
line("plan_prompt tool", planPromptTool ? "yes" : "no")
5357
line("submit_plan allowed", submitPlanAllowed ? "yes" : "no")
5458
line("plan_exit allowed", planExitAllowed ? "yes" : "no")
5559
line("submit_plan tool", submitPlanTool ? "yes" : "no")
@@ -70,6 +74,9 @@ console.log("\nAssessment:")
7074
if (!usingLocalPlugin) {
7175
console.log(`- OpenCode is not using the local repo plugin at ${localPlugin}.`)
7276
}
77+
if (!planPromptTool) {
78+
console.log("- plan_prompt is not registered as a runtime tool.")
79+
}
7380
if (!submitPlanTool) {
7481
console.log("- submit_plan is not registered as a runtime tool.")
7582
}

test/plugin.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ test("config hook registers the plan agent without plan_exit by default", async
4040

4141
assert.equal(cfg.agent.plan.mode, "primary")
4242
assert.equal(cfg.agent.plan.permission.bash, "deny")
43+
assert.equal(cfg.agent.plan.permission.plan_prompt, "allow")
4344
assert.equal(cfg.agent.plan.permission.submit_plan, "allow")
4445
assert.equal(cfg.agent.plan.permission.plan_exit, undefined)
4546
assert.match(cfg.agent.plan.prompt, /if the submit_plan tool is available/i)
@@ -49,6 +50,37 @@ test("config hook registers the plan agent without plan_exit by default", async
4950
)
5051
})
5152

53+
test("config hook lets users replace the plugin prompt", async () => {
54+
await withEnv(
55+
{
56+
OPENCODE_EXPERIMENTAL: undefined,
57+
OPENCODE_EXPERIMENTAL_PLAN_MODE: undefined,
58+
OPENCODE_CLIENT: undefined,
59+
},
60+
async () => {
61+
const plugin = await plannerPlugin()
62+
const cfg = {
63+
agent: {
64+
plan: {
65+
prompt: "Use my custom plan instructions only.",
66+
permission: {
67+
webfetch: "deny",
68+
},
69+
},
70+
},
71+
}
72+
73+
await plugin.config(cfg)
74+
75+
assert.equal(cfg.agent.plan.prompt, "Use my custom plan instructions only.")
76+
assert.equal(cfg.agent.plan.permission.webfetch, "deny")
77+
assert.equal(cfg.agent.plan.permission.bash, "deny")
78+
assert.equal(cfg.agent.plan.permission.plan_prompt, "allow")
79+
assert.doesNotMatch(cfg.agent.plan.prompt, /if the submit_plan tool is available/i)
80+
},
81+
)
82+
})
83+
5284
test("config hook enables plan_exit when experimental plan mode is active", async () => {
5385
await withEnv(
5486
{
@@ -139,3 +171,54 @@ test("system transform only applies after planner messages", async () => {
139171
},
140172
)
141173
})
174+
175+
test("plan_prompt tool returns the plugin prompt basis without plan_exit by default", async () => {
176+
await withEnv(
177+
{
178+
OPENCODE_EXPERIMENTAL: undefined,
179+
OPENCODE_EXPERIMENTAL_PLAN_MODE: undefined,
180+
OPENCODE_CLIENT: undefined,
181+
},
182+
async () => {
183+
const plugin = await plannerPlugin()
184+
const output = await plugin.tool.plan_prompt.execute(
185+
{},
186+
{
187+
sessionID: "ses_tool",
188+
},
189+
)
190+
191+
assert.match(output, /# opencode-planner prompt basis/)
192+
assert.match(output, /## Base prompt/)
193+
assert.match(output, /## Planner reminder/)
194+
assert.match(output, /injected by the plugin at runtime/i)
195+
assert.match(output, /not customized through `agent\.plan\.prompt`/i)
196+
assert.match(output, /```json/)
197+
assert.match(output, /"agent": \{/)
198+
assert.match(output, /\.opencode\/plans\/ses_tool\.md/)
199+
assert.match(output, /agent\.plan\.prompt/)
200+
assert.doesNotMatch(output, /call plan_exit/)
201+
},
202+
)
203+
})
204+
205+
test("plan_prompt tool mentions plan_exit when experimental plan mode is active", async () => {
206+
await withEnv(
207+
{
208+
OPENCODE_EXPERIMENTAL: undefined,
209+
OPENCODE_EXPERIMENTAL_PLAN_MODE: "1",
210+
OPENCODE_CLIENT: "cli",
211+
},
212+
async () => {
213+
const plugin = await plannerPlugin()
214+
const output = await plugin.tool.plan_prompt.execute(
215+
{},
216+
{
217+
sessionID: "ses_tool",
218+
},
219+
)
220+
221+
assert.match(output, /call plan_exit/)
222+
},
223+
)
224+
})

0 commit comments

Comments
 (0)