diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index 907cbc489..75601ebf6 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -24,6 +24,7 @@ import { loadConfig, resolveSharingEnabled } from "@plannotator/shared/config"; import { readImprovementHook } from "@plannotator/shared/improvement-hooks"; import { composeImproveContext } from "@plannotator/shared/pfm-reminder"; import { + composeSystemPrompt, stripConflictingPlanModeRules, } from "./plan-mode"; import { @@ -406,32 +407,32 @@ tools (except writing markdown files), or otherwise make changes to the system. if (shouldInjectFullPlanningPrompt(lastUserAgent, workflowOptions)) { const stripped = stripConflictingPlanModeRules(output.system); - output.system.length = 0; - output.system.push(...stripped); - output.system.push(getPlanningPrompt()); - const hook = readImprovementHook("enterplanmode-improve"); const pfmEnabled = loadConfig().pfmReminder === true; const improveContext = composeImproveContext({ pfmEnabled, improvementHookContent: hook?.content ?? null, }); - if (improveContext) { - output.system.push(improveContext); - } + const parts = [...stripped, getPlanningPrompt()]; + if (improveContext) parts.push(improveContext); + output.system.length = 0; + output.system.push(...composeSystemPrompt([], parts)); return; } if (!shouldInjectGenericPlanReminder(lastUserAgent, isSubagent, workflowOptions)) return; - output.system.push(`## Plan Submission + const planSubmissionReminder = `## Plan Submission When you have completed your plan, call the \`submit_plan\` tool to submit it for user review. Pass your full plan as a single edit: \`{ "edits": [{ "start": 1, "content": "..." }] }\`. The user will review your plan in a visual UI where they can annotate, approve, or request changes. If rejected, the response includes your plan with line numbers; use targeted edits to revise specific sections. -Do NOT proceed with implementation until your plan is approved.`); +Do NOT proceed with implementation until your plan is approved.`; + const composed = composeSystemPrompt(output.system, [planSubmissionReminder]); + output.system.length = 0; + output.system.push(...composed); }, // Intercept plannotator commands before the agent sees them. diff --git a/apps/opencode-plugin/plan-mode.test.ts b/apps/opencode-plugin/plan-mode.test.ts index d96eddfcd..4abc1a02d 100644 --- a/apps/opencode-plugin/plan-mode.test.ts +++ b/apps/opencode-plugin/plan-mode.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + composeSystemPrompt, normalizeEditPermission, stripConflictingPlanModeRules, } from "./plan-mode"; @@ -72,3 +73,17 @@ describe("stripConflictingPlanModeRules", () => { ).toEqual(["Keep the plan concise."]); }); }); + +describe("composeSystemPrompt", () => { + test.each([ + ["always one element", ["a"], ["b"], ["a\n\nb"]], + ["empty strip case", [], ["prompt"], ["prompt"]], + ["multi-element order", ["base", "extra"], ["add1", "add2"], ["base\n\nextra\n\nadd1\n\nadd2"]], + ["content order (plan path)", ["strip1"], ["plan", "improve"], ["strip1\n\nplan\n\nimprove"]], + ["trailing newlines trimmed", ["base\n"], ["add\n\n"], ["base\n\nadd"]], + ["empty string entry collapses", ["", "a"], ["b"], ["a\n\nb"]], + ["empty inputs", [], [], [""]], + ])("%s", (_name, system, additions, expected) => { + expect(composeSystemPrompt(system, additions)).toEqual(expected); + }); +}); diff --git a/apps/opencode-plugin/plan-mode.ts b/apps/opencode-plugin/plan-mode.ts index 09a8d1ef9..234df2350 100644 --- a/apps/opencode-plugin/plan-mode.ts +++ b/apps/opencode-plugin/plan-mode.ts @@ -54,3 +54,7 @@ export function stripConflictingPlanModeRules(systemEntries: string[]): string[] ) .filter(Boolean); } + +export function composeSystemPrompt(system: string[], additions: string[]): string[] { + return [system.concat(additions).map((s) => s.trim()).filter(Boolean).join("\n\n")]; +}