@jjdenhertog/ai-driven-development
Version:
AI-driven development workflow with learning capabilities for Claude
283 lines (278 loc) • 17 kB
JavaScript
;
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