From 6bc7be4c2173399147856270746480f0408ab5bc Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 14 Jun 2026 08:53:26 +0000 Subject: [PATCH] feat: avoid duplicate project-map hint injection by checking context The before_agent_start handler now inspects the current session context and skips injection if a pi-project-map-hint custom message is already present in the active branch. This prevents duplicate hints on every prompt while still re-injecting after compaction or tree navigation. Also removes the unused hooks/on-prompt.ts prompt-text injector. --- design-doc.md | 5 ++--- hooks/on-prompt.ts | 11 ---------- pi-extension.ts | 26 +++++++++++++++++++++-- tests/pi-extension.test.ts | 42 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 16 deletions(-) delete mode 100644 hooks/on-prompt.ts diff --git a/design-doc.md b/design-doc.md index fea79d0..4c80759 100644 --- a/design-doc.md +++ b/design-doc.md @@ -291,8 +291,6 @@ pi-project-map/ │ ├── merge.ts # Merge AST + LLM outputs │ ├── format.ts # Dense markdown formatter │ └── config.ts # Skill configuration (thresholds, ignore patterns) -├── hooks/ -│ └── on-prompt.ts # Injects maintenance command into prompts └── README.md # Setup and usage for humans ``` @@ -303,8 +301,9 @@ pi-project-map/ - `project-map:reinit [path]` — Force re-initialization of entire project or subtree. ### Prompt Hook -- On every prompt, the skill appends a lightweight instruction: +- On each prompt, the skill injects a lightweight custom message **only if it is not already present in the current branch of context**: > "If you modify any source file, run `project-map:patch ` to update the analysis. If you suspect staleness, run `project-map:validate`." +- The extension checks `ctx.sessionManager.buildSessionContext()` for an existing `pi-project-map-hint` custom message and skips injection when one is found. This prevents duplicate hints after steering, follow-up messages, or multi-turn conversations. The hint is automatically re-injected after compaction or `/tree` navigation removes it from the active path. ## 9. Risks and Tradeoffs diff --git a/hooks/on-prompt.ts b/hooks/on-prompt.ts deleted file mode 100644 index d9293d9..0000000 --- a/hooks/on-prompt.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Pi skill prompt hook -// Injected into every prompt to remind the agent to maintain .pi-map.md files - -export const MAINTENANCE_INSTRUCTION = ` -If you modify any source file, run \`project-map:patch \` to update the analysis. -If you suspect staleness, run \`project-map:validate\`. -`; - -export function injectPrompt(originalPrompt: string): string { - return `${originalPrompt}\n\n---\n${MAINTENANCE_INSTRUCTION}`; -} diff --git a/pi-extension.ts b/pi-extension.ts index 65ec434..b68bf53 100644 --- a/pi-extension.ts +++ b/pi-extension.ts @@ -50,6 +50,25 @@ function isDirty(content: string): boolean { return content.includes("## dirty") && !content.includes("## dirty\n-"); } +const HINT_CUSTOM_TYPE = "pi-project-map-hint"; + +function hintAlreadyInContext(ctx: any): boolean { + const manager = ctx?.sessionManager; + if (!manager || typeof manager.buildSessionContext !== "function") { + return false; + } + + const { messages } = manager.buildSessionContext(); + if (!Array.isArray(messages)) return false; + + return messages.some( + (m: any) => + m && + m.role === "custom" && + m.customType === HINT_CUSTOM_TYPE, + ); +} + export default function (pi: ExtensionAPI) { pi.registerTool({ name: "project_map_init", @@ -236,14 +255,17 @@ export default function (pi: ExtensionAPI) { } }); - // Inject maintenance instructions before agent starts + // Inject maintenance instructions before agent starts, but only once + // within the current branch of context. Re-inject after compaction or + // tree navigation removes the hint from the active path. pi.on("before_agent_start", async (_event, _ctx) => { const mapFiles = findPiMapFiles(_ctx.cwd); if (mapFiles.length === 0) return {}; + if (hintAlreadyInContext(_ctx)) return {}; return { message: { - customType: "pi-project-map-hint", + customType: HINT_CUSTOM_TYPE, content: "📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.", display: false, diff --git a/tests/pi-extension.test.ts b/tests/pi-extension.test.ts index b0d4e9b..6cc4ce2 100644 --- a/tests/pi-extension.test.ts +++ b/tests/pi-extension.test.ts @@ -48,6 +48,9 @@ describe("pi-extension", () => { getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })), }, ui: { notify: mockNotify }, + sessionManager: { + buildSessionContext: vi.fn(() => ({ messages: [] })), + }, }; const mockPi = { @@ -225,5 +228,44 @@ describe("pi-extension", () => { expect(result).toEqual({}); }); + + it("does not inject hint when it is already in context", async () => { + const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); + writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n"); + mockCtx.cwd = dir; + mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({ + messages: [ + { + role: "custom", + customType: "pi-project-map-hint", + content: "existing hint", + display: false, + }, + ], + })); + + const handler = registeredEvents.before_agent_start; + const result = await handler(null, mockCtx); + + expect(result).toEqual({}); + }); + + it("injects hint again when previous context did not contain it", async () => { + const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); + writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n"); + mockCtx.cwd = dir; + mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({ + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: [{ type: "text", text: "hi" }] }, + ], + })); + + const handler = registeredEvents.before_agent_start; + const result = await handler(null, mockCtx); + + expect(result).toHaveProperty("message"); + expect(result.message.customType).toBe("pi-project-map-hint"); + }); }); });