Commit 07d61f2200f8

Vincent Demeester <vincent@sbr.pm>
2026-03-09 11:33:58
feat(pi): enhance extensions with prompt guidelines
Added promptSnippet/promptGuidelines to org-todos and lsp tools for better LLM guidance. Added debug-requests extension for toggling provider payload logging. Added worktree-context extension to inject lazyworktree notes into system prompt when running inside a worktree.
1 parent d25724a
Changed files (4)
dots/pi/agent/extensions/lsp/lsp-tool.ts
@@ -225,6 +225,10 @@ export default function (pi: ExtensionAPI) {
   pi.registerTool({
     name: "lsp",
     label: "LSP",
+    promptGuidelines: [
+      "Use lsp for go-to-definition, find-references, hover, and rename — faster and more accurate than grep for supported languages",
+      "Use lsp diagnostics to check for errors before suggesting fixes",
+    ],
     description: `Query language server for definitions, references, types, symbols, diagnostics, rename, and code actions.
 
 Actions: definition, references, hover, signature, rename (require file + line/column or query), symbols (file, optional query), diagnostics (file), workspace-diagnostics (files array), codeAction (file + position).
dots/pi/agent/extensions/org-todos/index.ts
@@ -306,6 +306,12 @@ export default function (pi: ExtensionAPI) {
   (pi as any).registerTool({
     name: "org_todo",
     label: "Org TODO",
+    promptSnippet: "Manage org-mode TODOs. Actions: list, scheduled, upcoming, overdue, search, get, done, state, schedule, deadline, priority, add, append, inbox-list, inbox-count, inbox-add, refile-targets, refile",
+    promptGuidelines: [
+      "NEVER edit .org files directly — always use the org_todo tool for all TODO operations",
+      "For scheduling, use YYYY-MM-DD date format (natural language dates are NOT supported by this tool)",
+      "Use inbox-add for quick capture, then refile to the appropriate section later",
+    ],
     description: `Manage org-mode TODOs. Actions:
 - list: List active TODOs (TODO, NEXT, STRT)
 - scheduled: Get today's scheduled items
dots/pi/agent/extensions/debug-requests.ts
@@ -0,0 +1,90 @@
+/**
+ * Provider Request Debug Extension
+ *
+ * Logs provider request payloads for debugging cache behavior,
+ * token usage, and request structure.
+ *
+ * Toggle: PI_DEBUG_REQUESTS=1 or /debug-requests command
+ *
+ * Logs to: /tmp/pi-requests.log (truncated per session)
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { appendFileSync, writeFileSync } from "node:fs";
+
+const LOG_FILE = "/tmp/pi-requests.log";
+
+export default function (pi: ExtensionAPI) {
+  let enabled = process.env.PI_DEBUG_REQUESTS === "1";
+  let requestCount = 0;
+
+  pi.on("before_provider_request", (event) => {
+    if (!enabled) return;
+
+    requestCount++;
+    const timestamp = new Date().toISOString();
+    const payload = event.payload;
+
+    // Summarize rather than dump entire payload (messages can be huge)
+    const summary: Record<string, any> = {
+      timestamp,
+      request: requestCount,
+      model: payload.model,
+    };
+
+    // Count messages by role
+    if (payload.messages) {
+      const roles: Record<string, number> = {};
+      for (const msg of payload.messages) {
+        roles[msg.role] = (roles[msg.role] || 0) + 1;
+      }
+      summary.messages = { count: payload.messages.length, roles };
+    }
+
+    // System prompt length
+    if (payload.system) {
+      if (typeof payload.system === "string") {
+        summary.systemPromptChars = payload.system.length;
+      } else if (Array.isArray(payload.system)) {
+        summary.systemPromptChars = payload.system.reduce(
+          (acc: number, b: any) => acc + (b.text?.length || 0), 0
+        );
+      }
+    }
+
+    // Tool count
+    if (payload.tools) {
+      summary.toolCount = payload.tools.length;
+    }
+
+    // Thinking config
+    if (payload.thinking) {
+      summary.thinking = payload.thinking;
+    }
+
+    // Max tokens
+    summary.maxTokens = payload.max_tokens;
+
+    // Temperature
+    if (payload.temperature !== undefined) {
+      summary.temperature = payload.temperature;
+    }
+
+    appendFileSync(LOG_FILE, JSON.stringify(summary, null, 2) + "\n---\n", "utf8");
+  });
+
+  pi.registerCommand("debug-requests", {
+    description: "Toggle provider request payload logging to /tmp/pi-requests.log",
+    handler: async (_args, ctx) => {
+      enabled = !enabled;
+      if (enabled) {
+        requestCount = 0;
+        writeFileSync(LOG_FILE, `# Pi request debug log - ${new Date().toISOString()}\n\n`, "utf8");
+      }
+      ctx.ui.notify(
+        enabled ? `Request debugging ON → ${LOG_FILE}` : "Request debugging OFF",
+        "info",
+      );
+    },
+  });
+}
dots/pi/agent/extensions/worktree-context.ts
@@ -0,0 +1,135 @@
+/**
+ * Worktree Context Extension
+ *
+ * When running inside a lazyworktree worktree, injects the worktree note
+ * into the system prompt via before_agent_start. This gives the LLM context
+ * about what the worktree is for (PR summary, ticket notes, etc.).
+ *
+ * Detection: checks if cwd is under ~/.local/share/worktrees/
+ * Notes source: lazyworktree's worktree-notes.json or splitted markdown files
+ *
+ * Commands:
+ *   /worktree-note  — Show the current worktree note (if any)
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { existsSync, readFileSync } from "node:fs";
+import { basename, relative } from "node:path";
+import { homedir } from "node:os";
+import { join } from "node:path";
+
+const WORKTREE_BASE = join(homedir(), ".local/share/worktrees");
+const NOTES_JSON = join(homedir(), ".local/share/lazyworktree/worktree-notes.json");
+
+interface WorktreeInfo {
+  org: string;
+  repo: string;
+  branch: string;
+  relativePath: string;
+}
+
+function detectWorktree(cwd: string): WorktreeInfo | null {
+  if (!cwd.startsWith(WORKTREE_BASE)) return null;
+
+  const rel = relative(WORKTREE_BASE, cwd);
+  const parts = rel.split("/");
+  // Expected: org/repo/branch[/...]
+  if (parts.length < 3) return null;
+
+  return {
+    org: parts[0],
+    repo: parts[1],
+    branch: parts[2],
+    relativePath: parts.slice(0, 3).join("/"),
+  };
+}
+
+function loadNoteFromJson(relativePath: string): string | null {
+  if (!existsSync(NOTES_JSON)) return null;
+
+  try {
+    const data = JSON.parse(readFileSync(NOTES_JSON, "utf8"));
+    // Notes are keyed by relative path from worktree_dir
+    const note = data[relativePath];
+    if (note && typeof note === "object" && note.content) {
+      return note.content;
+    }
+    if (typeof note === "string" && note.trim()) {
+      return note;
+    }
+  } catch {
+    // Ignore parse errors
+  }
+  return null;
+}
+
+function loadSplittedNote(info: WorktreeInfo): string | null {
+  // Try common splitted note locations
+  const candidates = [
+    join(homedir(), ".local/share/lazyworktree/notes", info.org, info.repo, `${info.branch}.md`),
+    join(homedir(), ".local/share/lazyworktree/notes", `${info.org}/${info.repo}`, `${info.branch}.md`),
+  ];
+
+  for (const path of candidates) {
+    if (existsSync(path)) {
+      try {
+        const content = readFileSync(path, "utf8").trim();
+        // Strip YAML frontmatter if present
+        if (content.startsWith("---")) {
+          const endIdx = content.indexOf("---", 3);
+          if (endIdx > 0) {
+            return content.slice(endIdx + 3).trim();
+          }
+        }
+        return content;
+      } catch {
+        continue;
+      }
+    }
+  }
+  return null;
+}
+
+export default function (pi: ExtensionAPI) {
+  let cachedInfo: WorktreeInfo | null = null;
+  let cachedNote: string | null = null;
+
+  pi.on("session_start", async (_event, ctx) => {
+    cachedInfo = detectWorktree(ctx.cwd);
+    if (cachedInfo) {
+      cachedNote = loadSplittedNote(cachedInfo) ?? loadNoteFromJson(cachedInfo.relativePath);
+    }
+  });
+
+  pi.on("before_agent_start", async (event, _ctx) => {
+    if (!cachedInfo || !cachedNote) return;
+
+    const header = `\n\n## Worktree Context\nYou are working in a git worktree: ${cachedInfo.org}/${cachedInfo.repo} (branch: ${cachedInfo.branch})\n\nWorktree notes:\n${cachedNote}`;
+
+    return {
+      systemPrompt: event.systemPrompt + header,
+    };
+  });
+
+  pi.registerCommand("worktree-note", {
+    description: "Show the current worktree note (if any)",
+    handler: async (_args, ctx) => {
+      const info = detectWorktree(ctx.cwd);
+      if (!info) {
+        ctx.ui.notify("Not in a worktree", "info");
+        return;
+      }
+
+      const note = loadSplittedNote(info) ?? loadNoteFromJson(info.relativePath);
+      if (!note) {
+        ctx.ui.notify(`Worktree ${info.org}/${info.repo}/${info.branch}: no note found`, "info");
+        return;
+      }
+
+      ctx.ui.notify(
+        `📝 ${info.org}/${info.repo}/${info.branch}\n${note}`,
+        "info",
+      );
+    },
+  });
+}