@ariso-ai/ivan 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +412 -0
  2. package/dist/agent.d.ts +11 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +48 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/config/config.d.ts +20 -0
  7. package/dist/config/config.d.ts.map +1 -0
  8. package/dist/config/config.js +187 -0
  9. package/dist/config/config.js.map +1 -0
  10. package/dist/config.d.ts +46 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +414 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/database/database.d.ts +12 -0
  15. package/dist/database/database.d.ts.map +1 -0
  16. package/dist/database/database.js +45 -0
  17. package/dist/database/database.js.map +1 -0
  18. package/dist/database/migration.d.ts +11 -0
  19. package/dist/database/migration.d.ts.map +1 -0
  20. package/dist/database/migration.js +64 -0
  21. package/dist/database/migration.js.map +1 -0
  22. package/dist/database/migrations/001_create_jobs_table.d.ts +3 -0
  23. package/dist/database/migrations/001_create_jobs_table.d.ts.map +1 -0
  24. package/dist/database/migrations/001_create_jobs_table.js +14 -0
  25. package/dist/database/migrations/001_create_jobs_table.js.map +1 -0
  26. package/dist/database/migrations/001_initial_schema.d.ts +3 -0
  27. package/dist/database/migrations/001_initial_schema.d.ts.map +1 -0
  28. package/dist/database/migrations/001_initial_schema.js +66 -0
  29. package/dist/database/migrations/001_initial_schema.js.map +1 -0
  30. package/dist/database/migrations/002_create_tasks_table.d.ts +3 -0
  31. package/dist/database/migrations/002_create_tasks_table.d.ts.map +1 -0
  32. package/dist/database/migrations/002_create_tasks_table.js +16 -0
  33. package/dist/database/migrations/002_create_tasks_table.js.map +1 -0
  34. package/dist/database/migrations/003_add_log_to_tasks.d.ts +3 -0
  35. package/dist/database/migrations/003_add_log_to_tasks.d.ts.map +1 -0
  36. package/dist/database/migrations/003_add_log_to_tasks.js +11 -0
  37. package/dist/database/migrations/003_add_log_to_tasks.js.map +1 -0
  38. package/dist/database/migrations/004_add_branch_to_tasks.d.ts +3 -0
  39. package/dist/database/migrations/004_add_branch_to_tasks.d.ts.map +1 -0
  40. package/dist/database/migrations/004_add_branch_to_tasks.js +11 -0
  41. package/dist/database/migrations/004_add_branch_to_tasks.js.map +1 -0
  42. package/dist/database/migrations/005_add_type_to_tasks.d.ts +3 -0
  43. package/dist/database/migrations/005_add_type_to_tasks.d.ts.map +1 -0
  44. package/dist/database/migrations/005_add_type_to_tasks.js +11 -0
  45. package/dist/database/migrations/005_add_type_to_tasks.js.map +1 -0
  46. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts +3 -0
  47. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts.map +1 -0
  48. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js +15 -0
  49. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js.map +1 -0
  50. package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts +3 -0
  51. package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts.map +1 -0
  52. package/dist/database/migrations/006_add_comment_url_to_tasks.js +14 -0
  53. package/dist/database/migrations/006_add_comment_url_to_tasks.js.map +1 -0
  54. package/dist/database/migrations/007_add_commit_to_tasks.d.ts +3 -0
  55. package/dist/database/migrations/007_add_commit_to_tasks.d.ts.map +1 -0
  56. package/dist/database/migrations/007_add_commit_to_tasks.js +13 -0
  57. package/dist/database/migrations/007_add_commit_to_tasks.js.map +1 -0
  58. package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts +3 -0
  59. package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts.map +1 -0
  60. package/dist/database/migrations/008_add_lint_and_test_task_type.js +42 -0
  61. package/dist/database/migrations/008_add_lint_and_test_task_type.js.map +1 -0
  62. package/dist/database/migrations/index.d.ts +3 -0
  63. package/dist/database/migrations/index.d.ts.map +1 -0
  64. package/dist/database/migrations/index.js +19 -0
  65. package/dist/database/migrations/index.js.map +1 -0
  66. package/dist/database/types.d.ts +34 -0
  67. package/dist/database/types.d.ts.map +1 -0
  68. package/dist/database/types.js +2 -0
  69. package/dist/database/types.js.map +1 -0
  70. package/dist/database.d.ts +13 -0
  71. package/dist/database.d.ts.map +1 -0
  72. package/dist/database.js +32 -0
  73. package/dist/database.js.map +1 -0
  74. package/dist/index.d.ts +3 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +285 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/scripts/task-executor.d.ts +3 -0
  79. package/dist/scripts/task-executor.d.ts.map +1 -0
  80. package/dist/scripts/task-executor.js +139 -0
  81. package/dist/scripts/task-executor.js.map +1 -0
  82. package/dist/scripts/task-planner.d.ts +3 -0
  83. package/dist/scripts/task-planner.d.ts.map +1 -0
  84. package/dist/scripts/task-planner.js +81 -0
  85. package/dist/scripts/task-planner.js.map +1 -0
  86. package/dist/services/address-executor.d.ts +13 -0
  87. package/dist/services/address-executor.d.ts.map +1 -0
  88. package/dist/services/address-executor.js +202 -0
  89. package/dist/services/address-executor.js.map +1 -0
  90. package/dist/services/address-task-executor.d.ts +19 -0
  91. package/dist/services/address-task-executor.d.ts.map +1 -0
  92. package/dist/services/address-task-executor.js +736 -0
  93. package/dist/services/address-task-executor.js.map +1 -0
  94. package/dist/services/claude-cli-executor.d.ts +14 -0
  95. package/dist/services/claude-cli-executor.d.ts.map +1 -0
  96. package/dist/services/claude-cli-executor.js +241 -0
  97. package/dist/services/claude-cli-executor.js.map +1 -0
  98. package/dist/services/claude-executor.d.ts +14 -0
  99. package/dist/services/claude-executor.d.ts.map +1 -0
  100. package/dist/services/claude-executor.js +274 -0
  101. package/dist/services/claude-executor.js.map +1 -0
  102. package/dist/services/claude-planner.d.ts +15 -0
  103. package/dist/services/claude-planner.d.ts.map +1 -0
  104. package/dist/services/claude-planner.js +107 -0
  105. package/dist/services/claude-planner.js.map +1 -0
  106. package/dist/services/docker-orchestrator.d.ts +11 -0
  107. package/dist/services/docker-orchestrator.d.ts.map +1 -0
  108. package/dist/services/docker-orchestrator.js +85 -0
  109. package/dist/services/docker-orchestrator.js.map +1 -0
  110. package/dist/services/executor-factory.d.ts +14 -0
  111. package/dist/services/executor-factory.d.ts.map +1 -0
  112. package/dist/services/executor-factory.js +14 -0
  113. package/dist/services/executor-factory.js.map +1 -0
  114. package/dist/services/git-manager.d.ts +36 -0
  115. package/dist/services/git-manager.d.ts.map +1 -0
  116. package/dist/services/git-manager.js +728 -0
  117. package/dist/services/git-manager.js.map +1 -0
  118. package/dist/services/index.d.ts +9 -0
  119. package/dist/services/index.d.ts.map +1 -0
  120. package/dist/services/index.js +9 -0
  121. package/dist/services/index.js.map +1 -0
  122. package/dist/services/job-manager.d.ts +30 -0
  123. package/dist/services/job-manager.d.ts.map +1 -0
  124. package/dist/services/job-manager.js +337 -0
  125. package/dist/services/job-manager.js.map +1 -0
  126. package/dist/services/openai-service.d.ts +14 -0
  127. package/dist/services/openai-service.d.ts.map +1 -0
  128. package/dist/services/openai-service.js +186 -0
  129. package/dist/services/openai-service.js.map +1 -0
  130. package/dist/services/pr-service.d.ts +31 -0
  131. package/dist/services/pr-service.d.ts.map +1 -0
  132. package/dist/services/pr-service.js +291 -0
  133. package/dist/services/pr-service.js.map +1 -0
  134. package/dist/services/repository-manager.d.ts +12 -0
  135. package/dist/services/repository-manager.d.ts.map +1 -0
  136. package/dist/services/repository-manager.js +101 -0
  137. package/dist/services/repository-manager.js.map +1 -0
  138. package/dist/services/task-executor.d.ts +20 -0
  139. package/dist/services/task-executor.d.ts.map +1 -0
  140. package/dist/services/task-executor.js +717 -0
  141. package/dist/services/task-executor.js.map +1 -0
  142. package/dist/types/non-interactive-config.d.ts +28 -0
  143. package/dist/types/non-interactive-config.d.ts.map +1 -0
  144. package/dist/types/non-interactive-config.js +2 -0
  145. package/dist/types/non-interactive-config.js.map +1 -0
  146. package/dist/web-server.d.ts +16 -0
  147. package/dist/web-server.d.ts.map +1 -0
  148. package/dist/web-server.js +488 -0
  149. package/dist/web-server.js.map +1 -0
  150. package/package.json +71 -0
@@ -0,0 +1,717 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { JobManager } from './job-manager.js';
4
+ import { GitManager } from './git-manager.js';
5
+ import { ExecutorFactory } from './executor-factory.js';
6
+ import { OpenAIService } from './openai-service.js';
7
+ import { RepositoryManager } from './repository-manager.js';
8
+ import { ConfigManager } from '../config.js';
9
+ import { AddressTaskExecutor } from './address-task-executor.js';
10
+ import { PRService } from './pr-service.js';
11
+ export class TaskExecutor {
12
+ jobManager;
13
+ gitManager = null;
14
+ claudeExecutor = null;
15
+ openaiService = null;
16
+ repositoryManager;
17
+ configManager;
18
+ workingDir;
19
+ repoInstructions;
20
+ constructor() {
21
+ this.jobManager = new JobManager();
22
+ this.repositoryManager = new RepositoryManager();
23
+ this.configManager = new ConfigManager();
24
+ this.workingDir = '';
25
+ this.repoInstructions = undefined;
26
+ }
27
+ getClaudeExecutor() {
28
+ if (!this.claudeExecutor) {
29
+ this.claudeExecutor = ExecutorFactory.getExecutor();
30
+ }
31
+ return this.claudeExecutor;
32
+ }
33
+ getOpenAIService() {
34
+ if (!this.openaiService) {
35
+ this.openaiService = new OpenAIService();
36
+ }
37
+ return this.openaiService;
38
+ }
39
+ async executeWorkflow() {
40
+ try {
41
+ console.log(chalk.blue.bold('🚀 Starting Ivan workflow'));
42
+ console.log('');
43
+ console.log(chalk.blue('🔍 Validating dependencies...'));
44
+ await this.getClaudeExecutor().validateClaudeCodeInstallation();
45
+ console.log(chalk.green('✅ Claude Code SDK configured'));
46
+ this.workingDir = await this.repositoryManager.getValidWorkingDirectory();
47
+ this.gitManager = new GitManager(this.workingDir);
48
+ if (!this.gitManager) {
49
+ throw new Error('GitManager not initialized');
50
+ }
51
+ this.gitManager.validateGitHubCliInstallation();
52
+ console.log(chalk.green('✅ GitHub CLI is installed'));
53
+ if (!this.gitManager) {
54
+ throw new Error('GitManager not initialized');
55
+ }
56
+ this.gitManager.validateGitHubCliAuthentication();
57
+ console.log(chalk.green('✅ GitHub CLI is authenticated'));
58
+ const repoInfo = this.repositoryManager.getRepositoryInfo(this.workingDir);
59
+ console.log(chalk.blue(`📂 Working in: ${repoInfo.name} (${repoInfo.branch})`));
60
+ console.log('');
61
+ // Check for repository-specific instructions
62
+ this.repoInstructions = await this.configManager.getRepoInstructions(this.workingDir);
63
+ if (!this.repoInstructions) {
64
+ // Only prompt if the user hasn't previously declined for this repo
65
+ const hasDeclined = await this.configManager.hasDeclinedRepoInstructions(this.workingDir);
66
+ if (!hasDeclined) {
67
+ console.log(chalk.yellow('⚠️ No repository-specific instructions found for this repository.'));
68
+ const inquirer = (await import('inquirer')).default;
69
+ const { shouldSetInstructions } = await inquirer.prompt([
70
+ {
71
+ type: 'confirm',
72
+ name: 'shouldSetInstructions',
73
+ message: 'Would you like to set repository-specific instructions now?',
74
+ default: false
75
+ }
76
+ ]);
77
+ if (shouldSetInstructions) {
78
+ this.repoInstructions = await this.configManager.promptForRepoInstructions(this.workingDir);
79
+ }
80
+ else {
81
+ // Mark that the user declined so we don't ask again
82
+ await this.configManager.markRepoInstructionsDeclined(this.workingDir);
83
+ console.log(chalk.gray('You can configure instructions later with: ivan edit-repo-instructions'));
84
+ }
85
+ console.log('');
86
+ }
87
+ }
88
+ else {
89
+ console.log(chalk.green('✅ Repository-specific instructions loaded'));
90
+ }
91
+ const { tasks, prStrategy } = await this.jobManager.promptForTasks(this.workingDir);
92
+ console.log('');
93
+ // Check if these are address tasks
94
+ const addressTasks = tasks.filter(t => t.type === 'address');
95
+ const buildTasks = tasks.filter(t => t.type === 'build');
96
+ // Handle address tasks separately
97
+ if (addressTasks.length > 0) {
98
+ const addressExecutor = new AddressTaskExecutor();
99
+ await addressExecutor.executeAddressTasks(addressTasks);
100
+ }
101
+ // Handle build tasks
102
+ if (buildTasks.length > 0) {
103
+ // Ask about PR comment monitoring upfront
104
+ let shouldWaitForComments = false;
105
+ const inquirer = (await import('inquirer')).default;
106
+ const { waitForComments } = await inquirer.prompt([
107
+ {
108
+ type: 'confirm',
109
+ name: 'waitForComments',
110
+ message: 'After completing tasks, would you like to wait 30 minutes for PR reviews and automatically address any comments?',
111
+ default: false
112
+ }
113
+ ]);
114
+ shouldWaitForComments = waitForComments;
115
+ // Ask if user wants to confirm before each task
116
+ let confirmBeforeEach = false;
117
+ if (buildTasks.length > 1) {
118
+ const { shouldConfirm } = await inquirer.prompt([
119
+ {
120
+ type: 'confirm',
121
+ name: 'shouldConfirm',
122
+ message: 'Would you like to confirm before executing each task?',
123
+ default: false
124
+ }
125
+ ]);
126
+ confirmBeforeEach = shouldConfirm;
127
+ }
128
+ console.log(chalk.blue.bold('📋 Executing tasks...'));
129
+ // Handle single PR strategy
130
+ if (prStrategy === 'single' && buildTasks.length > 1) {
131
+ await this.executeTasksWithSinglePR(buildTasks, confirmBeforeEach);
132
+ }
133
+ else {
134
+ // Multiple PRs (default behavior)
135
+ for (let i = 0; i < buildTasks.length; i++) {
136
+ const task = buildTasks[i];
137
+ if (confirmBeforeEach) {
138
+ console.log('');
139
+ console.log(chalk.yellow(`Task ${i + 1} of ${buildTasks.length}: ${task.description}`));
140
+ const inquirer = (await import('inquirer')).default;
141
+ const { shouldExecute } = await inquirer.prompt([
142
+ {
143
+ type: 'confirm',
144
+ name: 'shouldExecute',
145
+ message: 'Execute this task?',
146
+ default: true
147
+ }
148
+ ]);
149
+ if (!shouldExecute) {
150
+ console.log(chalk.gray('⏭️ Skipping task'));
151
+ await this.jobManager.updateTaskStatus(task.uuid, 'not_started');
152
+ continue;
153
+ }
154
+ }
155
+ await this.executeTask(task);
156
+ }
157
+ }
158
+ console.log('');
159
+ console.log(chalk.green.bold('🎉 All initial tasks completed successfully!'));
160
+ // Collect PR URLs created during this session by reloading tasks from DB
161
+ const createdPRUrls = [];
162
+ for (const task of buildTasks) {
163
+ const updatedTask = await this.jobManager.getTask(task.uuid);
164
+ if (updatedTask && updatedTask.pr_link) {
165
+ createdPRUrls.push(updatedTask.pr_link);
166
+ }
167
+ }
168
+ if (createdPRUrls.length > 0) {
169
+ // Show PRs created
170
+ console.log('');
171
+ console.log(chalk.blue('📋 PRs created:'));
172
+ createdPRUrls.forEach(url => console.log(chalk.cyan(` - ${url}`)));
173
+ if (shouldWaitForComments) {
174
+ // Wait 30 minutes for reviewers to comment
175
+ console.log('');
176
+ console.log(chalk.blue('⏰ Waiting 30 minutes for PR reviews...'));
177
+ console.log(chalk.gray('PRs being monitored:'));
178
+ createdPRUrls.forEach(url => console.log(chalk.gray(` - ${url}`)));
179
+ console.log('');
180
+ const waitTime = 30 * 60 * 1000; // 30 minutes in milliseconds
181
+ const startTime = Date.now();
182
+ const interval = setInterval(() => {
183
+ const elapsed = Date.now() - startTime;
184
+ const remaining = waitTime - elapsed;
185
+ const minutes = Math.floor(remaining / 60000);
186
+ const seconds = Math.floor((remaining % 60000) / 1000);
187
+ process.stdout.write(`\r${chalk.blue('⏰')} Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')} `);
188
+ }, 1000);
189
+ await new Promise(resolve => setTimeout(resolve, waitTime));
190
+ clearInterval(interval);
191
+ console.log('\n');
192
+ // Check for unaddressed comments on created PRs
193
+ console.log(chalk.blue('🔍 Checking for PR review comments...'));
194
+ const prService = new PRService(this.workingDir);
195
+ const addressTasks = await this.checkAndCreateAddressTasks(createdPRUrls, prService);
196
+ if (addressTasks.length > 0) {
197
+ console.log('');
198
+ console.log(chalk.blue.bold(`📋 Found ${addressTasks.length} comments to address`));
199
+ // Execute address tasks automatically
200
+ const addressExecutor = new AddressTaskExecutor();
201
+ await addressExecutor.executeAddressTasks(addressTasks);
202
+ console.log('');
203
+ console.log(chalk.green.bold('🎉 All PR comments addressed successfully!'));
204
+ }
205
+ else {
206
+ console.log(chalk.green('✨ No unaddressed comments found on PRs!'));
207
+ }
208
+ }
209
+ else {
210
+ console.log(chalk.blue('💡 You can run "ivan address" later to handle any PR comments'));
211
+ }
212
+ }
213
+ }
214
+ }
215
+ catch (error) {
216
+ console.error(chalk.red.bold('❌ Workflow failed:'), error);
217
+ throw error;
218
+ }
219
+ finally {
220
+ this.jobManager.close();
221
+ }
222
+ }
223
+ async checkAndCreateAddressTasks(prUrls, prService) {
224
+ const addressTasks = [];
225
+ for (const prUrl of prUrls) {
226
+ // Extract PR number from URL
227
+ const prMatch = prUrl.match(/\/pull\/(\d+)/);
228
+ if (!prMatch)
229
+ continue;
230
+ const prNumber = parseInt(prMatch[1]);
231
+ // Get unaddressed comments for this PR
232
+ const comments = await prService.getUnaddressedComments(prNumber);
233
+ if (comments.length > 0) {
234
+ // Get the branch name for this PR
235
+ if (!this.gitManager) {
236
+ throw new Error('GitManager not initialized');
237
+ }
238
+ const prInfo = await this.gitManager.getPRInfo(prNumber);
239
+ const branch = prInfo.headRefName;
240
+ // Create address tasks for each comment
241
+ for (const comment of comments) {
242
+ let description = `Address PR #${prNumber} comment from @${comment.author}: "${comment.body.substring(0, 100)}${comment.body.length > 100 ? '...' : ''}"`;
243
+ if (comment.path) {
244
+ description += ` (in ${comment.path}${comment.line ? `:${comment.line}` : ''})`;
245
+ }
246
+ // Get the current job ID from one of the build tasks
247
+ const jobUuid = this.jobManager.getCurrentJobUuid() || (await this.jobManager.getLatestJobId(this.workingDir));
248
+ // Create the address task
249
+ const taskUuid = await this.jobManager.createTask(jobUuid, description, 'address');
250
+ await this.jobManager.updateTaskBranch(taskUuid, branch);
251
+ const task = await this.jobManager.getTask(taskUuid);
252
+ if (task) {
253
+ addressTasks.push(task);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return addressTasks;
259
+ }
260
+ async executeTask(task) {
261
+ console.log('');
262
+ console.log(chalk.cyan.bold(`📝 Task: ${task.description}`));
263
+ let spinner = ora('Updating task status...').start();
264
+ let worktreePath = null;
265
+ let branchName = null;
266
+ try {
267
+ await this.jobManager.updateTaskStatus(task.uuid, 'active');
268
+ spinner.succeed('Task marked as active');
269
+ spinner = ora('Cleaning up and syncing with main branch...').start();
270
+ if (!this.gitManager) {
271
+ throw new Error('GitManager not initialized');
272
+ }
273
+ await this.gitManager.cleanupAndSyncMain();
274
+ spinner.succeed('Repository cleaned and synced with main');
275
+ if (!this.gitManager) {
276
+ throw new Error('GitManager not initialized');
277
+ }
278
+ branchName = this.gitManager.generateBranchName(task.description);
279
+ spinner = ora(`Creating worktree for branch: ${branchName}`).start();
280
+ worktreePath = await this.gitManager.createWorktree(branchName);
281
+ this.gitManager.switchToWorktree(worktreePath);
282
+ spinner.succeed(`Worktree created: ${worktreePath}`);
283
+ await this.jobManager.updateTaskBranch(task.uuid, branchName);
284
+ spinner = ora('Executing task with Claude Code...').start();
285
+ // Append repository-specific instructions to the task if they exist
286
+ let taskWithInstructions = task.description;
287
+ if (this.repoInstructions) {
288
+ taskWithInstructions = `${task.description}\n\nRepository-specific instructions:\n${this.repoInstructions}`;
289
+ }
290
+ // Use worktree path for Claude execution, falling back to workingDir if needed
291
+ const executionPath = worktreePath || this.workingDir;
292
+ const result = await this.getClaudeExecutor().executeTask(taskWithInstructions, executionPath);
293
+ spinner.succeed('Claude Code execution completed');
294
+ spinner = ora('Storing execution log...').start();
295
+ await this.jobManager.updateTaskExecutionLog(task.uuid, result.log);
296
+ spinner.succeed('Execution log stored');
297
+ if (!this.gitManager) {
298
+ throw new Error('GitManager not initialized');
299
+ }
300
+ const changedFiles = this.gitManager.getChangedFiles();
301
+ if (changedFiles.length === 0) {
302
+ console.log(chalk.yellow('⚠️ No changes detected, skipping commit and PR creation'));
303
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
304
+ return;
305
+ }
306
+ if (!this.gitManager) {
307
+ throw new Error('GitManager not initialized');
308
+ }
309
+ const diff = this.gitManager.getDiff();
310
+ spinner = ora('Generating commit message...').start();
311
+ const commitMessage = await this.getOpenAIService().generateCommitMessage(diff, changedFiles);
312
+ spinner.succeed(`Commit message generated: ${commitMessage}`);
313
+ spinner = ora('Committing changes...').start();
314
+ if (!this.gitManager) {
315
+ throw new Error('GitManager not initialized');
316
+ }
317
+ // Try to commit, handling pre-commit hook failures
318
+ let commitAttempts = 0;
319
+ const maxCommitAttempts = 3;
320
+ let commitSucceeded = false;
321
+ while (commitAttempts < maxCommitAttempts && !commitSucceeded) {
322
+ try {
323
+ await this.gitManager.commitChanges(commitMessage);
324
+ // Only show success message if spinner is running (first attempt)
325
+ if (commitAttempts === 0) {
326
+ spinner.succeed('Changes committed');
327
+ }
328
+ else if (spinner.isSpinning) {
329
+ spinner.succeed('Commit successful after retry');
330
+ }
331
+ commitSucceeded = true;
332
+ }
333
+ catch (commitError) {
334
+ commitAttempts++;
335
+ const errorMessage = commitError instanceof Error ? commitError.message : String(commitError);
336
+ // Check if this is a pre-commit hook failure
337
+ if (errorMessage.includes('pre-commit') && commitAttempts < maxCommitAttempts) {
338
+ spinner.fail(`Pre-commit hook failed (attempt ${commitAttempts}/${maxCommitAttempts})`);
339
+ console.log(chalk.yellow('🔧 Running Claude to fix pre-commit errors...'));
340
+ // Extract the error details from the commit error
341
+ const errorDetails = errorMessage;
342
+ // Prepare prompt for Claude to fix the errors
343
+ const fixPrompt = `Fix the following pre-commit hook errors:\n\n${errorDetails}\n\nPlease fix all TypeScript errors, linting issues, and any other problems preventing the commit.`;
344
+ spinner = ora('Running Claude to fix pre-commit errors...').start();
345
+ try {
346
+ // Run Claude to fix the errors
347
+ const fixResult = await this.getClaudeExecutor().executeTask(fixPrompt, worktreePath || this.workingDir);
348
+ spinner.succeed('Claude attempted to fix the errors');
349
+ // Update the execution log with the fix attempt
350
+ const previousLog = await this.jobManager.getTaskExecutionLog(task.uuid);
351
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `${previousLog}\n\n--- Pre-commit Fix Attempt ${commitAttempts} ---\n${fixResult.log}`);
352
+ // Try to commit again on the next iteration
353
+ spinner = ora('Retrying commit...').start();
354
+ }
355
+ catch (fixError) {
356
+ spinner.fail('Failed to run Claude to fix errors');
357
+ console.error(chalk.red('Claude fix attempt failed:'), fixError);
358
+ throw commitError; // Re-throw the original error
359
+ }
360
+ }
361
+ else {
362
+ // Not a pre-commit error or max attempts reached
363
+ throw commitError;
364
+ }
365
+ }
366
+ }
367
+ if (!commitSucceeded) {
368
+ throw new Error(`Failed to commit after ${maxCommitAttempts} attempts due to pre-commit hook failures`);
369
+ }
370
+ spinner = ora('Pushing branch...').start();
371
+ if (!this.gitManager) {
372
+ throw new Error('GitManager not initialized');
373
+ }
374
+ await this.gitManager.pushBranch(branchName);
375
+ spinner.succeed('Branch pushed to origin');
376
+ spinner = ora('Generating PR description...').start();
377
+ const { title, body } = await this.getOpenAIService().generatePullRequestDescription(task.description, diff, changedFiles);
378
+ spinner.succeed('PR description generated');
379
+ spinner = ora('Creating pull request...').start();
380
+ if (!this.gitManager) {
381
+ throw new Error('GitManager not initialized');
382
+ }
383
+ const prUrl = await this.gitManager.createPullRequest(title, body);
384
+ spinner.succeed(`Pull request created: ${prUrl}`);
385
+ await this.jobManager.updateTaskPrLink(task.uuid, prUrl);
386
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
387
+ console.log(chalk.green(`✅ Task completed: ${task.description}`));
388
+ console.log(chalk.cyan(`🔗 PR: ${prUrl}`));
389
+ }
390
+ catch (error) {
391
+ if (spinner.isSpinning) {
392
+ spinner.fail('Task execution failed');
393
+ }
394
+ // Store error log if execution failed
395
+ try {
396
+ const errorLog = error instanceof Error ? error.message : String(error);
397
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `ERROR: ${errorLog}`);
398
+ console.log(chalk.gray('Error log stored to database'));
399
+ }
400
+ catch (logError) {
401
+ console.error(chalk.red('Failed to store error log:'), logError);
402
+ }
403
+ console.error(chalk.red(`❌ Failed to execute task: ${task.description}`), error);
404
+ throw error;
405
+ }
406
+ finally {
407
+ // Switch back to original directory and clean up worktree
408
+ if (this.gitManager && worktreePath && branchName) {
409
+ this.gitManager.switchToOriginalDir();
410
+ await this.gitManager.removeWorktree(branchName);
411
+ }
412
+ }
413
+ }
414
+ async executeTasksWithSinglePR(tasks, confirmBeforeEach) {
415
+ console.log('');
416
+ console.log(chalk.blue('📦 Creating single branch for all tasks...'));
417
+ let spinner = ora('Cleaning up and syncing with main branch...').start();
418
+ let worktreePath = null;
419
+ let branchName = null;
420
+ let sessionId;
421
+ try {
422
+ if (!this.gitManager) {
423
+ throw new Error('GitManager not initialized');
424
+ }
425
+ await this.gitManager.cleanupAndSyncMain();
426
+ spinner.succeed('Repository cleaned and synced with main');
427
+ // Generate branch name based on all tasks
428
+ const combinedDescription = tasks.length === 1
429
+ ? tasks[0].description
430
+ : `Multiple tasks: ${tasks.slice(0, 2).map(t => t.description).join(', ')}${tasks.length > 2 ? '...' : ''}`;
431
+ branchName = this.gitManager.generateBranchName(combinedDescription);
432
+ spinner = ora(`Creating worktree for branch: ${branchName}`).start();
433
+ worktreePath = await this.gitManager.createWorktree(branchName);
434
+ this.gitManager.switchToWorktree(worktreePath);
435
+ spinner.succeed(`Worktree created: ${worktreePath}`);
436
+ // Update all tasks with the same branch
437
+ for (const task of tasks) {
438
+ await this.jobManager.updateTaskBranch(task.uuid, branchName);
439
+ }
440
+ // Execute each task on the same branch
441
+ for (let i = 0; i < tasks.length; i++) {
442
+ const task = tasks[i];
443
+ if (confirmBeforeEach) {
444
+ console.log('');
445
+ console.log(chalk.yellow(`Task ${i + 1} of ${tasks.length}: ${task.description}`));
446
+ const inquirer = (await import('inquirer')).default;
447
+ const { shouldExecute } = await inquirer.prompt([
448
+ {
449
+ type: 'confirm',
450
+ name: 'shouldExecute',
451
+ message: 'Execute this task?',
452
+ default: true
453
+ }
454
+ ]);
455
+ if (!shouldExecute) {
456
+ console.log(chalk.gray('⏭️ Skipping task'));
457
+ await this.jobManager.updateTaskStatus(task.uuid, 'not_started');
458
+ continue;
459
+ }
460
+ }
461
+ console.log('');
462
+ console.log(chalk.cyan.bold(`📝 Task ${i + 1}/${tasks.length}: ${task.description}`));
463
+ spinner = ora('Updating task status...').start();
464
+ await this.jobManager.updateTaskStatus(task.uuid, 'active');
465
+ spinner.succeed('Task marked as active');
466
+ spinner = ora('Executing task with Claude Code...').start();
467
+ // Append repository-specific instructions to the task if they exist
468
+ let taskWithInstructions = task.description;
469
+ if (this.repoInstructions) {
470
+ taskWithInstructions = `${task.description}\n\nRepository-specific instructions:\n${this.repoInstructions}`;
471
+ }
472
+ // Use worktree path for Claude execution
473
+ const executionPath = worktreePath || this.workingDir;
474
+ // Pass session ID to maintain context between tasks
475
+ const result = await this.getClaudeExecutor().executeTask(taskWithInstructions, executionPath, sessionId);
476
+ // Store the session ID for the next task
477
+ sessionId = result.sessionId;
478
+ spinner.succeed('Claude Code execution completed');
479
+ spinner = ora('Storing execution log...').start();
480
+ await this.jobManager.updateTaskExecutionLog(task.uuid, result.log);
481
+ spinner.succeed('Execution log stored');
482
+ // Commit changes after each task (but don't create PR yet)
483
+ if (!this.gitManager) {
484
+ throw new Error('GitManager not initialized');
485
+ }
486
+ const changedFiles = this.gitManager.getChangedFiles();
487
+ if (changedFiles.length > 0) {
488
+ const diff = this.gitManager.getDiff();
489
+ spinner = ora('Generating commit message...').start();
490
+ const commitMessage = await this.getOpenAIService().generateCommitMessage(diff, changedFiles);
491
+ spinner.succeed(`Commit message generated: ${commitMessage}`);
492
+ spinner = ora('Committing changes...').start();
493
+ // Try to commit, handling pre-commit hook failures
494
+ let commitAttempts = 0;
495
+ const maxCommitAttempts = 3;
496
+ let commitSucceeded = false;
497
+ while (commitAttempts < maxCommitAttempts && !commitSucceeded) {
498
+ try {
499
+ await this.gitManager.commitChanges(commitMessage);
500
+ // Only show success message if spinner is running (first attempt)
501
+ if (commitAttempts === 0) {
502
+ spinner.succeed('Changes committed');
503
+ }
504
+ else if (spinner.isSpinning) {
505
+ spinner.succeed('Commit successful after retry');
506
+ }
507
+ commitSucceeded = true;
508
+ }
509
+ catch (commitError) {
510
+ commitAttempts++;
511
+ const errorMessage = commitError instanceof Error ? commitError.message : String(commitError);
512
+ // Check if this is a pre-commit hook failure
513
+ if (errorMessage.includes('pre-commit') && commitAttempts < maxCommitAttempts) {
514
+ spinner.fail(`Pre-commit hook failed (attempt ${commitAttempts}/${maxCommitAttempts})`);
515
+ console.log(chalk.yellow('🔧 Running Claude to fix pre-commit errors...'));
516
+ // Extract the error details from the commit error
517
+ const errorDetails = errorMessage;
518
+ // Prepare prompt for Claude to fix the errors
519
+ const fixPrompt = `Fix the following pre-commit hook errors:\n\n${errorDetails}\n\nPlease fix all TypeScript errors, linting issues, and any other problems preventing the commit.`;
520
+ spinner = ora('Running Claude to fix pre-commit errors...').start();
521
+ try {
522
+ // Run Claude to fix the errors (pass session ID to maintain context)
523
+ const fixResult = await this.getClaudeExecutor().executeTask(fixPrompt, executionPath, sessionId);
524
+ // Update session ID
525
+ sessionId = fixResult.sessionId;
526
+ spinner.succeed('Claude attempted to fix the errors');
527
+ // Update the execution log with the fix attempt
528
+ const previousLog = await this.jobManager.getTaskExecutionLog(task.uuid);
529
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `${previousLog}\n\n--- Pre-commit Fix Attempt ${commitAttempts} ---\n${fixResult.log}`);
530
+ // Try to commit again on the next iteration
531
+ spinner = ora('Retrying commit...').start();
532
+ }
533
+ catch (fixError) {
534
+ spinner.fail('Failed to run Claude to fix errors');
535
+ console.error(chalk.red('Claude fix attempt failed:'), fixError);
536
+ throw commitError; // Re-throw the original error
537
+ }
538
+ }
539
+ else {
540
+ // Not a pre-commit error or max attempts reached
541
+ throw commitError;
542
+ }
543
+ }
544
+ }
545
+ if (!commitSucceeded) {
546
+ throw new Error(`Failed to commit after ${maxCommitAttempts} attempts due to pre-commit hook failures`);
547
+ }
548
+ }
549
+ else {
550
+ console.log(chalk.yellow('⚠️ No changes detected for this task'));
551
+ }
552
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
553
+ console.log(chalk.green(`✅ Task ${i + 1}/${tasks.length} completed`));
554
+ }
555
+ // After all tasks are complete, create a single PR
556
+ spinner = ora('Pushing branch...').start();
557
+ if (!this.gitManager) {
558
+ throw new Error('GitManager not initialized');
559
+ }
560
+ await this.gitManager.pushBranch(branchName);
561
+ spinner.succeed('Branch pushed to origin');
562
+ // Generate PR description based on all tasks
563
+ spinner = ora('Generating pull request description...').start();
564
+ const allTaskDescriptions = tasks.map(t => `- ${t.description}`).join('\n');
565
+ const prTaskDescription = `Completed ${tasks.length} tasks:\n\n${allTaskDescriptions}`;
566
+ // Get combined diff for PR description
567
+ const finalDiff = this.gitManager.getDiff('origin/main', 'HEAD');
568
+ const allChangedFiles = this.gitManager.getChangedFiles('origin/main');
569
+ const { title, body } = await this.getOpenAIService().generatePullRequestDescription(prTaskDescription, finalDiff, allChangedFiles);
570
+ spinner.succeed('PR description generated');
571
+ spinner = ora('Creating pull request...').start();
572
+ const prUrl = await this.gitManager.createPullRequest(title, body);
573
+ spinner.succeed(`Pull request created: ${prUrl}`);
574
+ // Update all tasks with the same PR link
575
+ for (const task of tasks) {
576
+ await this.jobManager.updateTaskPrLink(task.uuid, prUrl);
577
+ }
578
+ console.log('');
579
+ console.log(chalk.green.bold(`✅ All ${tasks.length} tasks completed in a single PR!`));
580
+ console.log(chalk.cyan(`🔗 PR: ${prUrl}`));
581
+ }
582
+ catch (error) {
583
+ if (spinner && spinner.isSpinning) {
584
+ spinner.fail('Batch task execution failed');
585
+ }
586
+ console.error(chalk.red('❌ Failed to execute tasks with single PR:'), error);
587
+ throw error;
588
+ }
589
+ finally {
590
+ // Switch back to original directory and clean up worktree
591
+ if (this.gitManager && worktreePath && branchName) {
592
+ this.gitManager.switchToOriginalDir();
593
+ await this.gitManager.removeWorktree(branchName);
594
+ }
595
+ }
596
+ }
597
+ async executeNonInteractiveWorkflow(config) {
598
+ try {
599
+ console.log(chalk.blue.bold('🚀 Starting non-interactive workflow'));
600
+ console.log('');
601
+ console.log(chalk.blue('🔍 Validating dependencies...'));
602
+ await this.getClaudeExecutor().validateClaudeCodeInstallation();
603
+ console.log(chalk.green('✅ Claude Code SDK configured'));
604
+ this.workingDir = await this.repositoryManager.getValidWorkingDirectory();
605
+ this.gitManager = new GitManager(this.workingDir);
606
+ if (!this.gitManager) {
607
+ throw new Error('GitManager not initialized');
608
+ }
609
+ this.gitManager.validateGitHubCliInstallation();
610
+ console.log(chalk.green('✅ GitHub CLI is installed'));
611
+ if (!this.gitManager) {
612
+ throw new Error('GitManager not initialized');
613
+ }
614
+ this.gitManager.validateGitHubCliAuthentication();
615
+ console.log(chalk.green('✅ GitHub CLI is authenticated'));
616
+ const repoInfo = this.repositoryManager.getRepositoryInfo(this.workingDir);
617
+ console.log(chalk.blue(`📂 Working in: ${repoInfo.name} (${repoInfo.branch})`));
618
+ console.log('');
619
+ // Load repository-specific instructions
620
+ this.repoInstructions = await this.configManager.getRepoInstructions(this.workingDir);
621
+ if (this.repoInstructions) {
622
+ console.log(chalk.green('✅ Repository-specific instructions loaded'));
623
+ }
624
+ // Process tasks based on config
625
+ let finalTasks = config.tasks;
626
+ const prStrategy = config.prStrategy || 'multiple';
627
+ // If single task and generateSubtasks is true, break it down
628
+ if (config.tasks.length === 1 && config.generateSubtasks) {
629
+ console.log(chalk.blue('🔍 Generating subtasks...'));
630
+ finalTasks = await this.jobManager.generateTaskBreakdownWithClaude(config.tasks[0], this.workingDir);
631
+ console.log(chalk.green(`✅ Generated ${finalTasks.length} subtasks`));
632
+ }
633
+ // Create job and tasks
634
+ const jobDescription = config.tasks.length === 1 ? config.tasks[0] : `Multiple tasks: ${config.tasks.slice(0, 2).join(', ')}${config.tasks.length > 2 ? '...' : ''}`;
635
+ const jobUuid = await this.jobManager.createJob(jobDescription, this.workingDir);
636
+ const tasks = [];
637
+ for (const taskDescription of finalTasks) {
638
+ const taskUuid = await this.jobManager.createTask(jobUuid, taskDescription, 'build');
639
+ const task = await this.jobManager.getTask(taskUuid);
640
+ if (task) {
641
+ tasks.push(task);
642
+ }
643
+ }
644
+ console.log('');
645
+ console.log(chalk.blue.bold(`📋 Executing ${tasks.length} task(s)...`));
646
+ const shouldWaitForComments = config.waitForComments || false;
647
+ // Execute tasks based on PR strategy
648
+ if (prStrategy === 'single' && tasks.length > 1) {
649
+ await this.executeTasksWithSinglePR(tasks, false);
650
+ }
651
+ else {
652
+ for (const task of tasks) {
653
+ await this.executeTask(task);
654
+ }
655
+ }
656
+ console.log('');
657
+ console.log(chalk.green.bold('🎉 All initial tasks completed successfully!'));
658
+ // Collect PR URLs
659
+ const createdPRUrls = [];
660
+ for (const task of tasks) {
661
+ const updatedTask = await this.jobManager.getTask(task.uuid);
662
+ if (updatedTask && updatedTask.pr_link) {
663
+ createdPRUrls.push(updatedTask.pr_link);
664
+ }
665
+ }
666
+ if (createdPRUrls.length > 0) {
667
+ console.log('');
668
+ console.log(chalk.blue('📋 PRs created:'));
669
+ createdPRUrls.forEach(url => console.log(chalk.cyan(` - ${url}`)));
670
+ if (shouldWaitForComments) {
671
+ // Wait 30 minutes for reviewers to comment
672
+ console.log('');
673
+ console.log(chalk.blue('⏰ Waiting 30 minutes for PR reviews...'));
674
+ console.log(chalk.gray('PRs being monitored:'));
675
+ createdPRUrls.forEach(url => console.log(chalk.gray(` - ${url}`)));
676
+ console.log('');
677
+ const waitTime = 30 * 60 * 1000; // 30 minutes in milliseconds
678
+ const startTime = Date.now();
679
+ const interval = setInterval(() => {
680
+ const elapsed = Date.now() - startTime;
681
+ const remaining = waitTime - elapsed;
682
+ const minutes = Math.floor(remaining / 60000);
683
+ const seconds = Math.floor((remaining % 60000) / 1000);
684
+ process.stdout.write(`\r${chalk.blue('⏰')} Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')} `);
685
+ }, 1000);
686
+ await new Promise(resolve => setTimeout(resolve, waitTime));
687
+ clearInterval(interval);
688
+ console.log('\n');
689
+ // Check for unaddressed comments on created PRs
690
+ console.log(chalk.blue('🔍 Checking for PR review comments...'));
691
+ const prService = new PRService(this.workingDir);
692
+ const addressTasks = await this.checkAndCreateAddressTasks(createdPRUrls, prService);
693
+ if (addressTasks.length > 0) {
694
+ console.log('');
695
+ console.log(chalk.blue.bold(`📋 Found ${addressTasks.length} comments to address`));
696
+ // Execute address tasks automatically
697
+ const addressExecutor = new AddressTaskExecutor();
698
+ await addressExecutor.executeAddressTasks(addressTasks);
699
+ console.log('');
700
+ console.log(chalk.green.bold('🎉 All PR comments addressed successfully!'));
701
+ }
702
+ else {
703
+ console.log(chalk.green('✨ No unaddressed comments found on PRs!'));
704
+ }
705
+ }
706
+ }
707
+ }
708
+ catch (error) {
709
+ console.error(chalk.red.bold('❌ Non-interactive workflow failed:'), error);
710
+ throw error;
711
+ }
712
+ finally {
713
+ this.jobManager.close();
714
+ }
715
+ }
716
+ }
717
+ //# sourceMappingURL=task-executor.js.map