import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import * as fs from "node:fs"; import * as path from "node:path"; import { parseFeaturesMd, getStats, validateFeaturesMd } from "../lib/features-parser.js"; import { worktreeSplitPrompt } from "../lib/prompts.js"; /** * /hive:worktree-split — command that analyzes and delegates to the * hive_execute_split tool for git operations. * * Usage: /hive:worktree-split --count 3 --fork [warp|tmux] --sandbox --auto-approve --features "1,2;3,4;5,6" */ export function registerWorktreeSplitCommand(pi: ExtensionAPI) { pi.registerCommand("hive:worktree-split", { description: "Split into N zero-conflict worktrees (--count N --fork [warp|tmux] --sandbox --features GROUPS)", handler: async (rawArgs, ctx) => { const args = parseArgs(rawArgs || ""); // ── Validate features.md ───────────────────────────────── const featuresPath = path.join(ctx.cwd, "features.md"); if (!fs.existsSync(featuresPath)) { ctx.ui.notify( "features.md not found in the project root.\n\nCreate one with:\n /hive:spec", "warning", ); return; } const content = fs.readFileSync(featuresPath, "utf-8"); const validation = validateFeaturesMd(content); if (!validation.valid) { ctx.ui.notify( `features.md has invalid format:\n${validation.issues.map((i) => ` - ${i}`).join("\n")}\n\nFix with /hive:spec`, "error", ); return; } const features = parseFeaturesMd(content); const pendingCount = features.filter((f) => f.status !== "done").length; if (pendingCount === 0) { ctx.ui.notify("All features are already done. Nothing to split.", "info"); return; } // Store split config for the tool to use pi.appendEntry("hive-split-config", { count: args.count, fork: args.fork, sandbox: args.sandbox, autoApprove: args.autoApprove || (pi.getFlag("--auto-approve") as boolean), featureGroups: args.features, projectDir: ctx.cwd, }); const modeInfo: string[] = []; if (args.sandbox) modeInfo.push("Docker Sandbox isolation"); if (args.fork) modeInfo.push(`auto-launch via ${args.fork}`); const modeStr = modeInfo.length > 0 ? ` (${modeInfo.join(", ")})` : ""; ctx.ui.notify( `Splitting ${pendingCount} pending features into ${args.count} worktrees${modeStr}...\n` + `Analyzing project for zero-conflict territory mapping.`, "info", ); // Send prompt to LLM — it will analyze the project and call hive_execute_split pi.sendUserMessage(worktreeSplitPrompt(args.count, content, args.features)); }, }); } type ForkMode = false | "warp" | "tmux" | "iterm"; interface SplitArgs { count: number; fork: ForkMode; sandbox: boolean; autoApprove: boolean; features?: string; } function parseArgs(raw: string): SplitArgs { const result: SplitArgs = { count: 3, fork: false, sandbox: false, autoApprove: false }; const tokens = raw.split(/\s+/).filter(Boolean); for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t === "--count" && tokens[i + 1]) { result.count = Math.max(1, Math.min(10, parseInt(tokens[++i], 10) || 3)); } else if (t === "--warp") { result.fork = "warp"; } else if (t === "--fork") { const next = tokens[i + 1]; if (next === "tmux" || next === "iterm" || next === "warp") { result.fork = next as ForkMode; i++; } else { result.fork = "warp"; } } else if (t === "--sandbox") { result.sandbox = true; } else if (t === "--auto-approve") { result.autoApprove = true; } else if (t === "--features" && tokens[i + 1]) { result.features = tokens[++i]; } } return result; }