@ariso-ai/ivan 1.0.15 → 1.0.17

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.
@@ -8,11 +8,15 @@ export declare class AddressTaskExecutor {
8
8
  private prService;
9
9
  private workingDir;
10
10
  private repoInstructions;
11
+ private githubClient;
12
+ private repoOwner;
13
+ private repoName;
11
14
  constructor();
12
15
  private getClaudeExecutor;
13
16
  private getOpenAIService;
14
17
  executeAddressTasks(tasks: Task[], quiet?: boolean): Promise<void>;
15
18
  private getUnaddressedComments;
19
+ private findCommentId;
16
20
  private generateReviewInstructions;
17
21
  private tryCommitWithFixes;
18
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"address-task-executor.d.ts","sourceRoot":"","sources":["../../src/services/address-task-executor.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAItC,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAqB;;IAQ7C,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IAOlB,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YA8pBjE,sBAAsB;YAwGtB,0BAA0B;YAyD1B,kBAAkB;CAuEjC"}
1
+ {"version":3,"file":"address-task-executor.d.ts","sourceRoot":"","sources":["../../src/services/address-task-executor.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAKtC,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAc;;IAQ9B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IAOlB,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YA0tBjE,sBAAsB;YAoBtB,aAAa;YAyEb,0BAA0B;YAyD1B,kBAAkB;CAuEjC"}
@@ -6,6 +6,7 @@ import { ExecutorFactory } from './executor-factory.js';
6
6
  import { OpenAIService } from './openai-service.js';
7
7
  import { ConfigManager } from '../config.js';
8
8
  import { createGitManager, createPRService } from './service-factory.js';
9
+ import { GitHubAPIClient } from './github-api-client.js';
9
10
  export class AddressTaskExecutor {
10
11
  jobManager;
11
12
  gitManager = null;
@@ -15,6 +16,9 @@ export class AddressTaskExecutor {
15
16
  prService = null;
16
17
  workingDir;
17
18
  repoInstructions;
19
+ githubClient = null;
20
+ repoOwner = '';
21
+ repoName = '';
18
22
  constructor() {
19
23
  this.jobManager = new JobManager();
20
24
  this.configManager = new ConfigManager();
@@ -61,6 +65,33 @@ export class AddressTaskExecutor {
61
65
  this.gitManager.validateGitHubCliAuthentication();
62
66
  if (!quiet)
63
67
  console.log(chalk.green('✅ GitHub CLI is authenticated'));
68
+ // Initialize GitHub API client if using PAT
69
+ const authType = this.configManager.getGithubAuthType();
70
+ if (authType === 'pat') {
71
+ const pat = this.configManager.getGithubPat();
72
+ if (pat) {
73
+ this.githubClient = new GitHubAPIClient(pat);
74
+ const repoInfo = GitHubAPIClient.getRepoInfoFromRemote(this.workingDir);
75
+ this.repoOwner = repoInfo.owner;
76
+ this.repoName = repoInfo.repo;
77
+ }
78
+ }
79
+ // If not using PAT, get repo info from gh command
80
+ if (!this.githubClient) {
81
+ try {
82
+ const repoInfo = execSync('gh repo view --json owner,name', {
83
+ cwd: this.workingDir,
84
+ encoding: 'utf-8'
85
+ });
86
+ const parsed = JSON.parse(repoInfo);
87
+ this.repoOwner = parsed.owner.login;
88
+ this.repoName = parsed.name;
89
+ }
90
+ catch {
91
+ if (!quiet)
92
+ console.log(chalk.yellow('⚠️ Could not get repository info'));
93
+ }
94
+ }
64
95
  // Load repository instructions
65
96
  this.repoInstructions = await this.configManager.getRepoInstructions(this.workingDir);
66
97
  // Group tasks by branch
@@ -221,10 +252,16 @@ export class AddressTaskExecutor {
221
252
  try {
222
253
  const reviewAgent = this.configManager.getReviewAgent();
223
254
  const reviewComment = `${reviewAgent} please review the test and lint fixes that were applied to address the failing CI checks`;
224
- execSync(`gh pr comment ${taskPrNumber} --body "${reviewComment}"`, {
225
- cwd: worktreePath || this.workingDir,
226
- stdio: 'pipe'
227
- });
255
+ // Use GitHub API client if available, otherwise fall back to gh command
256
+ if (this.githubClient && this.repoOwner && this.repoName) {
257
+ await this.githubClient.addPRComment(this.repoOwner, this.repoName, taskPrNumber, reviewComment);
258
+ }
259
+ else {
260
+ execSync(`gh pr comment ${taskPrNumber} --body "${reviewComment}"`, {
261
+ cwd: worktreePath || this.workingDir,
262
+ stdio: 'pipe'
263
+ });
264
+ }
228
265
  if (spinner)
229
266
  spinner.succeed('Review request comment added');
230
267
  }
@@ -258,47 +295,49 @@ export class AddressTaskExecutor {
258
295
  if (addressTasks.length === 0) {
259
296
  continue;
260
297
  }
261
- // Get all unaddressed comments for this PR
262
- if (!quiet)
263
- spinner = ora('Fetching PR comments...').start();
264
- if (!prNumber) {
265
- throw new Error('PR number not found');
266
- }
267
- const comments = await this.getUnaddressedComments(parseInt(prNumber));
268
- if (spinner)
269
- spinner.succeed(`Found ${comments.length} unaddressed comments`);
270
- if (comments.length === 0 && addressTasks.some(t => t.description.includes('comment'))) {
271
- if (!quiet)
272
- console.log(chalk.yellow('⚠️ No unaddressed comments found'));
273
- continue;
274
- }
275
- // Process each comment
276
- for (const comment of comments) {
277
- // In quiet mode, output the comment being addressed
278
- console.log('');
279
- console.log(chalk.blue(`📝 Addressing comment from @${comment.author}:`));
280
- console.log(chalk.gray(` "${comment.body.substring(0, 100)}${comment.body.length > 100 ? '...' : ''}"`));
281
- // Find the corresponding task
282
- const task = addressTasks.find(t => t.description.includes(comment.author) &&
283
- t.description.includes(comment.body.substring(0, 50)));
284
- if (!task) {
298
+ // Process each address task directly (no need to re-fetch comments from GitHub)
299
+ for (const task of addressTasks) {
300
+ // Skip tasks that aren't comment-related
301
+ if (!task.description.includes('comment from @')) {
302
+ continue;
303
+ }
304
+ // Parse comment information from task description
305
+ // Format: "Address PR #123 comment from @author: "body" (in path:line)"
306
+ const authorMatch = task.description.match(/comment from @(\w+)/);
307
+ const bodyMatch = task.description.match(/: "(.+?)"/);
308
+ const pathMatch = task.description.match(/\(in (.+?)(:\d+)?\)$/);
309
+ const lineMatch = task.description.match(/:(\d+)\)$/);
310
+ if (!authorMatch || !bodyMatch) {
285
311
  if (!quiet)
286
- console.log(chalk.yellow('⚠️ No task found for this comment'));
312
+ console.log(chalk.yellow(`⚠️ Could not parse comment from task: ${task.description}`));
287
313
  continue;
288
314
  }
315
+ const comment = {
316
+ author: authorMatch[1],
317
+ body: bodyMatch[1],
318
+ path: pathMatch ? pathMatch[1] : undefined,
319
+ line: lineMatch ? parseInt(lineMatch[1]) : undefined,
320
+ id: '' // We'll fetch this if needed for replying
321
+ };
322
+ // Output what we're addressing
323
+ console.log('');
324
+ console.log(chalk.blue(`📝 Addressing comment from @${comment.author}:`));
325
+ console.log(chalk.gray(` "${comment.body}"`));
326
+ if (comment.path) {
327
+ console.log(chalk.gray(` File: ${comment.path}${comment.line ? `:${comment.line}` : ''}`));
328
+ }
289
329
  await this.jobManager.updateTaskStatus(task.uuid, 'active');
290
- // Save comment URL
291
- if (comment.id) {
292
- const repoInfo = execSync('gh repo view --json owner,name', {
293
- cwd: worktreePath || this.workingDir,
294
- encoding: 'utf-8'
295
- });
296
- const { owner, name: repoName } = JSON.parse(repoInfo);
297
- if (!prNumber) {
298
- throw new Error('PR number not found');
330
+ // Fetch the comment ID from GitHub for replying later
331
+ if (prNumber) {
332
+ const commentId = await this.findCommentId(parseInt(prNumber), comment.author, comment.body.substring(0, 50), comment.path, comment.line);
333
+ if (commentId) {
334
+ comment.id = commentId;
335
+ // Save comment URL
336
+ if (this.repoOwner && this.repoName) {
337
+ const commentUrl = `https://github.com/${this.repoOwner}/${this.repoName}/pull/${prNumber}#discussion_r${commentId}`;
338
+ await this.jobManager.updateTaskCommentUrl(task.uuid, commentUrl);
339
+ }
299
340
  }
300
- const commentUrl = `https://github.com/${owner.login}/${repoName}/pull/${prNumber}#discussion_r${comment.id}`;
301
- await this.jobManager.updateTaskCommentUrl(task.uuid, commentUrl);
302
341
  }
303
342
  if (!quiet)
304
343
  spinner = ora('Running Claude Code to address comment...').start();
@@ -343,78 +382,82 @@ export class AddressTaskExecutor {
343
382
  if (replyBody.length > maxLength) {
344
383
  replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)';
345
384
  }
346
- // Use a temporary file to avoid shell escaping issues
347
- const { writeFileSync, unlinkSync } = await import('fs');
348
- const { join } = await import('path');
349
- const { tmpdir } = await import('os');
350
- const tempFile = join(tmpdir(), `ivan-comment-${Date.now()}.txt`);
351
- writeFileSync(tempFile, replyBody);
352
- try {
353
- // Use GraphQL mutation to add a reply to the review thread
354
- const repoInfo = execSync('gh repo view --json owner,name', {
355
- cwd: worktreePath || this.workingDir,
356
- encoding: 'utf-8'
357
- });
358
- const { owner, name: repoName } = JSON.parse(repoInfo);
359
- // Get the review thread ID from the comment
360
- const threadQuery = `
361
- query {
362
- repository(owner: "${owner.login}", name: "${repoName}") {
363
- pullRequest(number: ${prNumber}) {
364
- reviewThreads(first: 100) {
365
- nodes {
366
- id
367
- comments(first: 100) {
368
- nodes {
369
- databaseId
385
+ // Use GitHub API client if available, otherwise fall back to gh command
386
+ if (this.githubClient && this.repoOwner && this.repoName && prNumber) {
387
+ await this.githubClient.addReviewThreadReply(this.repoOwner, this.repoName, parseInt(prNumber), comment.id, replyBody);
388
+ if (spinner)
389
+ spinner.succeed('Reply added to comment');
390
+ }
391
+ else {
392
+ // Fallback to gh command
393
+ const { writeFileSync, unlinkSync } = await import('fs');
394
+ const { join } = await import('path');
395
+ const { tmpdir } = await import('os');
396
+ const tempFile = join(tmpdir(), `ivan-comment-${Date.now()}.txt`);
397
+ writeFileSync(tempFile, replyBody);
398
+ try {
399
+ const repoInfo = execSync('gh repo view --json owner,name', {
400
+ cwd: worktreePath || this.workingDir,
401
+ encoding: 'utf-8'
402
+ });
403
+ const { owner, name: repoName } = JSON.parse(repoInfo);
404
+ const threadQuery = `
405
+ query {
406
+ repository(owner: "${owner.login}", name: "${repoName}") {
407
+ pullRequest(number: ${prNumber}) {
408
+ reviewThreads(first: 100) {
409
+ nodes {
410
+ id
411
+ comments(first: 100) {
412
+ nodes {
413
+ databaseId
414
+ }
370
415
  }
371
416
  }
372
417
  }
373
418
  }
374
419
  }
375
420
  }
376
- }
377
- `;
378
- const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
379
- cwd: worktreePath || this.workingDir,
380
- encoding: 'utf-8'
381
- });
382
- const threadData = JSON.parse(threadResult);
383
- const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
384
- // Find the thread containing this comment
385
- let threadId = null;
386
- for (const thread of threads) {
387
- const comments = thread.comments?.nodes || [];
388
- if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
389
- threadId = thread.id;
390
- break;
421
+ `;
422
+ const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
423
+ cwd: worktreePath || this.workingDir,
424
+ encoding: 'utf-8'
425
+ });
426
+ const threadData = JSON.parse(threadResult);
427
+ const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
428
+ let threadId = null;
429
+ for (const thread of threads) {
430
+ const comments = thread.comments?.nodes || [];
431
+ if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
432
+ threadId = thread.id;
433
+ break;
434
+ }
391
435
  }
392
- }
393
- if (!threadId) {
394
- throw new Error('Could not find review thread for comment');
395
- }
396
- // Add reply using GraphQL mutation
397
- const mutation = `
398
- mutation {
399
- addPullRequestReviewThreadReply(input: {
400
- pullRequestReviewThreadId: "${threadId}"
401
- body: ${JSON.stringify(replyBody)}
402
- }) {
403
- comment {
404
- id
436
+ if (!threadId) {
437
+ throw new Error('Could not find review thread for comment');
438
+ }
439
+ const mutation = `
440
+ mutation {
441
+ addPullRequestReviewThreadReply(input: {
442
+ pullRequestReviewThreadId: "${threadId}"
443
+ body: ${JSON.stringify(replyBody)}
444
+ }) {
445
+ comment {
446
+ id
447
+ }
405
448
  }
406
449
  }
407
- }
408
- `;
409
- execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
410
- cwd: worktreePath || this.workingDir,
411
- stdio: 'pipe'
412
- });
413
- if (spinner)
414
- spinner.succeed('Reply added to comment');
415
- }
416
- finally {
417
- unlinkSync(tempFile);
450
+ `;
451
+ execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
452
+ cwd: worktreePath || this.workingDir,
453
+ stdio: 'pipe'
454
+ });
455
+ if (spinner)
456
+ spinner.succeed('Reply added to comment');
457
+ }
458
+ finally {
459
+ unlinkSync(tempFile);
460
+ }
418
461
  }
419
462
  }
420
463
  catch (error) {
@@ -485,68 +528,73 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
485
528
  replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)\n\n' +
486
529
  `This has been addressed in commit ${commitHash.substring(0, 7)}`;
487
530
  }
488
- // Use GraphQL mutation to add a reply to the review thread
489
- const repoInfo = execSync('gh repo view --json owner,name', {
490
- cwd: worktreePath || this.workingDir,
491
- encoding: 'utf-8'
492
- });
493
- const { owner, name: repoName } = JSON.parse(repoInfo);
494
- // Get the review thread ID from the comment
495
- const threadQuery = `
496
- query {
497
- repository(owner: "${owner.login}", name: "${repoName}") {
498
- pullRequest(number: ${prNumber}) {
499
- reviewThreads(first: 100) {
500
- nodes {
501
- id
502
- comments(first: 100) {
503
- nodes {
504
- databaseId
531
+ // Use GitHub API client if available, otherwise fall back to gh command
532
+ if (this.githubClient && this.repoOwner && this.repoName && prNumber) {
533
+ await this.githubClient.addReviewThreadReply(this.repoOwner, this.repoName, parseInt(prNumber), comment.id, replyBody);
534
+ if (spinner)
535
+ spinner.succeed('Reply added to comment');
536
+ }
537
+ else {
538
+ // Fallback to gh command
539
+ const repoInfo = execSync('gh repo view --json owner,name', {
540
+ cwd: worktreePath || this.workingDir,
541
+ encoding: 'utf-8'
542
+ });
543
+ const { owner, name: repoName } = JSON.parse(repoInfo);
544
+ const threadQuery = `
545
+ query {
546
+ repository(owner: "${owner.login}", name: "${repoName}") {
547
+ pullRequest(number: ${prNumber}) {
548
+ reviewThreads(first: 100) {
549
+ nodes {
550
+ id
551
+ comments(first: 100) {
552
+ nodes {
553
+ databaseId
554
+ }
505
555
  }
506
556
  }
507
557
  }
508
558
  }
509
559
  }
510
560
  }
511
- }
512
- `;
513
- const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
514
- cwd: worktreePath || this.workingDir,
515
- encoding: 'utf-8'
516
- });
517
- const threadData = JSON.parse(threadResult);
518
- const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
519
- // Find the thread containing this comment
520
- let threadId = null;
521
- for (const thread of threads) {
522
- const comments = thread.comments?.nodes || [];
523
- if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
524
- threadId = thread.id;
525
- break;
561
+ `;
562
+ const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
563
+ cwd: worktreePath || this.workingDir,
564
+ encoding: 'utf-8'
565
+ });
566
+ const threadData = JSON.parse(threadResult);
567
+ const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
568
+ let threadId = null;
569
+ for (const thread of threads) {
570
+ const comments = thread.comments?.nodes || [];
571
+ if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
572
+ threadId = thread.id;
573
+ break;
574
+ }
526
575
  }
527
- }
528
- if (!threadId) {
529
- throw new Error('Could not find review thread for comment');
530
- }
531
- // Add reply using GraphQL mutation
532
- const mutation = `
533
- mutation {
534
- addPullRequestReviewThreadReply(input: {
535
- pullRequestReviewThreadId: "${threadId}"
536
- body: ${JSON.stringify(replyBody)}
537
- }) {
538
- comment {
539
- id
576
+ if (!threadId) {
577
+ throw new Error('Could not find review thread for comment');
578
+ }
579
+ const mutation = `
580
+ mutation {
581
+ addPullRequestReviewThreadReply(input: {
582
+ pullRequestReviewThreadId: "${threadId}"
583
+ body: ${JSON.stringify(replyBody)}
584
+ }) {
585
+ comment {
586
+ id
587
+ }
540
588
  }
541
589
  }
542
- }
543
- `;
544
- execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
545
- cwd: worktreePath || this.workingDir,
546
- stdio: 'pipe'
547
- });
548
- if (spinner)
549
- spinner.succeed('Reply added to comment');
590
+ `;
591
+ execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
592
+ cwd: worktreePath || this.workingDir,
593
+ stdio: 'pipe'
594
+ });
595
+ if (spinner)
596
+ spinner.succeed('Reply added to comment');
597
+ }
550
598
  }
551
599
  catch (error) {
552
600
  if (spinner)
@@ -593,10 +641,16 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
593
641
  spinner = ora('Adding review request comment...').start();
594
642
  const reviewAgent = this.configManager.getReviewAgent();
595
643
  const reviewComment = `${reviewAgent} ${reviewInstructions}`;
596
- execSync(`gh pr comment ${prNumber} --body "${reviewComment.replace(/"/g, '\\"')}"`, {
597
- cwd: worktreePath || this.workingDir,
598
- stdio: 'pipe'
599
- });
644
+ // Use GitHub API client if available, otherwise fall back to gh command
645
+ if (this.githubClient && this.repoOwner && this.repoName) {
646
+ await this.githubClient.addPRComment(this.repoOwner, this.repoName, parseInt(prNumber), reviewComment);
647
+ }
648
+ else {
649
+ execSync(`gh pr comment ${prNumber} --body "${reviewComment.replace(/"/g, '\\"')}"`, {
650
+ cwd: worktreePath || this.workingDir,
651
+ stdio: 'pipe'
652
+ });
653
+ }
600
654
  if (spinner)
601
655
  spinner.succeed('Review request comment added');
602
656
  }
@@ -631,30 +685,46 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
631
685
  }
632
686
  }
633
687
  async getUnaddressedComments(prNumber) {
688
+ if (!this.prService) {
689
+ throw new Error('PR service not initialized');
690
+ }
634
691
  try {
635
- // Get PR owner and repo name
636
- const repoInfo = execSync('gh repo view --json owner,name', {
637
- cwd: this.workingDir,
638
- encoding: 'utf-8'
639
- });
640
- const { owner, name: repoName } = JSON.parse(repoInfo);
641
- // Use GraphQL to get review threads with resolved status
692
+ return await this.prService.getUnaddressedComments(prNumber);
693
+ }
694
+ catch (error) {
695
+ console.error('Error fetching comments:', error);
696
+ return [];
697
+ }
698
+ }
699
+ async findCommentId(prNumber, author, bodySnippet, path, line) {
700
+ // Fetch ALL comments (including resolved ones) to find the matching comment
701
+ try {
702
+ // Get repo info if not already set
703
+ let owner = this.repoOwner;
704
+ let repoName = this.repoName;
705
+ if (!owner || !repoName) {
706
+ const repoInfo = execSync('gh repo view --json owner,name', {
707
+ cwd: this.workingDir,
708
+ encoding: 'utf-8'
709
+ });
710
+ const parsed = JSON.parse(repoInfo);
711
+ owner = parsed.owner.login;
712
+ repoName = parsed.name;
713
+ }
714
+ // Use GraphQL to get ALL review threads, not just unresolved ones
642
715
  const graphqlQuery = `
643
716
  query {
644
- repository(owner: "${owner.login}", name: "${repoName}") {
717
+ repository(owner: "${owner}", name: "${repoName}") {
645
718
  pullRequest(number: ${prNumber}) {
646
719
  reviewThreads(first: 100) {
647
720
  nodes {
648
- isResolved
649
721
  comments(first: 100) {
650
722
  nodes {
651
- id
652
723
  databaseId
653
724
  body
654
725
  author {
655
726
  login
656
727
  }
657
- createdAt
658
728
  path
659
729
  line
660
730
  }
@@ -665,44 +735,29 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
665
735
  }
666
736
  }
667
737
  `;
668
- const graphqlResult = execSync(`gh api graphql -f query='${graphqlQuery}'`, {
738
+ const graphqlResult = execSync(`gh api graphql -f query='${graphqlQuery.replace(/'/g, "'\\''")}'`, {
669
739
  cwd: this.workingDir,
670
740
  encoding: 'utf-8'
671
741
  });
672
742
  const result = JSON.parse(graphqlResult);
673
743
  const threads = result.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
674
- const unaddressedComments = [];
675
- // Process each thread
744
+ // Search through all comments in all threads
676
745
  for (const thread of threads) {
677
- // Skip resolved threads
678
- if (thread.isResolved) {
679
- continue;
680
- }
681
746
  const comments = thread.comments?.nodes || [];
682
- if (comments.length === 0) {
683
- continue;
684
- }
685
- // Get the first comment (the main review comment)
686
- const firstComment = comments[0];
687
- // Check if there are replies (more than one comment in thread)
688
- const hasReplies = comments.length > 1;
689
- if (!hasReplies && firstComment.path) {
690
- // Only include if it's an inline code comment (has a path) and has no replies
691
- unaddressedComments.push({
692
- id: firstComment.databaseId ? firstComment.databaseId.toString() : firstComment.id,
693
- author: firstComment.author.login,
694
- body: firstComment.body,
695
- createdAt: firstComment.createdAt,
696
- path: firstComment.path,
697
- line: firstComment.line
698
- });
747
+ for (const comment of comments) {
748
+ if (comment.author?.login === author &&
749
+ comment.body.includes(bodySnippet) &&
750
+ (!path || comment.path === path) &&
751
+ (!line || comment.line === line)) {
752
+ return comment.databaseId ? comment.databaseId.toString() : null;
753
+ }
699
754
  }
700
755
  }
701
- return unaddressedComments;
756
+ return null;
702
757
  }
703
758
  catch (error) {
704
- console.error('Error fetching comments:', error);
705
- return [];
759
+ console.error('Error finding comment ID:', error);
760
+ return null;
706
761
  }
707
762
  }
708
763
  async generateReviewInstructions(diff, changedFiles, _prNumber) {