@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,736 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { execSync } from 'child_process';
4
+ import { JobManager } from './job-manager.js';
5
+ import { GitManager } from './git-manager.js';
6
+ import { ExecutorFactory } from './executor-factory.js';
7
+ import { OpenAIService } from './openai-service.js';
8
+ import { ConfigManager } from '../config.js';
9
+ import { PRService } from './pr-service.js';
10
+ export class AddressTaskExecutor {
11
+ jobManager;
12
+ gitManager = null;
13
+ claudeExecutor = null;
14
+ openaiService = null;
15
+ configManager;
16
+ prService = null;
17
+ workingDir;
18
+ repoInstructions;
19
+ constructor() {
20
+ this.jobManager = new JobManager();
21
+ this.configManager = new ConfigManager();
22
+ this.workingDir = '';
23
+ }
24
+ getClaudeExecutor() {
25
+ if (!this.claudeExecutor) {
26
+ this.claudeExecutor = ExecutorFactory.getExecutor();
27
+ }
28
+ return this.claudeExecutor;
29
+ }
30
+ getOpenAIService() {
31
+ if (!this.openaiService) {
32
+ this.openaiService = new OpenAIService();
33
+ }
34
+ return this.openaiService;
35
+ }
36
+ async executeAddressTasks(tasks) {
37
+ try {
38
+ console.log(chalk.blue.bold('🚀 Starting address task workflow'));
39
+ console.log('');
40
+ // Validate dependencies
41
+ await this.getClaudeExecutor().validateClaudeCodeInstallation();
42
+ console.log(chalk.green('✅ Claude Code SDK configured'));
43
+ // Get working directory from first task's job
44
+ const db = this.jobManager['dbManager'].getKysely();
45
+ const job = await db
46
+ .selectFrom('jobs')
47
+ .selectAll()
48
+ .where('uuid', '=', tasks[0].job_uuid)
49
+ .executeTakeFirst();
50
+ if (!job) {
51
+ throw new Error('Job not found');
52
+ }
53
+ this.workingDir = job.directory;
54
+ this.gitManager = new GitManager(this.workingDir);
55
+ this.prService = new PRService(this.workingDir);
56
+ this.gitManager.validateGitHubCliInstallation();
57
+ console.log(chalk.green('✅ GitHub CLI is installed'));
58
+ this.gitManager.validateGitHubCliAuthentication();
59
+ console.log(chalk.green('✅ GitHub CLI is authenticated'));
60
+ // Load repository instructions
61
+ this.repoInstructions = await this.configManager.getRepoInstructions(this.workingDir);
62
+ // Group tasks by branch
63
+ const tasksByBranch = new Map();
64
+ for (const task of tasks) {
65
+ const branch = task.branch || 'unknown';
66
+ if (!tasksByBranch.has(branch)) {
67
+ tasksByBranch.set(branch, []);
68
+ }
69
+ const tasks = tasksByBranch.get(branch);
70
+ if (tasks) {
71
+ tasks.push(task);
72
+ }
73
+ }
74
+ // Execute tasks grouped by branch
75
+ for (const [branch, branchTasks] of tasksByBranch) {
76
+ if (branch === 'unknown') {
77
+ console.log(chalk.yellow('⚠️ Skipping tasks without branch information'));
78
+ continue;
79
+ }
80
+ console.log('');
81
+ console.log(chalk.cyan.bold(`🔄 Creating worktree for branch: ${branch}`));
82
+ let spinner = ora('Creating worktree...').start();
83
+ let worktreePath = null;
84
+ try {
85
+ // Create worktree for the branch
86
+ if (!this.gitManager) {
87
+ throw new Error('GitManager not initialized');
88
+ }
89
+ worktreePath = await this.gitManager.createWorktree(branch);
90
+ this.gitManager.switchToWorktree(worktreePath);
91
+ spinner.succeed(`Worktree created: ${worktreePath}`);
92
+ }
93
+ catch (error) {
94
+ spinner.fail(`Failed to create worktree for branch: ${branch}`);
95
+ console.error(error);
96
+ continue;
97
+ }
98
+ // Handle lint_and_test tasks separately
99
+ const lintAndTestTasks = branchTasks.filter(t => t.type === 'lint_and_test');
100
+ const addressTasks = branchTasks.filter(t => t.type === 'address');
101
+ // Process lint_and_test tasks first
102
+ for (const task of lintAndTestTasks) {
103
+ console.log('');
104
+ console.log(chalk.blue('🔧 Fixing test and lint failures'));
105
+ await this.jobManager.updateTaskStatus(task.uuid, 'active');
106
+ // Extract PR number from task description
107
+ const prNumberMatch = task.description.match(/PR #(\d+)/);
108
+ const taskPrNumber = prNumberMatch ? parseInt(prNumberMatch[1]) : null;
109
+ spinner = ora('Fetching GitHub Actions logs...').start();
110
+ let actionLogs = '';
111
+ if (taskPrNumber && this.prService) {
112
+ try {
113
+ actionLogs = await this.prService.getFailingActionLogs(taskPrNumber);
114
+ if (actionLogs) {
115
+ spinner.succeed('GitHub Actions logs fetched');
116
+ }
117
+ else {
118
+ spinner.info('No failing action logs found');
119
+ }
120
+ }
121
+ catch (error) {
122
+ spinner.warn('Could not fetch GitHub Actions logs');
123
+ console.error(error);
124
+ }
125
+ }
126
+ spinner = ora('Running Claude Code to fix test and lint failures...').start();
127
+ // Prepare the prompt for Claude
128
+ let prompt = 'Fix the failing tests and linting issues in this PR.\n\n';
129
+ prompt += 'The following GitHub Actions checks are failing:\n';
130
+ prompt += task.description.replace(/^Fix test and lint failures in PR #\d+: /, '') + '\n\n';
131
+ // Include the actual failing logs if available
132
+ if (actionLogs) {
133
+ prompt += '=== GitHub Actions Failure Logs ===\n';
134
+ prompt += actionLogs;
135
+ prompt += '\n=== End of Logs ===\n\n';
136
+ }
137
+ prompt += 'Please run the tests and linting locally, identify what is failing based on the logs above, and fix all issues.';
138
+ prompt += ' Make sure to run the tests again after fixing to verify they pass.';
139
+ if (this.repoInstructions) {
140
+ prompt += `\n\nRepository-specific instructions:\n${this.repoInstructions}`;
141
+ }
142
+ try {
143
+ const result = await this.getClaudeExecutor().executeTask(prompt, worktreePath || this.workingDir);
144
+ spinner.succeed('Claude Code execution completed');
145
+ await this.jobManager.updateTaskExecutionLog(task.uuid, result.log);
146
+ if (!this.gitManager) {
147
+ throw new Error('GitManager not initialized');
148
+ }
149
+ const changedFiles = this.gitManager.getChangedFiles();
150
+ if (changedFiles.length === 0) {
151
+ console.log(chalk.yellow('⚠️ No changes made'));
152
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
153
+ continue;
154
+ }
155
+ // Create commit with co-author
156
+ spinner = ora('Creating commit...').start();
157
+ const commitMessage = 'Fix test and lint failures\n\nCo-authored-by: ivan-agent <ivan-agent@users.noreply.github.com>';
158
+ if (!this.gitManager) {
159
+ throw new Error('GitManager not initialized');
160
+ }
161
+ // Try to commit, handling pre-commit hook failures
162
+ const commitResult = await this.tryCommitWithFixes(commitMessage, task, worktreePath || this.workingDir, spinner);
163
+ if (commitResult.succeeded) {
164
+ spinner.succeed('Changes committed');
165
+ }
166
+ else {
167
+ spinner.fail('Failed to commit after multiple attempts');
168
+ throw new Error('Pre-commit hook failures could not be fixed');
169
+ }
170
+ // Get the commit hash
171
+ const commitHash = execSync('git rev-parse HEAD', {
172
+ cwd: worktreePath || this.workingDir,
173
+ encoding: 'utf-8'
174
+ }).trim();
175
+ // Save commit hash to task
176
+ await this.jobManager.updateTaskCommit(task.uuid, commitHash);
177
+ // Push the commit immediately
178
+ spinner = ora('Pushing commit...').start();
179
+ try {
180
+ if (!this.gitManager) {
181
+ throw new Error('GitManager not initialized');
182
+ }
183
+ await this.gitManager.pushBranch(branch);
184
+ spinner.succeed('Commit pushed successfully');
185
+ }
186
+ catch (error) {
187
+ spinner.fail('Failed to push commit');
188
+ console.error(error);
189
+ }
190
+ // Add review comment for lint_and_test task
191
+ if (taskPrNumber) {
192
+ spinner = ora('Adding review request comment...').start();
193
+ try {
194
+ const reviewComment = '@codex please review the test and lint fixes that were applied to address the failing CI checks';
195
+ execSync(`gh pr comment ${taskPrNumber} --body "${reviewComment}"`, {
196
+ cwd: worktreePath || this.workingDir,
197
+ stdio: 'pipe'
198
+ });
199
+ spinner.succeed('Review request comment added');
200
+ }
201
+ catch (error) {
202
+ spinner.fail('Failed to add review comment');
203
+ console.error(error);
204
+ }
205
+ }
206
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
207
+ }
208
+ catch (error) {
209
+ spinner.fail('Failed to fix test and lint failures');
210
+ console.error(error);
211
+ const errorLog = error instanceof Error ? error.message : String(error);
212
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `ERROR: ${errorLog}`);
213
+ await this.jobManager.updateTaskStatus(task.uuid, 'not_started');
214
+ }
215
+ }
216
+ // Get PR number from branch name or task description
217
+ const prMatch = branchTasks[0].description.match(/PR #(\d+)/);
218
+ if (!prMatch && addressTasks.length > 0) {
219
+ console.log(chalk.yellow('⚠️ Could not extract PR number from task'));
220
+ continue;
221
+ }
222
+ const prNumber = prMatch ? prMatch[1] : null;
223
+ // Skip comment processing if there are no address tasks
224
+ if (addressTasks.length === 0) {
225
+ continue;
226
+ }
227
+ // Get all unaddressed comments for this PR
228
+ spinner = ora('Fetching PR comments...').start();
229
+ if (!prNumber) {
230
+ throw new Error('PR number not found');
231
+ }
232
+ const comments = await this.getUnaddressedComments(parseInt(prNumber));
233
+ spinner.succeed(`Found ${comments.length} unaddressed comments`);
234
+ if (comments.length === 0 && addressTasks.some(t => t.description.includes('comment'))) {
235
+ console.log(chalk.yellow('⚠️ No unaddressed comments found'));
236
+ continue;
237
+ }
238
+ // Process each comment
239
+ for (const comment of comments) {
240
+ console.log('');
241
+ console.log(chalk.blue(`📝 Addressing comment from @${comment.author}:`));
242
+ console.log(chalk.gray(` "${comment.body.substring(0, 100)}${comment.body.length > 100 ? '...' : ''}"`));
243
+ // Find the corresponding task
244
+ const task = addressTasks.find(t => t.description.includes(comment.author) &&
245
+ t.description.includes(comment.body.substring(0, 50)));
246
+ if (!task) {
247
+ console.log(chalk.yellow('⚠️ No task found for this comment'));
248
+ continue;
249
+ }
250
+ await this.jobManager.updateTaskStatus(task.uuid, 'active');
251
+ // Save comment URL
252
+ if (comment.id) {
253
+ const repoInfo = execSync('gh repo view --json owner,name', {
254
+ cwd: worktreePath || this.workingDir,
255
+ encoding: 'utf-8'
256
+ });
257
+ const { owner, name: repoName } = JSON.parse(repoInfo);
258
+ if (!prNumber) {
259
+ throw new Error('PR number not found');
260
+ }
261
+ const commentUrl = `https://github.com/${owner.login}/${repoName}/pull/${prNumber}#discussion_r${comment.id}`;
262
+ await this.jobManager.updateTaskCommentUrl(task.uuid, commentUrl);
263
+ }
264
+ spinner = ora('Running Claude Code to address comment...').start();
265
+ // Prepare the prompt for Claude
266
+ let prompt = 'Address the following PR review comment:\n\n';
267
+ prompt += `Comment from @${comment.author}:\n"${comment.body}"\n\n`;
268
+ if (comment.path) {
269
+ prompt += `File: ${comment.path}`;
270
+ if (comment.line) {
271
+ prompt += ` (line ${comment.line})`;
272
+ }
273
+ prompt += '\n\n';
274
+ }
275
+ prompt += 'Please make the necessary changes to address this comment.';
276
+ if (this.repoInstructions) {
277
+ prompt += `\n\nRepository-specific instructions:\n${this.repoInstructions}`;
278
+ }
279
+ try {
280
+ const result = await this.getClaudeExecutor().executeTask(prompt, worktreePath || this.workingDir);
281
+ spinner.succeed('Claude Code execution completed');
282
+ await this.jobManager.updateTaskExecutionLog(task.uuid, result.log);
283
+ // Use the last message from Claude's response
284
+ const lastMessage = result.lastMessage;
285
+ if (!this.gitManager) {
286
+ throw new Error('GitManager not initialized');
287
+ }
288
+ const changedFiles = this.gitManager.getChangedFiles();
289
+ if (changedFiles.length === 0) {
290
+ console.log(chalk.yellow('⚠️ No changes made - Claude determined no changes were needed'));
291
+ // Reply to the comment explaining why no changes were made
292
+ spinner = ora('Replying to comment...').start();
293
+ try {
294
+ // Truncate the message if it's too long (GitHub has a 65536 character limit)
295
+ const maxLength = 60000;
296
+ let replyBody = `Ivan: ${lastMessage || 'After reviewing, no code changes were necessary to address this comment.'}`;
297
+ if (replyBody.length > maxLength) {
298
+ replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)';
299
+ }
300
+ // Use a temporary file to avoid shell escaping issues
301
+ const { writeFileSync, unlinkSync } = await import('fs');
302
+ const { join } = await import('path');
303
+ const { tmpdir } = await import('os');
304
+ const tempFile = join(tmpdir(), `ivan-comment-${Date.now()}.txt`);
305
+ writeFileSync(tempFile, replyBody);
306
+ try {
307
+ // Use GraphQL mutation to add a reply to the review thread
308
+ const repoInfo = execSync('gh repo view --json owner,name', {
309
+ cwd: worktreePath || this.workingDir,
310
+ encoding: 'utf-8'
311
+ });
312
+ const { owner, name: repoName } = JSON.parse(repoInfo);
313
+ // Get the review thread ID from the comment
314
+ const threadQuery = `
315
+ query {
316
+ repository(owner: "${owner.login}", name: "${repoName}") {
317
+ pullRequest(number: ${prNumber}) {
318
+ reviewThreads(first: 100) {
319
+ nodes {
320
+ id
321
+ comments(first: 100) {
322
+ nodes {
323
+ databaseId
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+ `;
332
+ const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
333
+ cwd: worktreePath || this.workingDir,
334
+ encoding: 'utf-8'
335
+ });
336
+ const threadData = JSON.parse(threadResult);
337
+ const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
338
+ // Find the thread containing this comment
339
+ let threadId = null;
340
+ for (const thread of threads) {
341
+ const comments = thread.comments?.nodes || [];
342
+ if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
343
+ threadId = thread.id;
344
+ break;
345
+ }
346
+ }
347
+ if (!threadId) {
348
+ throw new Error('Could not find review thread for comment');
349
+ }
350
+ // Add reply using GraphQL mutation
351
+ const mutation = `
352
+ mutation {
353
+ addPullRequestReviewThreadReply(input: {
354
+ pullRequestReviewThreadId: "${threadId}"
355
+ body: ${JSON.stringify(replyBody)}
356
+ }) {
357
+ comment {
358
+ id
359
+ }
360
+ }
361
+ }
362
+ `;
363
+ execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
364
+ cwd: worktreePath || this.workingDir,
365
+ stdio: 'pipe'
366
+ });
367
+ spinner.succeed('Reply added to comment');
368
+ }
369
+ finally {
370
+ unlinkSync(tempFile);
371
+ }
372
+ }
373
+ catch (error) {
374
+ spinner.fail('Failed to reply to comment');
375
+ console.error(error);
376
+ }
377
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
378
+ continue;
379
+ }
380
+ // Create commit with co-author
381
+ spinner = ora('Creating commit...').start();
382
+ const commitMessage = `Address review comment from @${comment.author}
383
+
384
+ ${comment.body.substring(0, 200)}${comment.body.length > 200 ? '...' : ''}
385
+
386
+ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
387
+ if (!this.gitManager) {
388
+ throw new Error('GitManager not initialized');
389
+ }
390
+ // Try to commit, handling pre-commit hook failures
391
+ const commitResult = await this.tryCommitWithFixes(commitMessage, task, worktreePath || this.workingDir, spinner);
392
+ if (commitResult.succeeded) {
393
+ spinner.succeed('Changes committed');
394
+ }
395
+ else {
396
+ spinner.fail('Failed to commit after multiple attempts');
397
+ throw new Error('Pre-commit hook failures could not be fixed');
398
+ }
399
+ // Get the commit hash
400
+ const commitHash = execSync('git rev-parse HEAD', {
401
+ cwd: worktreePath || this.workingDir,
402
+ encoding: 'utf-8'
403
+ }).trim();
404
+ // Save commit hash to task
405
+ await this.jobManager.updateTaskCommit(task.uuid, commitHash);
406
+ // Push the commit immediately
407
+ spinner = ora('Pushing commit...').start();
408
+ try {
409
+ if (!this.gitManager) {
410
+ throw new Error('GitManager not initialized');
411
+ }
412
+ await this.gitManager.pushBranch(branch);
413
+ spinner.succeed('Commit pushed successfully');
414
+ }
415
+ catch (error) {
416
+ spinner.fail('Failed to push commit');
417
+ console.error(error);
418
+ }
419
+ // Reply to the comment with the fix
420
+ spinner = ora('Replying to comment...').start();
421
+ try {
422
+ // Truncate the message if it's too long (GitHub has a 65536 character limit)
423
+ const maxLength = 60000;
424
+ let replyBody = lastMessage
425
+ ? `Ivan: ${lastMessage}\n\nThis has been addressed in commit ${commitHash.substring(0, 7)}`
426
+ : `Ivan: This has been addressed in commit ${commitHash.substring(0, 7)}`;
427
+ if (replyBody.length > maxLength) {
428
+ replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)\n\n' +
429
+ `This has been addressed in commit ${commitHash.substring(0, 7)}`;
430
+ }
431
+ // Use GraphQL mutation to add a reply to the review thread
432
+ const repoInfo = execSync('gh repo view --json owner,name', {
433
+ cwd: worktreePath || this.workingDir,
434
+ encoding: 'utf-8'
435
+ });
436
+ const { owner, name: repoName } = JSON.parse(repoInfo);
437
+ // Get the review thread ID from the comment
438
+ const threadQuery = `
439
+ query {
440
+ repository(owner: "${owner.login}", name: "${repoName}") {
441
+ pullRequest(number: ${prNumber}) {
442
+ reviewThreads(first: 100) {
443
+ nodes {
444
+ id
445
+ comments(first: 100) {
446
+ nodes {
447
+ databaseId
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ `;
456
+ const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
457
+ cwd: worktreePath || this.workingDir,
458
+ encoding: 'utf-8'
459
+ });
460
+ const threadData = JSON.parse(threadResult);
461
+ const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
462
+ // Find the thread containing this comment
463
+ let threadId = null;
464
+ for (const thread of threads) {
465
+ const comments = thread.comments?.nodes || [];
466
+ if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
467
+ threadId = thread.id;
468
+ break;
469
+ }
470
+ }
471
+ if (!threadId) {
472
+ throw new Error('Could not find review thread for comment');
473
+ }
474
+ // Add reply using GraphQL mutation
475
+ const mutation = `
476
+ mutation {
477
+ addPullRequestReviewThreadReply(input: {
478
+ pullRequestReviewThreadId: "${threadId}"
479
+ body: ${JSON.stringify(replyBody)}
480
+ }) {
481
+ comment {
482
+ id
483
+ }
484
+ }
485
+ }
486
+ `;
487
+ execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
488
+ cwd: worktreePath || this.workingDir,
489
+ stdio: 'pipe'
490
+ });
491
+ spinner.succeed('Reply added to comment');
492
+ }
493
+ catch (error) {
494
+ spinner.fail('Failed to reply to comment');
495
+ console.error(error);
496
+ }
497
+ await this.jobManager.updateTaskStatus(task.uuid, 'completed');
498
+ }
499
+ catch (error) {
500
+ spinner.fail('Failed to address comment');
501
+ console.error(error);
502
+ const errorLog = error instanceof Error ? error.message : String(error);
503
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `ERROR: ${errorLog}`);
504
+ await this.jobManager.updateTaskStatus(task.uuid, 'not_started');
505
+ }
506
+ }
507
+ // Generate and add specific review comment (only if we have a PR number and made changes)
508
+ if (prNumber && (addressTasks.length > 0 || lintAndTestTasks.length > 0)) {
509
+ spinner = ora('Generating review request...').start();
510
+ try {
511
+ // Get the latest commit changes
512
+ const latestCommit = execSync('git rev-parse HEAD', {
513
+ cwd: worktreePath || this.workingDir,
514
+ encoding: 'utf-8'
515
+ }).trim();
516
+ const commitDiff = execSync(`git show ${latestCommit} --format="" --unified=3`, {
517
+ cwd: worktreePath || this.workingDir,
518
+ encoding: 'utf-8'
519
+ });
520
+ const changedFiles = execSync(`git show --name-only --format="" ${latestCommit}`, {
521
+ cwd: worktreePath || this.workingDir,
522
+ encoding: 'utf-8'
523
+ }).trim().split('\n').filter(Boolean);
524
+ // Generate specific review instructions using OpenAI
525
+ const reviewInstructions = await this.generateReviewInstructions(commitDiff, changedFiles, parseInt(prNumber));
526
+ spinner.succeed('Review request generated');
527
+ // Add the review comment
528
+ spinner = ora('Adding review request comment...').start();
529
+ const reviewComment = `@codex ${reviewInstructions}`;
530
+ execSync(`gh pr comment ${prNumber} --body "${reviewComment.replace(/"/g, '\\"')}"`, {
531
+ cwd: worktreePath || this.workingDir,
532
+ stdio: 'pipe'
533
+ });
534
+ spinner.succeed('Review request comment added');
535
+ }
536
+ catch (error) {
537
+ spinner.fail('Failed to add review comment');
538
+ console.error(error);
539
+ }
540
+ }
541
+ // Clean up worktree after processing branch
542
+ if (this.gitManager && worktreePath) {
543
+ try {
544
+ this.gitManager.switchToOriginalDir();
545
+ await this.gitManager.removeWorktree(branch);
546
+ }
547
+ catch (error) {
548
+ console.log(chalk.yellow(`⚠️ Could not clean up worktree: ${error}`));
549
+ }
550
+ }
551
+ }
552
+ console.log('');
553
+ console.log(chalk.green.bold('🎉 All address tasks completed!'));
554
+ }
555
+ catch (error) {
556
+ console.error(chalk.red.bold('❌ Address workflow failed:'), error);
557
+ throw error;
558
+ }
559
+ }
560
+ async getUnaddressedComments(prNumber) {
561
+ try {
562
+ // Get PR owner and repo name
563
+ const repoInfo = execSync('gh repo view --json owner,name', {
564
+ cwd: this.gitManager?.['originalWorkingDir'] || this.workingDir,
565
+ encoding: 'utf-8'
566
+ });
567
+ const { owner, name: repoName } = JSON.parse(repoInfo);
568
+ // Use GraphQL to get review threads with resolved status
569
+ const graphqlQuery = `
570
+ query {
571
+ repository(owner: "${owner.login}", name: "${repoName}") {
572
+ pullRequest(number: ${prNumber}) {
573
+ reviewThreads(first: 100) {
574
+ nodes {
575
+ isResolved
576
+ comments(first: 100) {
577
+ nodes {
578
+ id
579
+ databaseId
580
+ body
581
+ author {
582
+ login
583
+ }
584
+ createdAt
585
+ path
586
+ line
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }
594
+ `;
595
+ const graphqlResult = execSync(`gh api graphql -f query='${graphqlQuery}'`, {
596
+ cwd: this.gitManager?.['originalWorkingDir'] || this.workingDir,
597
+ encoding: 'utf-8'
598
+ });
599
+ const result = JSON.parse(graphqlResult);
600
+ const threads = result.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
601
+ const unaddressedComments = [];
602
+ // Process each thread
603
+ for (const thread of threads) {
604
+ // Skip resolved threads
605
+ if (thread.isResolved) {
606
+ continue;
607
+ }
608
+ const comments = thread.comments?.nodes || [];
609
+ if (comments.length === 0) {
610
+ continue;
611
+ }
612
+ // Get the first comment (the main review comment)
613
+ const firstComment = comments[0];
614
+ // Check if there are replies (more than one comment in thread)
615
+ const hasReplies = comments.length > 1;
616
+ if (!hasReplies && firstComment.path) {
617
+ // Only include if it's an inline code comment (has a path) and has no replies
618
+ unaddressedComments.push({
619
+ id: firstComment.databaseId ? firstComment.databaseId.toString() : firstComment.id,
620
+ author: firstComment.author.login,
621
+ body: firstComment.body,
622
+ createdAt: firstComment.createdAt,
623
+ path: firstComment.path,
624
+ line: firstComment.line
625
+ });
626
+ }
627
+ }
628
+ return unaddressedComments;
629
+ }
630
+ catch (error) {
631
+ console.error('Error fetching comments:', error);
632
+ return [];
633
+ }
634
+ }
635
+ async generateReviewInstructions(diff, changedFiles, _prNumber) {
636
+ try {
637
+ const openaiService = this.getOpenAIService();
638
+ const client = await openaiService.getClient();
639
+ const prompt = `You are reviewing code changes that were made to address PR review comments.
640
+ Based on the following diff and changed files, generate a concise, specific review request that tells the reviewer what to focus on.
641
+
642
+ Changed files:
643
+ ${changedFiles.join('\n')}
644
+
645
+ Diff (last commit):
646
+ ${diff.substring(0, 8000)}${diff.length > 8000 ? '\n... (diff truncated)' : ''}
647
+
648
+ Generate a brief (1-2 sentences) review request that:
649
+ 1. Mentions the key changes made
650
+ 2. Asks the reviewer to verify specific aspects that were addressed
651
+ 3. Is conversational and clear
652
+
653
+ Example format: "please review the updates to the reflection service integration and verify that the null checks properly handle missing configuration objects"
654
+
655
+ Return ONLY the review request text, without any prefix like "Please review" since @codex will already be prepended.`;
656
+ const completion = await client.chat.completions.create({
657
+ model: 'gpt-4o-mini',
658
+ messages: [
659
+ {
660
+ role: 'system',
661
+ content: 'You are a helpful assistant that generates specific code review requests based on git diffs.'
662
+ },
663
+ {
664
+ role: 'user',
665
+ content: prompt
666
+ }
667
+ ],
668
+ temperature: 0.3,
669
+ max_tokens: 150
670
+ });
671
+ const reviewRequest = completion.choices[0]?.message?.content?.trim();
672
+ if (!reviewRequest) {
673
+ return 'please review the latest changes and verify all review comments have been properly addressed';
674
+ }
675
+ return reviewRequest;
676
+ }
677
+ catch (error) {
678
+ console.error('Error generating review instructions:', error);
679
+ // Fallback to a generic message if OpenAI fails
680
+ return 'please review the latest changes and verify all review comments have been properly addressed';
681
+ }
682
+ }
683
+ async tryCommitWithFixes(commitMessage, task, workingDir, spinner) {
684
+ let commitAttempts = 0;
685
+ const maxCommitAttempts = 3;
686
+ let commitSucceeded = false;
687
+ while (commitAttempts < maxCommitAttempts && !commitSucceeded) {
688
+ try {
689
+ if (!this.gitManager) {
690
+ throw new Error('GitManager not initialized');
691
+ }
692
+ await this.gitManager.commitChanges(commitMessage);
693
+ commitSucceeded = true;
694
+ // Stop the spinner if we're retrying
695
+ if (commitAttempts > 0 && spinner.isSpinning) {
696
+ spinner.succeed('Commit successful after retry');
697
+ }
698
+ }
699
+ catch (commitError) {
700
+ commitAttempts++;
701
+ const errorMessage = commitError instanceof Error ? commitError.message : String(commitError);
702
+ // Check if this is a pre-commit hook failure
703
+ if (errorMessage.includes('pre-commit') && commitAttempts < maxCommitAttempts) {
704
+ spinner.fail(`Pre-commit hook failed (attempt ${commitAttempts}/${maxCommitAttempts})`);
705
+ console.log(chalk.yellow('🔧 Running Claude to fix pre-commit errors...'));
706
+ // Extract the error details from the commit error
707
+ const errorDetails = errorMessage;
708
+ // Prepare prompt for Claude to fix the errors
709
+ 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.`;
710
+ spinner = ora('Running Claude to fix pre-commit errors...').start();
711
+ try {
712
+ // Run Claude to fix the errors
713
+ const fixResult = await this.getClaudeExecutor().executeTask(fixPrompt, workingDir);
714
+ spinner.succeed('Claude attempted to fix the errors');
715
+ // Update the execution log with the fix attempt
716
+ const previousLog = await this.jobManager.getTaskExecutionLog(task.uuid);
717
+ await this.jobManager.updateTaskExecutionLog(task.uuid, `${previousLog}\n\n--- Pre-commit Fix Attempt ${commitAttempts} ---\n${fixResult.log}`);
718
+ // Try to commit again on the next iteration
719
+ spinner = ora('Retrying commit...').start();
720
+ }
721
+ catch (fixError) {
722
+ spinner.fail('Failed to run Claude to fix errors');
723
+ console.error(chalk.red('Claude fix attempt failed:'), fixError);
724
+ throw commitError; // Re-throw the original error
725
+ }
726
+ }
727
+ else {
728
+ // Not a pre-commit error or max attempts reached
729
+ throw commitError;
730
+ }
731
+ }
732
+ }
733
+ return { succeeded: commitSucceeded };
734
+ }
735
+ }
736
+ //# sourceMappingURL=address-task-executor.js.map