Commit 0ba65c4540b7

Vincent Demeester <vincent@sbr.pm>
2026-02-23 10:37:47
feat: use lazyworktree as default worktree backend
Updated pi git extension and UsingGitWorktrees skill to prefer lazyworktree for worktree creation and deletion, falling back to raw git commands when unavailable. Switched lazyworktree package source from nixpkgs-master to chick-group overlay (1.38.0). Updated chick-group flake input.
1 parent 3338ac8
Changed files (4)
dots
pi
agent
home
dots/pi/agent/extensions/git/index.ts
@@ -296,7 +296,7 @@ export default function (pi: ExtensionAPI) {
 		name: "git_worktree",
 		label: "Git Worktree",
 		description:
-			"Manage git worktrees in ~/.local/share/worktrees/<org>/<repo>/<branch>. Create isolated working directories for different branches without switching in the main repository. USE WHEN starting feature work that needs isolation OR working on multiple branches simultaneously.",
+			"Manage git worktrees in ~/.local/share/worktrees/<org>/<repo>/<branch>. Uses lazyworktree when available for consistent org/repo/name layout, falls back to raw git. Create isolated working directories for different branches without switching in the main repository. USE WHEN starting feature work that needs isolation OR working on multiple branches simultaneously.",
 		parameters: Type.Object({
 			action: Type.Union([
 				Type.Literal("list"),
dots/pi/agent/extensions/git/worktree.ts
@@ -1,5 +1,6 @@
 // =============================================================================
 // Git Worktree Management
+// Uses lazyworktree when available, falls back to raw git commands.
 // =============================================================================
 
 import { execSync } from "child_process";
@@ -18,6 +19,27 @@ import {
 	ensureWorktreeDir,
 } from "./utils.js";
 
+// =============================================================================
+// Lazyworktree Detection
+// =============================================================================
+
+let _hasLazyworktree: boolean | null = null;
+
+function hasLazyworktree(): boolean {
+	if (_hasLazyworktree === null) {
+		try {
+			execSync("command -v lazyworktree", {
+				encoding: "utf-8",
+				stdio: "pipe",
+			});
+			_hasLazyworktree = true;
+		} catch {
+			_hasLazyworktree = false;
+		}
+	}
+	return _hasLazyworktree;
+}
+
 // =============================================================================
 // List Worktrees
 // =============================================================================
@@ -43,9 +65,9 @@ export function listWorktrees(cwd: string = process.cwd()): WorktreeInfo[] {
 function parseWorktreeList(output: string): WorktreeInfo[] {
 	const worktrees: WorktreeInfo[] = [];
 	const lines = output.trim().split("\n");
-	
+
 	let current: Partial<WorktreeInfo> = {};
-	
+
 	for (const line of lines) {
 		if (line === "") {
 			if (current.path) {
@@ -110,6 +132,41 @@ export function createWorktree(options: WorktreeCreateOptions, cwd: string = pro
 		throw new Error("Not in a git repository");
 	}
 
+	// If a custom path is provided, skip lazyworktree and use raw git
+	if (!options.path && hasLazyworktree()) {
+		return createWithLazyworktree(options, repoRoot);
+	}
+
+	return createWithGit(options, repoRoot, cwd);
+}
+
+function createWithLazyworktree(options: WorktreeCreateOptions, repoRoot: string): string {
+	try {
+		const output = execSync(
+			`lazyworktree create --from-branch ${options.branch} --silent`,
+			{
+				cwd: repoRoot,
+				encoding: "utf-8",
+				stdio: "pipe",
+			}
+		).trim();
+
+		// lazyworktree prints the worktree path to stdout
+		if (!output) {
+			throw new Error("lazyworktree returned empty output");
+		}
+
+		return output;
+	} catch (error: any) {
+		// If lazyworktree fails, fall back to raw git
+		if (error.message?.includes("lazyworktree returned empty output")) {
+			throw error;
+		}
+		return createWithGit({ ...options }, repoRoot, repoRoot);
+	}
+}
+
+function createWithGit(options: WorktreeCreateOptions, repoRoot: string, cwd: string): string {
 	// Ensure worktree directory exists
 	ensureWorktreeDir(cwd);
 
@@ -154,7 +211,7 @@ export function createWorktree(options: WorktreeCreateOptions, cwd: string = pro
 		execSync(cmd, {
 			cwd: repoRoot,
 			encoding: "utf-8",
-			stdio: "pipe", // Capture output instead of inheriting
+			stdio: "pipe",
 		});
 
 		return worktreePath;
@@ -188,18 +245,31 @@ export function removeWorktree(options: WorktreeRemoveOptions, cwd: string = pro
 		);
 	}
 
-	// Build command
+	// Try lazyworktree first for removal (handles branch cleanup too)
+	if (hasLazyworktree()) {
+		try {
+			execSync(`lazyworktree delete --silent "${worktree.path}"`, {
+				cwd: repoRoot,
+				encoding: "utf-8",
+				stdio: "pipe",
+			});
+			return;
+		} catch {
+			// Fall through to raw git
+		}
+	}
+
+	// Fall back to raw git
 	let cmd = `git worktree remove "${worktree.path}"`;
 	if (options.force) {
 		cmd += " --force";
 	}
 
-	// Execute command
 	try {
 		execSync(cmd, {
 			cwd: repoRoot,
 			encoding: "utf-8",
-			stdio: "pipe", // Capture output instead of inheriting
+			stdio: "pipe",
 		});
 	} catch (error) {
 		throw new Error(`Failed to remove worktree: ${error}`);
@@ -220,7 +290,7 @@ export function pruneWorktrees(cwd: string = process.cwd()): void {
 		execSync("git worktree prune", {
 			cwd: repoRoot,
 			encoding: "utf-8",
-			stdio: "pipe", // Capture output instead of inheriting
+			stdio: "pipe",
 		});
 	} catch (error) {
 		throw new Error(`Failed to prune worktrees: ${error}`);
@@ -234,6 +304,6 @@ export function pruneWorktrees(cwd: string = process.cwd()): void {
 export function getCurrentWorktreeInfo(cwd: string = process.cwd()): WorktreeInfo | null {
 	const worktrees = listWorktrees(cwd);
 	const currentPath = path.resolve(cwd);
-	
+
 	return worktrees.find((wt) => path.resolve(wt.path) === currentPath) ?? null;
 }
home/common/dev/lazyworktree.nix
@@ -6,7 +6,7 @@
   programs.lazyworktree = {
     enable = true;
     enableZshIntegration = true;
-    package = pkgs.master.lazyworktree;
+    package = pkgs.lazyworktree;
 
     # Explicit aliases for commonly used repositories
     aliases = {
flake.lock
@@ -128,11 +128,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1771565661,
-        "narHash": "sha256-RP+biroKgfJdmCzol02gMglC8fyMAIjV27k9xMoGne4=",
+        "lastModified": 1771838063,
+        "narHash": "sha256-BviEpCy+RUEperWTlnSWFBFdv3wOPDr/yV/L+6IZjio=",
         "owner": "vdemeester",
         "repo": "chick-group",
-        "rev": "20e381b00673a9d97a7819d3ec832d030aeb0a06",
+        "rev": "428104bedcafedad45a58544ed73ed368aff8408",
         "type": "github"
       },
       "original": {