mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
807cc5f09b
* feat(submit-plan): replace text/file-path mode with edit-based interface Switches the OpenCode submit_plan tool from a dual-mode interface (inline text or file path) to an edit-based one. The plugin now owns a backing file at .opencode/plans/_active-plan.md; the agent never reads or writes it directly. On denial, the response includes the current plan with line numbers so the agent can apply surgical edits instead of resubmitting the entire document, reducing token waste on iterative revisions. - Add applyEdits, validateEdits, formatWithLineNumbers, and getPlanBackingPath helpers to the plugin - Validate edit ranges (bounds, overlap, size limit) before mutating the backing file - Return line-numbered plan in denial responses to anchor targeted edits - Remove getPlanDirectory, validatePlanPath, and file-path auto- detection from plan-mode.ts - Replace plan-mode.test.ts path-validation coverage with submit- plan.test.ts for the edit engine - Update custom-feedback.md and opencode.md docs for edit-based semantics Refs #365 * chore(opencode): drop unused buildPlanFileRule import Removed in PR #730 deny path along with the only call site, but the import was left behind. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com>
57 lines
2.1 KiB
TypeScript
57 lines
2.1 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);
|
|
}
|