automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
157 lines (156 loc) • 5.98 kB
JavaScript
;
/**
* Git State Validation - Ensure clean working tree before spawning agents
*
* Critical Rule: wish, forge, review, and run cannot execute with unstaged or
* unpushed changes. Fellow agents in separate worktrees won't see the context.
*
* This prevents context drift and ensures all agents work from the same state.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkGitState = checkGitState;
exports.formatGitStateError = formatGitStateError;
exports.validateGitStateOrThrow = validateGitStateOrThrow;
exports.detectProjectFromWorktree = detectProjectFromWorktree;
const child_process_1 = require("child_process");
/**
* Check if git working tree is clean and pushed
*/
function checkGitState() {
try {
// Check for unstaged changes
const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const hasUnstagedChanges = statusOutput.length > 0;
// Check for uncommitted changes (staged but not committed)
const stagedOutput = (0, child_process_1.execSync)('git diff --cached --name-only', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const hasUncommittedChanges = stagedOutput.length > 0;
// Check for unpushed commits
let hasUnpushedCommits = false;
try {
const unpushedOutput = (0, child_process_1.execSync)('git log @{u}.. --oneline', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
hasUnpushedCommits = unpushedOutput.length > 0;
}
catch (error) {
// No upstream branch configured, treat as unpushed
hasUnpushedCommits = true;
}
const isClean = !hasUnstagedChanges && !hasUncommittedChanges && !hasUnpushedCommits;
let message = '';
if (!isClean) {
const issues = [];
if (hasUnstagedChanges) {
issues.push('❌ Unstaged changes detected');
}
if (hasUncommittedChanges) {
issues.push('❌ Uncommitted changes detected (staged but not committed)');
}
if (hasUnpushedCommits) {
issues.push('❌ Unpushed commits detected');
}
message = issues.join('\n');
}
else {
message = '✅ Git working tree is clean and pushed';
}
return {
isClean,
hasUnstagedChanges,
hasUncommittedChanges,
hasUnpushedCommits,
message
};
}
catch (error) {
// Not a git repository or git command failed
return {
isClean: false,
hasUnstagedChanges: false,
hasUncommittedChanges: false,
hasUnpushedCommits: false,
message: `⚠️ Git validation failed: ${error.message}`
};
}
}
/**
* Format git state validation error message
*/
function formatGitStateError(check) {
let error = `🚫 **Commit your changes first**\n\n`;
error += `${check.message}\n\n`;
error += `**Why:** Forge uses isolated worktrees. Uncommitted files won't be available (@ references break).\n\n`;
error += `**Fix:** Commit your changes, then retry.\n`;
return error;
}
/**
* Validate git state and throw if not clean
*/
function validateGitStateOrThrow() {
const check = checkGitState();
if (!check.isClean) {
throw new Error(formatGitStateError(check));
}
}
/**
* Detect project ID from current worktree
*
* When an agent is running in a Forge worktree and calls MCP to start another agent,
* we need to detect which project the worktree belongs to so we can create the new
* task in the same project (not create a duplicate project).
*
* @param forgeClient - ForgeClient instance to query the API
* @returns Project ID if detected, null otherwise
*/
async function detectProjectFromWorktree(forgeClient) {
try {
const cwd = process.cwd();
// Check if we're in a Forge worktree
if (!cwd.includes('/worktrees/')) {
return null; // Not in a worktree
}
// Get the worktree path (normalize to absolute path)
const worktreePath = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Query all projects from Forge
const projects = await forgeClient.listProjects();
// For each project, check if any tasks have attempts with this worktree
for (const project of projects) {
try {
const tasks = await forgeClient.listTasks(project.id);
for (const task of tasks) {
// Check if task has a latest attempt
if (task.latest_attempt && task.latest_attempt.worktree_path) {
// Normalize worktree path comparison
const attemptWorktree = task.latest_attempt.worktree_path;
if (attemptWorktree === worktreePath ||
worktreePath.includes(attemptWorktree) ||
attemptWorktree.includes(worktreePath)) {
// Found matching project
return project.id;
}
}
}
}
catch (taskError) {
// Skip project if we can't list tasks
continue;
}
}
return null; // No matching project found
}
catch (error) {
// Error detecting project (git command failed, API error, etc)
console.error('Failed to detect project from worktree:', error.message);
return null;
}
}