@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
JavaScript
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