UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

429 lines (425 loc) 13.6 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { execSync } from "child_process"; import { logger } from "../../../core/monitoring/logger.js"; class GitWorkflowError extends Error { constructor(message, context) { super(message); this.context = context; this.name = "GitWorkflowError"; } } class GitWorkflowManager { config; agentBranches = /* @__PURE__ */ new Map(); baselineBranch; mainBranch; constructor(config) { this.config = { enableGitWorkflow: true, branchStrategy: "agent", autoCommit: true, commitFrequency: 5, mergStrategy: "squash", requirePR: false, ...config }; try { this.baselineBranch = this.getCurrentBranch(); this.mainBranch = this.getMainBranch(); } catch (error) { logger.warn("Git not initialized, workflow features disabled"); this.config.enableGitWorkflow = false; } } /** * Initialize git workflow for an agent */ async initializeAgentWorkflow(agent, task) { if (!this.config.enableGitWorkflow) return; let branchName = this.generateBranchName(agent, task); try { if (this.branchExists(branchName)) { const existingHasChanges = this.branchHasUnmergedChanges(branchName); if (existingHasChanges) { const timestamp = Date.now(); branchName = `${branchName}-${timestamp}`; logger.info( `Branch already exists with changes, creating unique branch: ${branchName}` ); this.createBranch(branchName); } else { logger.info( `Reusing existing branch for agent ${agent.role}: ${branchName}` ); this.checkoutBranch(branchName); } } else { this.createBranch(branchName); logger.info( `Created git branch for agent ${agent.role}: ${branchName}` ); } this.agentBranches.set(agent.id, branchName); if (this.config.autoCommit) { this.scheduleAutoCommit(agent, task); } } catch (error) { logger.error( `Failed to initialize git workflow for agent ${agent.role}`, error ); } } /** * Commit agent work */ async commitAgentWork(agent, task, message) { if (!this.config.enableGitWorkflow) return; const branchName = this.agentBranches.get(agent.id); if (!branchName) { logger.warn(`No branch found for agent ${agent.id}`); return; } try { this.checkoutBranch(branchName); const hasChanges = this.hasUncommittedChanges(); if (!hasChanges) { logger.debug(`No changes to commit for agent ${agent.role}`); return; } execSync("git add -A", { encoding: "utf8" }); const commitMessage = message || this.generateCommitMessage(agent, task); execSync(`git commit -m "${commitMessage}"`, { encoding: "utf8" }); logger.info(`Agent ${agent.role} committed: ${commitMessage}`); if (this.hasRemote()) { try { execSync(`git push origin ${branchName}`, { encoding: "utf8" }); logger.info(`Pushed branch ${branchName} to remote`); } catch (error) { logger.warn(`Could not push to remote: ${error}`); } } } catch (error) { logger.error(`Failed to commit agent work`, error); } } /** * Merge agent work back to baseline */ async mergeAgentWork(agent, task) { if (!this.config.enableGitWorkflow) return; const branchName = this.agentBranches.get(agent.id); if (!branchName) { logger.warn(`No branch found for agent ${agent.id}`); return; } try { this.checkoutBranch(this.baselineBranch); if (this.config.requirePR) { await this.createPullRequest(agent, task, branchName); } else { this.mergeBranch(branchName); logger.info(`Merged agent ${agent.role} work from ${branchName}`); } this.deleteBranch(branchName); this.agentBranches.delete(agent.id); } catch (error) { logger.error(`Failed to merge agent work`, error); } } /** * Coordinate merges between multiple agents */ async coordinateMerges(agents) { if (!this.config.enableGitWorkflow) return; logger.info("Coordinating merges from all agents"); const integrationBranch = `swarm-integration-${Date.now()}`; this.createBranch(integrationBranch); for (const agent of agents) { const branchName = this.agentBranches.get(agent.id); if (branchName && this.branchExists(branchName)) { try { this.mergeBranch(branchName); logger.info(`Integrated ${agent.role} work`); } catch (error) { logger.error(`Failed to integrate ${agent.role} work: ${error}`); } } } const testsPass = await this.runIntegrationTests(); if (testsPass) { this.checkoutBranch(this.baselineBranch); this.mergeBranch(integrationBranch); logger.info("Successfully integrated all agent work"); } else { logger.warn( "Integration tests failed, keeping changes in branch: " + integrationBranch ); } } /** * Handle merge conflicts */ async resolveConflicts(agent) { const conflicts = this.getConflictedFiles(); if (conflicts.length === 0) return; logger.warn( `Agent ${agent.role} encountering merge conflicts: ${conflicts.join(", ")}` ); for (const file of conflicts) { try { if (this.isAgentFile(file, agent)) { execSync(`git checkout --ours ${file}`, { encoding: "utf8" }); execSync(`git add ${file}`, { encoding: "utf8" }); } else { execSync(`git checkout --theirs ${file}`, { encoding: "utf8" }); execSync(`git add ${file}`, { encoding: "utf8" }); } } catch (error) { logger.error(`Could not auto-resolve conflict in ${file}`); } } const remainingConflicts = this.getConflictedFiles(); if (remainingConflicts.length === 0) { execSync("git commit --no-edit", { encoding: "utf8" }); logger.info("All conflicts resolved automatically"); } else { logger.error( `Manual intervention needed for: ${remainingConflicts.join(", ")}` ); } } // Private helper methods getCurrentBranch() { try { return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim(); } catch (error) { logger.warn("Failed to get current branch", error); return "main"; } return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim(); } getMainBranch() { try { const branches = execSync("git branch -r", { encoding: "utf8" }); if (branches.includes("origin/main")) return "main"; if (branches.includes("origin/master")) return "master"; } catch (error) { logger.debug("Could not detect main branch from remotes", error); } return this.getCurrentBranch(); } generateBranchName(agent, task) { const sanitizedTitle = task.title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 30); switch (this.config.branchStrategy) { case "feature": return `feature/${sanitizedTitle}`; case "task": return `task/${task.id}`; case "agent": default: return `swarm/${agent.role}-${sanitizedTitle}`; } } createBranch(branchName) { try { try { const currentBranch = execSync("git branch --show-current", { encoding: "utf8" }).trim(); if (currentBranch === branchName) { try { execSync("git checkout main", { encoding: "utf8" }); } catch { execSync("git checkout master", { encoding: "utf8" }); } } try { const worktrees = execSync("git worktree list --porcelain", { encoding: "utf8" }); const lines = worktrees.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith("branch ") && lines[i].includes(branchName)) { const worktreetPath = lines[i - 1].replace("worktree ", ""); execSync(`git worktree remove --force "${worktreetPath}"`, { encoding: "utf8" }); logger.info(`Removed worktree at ${worktreetPath} for branch ${branchName}`); } } } catch (worktreeError) { logger.warn("Failed to check/remove worktrees", worktreeError); } execSync(`git branch -D ${branchName}`, { encoding: "utf8" }); logger.info(`Deleted existing branch ${branchName} for fresh start`); } catch { } execSync(`git checkout -b ${branchName}`, { encoding: "utf8" }); } catch (error) { logger.error(`Failed to create branch ${branchName}`, error); throw new GitWorkflowError(`Failed to create branch: ${branchName}`, { branchName }); } } checkoutBranch(branchName) { try { execSync(`git checkout ${branchName}`, { encoding: "utf8" }); } catch (error) { logger.error(`Failed to checkout branch ${branchName}`, error); throw new GitWorkflowError(`Failed to checkout branch: ${branchName}`, { branchName }); } } branchExists(branchName) { try { execSync(`git rev-parse --verify ${branchName}`, { encoding: "utf8", stdio: "pipe" }); return true; } catch { return false; } } deleteBranch(branchName) { try { execSync(`git branch -d ${branchName}`, { encoding: "utf8" }); } catch (error) { execSync(`git branch -D ${branchName}`, { encoding: "utf8" }); } } mergeBranch(branchName) { const strategy = this.config.mergStrategy; switch (strategy) { case "squash": execSync(`git merge --squash ${branchName}`, { encoding: "utf8" }); execSync('git commit -m "Squashed agent changes"', { encoding: "utf8" }); break; case "rebase": execSync(`git rebase ${branchName}`, { encoding: "utf8" }); break; case "merge": default: execSync(`git merge ${branchName}`, { encoding: "utf8" }); break; } } hasUncommittedChanges() { try { const status = execSync("git status --porcelain", { encoding: "utf8" }); return status.trim().length > 0; } catch (error) { logger.warn("Failed to check git status", error); return false; } } hasRemote() { try { execSync("git remote get-url origin", { encoding: "utf8" }); return true; } catch { return false; } } getConflictedFiles() { try { const conflicts = execSync("git diff --name-only --diff-filter=U", { encoding: "utf8" }); return conflicts.trim().split("\n").filter((f) => f.length > 0); } catch { return []; } } isAgentFile(file, agent) { if (!file || !agent?.role || !agent?.id) { return false; } return file.includes(agent.role) || file.includes(agent.id); } generateCommitMessage(agent, task) { const role = agent?.role || "agent"; const title = task?.title || "task"; const iteration = agent?.performance?.tasksCompleted || 1; return `[${role}] ${title} - Iteration ${iteration}`; } scheduleAutoCommit(agent, task) { const intervalMs = this.config.commitFrequency * 60 * 1e3; setInterval(async () => { await this.commitAgentWork( agent, task, `[${agent.role}] Auto-commit: ${task.title}` ); }, intervalMs); } async createPullRequest(agent, task, branchName) { try { const title = `[Swarm ${agent.role}] ${task.title}`; const body = ` ## Agent: ${agent.role} ## Task: ${task.title} ### Acceptance Criteria: ${task.acceptanceCriteria.map((c) => `- ${c}`).join("\n")} ### Status: - Tasks Completed: ${agent.performance?.tasksCompleted || 0} - Success Rate: ${agent.performance?.successRate || 0}% Generated by Swarm Coordinator `; execSync( `gh pr create --title "${title}" --body "${body}" --base ${this.baselineBranch}`, { encoding: "utf8" } ); logger.info(`Created PR for agent ${agent.role}`); } catch (error) { logger.warn(`Could not create PR: ${error}`); } } async runIntegrationTests() { try { execSync("npm test", { encoding: "utf8" }); return true; } catch { return false; } } branchHasUnmergedChanges(branchName) { try { const currentBranch = this.getCurrentBranch(); const unmerged = execSync( `git log ${currentBranch}..${branchName} --oneline`, { encoding: "utf8", stdio: "pipe" } ); return unmerged.trim().length > 0; } catch { return true; } } /** * Get status of all agent branches */ getGitStatus() { const status = { enabled: this.config.enableGitWorkflow, currentBranch: this.getCurrentBranch(), agentBranches: Array.from(this.agentBranches.entries()).map( ([agentId, branch]) => ({ agentId, branch, exists: this.branchExists(branch) }) ), hasUncommittedChanges: this.hasUncommittedChanges() }; return status; } } const gitWorkflowManager = new GitWorkflowManager(); export { GitWorkflowError, GitWorkflowManager, gitWorkflowManager }; //# sourceMappingURL=git-workflow-manager.js.map