UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

321 lines (320 loc) β€’ 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ForgeExecutor = void 0; exports.createForgeExecutor = createForgeExecutor; // @ts-ignore - forge-client.js is compiled JS without type declarations const service_config_js_1 = require("./service-config.js"); const forge_client_js_1 = require("../../../src/lib/forge-client.js"); const child_process_1 = require("child_process"); const path_1 = __importDefault(require("path")); // NOTE: AgentSyncCache interface removed - Forge discovers .genie folders natively class ForgeExecutor { constructor(config) { this.config = config; this.forge = new forge_client_js_1.ForgeClient(config.forgeBaseUrl, config.forgeToken); } /** * NOTE: syncProfiles and helper methods removed - Forge discovers .genie folders natively * Removed: syncProfiles, loadSyncCache, saveSyncCache, hashContent, cleanNullValues, mergeProfiles */ async createTask(params) { const { agentName, prompt, executorKey, executorVariant, executionMode, model, name } = params; const projectId = await this.getOrCreateGenieProject(); // Detect current git branch and use it as base_branch let baseBranch = 'main'; // Default fallback try { baseBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', cwd: process.cwd() }).trim(); await this.forge.updateProject(projectId, { default_base_branch: baseBranch }); } catch (error) { // If git detection fails, try to get default_base_branch from project try { const project = await this.forge.getProject(projectId); if (project.default_base_branch) { baseBranch = project.default_base_branch; } } catch { // Use fallback 'main' } } // Generate task title: use provided name or extract from prompt const emojiPrefix = this.getAgentEmoji(agentName); let taskTitle; if (name) { // User-provided name taskTitle = `[${emojiPrefix}] ${name}`; } else { // Smart default: extract keywords from prompt const smartName = this.generateSmartTaskName(prompt); taskTitle = `[${emojiPrefix}] ${smartName}`; } const requestBody = { task: { project_id: projectId, title: taskTitle, description: prompt }, executor_profile_id: this.mapExecutorToProfile(executorKey, executorVariant, model), base_branch: baseBranch }; const response = await this.forge.createAndStartTask(requestBody); // Response contains: { id: taskId, project_id: projectId, attempts: [{ id: attemptId, ... }] } const taskId = response.id; // WORKAROUND: Forge API returns empty attempts array, need to fetch separately let attemptId; if (response.attempts && response.attempts.length > 0 && response.attempts[0].id) { attemptId = response.attempts[0].id; } else { // Fetch the task attempt that was just created const attempts = await this.forge.listTaskAttempts(); const taskAttempt = attempts.find((a) => a.task_id === taskId); if (taskAttempt) { attemptId = taskAttempt.id; } else { // Last resort fallback attemptId = taskId; } } // Build Forge URL const forgeUrl = `${this.config.forgeBaseUrl}/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}?view=diffs`; return { attemptId, taskId, projectId, forgeUrl }; } async resumeTask(taskId, followUpPrompt) { await this.forge.followUpTaskAttempt(taskId, followUpPrompt); } async stopTask(taskId) { await this.forge.stopTaskAttemptExecution(taskId); } async getTaskStatus(taskId) { const attempt = await this.forge.getTaskAttempt(taskId); return { status: attempt.status || 'unknown' }; } async fetchTaskLogs(taskId) { try { const processes = await this.forge.listExecutionProcesses(taskId); if (!Array.isArray(processes) || !processes.length) return null; const latest = processes[processes.length - 1]; return latest?.output || null; } catch { return null; } } async listTasks() { const projectId = await this.getOrCreateGenieProject(); // Query tasks and attempts separately const [tasks, allAttempts] = await Promise.all([ this.forge.listTasks(projectId), this.forge.listTaskAttempts() ]); // Filter attempts to current project const projectAttempts = allAttempts.filter((attempt) => attempt.project_id === projectId); // Build task lookup map (task_id -> task) const taskMap = new Map(); for (const task of tasks) { taskMap.set(task.id, task); } // Group attempts by task_id and get latest attempt for each task const latestAttemptsByTask = new Map(); for (const attempt of projectAttempts) { const taskId = attempt.task_id; const existing = latestAttemptsByTask.get(taskId); // Keep the most recently created attempt for each task if (!existing || new Date(attempt.created_at) > new Date(existing.created_at)) { latestAttemptsByTask.set(taskId, attempt); } } // Return attempt summaries (using attempt ID, not task ID) return Array.from(latestAttemptsByTask.values()).map((attempt) => { const task = taskMap.get(attempt.task_id); const displayName = task?.title || attempt.description || attempt.id; return { id: attempt.id, // βœ… Now returns attempt ID (UUID) instead of task ID name: displayName, agent: this.extractAgentNameFromTitle(task?.title || ''), status: attempt.status || 'unknown', executor: (attempt.executor_profile_id?.executor || '').toLowerCase() || null, variant: attempt.executor_profile_id?.variant || null, model: attempt.executor_profile_id?.model || null, created: attempt.created_at, updated: attempt.updated_at }; }); } /** * Get or create the Genie project for the current workspace * @returns Project ID * @public Exposed for install helpers and other orchestration needs */ async getOrCreateGenieProject() { if (this.config.genieProjectId) { return this.config.genieProjectId; } try { // Use git root directory (works for both main workspace and worktrees) // This prevents worktrees from creating duplicate projects let repoRoot; try { repoRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8', cwd: process.cwd(), stdio: ['pipe', 'pipe', 'ignore'] }).trim(); } catch { // Fallback to process.cwd() if not in git repo repoRoot = process.cwd(); } const projects = await this.forge.listProjects(); const existingProject = projects.find((p) => p.git_repo_path === repoRoot); if (existingProject) { this.config.genieProjectId = existingProject.id; return existingProject.id; } // Auto-detect project name from git repo or directory name let projectName = 'Genie Project'; try { // Try git remote first const remoteUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', { encoding: 'utf8', cwd: repoRoot, stdio: ['pipe', 'pipe', 'ignore'] }).trim(); // Extract repo name from URL (e.g., "automagik-genie.git" β†’ "automagik-genie") const match = remoteUrl.match(/\/([^\/]+?)(\.git)?$/); if (match && match[1]) { projectName = match[1].replace(/\.git$/, ''); } } catch { // Fallback to directory name if git fails try { const dirName = path_1.default.basename(repoRoot); if (dirName) { projectName = dirName; } } catch { // Keep default "Genie Project" } } const newProject = await this.forge.createProject({ name: projectName, git_repo_path: repoRoot, use_existing_repo: true }); this.config.genieProjectId = newProject.id; return newProject.id; } catch (error) { // Provide context for downstream error handlers const message = error.message || String(error); throw new Error(`Failed to create Forge project: ${message}`); } } mapExecutorToProfile(executorKey, variant, model) { // Frontmatter now uses Forge format directly (CLAUDE_CODE, OPENCODE, etc.) // No mapping needed - use executor as-is const executor = executorKey.trim().toUpperCase(); const resolvedVariant = (variant || 'DEFAULT').toUpperCase(); const profile = { executor, variant: resolvedVariant }; if (model && model.trim().length) { profile.model = model.trim(); } return profile; } extractAgentNameFromTitle(title) { // Handle old format "Genie: agent (mode)" and new emoji format "[🧞] agent: mode" const oldMatch = title.match(/^Genie: ([^\(]+)/); if (oldMatch) return oldMatch[1].trim(); const emojiMatch = title.match(/^\[[\p{Emoji}]\]\s+([^:]+)/u); return emojiMatch ? emojiMatch[1].trim() : title; } /** * Generate a smart task name from the prompt by extracting keywords * Examples: * "Fix authentication bug in login" β†’ "fix-authentication-bug" * "Daily health check" β†’ "daily-health-check" * "Analyze performance metrics" β†’ "analyze-performance-metrics" */ generateSmartTaskName(prompt) { // Remove common filler words const stopWords = new Set([ 'a', 'an', 'the', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'can', 'this', 'that', 'these', 'those', 'i', 'you', 'we', 'they' ]); // Extract first ~6 meaningful words const words = prompt .toLowerCase() .replace(/[^a-z0-9\s-]/g, ' ') // Remove special chars .split(/\s+/) .filter(w => w.length > 0 && !stopWords.has(w)) .slice(0, 6); // Take first 6 keywords if (words.length === 0) { // Fallback if no keywords found return 'task-' + Date.now().toString().slice(-6); } return words.join('-'); } getAgentEmoji(agentName) { // Map agent names to emojis per @.genie/code/skills/emoji-naming-convention.md const normalized = agentName.toLowerCase().trim(); // Agent emojis const agentEmojis = { // Orchestrators & Planning 'genie': '🧞', 'wish': 'πŸ’­', 'plan': 'πŸ“‹', 'forge': 'βš™οΈ', // Execution agents (robots do the work) 'implementor': 'πŸ€–', 'tests': 'πŸ€–', 'polish': 'πŸ€–', 'refactor': 'πŸ€–', // Validation & Review 'review': 'βœ…', // Tools & Utilities 'git': 'πŸ”§', 'release': 'πŸš€', 'commit': 'πŸ“¦', // Analysis & Learning 'learn': 'πŸ“š', 'debug': '🐞', 'analyze': 'πŸ”', 'thinkdeep': '🧠', // Communication & Consensus 'consensus': '🀝', 'prompt': 'πŸ“', 'roadmap': 'πŸ—ΊοΈ' }; return agentEmojis[normalized] || '🧞'; // Default to genie emoji } } exports.ForgeExecutor = ForgeExecutor; function createForgeExecutor(config = {}) { const defaultConfig = { forgeBaseUrl: process.env.FORGE_BASE_URL || (0, service_config_js_1.getForgeConfig)().baseUrl, forgeToken: process.env.FORGE_TOKEN, genieProjectId: process.env.GENIE_PROJECT_ID }; return new ForgeExecutor({ ...defaultConfig, ...config }); }