Commit 07d61f2200f8
Changed files (4)
dots
pi
agent
extensions
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",
+ );
+ },
+ });
+}