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.
This commit is contained in:
Developer
2026-06-14 08:53:26 +00:00
parent 5c6719817f
commit 6bc7be4c21
4 changed files with 68 additions and 16 deletions
+2 -3
View File
@@ -291,8 +291,6 @@ pi-project-map/
│ ├── merge.ts # Merge AST + LLM outputs │ ├── merge.ts # Merge AST + LLM outputs
│ ├── format.ts # Dense markdown formatter │ ├── format.ts # Dense markdown formatter
│ └── config.ts # Skill configuration (thresholds, ignore patterns) │ └── config.ts # Skill configuration (thresholds, ignore patterns)
├── hooks/
│ └── on-prompt.ts # Injects maintenance command into prompts
└── README.md # Setup and usage for humans └── 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. - `project-map:reinit [path]` — Force re-initialization of entire project or subtree.
### Prompt Hook ### 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 <path>` to update the analysis. If you suspect staleness, run `project-map:validate`." > "If you modify any source file, run `project-map:patch <path>` 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 ## 9. Risks and Tradeoffs
-11
View File
@@ -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 <file-path>\` 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}`;
}
+24 -2
View File
@@ -50,6 +50,25 @@ function isDirty(content: string): boolean {
return content.includes("## dirty") && !content.includes("## dirty\n-"); 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) { export default function (pi: ExtensionAPI) {
pi.registerTool({ pi.registerTool({
name: "project_map_init", 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) => { pi.on("before_agent_start", async (_event, _ctx) => {
const mapFiles = findPiMapFiles(_ctx.cwd); const mapFiles = findPiMapFiles(_ctx.cwd);
if (mapFiles.length === 0) return {}; if (mapFiles.length === 0) return {};
if (hintAlreadyInContext(_ctx)) return {};
return { return {
message: { message: {
customType: "pi-project-map-hint", customType: HINT_CUSTOM_TYPE,
content: 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`.", "📋 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, display: false,
+42
View File
@@ -48,6 +48,9 @@ describe("pi-extension", () => {
getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })), getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })),
}, },
ui: { notify: mockNotify }, ui: { notify: mockNotify },
sessionManager: {
buildSessionContext: vi.fn(() => ({ messages: [] })),
},
}; };
const mockPi = { const mockPi = {
@@ -225,5 +228,44 @@ describe("pi-extension", () => {
expect(result).toEqual({}); 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");
});
}); });
}); });