Files
Andrew bb6a65ac76 Fix OpenCode plugin Jinja template corruption with Qwen3.6 (#1114)
* fix(plugin): consolidate system prompt injections into single array element

The plugin previously pushes planning prompts and improvement contexts as
separate elements in the output.system array. This change appends them to
output.system[0] with newline separators instead. This keeps all system
instructions within a single message block to prevent potential parsing or
formatting issues when the agent processes the context.

* refactor(opencode-plugin): extract composeSystemPrompt helper to centralize system prompt assembly and add unit tests

* style(opencode-plugin): remove extra newline before plan submission reminder heading

* fix(opencode-plugin): store composed prompt result before clearing system array to prevent data loss

Previously, `output.system` was cleared with `length = 0` before being passed into `composeSystemPrompt`, causing the function to compose from an empty array instead of the original system content. The fix stores the composition result in a variable first, then pushes it after clearing. Additionally, add `.trim()` in `stripConflictingPlanModeRules` to normalize whitespace before filtering empty entries, and include a test case for empty string collapse behavior.

* refactor(plan-mode.ts): move string trimming from stripConflictingPlanModeRules to composeSystemPrompt for centralized whitespace handling

* test(plan-mode): add test case for trimming trailing newlines in composeSystemPrompt
2026-08-10 10:09:58 -07:00

61 lines
2.3 KiB
TypeScript

// ── Permission helpers ────────────────────────────────────────────────────
/**
* Normalize an `edit` permission value before merging additional rules.
*
* OpenCode's zod transform converts legacy `tools: { edit: false }` to
* `permission.edit = "deny"` (a plain string) before any plugin sees the
* config. Spreading a string in JS produces char-index keys:
* `{ ..."deny" }` → `{ "0": "d", "1": "e", "2": "n", "3": "y" }`
* which corrupt the permission ruleset and cause zod validation failures.
*
* This function converts a string action to `{ "*": action }` (equivalent
* wildcard object) so the caller can safely spread it and add overrides.
*/
export function normalizeEditPermission(
edit: string | Record<string, string> | undefined,
): Record<string, string> {
if (typeof edit === "string") {
return { "*": edit };
}
return edit ?? {};
}
// ── Prompt stripping ──────────────────────────────────────────────────────
function shouldStripPlanModeLine(line: string): boolean {
const normalized = line.trim().toLowerCase();
return normalized.includes("strictly forbidden: any file edits")
|| normalized.includes("your plan at ")
|| normalized.includes("plan file already exists at ")
|| normalized.includes(".opencode/plans/")
|| normalized.includes("plan_exit")
|| (normalized.includes("agent's conversation") && normalized.includes("not on disk"));
}
function cleanupSystemEntry(entry: string): string {
return entry
.split("\n")
.map((line) => line.replace(/[ \t]+$/g, ""))
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
export function stripConflictingPlanModeRules(systemEntries: string[]): string[] {
return systemEntries
.map((entry) =>
cleanupSystemEntry(
entry
.split("\n")
.filter((line) => !shouldStripPlanModeLine(line))
.join("\n"),
),
)
.filter(Boolean);
}
export function composeSystemPrompt(system: string[], additions: string[]): string[] {
return [system.concat(additions).map((s) => s.trim()).filter(Boolean).join("\n\n")];
}