/** * hive_execute_split tool — called by the LLM after territory analysis. * Handles all deterministic operations: git worktree creation, PROMPT.md, * territory_map.json, and optional Warp/tmux/sandbox launch. */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { execFileSync, execSync } from "node:child_process"; import { createWorktree, getProjectName } from "../lib/git-worktree.js"; import { generatePromptMd, generateWarpYaml, generateTmuxCommands, type WorktreePromptData, type WarpWorktree, } from "../lib/prompts.js"; import { parseFeaturesMd } from "../lib/features-parser.js"; const SharedFileSchema = Type.Object({ path: Type.String(), strategy: Type.String({ description: "append_only | per_feature_files | single_owner | deferred" }), instructions: Type.String({ description: "Instructions for agents handling this file" }), }); const ContractProvides = Type.Object({ file: Type.String(), exports: Type.Array(Type.String()), types: Type.Array(Type.String()), }); const ContractConsumes = Type.Object({ sourceWt: Type.String(), file: Type.String(), stubCode: Type.String(), }); const WorktreeSchema = Type.Object({ id: Type.String({ description: "e.g. wt1, wt2" }), features: Type.Array(Type.Number(), { description: "Feature numbers assigned to this worktree" }), ownedFiles: Type.Array(Type.String(), { description: "Files this worktree can freely edit" }), ownedDirs: Type.Array(Type.String(), { description: "Directories this worktree owns" }), readOnlyFiles: Type.Array(Type.String(), { description: "Files this worktree can import but not modify" }), forbiddenDirs: Type.Array(Type.String(), { description: "Directories owned by other worktrees" }), sharedFiles: Type.Array(SharedFileSchema, { description: "Files with special handling strategies" }), provides: Type.Optional(Type.Array(ContractProvides)), consumes: Type.Optional(Type.Array(ContractConsumes)), }); const SplitParams = Type.Object({ worktrees: Type.Array(WorktreeSchema, { description: "Territory assignments for each worktree" }), mergeOrder: Type.Array(Type.String(), { description: "Order to merge worktrees, e.g. ['wt1','wt2','wt3']" }), sharedFiles: Type.Array(SharedFileSchema, { description: "Global shared file strategies" }), techStack: Type.String({ description: "Project tech stack summary" }), testingInfo: Type.Optional(Type.String({ description: "Testing setup info" })), conventions: Type.Optional(Type.String({ description: "Project conventions" })), }); function dockerSandboxAvailable(): boolean { try { execFileSync("docker", ["sandbox", "version"], { stdio: "ignore" }); return true; } catch { return false; } } function tmuxAvailable(): boolean { try { execFileSync("tmux", ["-V"], { stdio: "ignore" }); return true; } catch { return false; } } export function registerWorktreeSplitTool(pi: ExtensionAPI) { pi.registerTool({ name: "hive_execute_split", label: "Hive Split", description: "Execute a worktree split based on territory decisions. Only call this after analyzing the project structure and determining feature-to-file territory assignments via /hive:worktree-split.", parameters: SplitParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { // Retrieve config stored by the command const configEntry = getLatestEntry(pi, ctx, "hive-split-config"); const projectDir = configEntry?.projectDir ?? ctx.cwd; const fork: string | false = configEntry?.fork ?? false; const useSandbox: boolean = configEntry?.sandbox ?? false; const autoApprove: boolean = configEntry?.autoApprove ?? false; const projectName = getProjectName(projectDir); // Pre-flight checks if (useSandbox && !dockerSandboxAvailable()) { return { content: [{ type: "text" as const, text: "Docker Sandbox not available. Install Docker Desktop 4.58+ or the sandbox plugin.\nFalling back to standard mode." }], details: {}, isError: true, }; } if (fork === "tmux" && !tmuxAvailable()) { return { content: [{ type: "text" as const, text: "tmux not found. Install: brew install tmux (macOS), apt install tmux (Ubuntu).\nFalling back to manual commands." }], details: {}, isError: true, }; } // Read features for PROMPT.md generation const featuresContent = fs.readFileSync(path.join(projectDir, "features.md"), "utf-8"); const allFeatures = parseFeaturesMd(featuresContent); const createdWorktrees: Array<{ id: string; path: string; branch: string; featureNums: number[] }> = []; const errors: string[] = []; // ── Create git worktrees ────────────────────────────────── for (const wt of params.worktrees) { const wtName = `${projectName}-${wt.id}`; try { const created = await createWorktree(projectDir, wtName); createdWorktrees.push({ id: wt.id, path: created.path, branch: created.branch, featureNums: wt.features, }); } catch (err: any) { errors.push(`Failed to create ${wtName}: ${err.message}`); } } if (createdWorktrees.length === 0) { return { content: [{ type: "text" as const, text: `Failed to create any worktrees:\n${errors.join("\n")}` }], details: {}, isError: true, }; } // ── Generate PROMPT.md in each worktree ─────────────────── for (const cwt of createdWorktrees) { const wtSpec = params.worktrees.find((w) => w.id === cwt.id)!; const features = cwt.featureNums .map((n) => allFeatures.find((f) => f.number === n)) .filter(Boolean) as typeof allFeatures; const promptData: WorktreePromptData = { worktreeId: cwt.id, branchName: cwt.branch, features: features.map((f) => ({ number: f.number, title: f.title, description: f.description, dependencies: f.dependencies, })), ownedFiles: wtSpec.ownedFiles, ownedDirs: wtSpec.ownedDirs, readOnlyFiles: wtSpec.readOnlyFiles, forbiddenDirs: wtSpec.forbiddenDirs, sharedFiles: wtSpec.sharedFiles, provides: wtSpec.provides ?? [], consumes: wtSpec.consumes ?? [], techStack: params.techStack, testingInfo: params.testingInfo || "Not specified", conventions: params.conventions || "Follow existing patterns", }; const promptMd = generatePromptMd(promptData); fs.writeFileSync(path.join(cwt.path, "PROMPT.md"), promptMd); } // ── Generate territory_map.json ─────────────────────────── const territoryMap: any = { createdAt: new Date().toISOString(), projectName, worktrees: createdWorktrees.map((cwt) => { const wtSpec = params.worktrees.find((w) => w.id === cwt.id)!; const entry: any = { id: cwt.id, path: cwt.path, branch: cwt.branch, features: cwt.featureNums, ownedFiles: wtSpec.ownedFiles, ownedDirs: wtSpec.ownedDirs, readOnlyFiles: wtSpec.readOnlyFiles, sharedFiles: Object.fromEntries(wtSpec.sharedFiles.map((s) => [s.path, s.strategy])), provides: wtSpec.provides ?? [], consumes: wtSpec.consumes ?? [], }; if (useSandbox) { entry.sandbox = { enabled: true, name: `${projectName}-${cwt.id}` }; } return entry; }), mergeOrder: params.mergeOrder, sharedFiles: params.sharedFiles.map((s) => ({ path: s.path, strategy: s.strategy })), }; fs.writeFileSync( path.join(projectDir, "territory_map.json"), JSON.stringify(territoryMap, null, 2), ); // ── Update features.md — mark distributed features as in_progress ── let updatedContent = featuresContent; for (const cwt of createdWorktrees) { for (const fNum of cwt.featureNums) { updatedContent = updatedContent.replace( new RegExp(`(## Feature ${fNum}:[\\s\\S]*?- Status: )pending`, "m"), `$1in_progress`, ); } } fs.writeFileSync(path.join(projectDir, "features.md"), updatedContent); // ── Build agent commands ────────────────────────────────── const agentPrompt = "'Read PROMPT.md and implement all assigned features. Report progress after each feature.'"; function agentCmdForWt(cwt: { id: string; path: string }) { if (useSandbox) { return `docker sandbox run --name ${projectName}-${cwt.id} -w "${cwt.path}" claude`; } const base = `pi ${agentPrompt}`; return (autoApprove || useSandbox) ? base.replace("pi ", "pi --yolo ") : base; } const wtSummary = createdWorktrees .map((cwt) => ` ${cwt.id}: ${projectName}-${cwt.id} (Features ${cwt.featureNums.join(", ")})`) .join("\n"); const modeLabel = useSandbox ? "Sandbox " : ""; const mergeCmds = createdWorktrees .map((cwt) => ` /hive:worktree-merge ${projectName}-${cwt.id}`) .join("\n"); // ── Handle fork modes ───────────────────────────────────── if (fork === "warp") { const warpWorktrees: WarpWorktree[] = createdWorktrees.map((cwt) => ({ path: cwt.path, command: agentCmdForWt(cwt), })); const yamlContent = generateWarpYaml(warpWorktrees); const warpDir = path.join(os.homedir(), ".warp", "launch_configurations"); fs.mkdirSync(warpDir, { recursive: true }); const warpFile = path.join(warpDir, "hive-agents.yaml"); fs.writeFileSync(warpFile, yamlContent); try { const { execFile } = await import("node:child_process"); execFile("open", ["warp://launch/Hive%20Agents"]); } catch { /* Warp may not be installed */ } return { content: [{ type: "text" as const, text: `Worktree split complete!\n\n${wtSummary}\n\nTerritory mapping: zero file overlap\n\nWarp launched with ${createdWorktrees.length} ${modeLabel}panes!\n Config: ${warpFile}\n\nAfter completion:\n${mergeCmds}`, }], details: {}, }; } if (fork === "tmux") { const tmuxWts: WarpWorktree[] = createdWorktrees.map((cwt) => ({ path: cwt.path, command: useSandbox ? agentCmdForWt(cwt) : `cd ${cwt.path} && ${agentCmdForWt(cwt)}`, })); const tmuxCmds = generateTmuxCommands(tmuxWts); // Execute all but the attach command for (const cmd of tmuxCmds.slice(0, -1)) { try { execSync(cmd, { stdio: "ignore" }); } catch { /* ignore */ } } return { content: [{ type: "text" as const, text: `Worktree split complete!\n\n${wtSummary}\n\nTerritory mapping: zero file overlap\n\ntmux session 'hive' launched with ${createdWorktrees.length} ${modeLabel}panes!\n Re-attach: tmux attach -t hive\n List panes: tmux list-panes -t hive\n Kill all: tmux kill-session -t hive\n\nAfter completion:\n${mergeCmds}`, }], details: {}, }; } // ── Manual launch (default) ─────────────────────────────── const launchCmds = createdWorktrees .map((cwt) => { const cmd = agentCmdForWt(cwt); return useSandbox ? ` ${cmd}` : ` cd ${cwt.path} && ${cmd}`; }) .join("\n"); let text = `Worktree split complete!${useSandbox ? " (Docker Sandbox mode)" : ""}\n\n${wtSummary}\n\nTerritory mapping: zero file overlap\n`; if (useSandbox) text += `Isolation: Docker Sandbox (each agent in its own microVM)\n`; text += `\nStart agents (open one terminal per worktree):\n${launchCmds}\n`; if (useSandbox) text += `\nMonitor sandboxes: docker sandbox ls\n`; text += `\nAfter completion:\n`; if (useSandbox) { text += ` docker sandbox rm ${createdWorktrees.map((cwt) => `${projectName}-${cwt.id}`).join(" ")}\n`; } text += mergeCmds; return { content: [{ type: "text" as const, text }], details: {}, }; }, }); } /** * Get the latest entry of a given custom type from the session. */ function getLatestEntry(pi: ExtensionAPI, ctx: any, customType: string): any { const entries = ctx.sessionManager.getEntries(); for (let i = entries.length - 1; i >= 0; i--) { const e = entries[i] as any; if (e.type === "custom" && e.customType === customType) { return e.data; } } return null; }