@link-assistant/hive-mind 0.39.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 (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Feedback detection module for solve.mjs
3
+ * Handles comment counting and feedback detection for continue mode
4
+ */
5
+
6
+ // Import Sentry integration
7
+ import { reportError } from './sentry.lib.mjs';
8
+
9
+ export const detectAndCountFeedback = async (params) => {
10
+ const {
11
+ prNumber,
12
+ branchName,
13
+ owner,
14
+ repo,
15
+ issueNumber,
16
+ isContinueMode,
17
+ argv,
18
+ mergeStateStatus,
19
+ prState,
20
+ workStartTime,
21
+ log,
22
+ formatAligned,
23
+ cleanErrorMessage,
24
+ $
25
+ } = params;
26
+
27
+ let newPrComments = 0;
28
+ let newIssueComments = 0;
29
+ let commentInfo = '';
30
+ let feedbackLines = [];
31
+ let currentUser = null;
32
+
33
+ // Get current GitHub user to filter out own comments
34
+ try {
35
+ const userResult = await $`gh api user --jq .login`;
36
+ if (userResult.code === 0) {
37
+ currentUser = userResult.stdout.toString().trim();
38
+ await log(formatAligned('👤', 'Current user:', currentUser, 2));
39
+ }
40
+ } catch (error) {
41
+ reportError(error, {
42
+ context: 'get_current_user',
43
+ operation: 'gh_api_user'
44
+ });
45
+ await log('Warning: Could not get current GitHub user', { level: 'warning' });
46
+ }
47
+
48
+ // Debug logging to understand when comment counting doesn't run
49
+ if (argv.verbose) {
50
+ await log('\n📊 Comment counting conditions:', { verbose: true });
51
+ await log(` prNumber: ${prNumber || 'NOT SET'}`, { verbose: true });
52
+ await log(` branchName: ${branchName || 'NOT SET'}`, { verbose: true });
53
+ await log(` isContinueMode: ${isContinueMode}`, { verbose: true });
54
+ await log(` Will count comments: ${!!(prNumber && branchName)}`, { verbose: true });
55
+ if (!prNumber) {
56
+ await log(' ⚠️ Skipping: prNumber not set', { verbose: true });
57
+ }
58
+ if (!branchName) {
59
+ await log(' ⚠️ Skipping: branchName not set', { verbose: true });
60
+ }
61
+ }
62
+
63
+ if (prNumber && branchName) {
64
+ try {
65
+ await log(`${formatAligned('💬', 'Counting comments:', 'Checking for new comments since last commit...')}`);
66
+ if (argv.verbose) {
67
+ await log(` PR #${prNumber} on branch: ${branchName}`, { verbose: true });
68
+ await log(` Owner/Repo: ${owner}/${repo}`, { verbose: true });
69
+ }
70
+
71
+ // Get the last commit timestamp from the PR branch
72
+ let lastCommitTime = null;
73
+ let lastCommitResult = await $`git log -1 --format="%aI" origin/${branchName}`;
74
+ if (lastCommitResult.code !== 0) {
75
+ // Fallback to local branch if remote doesn't exist
76
+ lastCommitResult = await $`git log -1 --format="%aI" ${branchName}`;
77
+ }
78
+
79
+ if (lastCommitResult.code === 0) {
80
+ lastCommitTime = new Date(lastCommitResult.stdout.toString().trim());
81
+ await log(formatAligned('📅', 'Last commit time:', lastCommitTime.toISOString(), 2));
82
+ } else {
83
+ // Fallback: Get last commit time from GitHub API
84
+ try {
85
+ const prCommitsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/commits --jq 'last.commit.author.date'`;
86
+ if (prCommitsResult.code === 0 && prCommitsResult.stdout) {
87
+ lastCommitTime = new Date(prCommitsResult.stdout.toString().trim());
88
+ await log(formatAligned('📅', 'Last commit time (from API):', lastCommitTime.toISOString(), 2));
89
+ }
90
+ } catch (error) {
91
+ reportError(error, {
92
+ context: 'get_last_commit_time',
93
+ prNumber,
94
+ operation: 'fetch_commit_timestamp'
95
+ });
96
+ await log(`Warning: Could not get last commit time: ${cleanErrorMessage(error)}`, { level: 'warning' });
97
+ }
98
+ }
99
+
100
+ // Only proceed if we have a last commit time
101
+ if (lastCommitTime) {
102
+
103
+ // Define log patterns to filter out comments containing logs from solve.mjs
104
+ const logPatterns = [
105
+ /📊.*Log file|solution\s+draft.*log/i,
106
+ /🔗.*Link:|💻.*Session:/i,
107
+ /Generated with.*solve\.mjs/i,
108
+ /Session ID:|Log file available:/i
109
+ ];
110
+
111
+ // Count new PR comments after last commit (both code review comments and conversation comments)
112
+ let prReviewComments = [];
113
+ let prConversationComments = [];
114
+
115
+ // Get PR code review comments
116
+ const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments`;
117
+ if (prReviewCommentsResult.code === 0) {
118
+ prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString());
119
+ }
120
+
121
+ // Get PR conversation comments (PR is also an issue)
122
+ const prConversationCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments`;
123
+ if (prConversationCommentsResult.code === 0) {
124
+ prConversationComments = JSON.parse(prConversationCommentsResult.stdout.toString());
125
+ }
126
+
127
+ // Combine and count all PR comments after last commit
128
+ // Filter out comments from current user if made after work started AND filter out log comments
129
+ const allPrComments = [...prReviewComments, ...prConversationComments];
130
+ const filteredPrComments = allPrComments.filter(comment => {
131
+ const commentTime = new Date(comment.created_at);
132
+ const isAfterCommit = commentTime > lastCommitTime;
133
+ const isNotLogPattern = !logPatterns.some(pattern => pattern.test(comment.body || ''));
134
+
135
+ // If we have a work start time and current user, filter out comments made by claude tool after work started
136
+ if (workStartTime && currentUser && comment.user && comment.user.login === currentUser) {
137
+ const isAfterWorkStart = commentTime > new Date(workStartTime);
138
+ if (isAfterWorkStart && argv.verbose) {
139
+ // Note: Filtering out own comment from user after work started
140
+ }
141
+ return isAfterCommit && !isAfterWorkStart && isNotLogPattern;
142
+ }
143
+
144
+ return isAfterCommit && isNotLogPattern;
145
+ });
146
+ newPrComments = filteredPrComments.length;
147
+
148
+ // Count new issue comments after last commit
149
+ const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments`;
150
+ if (issueCommentsResult.code === 0) {
151
+ const issueComments = JSON.parse(issueCommentsResult.stdout.toString());
152
+ const filteredIssueComments = issueComments.filter(comment => {
153
+ const commentTime = new Date(comment.created_at);
154
+ const isAfterCommit = commentTime > lastCommitTime;
155
+ const isNotLogPattern = !logPatterns.some(pattern => pattern.test(comment.body || ''));
156
+
157
+ // If we have a work start time and current user, filter out comments made by claude tool after work started
158
+ if (workStartTime && currentUser && comment.user && comment.user.login === currentUser) {
159
+ const isAfterWorkStart = commentTime > new Date(workStartTime);
160
+ if (isAfterWorkStart && argv.verbose) {
161
+ // Note: Filtering out own issue comment from user after work started
162
+ }
163
+ return isAfterCommit && !isAfterWorkStart && isNotLogPattern;
164
+ }
165
+
166
+ return isAfterCommit && isNotLogPattern;
167
+ });
168
+ newIssueComments = filteredIssueComments.length;
169
+ }
170
+
171
+ await log(formatAligned('💬', 'New PR comments:', newPrComments.toString(), 2));
172
+ await log(formatAligned('💬', 'New issue comments:', newIssueComments.toString(), 2));
173
+
174
+ if (argv.verbose) {
175
+ await log(` Total new comments: ${newPrComments + newIssueComments}`, { verbose: true });
176
+ await log(` Comment lines to add: ${newPrComments > 0 || newIssueComments > 0 ? 'Yes' : 'No (saving tokens)'}`, { verbose: true });
177
+ await log(` PR review comments fetched: ${prReviewComments.length}`, { verbose: true });
178
+ await log(` PR conversation comments fetched: ${prConversationComments.length}`, { verbose: true });
179
+ await log(` Total PR comments checked: ${allPrComments.length}`, { verbose: true });
180
+ }
181
+
182
+ // Check if --auto-continue-only-on-new-comments is enabled and fail if no new comments
183
+ if (argv.autoContinueOnlyOnNewComments && (isContinueMode || argv.autoContinue)) {
184
+ const totalNewComments = newPrComments + newIssueComments;
185
+ if (totalNewComments === 0) {
186
+ await log('❌ auto-continue-only-on-new-comments: No new comments found since last commit');
187
+ await log(' This option requires new comments to proceed with auto-continue or continue mode.');
188
+ process.exit(1);
189
+ } else {
190
+ await log(`✅ auto-continue-only-on-new-comments: Found ${totalNewComments} new comments, continuing...`);
191
+ }
192
+ }
193
+
194
+ // Build comprehensive feedback info for system prompt
195
+ feedbackLines = []; // Reset for this execution
196
+ let feedbackDetected = false;
197
+ const feedbackSources = [];
198
+
199
+ // Add comment info if counts are > 0 to avoid wasting tokens
200
+ if (newPrComments > 0) {
201
+ feedbackLines.push(`New comments on the pull request: ${newPrComments}`);
202
+ }
203
+ if (newIssueComments > 0) {
204
+ feedbackLines.push(`New comments on the issue: ${newIssueComments}`);
205
+ }
206
+
207
+ // Enhanced feedback detection for all continue modes
208
+ if (isContinueMode || argv.autoContinue) {
209
+ if (argv.continueOnlyOnFeedback) {
210
+ await log(`${formatAligned('🔍', 'Feedback detection:', 'Checking for any feedback since last commit...')}`);
211
+ }
212
+
213
+ // 1. Check for new comments (already filtered above)
214
+ const totalNewComments = newPrComments + newIssueComments;
215
+ if (totalNewComments > 0) {
216
+ feedbackDetected = true;
217
+ feedbackSources.push(`New comments (${totalNewComments})`);
218
+ }
219
+
220
+ // 2. Check for edited descriptions
221
+ // Issue #895: Filter out edits made during current work session to prevent
222
+ // infinite restart loops. When the agent updates the PR description as part of
223
+ // its work, this should not trigger a restart. Only external edits (before work
224
+ // started) should be considered feedback.
225
+ try {
226
+ // Check PR description edit time
227
+ const prDetailsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
228
+ if (prDetailsResult.code === 0) {
229
+ const prDetails = JSON.parse(prDetailsResult.stdout.toString());
230
+ const prUpdatedAt = new Date(prDetails.updated_at);
231
+ if (prUpdatedAt > lastCommitTime) {
232
+ // Issue #895: Check if the edit happened during current work session
233
+ // If the PR was updated after work started, it's likely the agent's own edit
234
+ if (workStartTime && prUpdatedAt > new Date(workStartTime)) {
235
+ if (argv.verbose) {
236
+ await log(' Note: PR description updated during current work session (likely by agent itself) - ignoring', { verbose: true });
237
+ }
238
+ // Don't treat this as external feedback
239
+ } else {
240
+ // The PR was updated after last commit but before work started - external feedback
241
+ feedbackLines.push('Pull request description was edited after last commit');
242
+ feedbackDetected = true;
243
+ feedbackSources.push('PR description edited');
244
+ }
245
+ }
246
+ }
247
+
248
+ // Check issue description edit time if we have an issue
249
+ if (issueNumber) {
250
+ const issueDetailsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}`;
251
+ if (issueDetailsResult.code === 0) {
252
+ const issueDetails = JSON.parse(issueDetailsResult.stdout.toString());
253
+ const issueUpdatedAt = new Date(issueDetails.updated_at);
254
+ if (issueUpdatedAt > lastCommitTime) {
255
+ // Issue #895: Check if the edit happened during current work session
256
+ if (workStartTime && issueUpdatedAt > new Date(workStartTime)) {
257
+ if (argv.verbose) {
258
+ await log(' Note: Issue description updated during current work session (likely by agent itself) - ignoring', { verbose: true });
259
+ }
260
+ // Don't treat this as external feedback
261
+ } else {
262
+ // The issue was updated after last commit but before work started - external feedback
263
+ feedbackLines.push('Issue description was edited after last commit');
264
+ feedbackDetected = true;
265
+ feedbackSources.push('Issue description edited');
266
+ }
267
+ }
268
+ }
269
+ }
270
+ } catch (error) {
271
+ reportError(error, {
272
+ context: 'check_description_edits',
273
+ prNumber,
274
+ operation: 'fetch_pr_timeline'
275
+ });
276
+ if (argv.verbose) {
277
+ await log(`Warning: Could not check description edit times: ${cleanErrorMessage(error)}`, { level: 'warning' });
278
+ }
279
+ }
280
+
281
+ // 3. Check for new commits on default branch
282
+ try {
283
+ const defaultBranchResult = await $`gh api repos/${owner}/${repo}`;
284
+ if (defaultBranchResult.code === 0) {
285
+ const repoData = JSON.parse(defaultBranchResult.stdout.toString());
286
+ const defaultBranch = repoData.default_branch;
287
+
288
+ const commitsResult = await $`gh api repos/${owner}/${repo}/commits --field sha=${defaultBranch} --field since=${lastCommitTime.toISOString()}`;
289
+ if (commitsResult.code === 0) {
290
+ const commits = JSON.parse(commitsResult.stdout.toString());
291
+ if (commits.length > 0) {
292
+ feedbackLines.push(`New commits on ${defaultBranch} branch: ${commits.length}`);
293
+ feedbackDetected = true;
294
+ feedbackSources.push(`New commits on ${defaultBranch} (${commits.length})`);
295
+ }
296
+ }
297
+ }
298
+ } catch (error) {
299
+ reportError(error, {
300
+ context: 'check_branch_commits',
301
+ branchName,
302
+ operation: 'fetch_commit_messages'
303
+ });
304
+ if (argv.verbose) {
305
+ await log(`Warning: Could not check default branch commits: ${cleanErrorMessage(error)}`, { level: 'warning' });
306
+ }
307
+ }
308
+
309
+ // 4. Check pull request state (non-open indicates closed or merged)
310
+ if (prState && prState !== 'OPEN') {
311
+ feedbackLines.push(`Pull request state: ${prState}`);
312
+ feedbackDetected = true;
313
+ feedbackSources.push(`PR state ${prState}`);
314
+ }
315
+
316
+ // 5. Check merge status (non-clean indicates issues with merging)
317
+ if (mergeStateStatus && mergeStateStatus !== 'CLEAN') {
318
+ const statusDescriptions = {
319
+ 'DIRTY': 'Merge status is DIRTY (conflicts detected)',
320
+ 'UNSTABLE': 'Merge status is UNSTABLE (non-passing commit status)',
321
+ 'BLOCKED': 'Merge status is BLOCKED',
322
+ 'BEHIND': 'Merge status is BEHIND (head ref is out of date)',
323
+ 'HAS_HOOKS': 'Merge status is HAS_HOOKS (has pre-receive hooks)',
324
+ 'UNKNOWN': 'Merge status is UNKNOWN'
325
+ };
326
+ const description = statusDescriptions[mergeStateStatus] || `Merge status is ${mergeStateStatus}`;
327
+ feedbackLines.push(description);
328
+ feedbackDetected = true;
329
+ feedbackSources.push(`Merge status ${mergeStateStatus}`);
330
+ }
331
+
332
+ // 6. Check for failed PR checks
333
+ try {
334
+ const checksResult = await $`gh api repos/${owner}/${repo}/commits/$(gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha')/check-runs`;
335
+ if (checksResult.code === 0) {
336
+ const checksData = JSON.parse(checksResult.stdout.toString());
337
+ const failedChecks = checksData.check_runs?.filter(check =>
338
+ check.conclusion === 'failure' && new Date(check.completed_at) > lastCommitTime
339
+ ) || [];
340
+
341
+ if (failedChecks.length > 0) {
342
+ feedbackLines.push(`Failed pull request checks: ${failedChecks.length}`);
343
+ feedbackDetected = true;
344
+ feedbackSources.push(`Failed PR checks (${failedChecks.length})`);
345
+ }
346
+ }
347
+ } catch (error) {
348
+ reportError(error, {
349
+ context: 'check_pr_status_checks',
350
+ prNumber,
351
+ operation: 'fetch_status_checks'
352
+ });
353
+ if (argv.verbose) {
354
+ await log(`Warning: Could not check PR status checks: ${cleanErrorMessage(error)}`, { level: 'warning' });
355
+ }
356
+ }
357
+
358
+ // 7. Check for review requests with changes requested
359
+ try {
360
+ const reviewsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
361
+ if (reviewsResult.code === 0) {
362
+ const reviews = JSON.parse(reviewsResult.stdout.toString());
363
+ const changesRequestedReviews = reviews.filter(review =>
364
+ review.state === 'CHANGES_REQUESTED' && new Date(review.submitted_at) > lastCommitTime
365
+ );
366
+
367
+ if (changesRequestedReviews.length > 0) {
368
+ feedbackLines.push(`Changes requested in reviews: ${changesRequestedReviews.length}`);
369
+ feedbackDetected = true;
370
+ feedbackSources.push(`Changes requested (${changesRequestedReviews.length})`);
371
+ }
372
+ }
373
+ } catch (error) {
374
+ reportError(error, {
375
+ context: 'check_pr_reviews',
376
+ prNumber,
377
+ operation: 'fetch_pr_reviews'
378
+ });
379
+ if (argv.verbose) {
380
+ await log(`Warning: Could not check PR reviews: ${cleanErrorMessage(error)}`, { level: 'warning' });
381
+ }
382
+ }
383
+
384
+ // Handle --continue-only-on-feedback option
385
+ if (argv.continueOnlyOnFeedback) {
386
+ if (feedbackDetected) {
387
+ await log('✅ continue-only-on-feedback: Feedback detected, continuing...');
388
+ await log(formatAligned('📋', 'Feedback sources:', feedbackSources.join(', '), 2));
389
+ } else {
390
+ await log('❌ continue-only-on-feedback: No feedback detected since last commit');
391
+ await log(' This option requires any of the following to proceed:');
392
+ await log(' • New comments (excluding solve.mjs logs)');
393
+ await log(' • Edited issue/PR descriptions');
394
+ await log(' • New commits on default branch');
395
+ await log(' • Pull request state is not OPEN (closed or merged)');
396
+ await log(' • Merge status is not CLEAN (conflicts, unstable, blocked, etc.)');
397
+ await log(' • Failed pull request checks');
398
+ await log(' • Changes requested via review');
399
+ process.exit(1);
400
+ }
401
+ }
402
+ }
403
+
404
+ if (feedbackLines.length > 0) {
405
+ commentInfo = '\n\n' + feedbackLines.join('\n') + '\n';
406
+ if (argv.verbose) {
407
+ await log(' Feedback info will be added to prompt:', { verbose: true });
408
+ feedbackLines.forEach(async line => {
409
+ await log(` - ${line}`, { verbose: true });
410
+ });
411
+ }
412
+ } else if (argv.verbose) {
413
+ await log(' No feedback info to add (0 new items, saving tokens)', { verbose: true });
414
+ }
415
+ } else {
416
+ await log('Warning: Could not determine last commit time, skipping comment counting', { level: 'warning' });
417
+ }
418
+ } catch (error) {
419
+ reportError(error, {
420
+ context: 'count_new_comments',
421
+ prNumber,
422
+ operation: 'detect_and_count_feedback'
423
+ });
424
+ await log(`Warning: Could not count new comments: ${cleanErrorMessage(error)}`, { level: 'warning' });
425
+ }
426
+ } else {
427
+ await log(formatAligned('⚠️', 'Skipping comment count:', prNumber ? 'branchName not set' : 'prNumber not set', 2));
428
+ if (argv.verbose) {
429
+ await log(` prNumber: ${prNumber || 'NOT SET'}`, { verbose: true });
430
+ await log(` branchName: ${branchName || 'NOT SET'}`, { verbose: true });
431
+ await log(' This means no new comment detection will run', { verbose: true });
432
+ }
433
+ }
434
+
435
+ return { newPrComments, newIssueComments, commentInfo, feedbackLines };
436
+ };