From 15f09de12eda797dac6fe9f82dd23a1c594ca083 Mon Sep 17 00:00:00 2001 From: Aviad Shiber Date: Mon, 30 Mar 2026 11:58:45 +0300 Subject: [PATCH 1/4] feat(feedback): add context anchoring instructions to plan denial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a plan is denied, instruct the agent to maintain a `## Decisions Log` section in the plan tracking rejected approaches and their reasons. This implements the context anchoring pattern from Martin Fowler's article: the plan itself becomes the persistent ADR, visible in diffs and archived with every decision — at zero infrastructure cost. Closes backnotprop/plannotator#431 Co-Authored-By: Claude Sonnet 4.6 --- packages/shared/feedback-templates.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/feedback-templates.ts b/packages/shared/feedback-templates.ts index 8d6c3426..3c293547 100644 --- a/packages/shared/feedback-templates.ts +++ b/packages/shared/feedback-templates.ts @@ -18,5 +18,7 @@ export const planDenyFeedback = ( ? `- Your plan is saved at: ${options.planFilePath}\n You can edit this file to make targeted changes, then pass its path to ${toolName}.\n` : ""; - return `YOUR PLAN WAS NOT APPROVED.\n\nYou MUST revise the plan to address ALL of the feedback below before calling ${toolName} again.\n\nRules:\n${planFileRule}- Do not resubmit the same plan unchanged.\n- Do NOT change the plan title (first # heading) unless the user explicitly asks you to.\n\n${feedback || "Plan changes requested"}`; + const contextAnchoringInstructions = `\n## Context Anchoring\n\nBefore revising your plan:\n1. Add (or update) a \`## Decisions Log\` section at the bottom of the plan.\n2. For each rejected approach from this feedback, add an entry:\n - **Rejected:** [brief description of the rejected approach] **Why:** [reason from this feedback]\n3. Do NOT re-propose approaches already listed in the Decisions Log — it is your cross-session memory.\n`; + + return `YOUR PLAN WAS NOT APPROVED.\n\nYou MUST revise the plan to address ALL of the feedback below before calling ${toolName} again.\n\nRules:\n${planFileRule}- Do not resubmit the same plan unchanged.\n- Do NOT change the plan title (first # heading) unless the user explicitly asks you to.\n${contextAnchoringInstructions}\n${feedback || "Plan changes requested"}`; }; From c77fa3f41162ce526b21f4dbbbe7273f9bcc3841 Mon Sep 17 00:00:00 2001 From: Aviad Shiber Date: Mon, 30 Mar 2026 12:03:37 +0300 Subject: [PATCH 2/4] feat(feedback): add planApproveFeedback with Decisions Log reminder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds planApproveFeedback() to shared feedback templates and uses it in the OpenCode plugin approval paths. On approval, the agent is reminded to reference the Decisions Log (built up during denial iterations) during implementation — completing the context anchoring loop for both deny and approve flows. Co-Authored-By: Claude Sonnet 4.6 --- apps/opencode-plugin/index.ts | 17 ++--------------- packages/shared/feedback-templates.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index 0bb75c8d..6868a076 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -40,7 +40,7 @@ import { handleArchiveCommand, type CommandDeps, } from "./commands"; -import { planDenyFeedback } from "@plannotator/shared/feedback-templates"; +import { planDenyFeedback, planApproveFeedback } from "@plannotator/shared/feedback-templates"; import { normalizeEditPermission, stripConflictingPlanModeRules, @@ -473,20 +473,7 @@ Do NOT proceed with implementation until your plan is approved.`); } } - if (result.feedback) { - return `Plan approved with notes! -${result.savedPath ? `Saved to: ${result.savedPath}` : ""} - -## Implementation Notes - -The user approved your plan but added the following notes to consider during implementation: - -${result.feedback} - -Proceed with implementation, incorporating these notes where applicable.`; - } - - return `Plan approved!${result.savedPath ? ` Saved to: ${result.savedPath}` : ""}`; + return planApproveFeedback(result.feedback, result.savedPath); } else { return planDenyFeedback(result.feedback || "", "submit_plan", { planFilePath: sourceFilePath, diff --git a/packages/shared/feedback-templates.ts b/packages/shared/feedback-templates.ts index 3c293547..fe07e984 100644 --- a/packages/shared/feedback-templates.ts +++ b/packages/shared/feedback-templates.ts @@ -9,6 +9,19 @@ export interface PlanDenyFeedbackOptions { planFilePath?: string; } +export const planApproveFeedback = ( + notes?: string, + savedPath?: string, +): string => { + const savedNote = savedPath ? `\nSaved to: ${savedPath}` : ""; + const notesSection = notes + ? `\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n${notes}\n\nProceed with implementation, incorporating these notes where applicable.` + : ""; + const decisionsLogNote = `\n\nIf your plan contains a \`## Decisions Log\`, keep it as a reference during implementation — it documents the rejected alternatives that shaped this design.`; + + return `Plan approved!${savedNote}${notesSection}${decisionsLogNote}`; +}; + export const planDenyFeedback = ( feedback: string, toolName: string = "ExitPlanMode", From c1eec198160a03a6319527a5273d486834904437 Mon Sep 17 00:00:00 2001 From: Aviad Shiber Date: Mon, 30 Mar 2026 12:11:33 +0300 Subject: [PATCH 3/4] test(feedback-templates): add context anchoring test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests that planDenyFeedback includes Decisions Log instructions and planApproveFeedback includes the Decisions Log reminder — for both plain approval and approval-with-notes paths. Co-Authored-By: Claude Sonnet 4.6 --- packages/shared/feedback-templates.test.ts | 44 +++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/shared/feedback-templates.test.ts b/packages/shared/feedback-templates.test.ts index 47564a00..afe79c06 100644 --- a/packages/shared/feedback-templates.test.ts +++ b/packages/shared/feedback-templates.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { planDenyFeedback } from "./feedback-templates"; +import { planDenyFeedback, planApproveFeedback } from "./feedback-templates"; describe("feedback-templates", () => { /** @@ -63,3 +63,45 @@ describe("feedback-templates", () => { }); }); + +describe("context anchoring", () => { + /** + * On denial, the agent must be instructed to maintain a Decisions Log + * so that rejected approaches are documented and not re-proposed. + */ + test("plan deny includes context anchoring instructions", () => { + const result = planDenyFeedback("some feedback"); + expect(result).toContain("Decisions Log"); + expect(result).toContain("Rejected:"); + expect(result).toContain("cross-session memory"); + }); + + /** + * On approval, the agent must be reminded to reference the Decisions Log + * during implementation — closing the context anchoring loop. + */ + test("plan approve includes Decisions Log reminder", () => { + const result = planApproveFeedback(); + expect(result).toContain("Plan approved"); + expect(result).toContain("Decisions Log"); + }); + + /** + * Approval with notes must include both the user's notes and the + * Decisions Log reminder — neither should displace the other. + */ + test("plan approve with notes includes both notes and Decisions Log reminder", () => { + const result = planApproveFeedback("Use the adapter pattern here."); + expect(result).toContain("Implementation Notes"); + expect(result).toContain("Use the adapter pattern here."); + expect(result).toContain("Decisions Log"); + }); + + /** + * Approval with saved path must surface the file path. + */ + test("plan approve with savedPath includes the path", () => { + const result = planApproveFeedback(undefined, "/tmp/plans/auth.md"); + expect(result).toContain("/tmp/plans/auth.md"); + }); +}); From f93f64d8937b7259808f7321c3e60d3af4b8427e Mon Sep 17 00:00:00 2001 From: Aviad Shiber Date: Mon, 30 Mar 2026 14:54:08 +0300 Subject: [PATCH 4/4] fix(feedback): address code review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - planApproveFeedback: header now reads "Plan approved with notes!" when notes are present, so the agent knows content follows - Extract DECISIONS_LOG_NOTE as a named export so Pi (and other integrations with custom approval messages) can append it without duplicating the string - Pi approval paths now include DECISIONS_LOG_NOTE in both the with-notes and plain-approval messages - planDenyFeedback: soften "Do NOT re-propose" to "Once added, do NOT re-propose" — accurate on first denial when the log doesn't exist yet - Add tests: "with notes" header, "without notes" header, DECISIONS_LOG_NOTE export Co-Authored-By: Claude Sonnet 4.6 --- apps/pi-extension/index.ts | 6 ++--- packages/shared/feedback-templates.test.ts | 28 +++++++++++++++++++--- packages/shared/feedback-templates.ts | 12 +++++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/apps/pi-extension/index.ts b/apps/pi-extension/index.ts index db58ef9a..db029a33 100644 --- a/apps/pi-extension/index.ts +++ b/apps/pi-extension/index.ts @@ -34,7 +34,7 @@ import { markCompletedSteps, parseChecklist, } from "./generated/checklist.js"; -import { planDenyFeedback } from "./generated/feedback-templates.js"; +import { planDenyFeedback, DECISIONS_LOG_NOTE } from "./generated/feedback-templates.js"; import { hasMarkdownFiles } from "./generated/resolve-file.js"; import { FILE_BROWSER_EXCLUDED } from "./generated/reference-common.js"; import { openBrowser } from "./server/network.js"; @@ -666,7 +666,7 @@ export default function plannotator(pi: ExtensionAPI): void { content: [ { type: "text", - text: `Plan approved with notes! You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n${result.feedback}\n\nProceed with implementation, incorporating these notes where applicable.`, + text: `Plan approved with notes! You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n${result.feedback}\n\nProceed with implementation, incorporating these notes where applicable.${DECISIONS_LOG_NOTE}`, }, ], details: { approved: true, feedback: result.feedback }, @@ -677,7 +677,7 @@ export default function plannotator(pi: ExtensionAPI): void { content: [ { type: "text", - text: `Plan approved. You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}`, + text: `Plan approved. You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}${DECISIONS_LOG_NOTE}`, }, ], details: { approved: true }, diff --git a/packages/shared/feedback-templates.test.ts b/packages/shared/feedback-templates.test.ts index afe79c06..aa8ed64b 100644 --- a/packages/shared/feedback-templates.test.ts +++ b/packages/shared/feedback-templates.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { planDenyFeedback, planApproveFeedback } from "./feedback-templates"; +import { planDenyFeedback, planApproveFeedback, DECISIONS_LOG_NOTE } from "./feedback-templates"; describe("feedback-templates", () => { /** @@ -87,16 +87,38 @@ describe("context anchoring", () => { }); /** - * Approval with notes must include both the user's notes and the - * Decisions Log reminder — neither should displace the other. + * Approval with notes must signal "with notes" in the header so the + * agent knows content follows, and include both the notes and the + * Decisions Log reminder. */ test("plan approve with notes includes both notes and Decisions Log reminder", () => { const result = planApproveFeedback("Use the adapter pattern here."); + expect(result).toContain("Plan approved with notes!"); expect(result).toContain("Implementation Notes"); expect(result).toContain("Use the adapter pattern here."); expect(result).toContain("Decisions Log"); }); + /** + * Approval without notes should NOT say "with notes". + */ + test("plan approve without notes does not include 'with notes' in header", () => { + const result = planApproveFeedback(); + expect(result).toContain("Plan approved!"); + expect(result).not.toContain("with notes"); + }); + + /** + * DECISIONS_LOG_NOTE is exported as a named constant so Pi and other + * integrations that compose their own approval messages can include it + * without duplicating the text. + */ + test("DECISIONS_LOG_NOTE is a non-empty string containing 'Decisions Log'", () => { + expect(typeof DECISIONS_LOG_NOTE).toBe("string"); + expect(DECISIONS_LOG_NOTE.length).toBeGreaterThan(0); + expect(DECISIONS_LOG_NOTE).toContain("Decisions Log"); + }); + /** * Approval with saved path must surface the file path. */ diff --git a/packages/shared/feedback-templates.ts b/packages/shared/feedback-templates.ts index fe07e984..8beed2a3 100644 --- a/packages/shared/feedback-templates.ts +++ b/packages/shared/feedback-templates.ts @@ -9,17 +9,23 @@ export interface PlanDenyFeedbackOptions { planFilePath?: string; } +/** + * Appended to all approval messages so the agent knows to carry the + * Decisions Log forward into implementation. + */ +export const DECISIONS_LOG_NOTE = `\n\nIf your plan contains a \`## Decisions Log\`, keep it as a reference during implementation — it documents the rejected alternatives that shaped this design.`; + export const planApproveFeedback = ( notes?: string, savedPath?: string, ): string => { + const header = `Plan approved${notes ? " with notes" : ""}!`; const savedNote = savedPath ? `\nSaved to: ${savedPath}` : ""; const notesSection = notes ? `\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n${notes}\n\nProceed with implementation, incorporating these notes where applicable.` : ""; - const decisionsLogNote = `\n\nIf your plan contains a \`## Decisions Log\`, keep it as a reference during implementation — it documents the rejected alternatives that shaped this design.`; - return `Plan approved!${savedNote}${notesSection}${decisionsLogNote}`; + return `${header}${savedNote}${notesSection}${DECISIONS_LOG_NOTE}`; }; export const planDenyFeedback = ( @@ -31,7 +37,7 @@ export const planDenyFeedback = ( ? `- Your plan is saved at: ${options.planFilePath}\n You can edit this file to make targeted changes, then pass its path to ${toolName}.\n` : ""; - const contextAnchoringInstructions = `\n## Context Anchoring\n\nBefore revising your plan:\n1. Add (or update) a \`## Decisions Log\` section at the bottom of the plan.\n2. For each rejected approach from this feedback, add an entry:\n - **Rejected:** [brief description of the rejected approach] **Why:** [reason from this feedback]\n3. Do NOT re-propose approaches already listed in the Decisions Log — it is your cross-session memory.\n`; + const contextAnchoringInstructions = `\n## Context Anchoring\n\nBefore revising your plan:\n1. Add (or update) a \`## Decisions Log\` section at the bottom of the plan.\n2. For each rejected approach from this feedback, add an entry:\n - **Rejected:** [brief description of the rejected approach] **Why:** [reason from this feedback]\n3. Once added, do NOT re-propose approaches listed in the Decisions Log — it is your cross-session memory.\n`; return `YOUR PLAN WAS NOT APPROVED.\n\nYou MUST revise the plan to address ALL of the feedback below before calling ${toolName} again.\n\nRules:\n${planFileRule}- Do not resubmit the same plan unchanged.\n- Do NOT change the plan title (first # heading) unless the user explicitly asks you to.\n${contextAnchoringInstructions}\n${feedback || "Plan changes requested"}`; };