Commit 7aea508579dc

Vincent Demeester <vincent@sbr.pm>
2026-02-06 22:38:04
remove(pi): delete pre-commit.ts extension - API doesn't support the pattern
Removed pre-commit.ts extension after research confirmed it cannot work. Why it doesn't work: - tool_call event can only block or allow (return {block, reason}) - Cannot modify state (run formatters/git add) and then allow execution - No API support for "pause, run commands, resume" pattern - Event flow: tool_call (validate) โ†’ execute โ†’ tool_result (modify output) Git hooks already provide this functionality: - .pre-commit-config.yaml runs formatters on pre-push - Actually blocks commits (tested and verified) - Properly integrated with git via git-hooks.nix validate-git-push.ts is the correct pattern: - Uses tool_call to block dangerous commands - No side effects during interception - Pure validation/blocking logic Research: ~/.local/share/ai/research/2026-02/2026-02-06-pi-tool-call-interception-limitations.md The pre-commit.ts extension attempted to: 1. Intercept git commit commands 2. Run formatters (nixfmt, ruff, prettier) 3. Re-stage formatted files 4. Allow original commit to proceed This pattern is fundamentally unsupported by the pi extension API.
1 parent 4bba668
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/pre-commit.ts
@@ -1,298 +0,0 @@
-/**
- * Pi Extension: Pre-commit Linting and Formatting
- *
- * Intercepts git commit commands and runs appropriate linters/formatters
- * before allowing the commit. Detects project type and runs the right tools.
- *
- * Detection and tools:
- * - Nix files (.nix): nixfmt, deadnix, statix
- * - Go files (.go): gofmt, go vet
- * - Python files (.py): ruff format, ruff check
- * - TypeScript/JavaScript: prettier
- * - Shell scripts (.sh): shellcheck
- *
- * Also checks for:
- * - pre-commit hooks (runs `pre-commit run --files` if .pre-commit-config.yaml exists)
- * - Makefile targets (runs `make fmt` or `make format` if available)
- *
- * The extension only formats staged files, not the entire project.
- */
-
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
-import { execSync, spawnSync } from "node:child_process";
-import { existsSync, readFileSync } from "node:fs";
-import { join, extname } from "node:path";
-
-// Check if a command exists
-function commandExists(cmd: string): boolean {
-  try {
-    execSync(`which ${cmd}`, { stdio: "ignore" });
-    return true;
-  } catch {
-    return false;
-  }
-}
-
-// Get staged files from git
-function getStagedFiles(): string[] {
-  try {
-    const output = execSync("git diff --cached --name-only --diff-filter=ACMR", {
-      encoding: "utf-8",
-    });
-    return output.trim().split("\n").filter(Boolean);
-  } catch {
-    return [];
-  }
-}
-
-// Group files by extension/type
-function groupFilesByType(files: string[]): Map<string, string[]> {
-  const groups = new Map<string, string[]>();
-
-  for (const file of files) {
-    const ext = extname(file).toLowerCase();
-    let type: string;
-
-    switch (ext) {
-      case ".nix":
-        type = "nix";
-        break;
-      case ".go":
-        type = "go";
-        break;
-      case ".py":
-        type = "python";
-        break;
-      case ".ts":
-      case ".tsx":
-      case ".js":
-      case ".jsx":
-      case ".json":
-      case ".md":
-      case ".yaml":
-      case ".yml":
-        type = "prettier";
-        break;
-      case ".sh":
-      case ".bash":
-        type = "shell";
-        break;
-      case ".el":
-        type = "elisp";
-        break;
-      default:
-        continue;
-    }
-
-    if (!groups.has(type)) {
-      groups.set(type, []);
-    }
-    groups.get(type)!.push(file);
-  }
-
-  return groups;
-}
-
-// Run a formatter/linter on files
-function runTool(
-  cmd: string,
-  args: string[],
-  files: string[],
-  cwd: string
-): { success: boolean; output: string } {
-  if (files.length === 0) {
-    return { success: true, output: "" };
-  }
-
-  const result = spawnSync(cmd, [...args, ...files], {
-    cwd,
-    encoding: "utf-8",
-    stdio: ["pipe", "pipe", "pipe"],
-  });
-
-  const output = (result.stdout || "") + (result.stderr || "");
-  return {
-    success: result.status === 0,
-    output: output.trim(),
-  };
-}
-
-// Check if pre-commit is available and configured
-function hasPreCommitConfig(cwd: string): boolean {
-  return existsSync(join(cwd, ".pre-commit-config.yaml"));
-}
-
-// Check if Makefile has format target
-function hasMakeFormatTarget(cwd: string): boolean {
-  const makefilePath = join(cwd, "Makefile");
-  if (!existsSync(makefilePath)) return false;
-
-  try {
-    const content = readFileSync(makefilePath, "utf-8");
-    return /^(fmt|format):/m.test(content);
-  } catch {
-    return false;
-  }
-}
-
-export default function (pi: ExtensionAPI) {
-  // Intercept git commit commands
-  pi.on("tool_call", async (event, ctx) => {
-    if (event.toolName.toLowerCase() !== "bash") {
-      return undefined;
-    }
-
-    const command = event.input?.command;
-    if (!command || typeof command !== "string") {
-      return undefined;
-    }
-
-    // Check if this is a git commit command
-    const gitCommitPattern = /(^|&&|\|\||;|\||\$\()\s*git\s+commit(\s|$)/;
-    if (!gitCommitPattern.test(command)) {
-      return undefined;
-    }
-
-    // Skip if --no-verify flag is present (user explicitly wants to skip hooks)
-    if (/--no-verify/.test(command)) {
-      ctx.ui.notify("[pre-commit] Skipping checks (--no-verify)", "warning");
-      return undefined;
-    }
-
-    const cwd = process.cwd();
-    const stagedFiles = getStagedFiles();
-
-    if (stagedFiles.length === 0) {
-      return undefined; // No staged files, let git handle it
-    }
-
-    ctx.ui.notify(`[pre-commit] Checking ${stagedFiles.length} staged files...`, "info");
-
-    const errors: string[] = [];
-    const formatted: string[] = [];
-
-    // If pre-commit is configured, use it
-    if (hasPreCommitConfig(cwd) && commandExists("pre-commit")) {
-      ctx.ui.notify("[pre-commit] Running pre-commit hooks...", "info");
-      const result = runTool("pre-commit", ["run", "--files"], stagedFiles, cwd);
-      if (!result.success) {
-        errors.push(`pre-commit failed:\n${result.output}`);
-      } else {
-        formatted.push("pre-commit hooks passed");
-      }
-    } else {
-      // Run individual formatters based on file types
-      const fileGroups = groupFilesByType(stagedFiles);
-
-      // Nix files
-      const nixFiles = fileGroups.get("nix") || [];
-      if (nixFiles.length > 0) {
-        if (commandExists("nixfmt")) {
-          const result = runTool("nixfmt", ["--check"], nixFiles, cwd);
-          if (!result.success) {
-            // Try to format
-            const fmtResult = runTool("nixfmt", [], nixFiles, cwd);
-            if (fmtResult.success) {
-              formatted.push(`nixfmt: formatted ${nixFiles.length} files`);
-            } else {
-              errors.push(`nixfmt failed:\n${fmtResult.output}`);
-            }
-          }
-        }
-        if (commandExists("deadnix")) {
-          const result = runTool("deadnix", ["--fail"], nixFiles, cwd);
-          if (!result.success) {
-            errors.push(`deadnix found issues:\n${result.output}`);
-          }
-        }
-      }
-
-      // Go files
-      const goFiles = fileGroups.get("go") || [];
-      if (goFiles.length > 0 && commandExists("gofmt")) {
-        const result = runTool("gofmt", ["-l"], goFiles, cwd);
-        if (result.output) {
-          // Files need formatting
-          const fmtResult = runTool("gofmt", ["-w"], goFiles, cwd);
-          if (fmtResult.success) {
-            formatted.push(`gofmt: formatted ${goFiles.length} files`);
-          } else {
-            errors.push(`gofmt failed:\n${fmtResult.output}`);
-          }
-        }
-      }
-
-      // Python files
-      const pyFiles = fileGroups.get("python") || [];
-      if (pyFiles.length > 0 && commandExists("ruff")) {
-        // Format first
-        const fmtResult = runTool("ruff", ["format"], pyFiles, cwd);
-        if (fmtResult.success) {
-          formatted.push(`ruff format: ${pyFiles.length} files`);
-        }
-        // Then check
-        const checkResult = runTool("ruff", ["check", "--fix"], pyFiles, cwd);
-        if (!checkResult.success) {
-          errors.push(`ruff check found issues:\n${checkResult.output}`);
-        }
-      }
-
-      // Prettier files (JS/TS/JSON/MD/YAML)
-      const prettierFiles = fileGroups.get("prettier") || [];
-      if (prettierFiles.length > 0 && commandExists("prettier")) {
-        const result = runTool("prettier", ["--write"], prettierFiles, cwd);
-        if (result.success) {
-          formatted.push(`prettier: formatted ${prettierFiles.length} files`);
-        } else {
-          errors.push(`prettier failed:\n${result.output}`);
-        }
-      }
-
-      // Shell files
-      const shellFiles = fileGroups.get("shell") || [];
-      if (shellFiles.length > 0 && commandExists("shellcheck")) {
-        const result = runTool("shellcheck", [], shellFiles, cwd);
-        if (!result.success) {
-          errors.push(`shellcheck found issues:\n${result.output}`);
-        }
-      }
-    }
-
-    // Report results
-    if (formatted.length > 0) {
-      ctx.ui.notify(`[pre-commit] Formatted: ${formatted.join(", ")}`, "info");
-      // Re-stage formatted files
-      try {
-        execSync(`git add ${stagedFiles.join(" ")}`, { cwd, stdio: "ignore" });
-        ctx.ui.notify("[pre-commit] Re-staged formatted files", "info");
-      } catch {
-        ctx.ui.notify("[pre-commit] Warning: Could not re-stage files", "warning");
-      }
-    }
-
-    if (errors.length > 0) {
-      const errorMessage = [
-        "BLOCKED: Pre-commit checks failed!",
-        "",
-        ...errors,
-        "",
-        "Fix the issues above and try again.",
-        "Use --no-verify to skip checks (not recommended).",
-      ].join("\n");
-
-      ctx.ui.notify(errorMessage, "error");
-
-      return {
-        block: true,
-        reason: errorMessage,
-      };
-    }
-
-    ctx.ui.notify("[pre-commit] All checks passed โœ“", "info");
-    return undefined; // Allow the commit
-  });
-
-  pi.on("session_start", (_event, ctx) => {
-    ctx.ui.notify("Pre-commit linting active", "info");
-  });
-}