UNPKG

@jjdenhertog/ai-driven-development

Version:

AI-driven development workflow with learning capabilities for Claude

283 lines (278 loc) 17 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeTaskCommand = executeTaskCommand; const fs_extra_1 = require("fs-extra"); /* eslint-disable @typescript-eslint/restrict-template-expressions */ const node_path_1 = require("node:path"); const claude_wrapper_1 = require("../../claude-wrapper"); const config_1 = require("../config"); const addHooks_1 = __importDefault(require("../utils/claude/addHooks")); const autoRetryClaude_1 = require("../utils/claude/autoRetryClaude"); const removeHooks_1 = __importDefault(require("../utils/claude/removeHooks")); const addToGitignore_1 = require("../utils/git/addToGitignore"); const checkGitInitialized_1 = require("../utils/git/checkGitInitialized"); const createCommit_1 = require("../utils/git/createCommit"); const ensureBranch_1 = require("../utils/git/ensureBranch"); const ensureWorktree_1 = require("../utils/git/ensureWorktree"); const getGitInstance_1 = require("../utils/git/getGitInstance"); const isInWorktree_1 = require("../utils/git/isInWorktree"); const pullBranch_1 = require("../utils/git/pullBranch"); const pushBranch_1 = require("../utils/git/pushBranch"); const logger_1 = require("../utils/logger"); const createSession_1 = require("../utils/storage/createSession"); const createSessionReport_1 = require("../utils/storage/createSessionReport"); const getBranchName_1 = require("../utils/tasks/getBranchName"); const updateTaskFile_1 = require("../utils/tasks/updateTaskFile"); const validateTaskForExecution_1 = require("../utils/tasks/validateTaskForExecution"); function executeTaskCommand(options) { return __awaiter(this, void 0, void 0, function* () { const { taskId, dryRun, force, dangerouslySkipPermission } = options; // Ensure git auth if (!(yield (0, checkGitInitialized_1.checkGitInitialized)())) throw new Error('Git is not initialized. Please run `git init` in the root of the repository.'); // Check if we are in a worktree if (yield (0, isInWorktree_1.isInWorktree)()) throw new Error('This command must be run from the root of the repository.'); const { logsDir, logPath } = (0, createSession_1.createSession)(taskId); // Validate the task - expecting pending or in-progress status const task = yield (0, validateTaskForExecution_1.validateTaskForExecution)({ taskId, expectedStatuses: ['pending'], force }); /////////////////////////////////////////////////////////// // Goal: // 1. Ensure the branch for the task exists // 2. Ensure the worktree for the task exists // 3. Ensure the worktree branh is pulled /////////////////////////////////////////////////////////// (0, logger_1.log)(`Preparing git branch...`, 'success', undefined, logPath); const branchName = (0, getBranchName_1.getBranchName)(task); yield (0, ensureBranch_1.ensureBranch)(branchName); const worktreeFolder = branchName.split('/').at(-1) || branchName; const worktreePath = `.aidev-${worktreeFolder}`; yield (0, ensureWorktree_1.ensureWorktree)({ branch: branchName, path: worktreePath }); yield (0, pullBranch_1.pullBranch)(branchName, worktreePath); // Ensure common directories are in .gitignore const commonIgnores = ['node_modules', '.next', 'dist', 'build', '*.log', '.DS_Store']; for (const ignore of commonIgnores) (0, addToGitignore_1.addToGitignore)(worktreePath, ignore); (0, logger_1.log)(`Executing Task: ${task.id} - ${task.name}`, 'info', undefined, logPath); if (dryRun) { (0, logger_1.log)('Dry Run Mode - No changes will be made', 'warn', undefined, logPath); (0, logger_1.log)(` Task File: ${task.path}`, 'info', undefined, logPath); (0, logger_1.log)(` Branch Name: ${(0, getBranchName_1.getBranchName)(task)}`, 'info', undefined, logPath); return; } const removeWorktree = () => __awaiter(this, void 0, void 0, function* () { (0, logger_1.log)(`Removing worktree...`, 'info', undefined, logPath); // Use git instance from the parent repository, not the worktree const git = (0, getGitInstance_1.getGitInstance)(process.cwd()); try { yield git.raw(['worktree', 'remove', '--force', worktreePath]); } catch (_error) { (0, fs_extra_1.rmSync)(worktreePath, { force: true, recursive: true }); } try { yield git.raw(['branch', '-D', branchName, '--force']); } catch (_error) { // Branch might not exist or might be checked out elsewhere (0, logger_1.log)(`Could not delete branch ${branchName}: ${_error}`, 'warn', undefined, logPath); } }); // Create session (0, logger_1.log)(`Starting execution of task ${task.id}`, 'info', undefined, logPath); // Update task file with execution metadata (0, updateTaskFile_1.updateTaskFile)(task.path, { branch: branchName, status: 'in-progress', started_at: new Date().toISOString() }); // Step 3: Execute Claude (0, logger_1.log)('Starting Claude with aidev-code-task command...', 'success', undefined, logPath); if (dangerouslySkipPermission) (0, logger_1.log)('Dangerously skipping permission checks of Claude Code', 'warn', undefined, logPath); // Add hooks for claude (0, addHooks_1.default)(worktreePath); // Set the output path for the task const outputPath = (0, node_path_1.join)('.aidev-storage', 'tasks_output', task.id); const outputAbsolutPath = (0, node_path_1.join)(config_1.STORAGE_PATH, 'tasks_output', task.id); (0, fs_extra_1.ensureDirSync)(outputAbsolutPath); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'inventory')); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'architect')); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'implement')); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'validate')); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'test_fix')); (0, fs_extra_1.ensureDirSync)((0, node_path_1.join)(outputAbsolutPath, 'phase_outputs', 'review')); const claudeCommand = (prompt) => { return () => __awaiter(this, void 0, void 0, function* () { const args = []; if (dangerouslySkipPermission) args.push('--dangerously-skip-permissions'); // Execute Claude and wait for completion const result = yield (0, claude_wrapper_1.executeClaudeCommand)({ cwd: worktreePath, command: `Please complete the following steps IN ORDER: 1. First, use the Read tool to read the entire contents of the file: .aidev-storage/prompts/${prompt} IMPORTANT: The .aidev-storage directory is in your current working directory. Do NOT use ../.aidev-storage 2. After reading the file, list the key constraints and outputs for this phase. 3. Then execute the instructions from that file with these parameters: {"task_filename": "${task.id}-${task.name}", "task_output_folder": "${outputPath}", "use_preference_files": true, "use_examples": true } 4. Show me progress as you work through the phase. CRITICAL: You are in a git worktree. ALL work must be done within the current directory. NEVER use ../ paths.`, args, }); // We no longer capture output - hooks will handle logging (0, logger_1.log)(`\nClaude command exited with code: ${result.exitCode}`, 'info', undefined, logPath); // Create session report from debug logs and transcript (0, logger_1.log)('Creating session report...', 'info', undefined, logPath); const promptBase = (0, node_path_1.parse)(prompt).name; const sessionReport = yield (0, createSessionReport_1.createSessionReport)({ taskId: task.id, taskName: task.name, worktreePath, logsDir, exitCode: result.exitCode, fileName: promptBase }); return sessionReport; }); }; const commitAndPush = (success) => __awaiter(this, void 0, void 0, function* () { const gitWorktree = (0, getGitInstance_1.getGitInstance)(worktreePath); yield gitWorktree.add('-A'); const message = success ? `complete task ${task.id} - ${task.name} (AI-generated)` : `failed to complete task ${task.id} - ${task.name} (AI-generated)`; yield (0, createCommit_1.createCommit)(message, { prefix: 'feat', cwd: worktreePath }); const pushResult = yield (0, pushBranch_1.pushBranch)(branchName, worktreePath); if (!pushResult.success) { (0, logger_1.log)(`Failed to push changes to remote: ${pushResult.error}`, 'error', undefined, logPath); (0, logger_1.log)(`IMPORTANT: Worktree preserved at ${worktreePath}`, 'warn', undefined, logPath); (0, logger_1.log)(`Your completed work is safe. To push manually:`, 'info', undefined, logPath); (0, logger_1.log)(` cd ${worktreePath}`, 'info', undefined, logPath); (0, logger_1.log)(` git push origin ${branchName}`, 'info', undefined, logPath); return false; } // Verify that the remote branch has commits (not just branch creation) (0, logger_1.log)(`Verifying remote branch has commits...`, 'info', undefined, logPath); const git = (0, getGitInstance_1.getGitInstance)(process.cwd()); try { // Fetch the latest remote state yield git.fetch(['origin', branchName]); // Get commit count on remote branch const remoteCommitCount = yield git.raw(['rev-list', '--count', `origin/${branchName}`]); const commitCount = parseInt(remoteCommitCount.trim()); // Check if we have more than just the initial branch creation // Count commits that are in our branch but NOT in the starting point const newCommitCount = yield git.raw(['rev-list', '--count', `origin/${config_1.BRANCH_STARTING_POINT}..origin/${branchName}`]); const newCommits = parseInt(newCommitCount.trim()); if (commitCount <= 1 || newCommits === 0) { (0, logger_1.log)(`Remote branch appears to have no new commits (total: ${commitCount}, new: ${newCommits})`, 'error', undefined, logPath); (0, logger_1.log)(`Push appeared successful but no changes were pushed`, 'error', undefined, logPath); (0, logger_1.log)(`IMPORTANT: Worktree preserved at ${worktreePath}`, 'warn', undefined, logPath); (0, logger_1.log)(`Your completed work is safe. To push manually:`, 'info', undefined, logPath); (0, logger_1.log)(` cd ${worktreePath}`, 'info', undefined, logPath); (0, logger_1.log)(` git push --set-upstream origin ${branchName}`, 'info', undefined, logPath); return false; // Exit without removing worktree } (0, logger_1.log)(`Remote branch verified with ${newCommits} total commits`, 'success', undefined, logPath); } catch (error) { (0, logger_1.log)(`Failed to verify remote branch: ${error instanceof Error ? error.message : 'Unknown error'}`, 'warn', undefined, logPath); } return true; }); //@TODO: Check if indexation exists, and if not run the indexation command const indexFolder = (0, node_path_1.resolve)(process.cwd(), '.aidev-storage/index'); if (!(0, fs_extra_1.existsSync)(indexFolder)) { yield (0, autoRetryClaude_1.autoRetryClaude)({ claudeCommand: claudeCommand('aidev-index.md'), logPath }); } let phases = [ 'aidev-code-phase0.md', 'aidev-code-phase1.md', 'aidev-code-phase2.md', 'aidev-code-phase3.md', 'aidev-code-phase4a.md', 'aidev-code-phase4b.md', 'aidev-code-phase5.md' ]; if (task.type == 'instruction') phases = ['aidev-instruction-task.md']; if (task.type == 'scaffolding') phases = ['aidev-scaffolding-task.md']; for (const phase of phases) { const sessionReport = yield (0, autoRetryClaude_1.autoRetryClaude)({ claudeCommand: claudeCommand(phase), logPath }); if (!(sessionReport === null || sessionReport === void 0 ? void 0 : sessionReport.success)) { (0, logger_1.log)(`Phase ${phase} failed`, 'error', undefined, logPath); (0, removeHooks_1.default)(worktreePath); (0, updateTaskFile_1.updateTaskFile)(task.path, { status: 'failed' }); const success = yield commitAndPush(false); if (!success) { (0, updateTaskFile_1.updateTaskFile)(task.path, { status: 'failed-completing', notes: 'Task failed and changes not pushed. Manual push required.' }); } yield removeWorktree(); return; } } // await autoRetryClaude({ claudeCommand: claudeCommand('aidev-update-index.md'), logPath }); /////////////////////////////////////////////////////////// // Update task status and create PR /////////////////////////////////////////////////////////// try { (0, logger_1.log)(`Claude command success...`, 'success', undefined, logPath); // Then immediately to completed for PR creation (0, updateTaskFile_1.updateTaskFile)(task.path, { status: 'completed' }); (0, logger_1.log)(`Committing and pushing changes...`, 'info', undefined, logPath); // Remove the stoarge path const storagePath = (0, node_path_1.join)(worktreePath, '.aidev-storage'); if ((0, fs_extra_1.existsSync)(storagePath)) (0, fs_extra_1.rmSync)(storagePath, { force: true, recursive: true }); // Stage all files except ignored ones const success = yield commitAndPush(true); if (!success) { // Clean up hooks but preserve the worktree with all changes (0, removeHooks_1.default)(worktreePath); // Update task status to indicate push failure but work is complete (0, updateTaskFile_1.updateTaskFile)(task.path, { status: 'failed-completing', notes: 'Task completed but push failed. Manual push required.' }); return; } /////////////////////////////////////////////////////////// // Remove work tree - only if push was successful /////////////////////////////////////////////////////////// yield removeWorktree(); } catch (error) { (0, updateTaskFile_1.updateTaskFile)(task.path, { status: 'failed' }); yield removeWorktree(); (0, logger_1.log)(`Failed to finish task ${task.id} - ${task.name}: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); } }); } //# sourceMappingURL=executeTaskCommand.js.map