@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- package/src/youtrack/youtrack.lib.mjs +425 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
4
|
+
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
5
|
+
|
|
6
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
7
|
+
const { $ } = await use('command-stream');
|
|
8
|
+
|
|
9
|
+
const yargs = (await use('yargs@latest')).default;
|
|
10
|
+
const path = (await use('path')).default;
|
|
11
|
+
const fs = (await use('fs')).promises;
|
|
12
|
+
|
|
13
|
+
// Global log file reference
|
|
14
|
+
let logFile = null;
|
|
15
|
+
|
|
16
|
+
// Helper function to log to both console and file
|
|
17
|
+
const log = async (message, options = {}) => {
|
|
18
|
+
const { level = 'info', verbose = false } = options;
|
|
19
|
+
|
|
20
|
+
// Skip verbose logs unless --verbose is enabled
|
|
21
|
+
if (verbose && !global.verboseMode) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Write to file if log file is set
|
|
26
|
+
if (logFile) {
|
|
27
|
+
const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
|
|
28
|
+
await fs.appendFile(logFile, logMessage + '\n').catch(() => {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Write to console based on level
|
|
32
|
+
switch (level) {
|
|
33
|
+
case 'error':
|
|
34
|
+
console.error(message);
|
|
35
|
+
break;
|
|
36
|
+
case 'warning':
|
|
37
|
+
case 'warn':
|
|
38
|
+
console.warn(message);
|
|
39
|
+
break;
|
|
40
|
+
case 'info':
|
|
41
|
+
default:
|
|
42
|
+
console.log(message);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Configure command line arguments
|
|
48
|
+
const argv = yargs(process.argv.slice(2))
|
|
49
|
+
.usage('Usage: $0 <github-url> [options]')
|
|
50
|
+
.positional('github-url', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'GitHub organization, repository, or user URL to monitor for pull requests'
|
|
53
|
+
})
|
|
54
|
+
.option('review-label', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'GitHub label to identify PRs needing review',
|
|
57
|
+
default: 'needs-review',
|
|
58
|
+
alias: 'l'
|
|
59
|
+
})
|
|
60
|
+
.option('all-prs', {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
description: 'Review all open pull requests regardless of labels',
|
|
63
|
+
default: false,
|
|
64
|
+
alias: 'a'
|
|
65
|
+
})
|
|
66
|
+
.option('skip-draft', {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
description: 'Skip draft pull requests',
|
|
69
|
+
default: true,
|
|
70
|
+
alias: 'd'
|
|
71
|
+
})
|
|
72
|
+
.option('skip-approved', {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Skip pull requests that already have approvals',
|
|
75
|
+
default: true
|
|
76
|
+
})
|
|
77
|
+
.option('concurrency', {
|
|
78
|
+
type: 'number',
|
|
79
|
+
description: 'Number of concurrent review.mjs instances',
|
|
80
|
+
default: 2,
|
|
81
|
+
alias: 'c'
|
|
82
|
+
})
|
|
83
|
+
.option('reviews-per-pr', {
|
|
84
|
+
type: 'number',
|
|
85
|
+
description: 'Number of reviews to generate per PR (for diverse perspectives)',
|
|
86
|
+
default: 1,
|
|
87
|
+
alias: 'r'
|
|
88
|
+
})
|
|
89
|
+
.option('model', {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Model to use for review.mjs (opus or sonnet)',
|
|
92
|
+
alias: 'm',
|
|
93
|
+
default: 'opus',
|
|
94
|
+
choices: ['opus', 'sonnet']
|
|
95
|
+
})
|
|
96
|
+
.option('focus', {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Focus areas for reviews (security, performance, logic, style, tests, all)',
|
|
99
|
+
default: 'all',
|
|
100
|
+
alias: 'f'
|
|
101
|
+
})
|
|
102
|
+
.option('auto-approve', {
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
description: 'Auto-approve PRs that pass review criteria',
|
|
105
|
+
default: false
|
|
106
|
+
})
|
|
107
|
+
.option('interval', {
|
|
108
|
+
type: 'number',
|
|
109
|
+
description: 'Polling interval in seconds',
|
|
110
|
+
default: 300, // 5 minutes
|
|
111
|
+
alias: 'i'
|
|
112
|
+
})
|
|
113
|
+
.option('max-prs', {
|
|
114
|
+
type: 'number',
|
|
115
|
+
description: 'Maximum number of PRs to process (0 = unlimited)',
|
|
116
|
+
default: 0
|
|
117
|
+
})
|
|
118
|
+
.option('dry-run', {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
description: 'List PRs that would be reviewed without actually reviewing them',
|
|
121
|
+
default: false
|
|
122
|
+
})
|
|
123
|
+
.option('verbose', {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
description: 'Enable verbose logging',
|
|
126
|
+
alias: 'v',
|
|
127
|
+
default: false
|
|
128
|
+
})
|
|
129
|
+
.option('once', {
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
description: 'Run once and exit instead of continuous monitoring',
|
|
132
|
+
default: false
|
|
133
|
+
})
|
|
134
|
+
.demandCommand(1, 'GitHub URL is required')
|
|
135
|
+
.help('h')
|
|
136
|
+
.alias('h', 'help')
|
|
137
|
+
.argv;
|
|
138
|
+
|
|
139
|
+
const githubUrl = argv['github-url'] || argv._[0];
|
|
140
|
+
|
|
141
|
+
// Set global verbose mode
|
|
142
|
+
global.verboseMode = argv.verbose;
|
|
143
|
+
|
|
144
|
+
// Create log file with timestamp
|
|
145
|
+
const scriptDir = path.dirname(process.argv[1]);
|
|
146
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
147
|
+
logFile = path.join(scriptDir, `reviewers-hive-${timestamp}.log`);
|
|
148
|
+
|
|
149
|
+
// Create the log file immediately
|
|
150
|
+
await fs.writeFile(logFile, `# Reviewers-Hive.mjs Log - ${new Date().toISOString()}\n\n`);
|
|
151
|
+
await log(`š Log file: ${logFile}`);
|
|
152
|
+
await log(' (All output will be logged here)\n');
|
|
153
|
+
|
|
154
|
+
// Parse GitHub URL to determine organization, repository, or user
|
|
155
|
+
let scope = 'repository';
|
|
156
|
+
let owner = null;
|
|
157
|
+
let repo = null;
|
|
158
|
+
|
|
159
|
+
// Parse URL format: https://github.com/owner or https://github.com/owner/repo
|
|
160
|
+
const urlMatch = githubUrl.match(/^https:\/\/github\.com\/([^/]+)(\/([^/]+))?$/);
|
|
161
|
+
if (!urlMatch) {
|
|
162
|
+
await log('Error: Invalid GitHub URL format', { level: 'error' });
|
|
163
|
+
await log('Expected: https://github.com/owner or https://github.com/owner/repo', { level: 'error' });
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
owner = urlMatch[1];
|
|
168
|
+
repo = urlMatch[3] || null;
|
|
169
|
+
|
|
170
|
+
// Determine scope
|
|
171
|
+
if (!repo) {
|
|
172
|
+
// Check if it's an organization or user
|
|
173
|
+
try {
|
|
174
|
+
const typeResult = await $`gh api users/${owner} --jq .type`;
|
|
175
|
+
const accountType = typeResult.stdout.toString().trim();
|
|
176
|
+
scope = accountType === 'Organization' ? 'organization' : 'user';
|
|
177
|
+
} catch {
|
|
178
|
+
// Default to user if API call fails
|
|
179
|
+
scope = 'user';
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
scope = 'repository';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await log('šÆ PR Review Monitoring Configuration:');
|
|
186
|
+
await log(` š Target: ${scope.charAt(0).toUpperCase() + scope.slice(1)} - ${owner}${repo ? `/${repo}` : ''}`);
|
|
187
|
+
if (argv.allPrs) {
|
|
188
|
+
await log(' š·ļø Mode: ALL PULL REQUESTS (no label filter)');
|
|
189
|
+
} else {
|
|
190
|
+
await log(` š·ļø Label: "${argv.reviewLabel}"`);
|
|
191
|
+
}
|
|
192
|
+
if (argv.skipDraft) {
|
|
193
|
+
await log(' š« Skipping: Draft PRs');
|
|
194
|
+
}
|
|
195
|
+
if (argv.skipApproved) {
|
|
196
|
+
await log(' š« Skipping: Already approved PRs');
|
|
197
|
+
}
|
|
198
|
+
await log(` š Concurrency: ${argv.concurrency} parallel reviewers`);
|
|
199
|
+
await log(` š Reviews per PR: ${argv.reviewsPerPr}`);
|
|
200
|
+
await log(` š¤ Model: ${argv.model}`);
|
|
201
|
+
await log(` šÆ Focus: ${argv.focus}`);
|
|
202
|
+
if (argv.autoApprove) {
|
|
203
|
+
await log(' ā
Auto-approve: Enabled');
|
|
204
|
+
}
|
|
205
|
+
if (!argv.once) {
|
|
206
|
+
await log(` ā±ļø Polling Interval: ${argv.interval} seconds`);
|
|
207
|
+
}
|
|
208
|
+
await log(` ${argv.once ? 'š Mode: Single run' : 'ā¾ļø Mode: Continuous monitoring'}`);
|
|
209
|
+
if (argv.maxPrs > 0) {
|
|
210
|
+
await log(` š¢ Max PRs: ${argv.maxPrs}`);
|
|
211
|
+
}
|
|
212
|
+
if (argv.dryRun) {
|
|
213
|
+
await log(' š§Ŗ DRY RUN MODE - No actual reviewing');
|
|
214
|
+
}
|
|
215
|
+
await log('');
|
|
216
|
+
|
|
217
|
+
// Producer/Consumer Queue implementation for PRs
|
|
218
|
+
class PRQueue {
|
|
219
|
+
constructor() {
|
|
220
|
+
this.queue = [];
|
|
221
|
+
this.processing = new Set();
|
|
222
|
+
this.completed = new Set();
|
|
223
|
+
this.failed = new Set();
|
|
224
|
+
this.workers = [];
|
|
225
|
+
this.isRunning = true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Add PR to queue if not already processed or in queue
|
|
229
|
+
enqueue(prUrl) {
|
|
230
|
+
if (this.completed.has(prUrl) ||
|
|
231
|
+
this.processing.has(prUrl) ||
|
|
232
|
+
this.queue.includes(prUrl)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
this.queue.push(prUrl);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Get next PR from queue
|
|
240
|
+
dequeue() {
|
|
241
|
+
if (this.queue.length === 0) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const pr = this.queue.shift();
|
|
245
|
+
this.processing.add(pr);
|
|
246
|
+
return pr;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mark PR as completed
|
|
250
|
+
markCompleted(prUrl) {
|
|
251
|
+
this.processing.delete(prUrl);
|
|
252
|
+
this.completed.add(prUrl);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Mark PR as failed
|
|
256
|
+
markFailed(prUrl) {
|
|
257
|
+
this.processing.delete(prUrl);
|
|
258
|
+
this.failed.add(prUrl);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Get queue statistics
|
|
262
|
+
getStats() {
|
|
263
|
+
return {
|
|
264
|
+
queued: this.queue.length,
|
|
265
|
+
processing: this.processing.size,
|
|
266
|
+
completed: this.completed.size,
|
|
267
|
+
failed: this.failed.size
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Stop all workers
|
|
272
|
+
stop() {
|
|
273
|
+
this.isRunning = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create global queue instance
|
|
278
|
+
const prQueue = new PRQueue();
|
|
279
|
+
|
|
280
|
+
// Worker function to review PRs from queue
|
|
281
|
+
async function reviewer(reviewerId) {
|
|
282
|
+
await log(`š Reviewer ${reviewerId} started`, { verbose: true });
|
|
283
|
+
|
|
284
|
+
while (prQueue.isRunning) {
|
|
285
|
+
const prUrl = prQueue.dequeue();
|
|
286
|
+
|
|
287
|
+
if (!prUrl) {
|
|
288
|
+
// No work available, wait a bit
|
|
289
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await log(`\nš Reviewer ${reviewerId} reviewing: ${prUrl}`);
|
|
294
|
+
|
|
295
|
+
// Review the PR multiple times if needed (for diverse perspectives)
|
|
296
|
+
for (let reviewNum = 1; reviewNum <= argv.reviewsPerPr; reviewNum++) {
|
|
297
|
+
if (argv.reviewsPerPr > 1) {
|
|
298
|
+
await log(` š Creating review ${reviewNum}/${argv.reviewsPerPr} for PR`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
if (argv.dryRun) {
|
|
303
|
+
await log(` š§Ŗ [DRY RUN] Would execute: ./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}${argv.autoApprove ? ' --approve' : ''}`);
|
|
304
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work
|
|
305
|
+
} else {
|
|
306
|
+
// Execute review.mjs using command-stream
|
|
307
|
+
await log(` š Executing review.mjs for ${prUrl}...`);
|
|
308
|
+
|
|
309
|
+
const startTime = Date.now();
|
|
310
|
+
let reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}`;
|
|
311
|
+
|
|
312
|
+
if (argv.autoApprove) {
|
|
313
|
+
reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus} --approve`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Stream output and capture result
|
|
317
|
+
let exitCode = 0;
|
|
318
|
+
for await (const chunk of reviewCommand.stream()) {
|
|
319
|
+
if (chunk.type === 'stdout') {
|
|
320
|
+
const output = chunk.data.toString().trim();
|
|
321
|
+
if (output) {
|
|
322
|
+
await log(` [review.mjs] ${output}`, { verbose: true });
|
|
323
|
+
}
|
|
324
|
+
} else if (chunk.type === 'stderr') {
|
|
325
|
+
const error = chunk.data.toString().trim();
|
|
326
|
+
if (error) {
|
|
327
|
+
await log(` [review.mjs ERROR] ${error}`, { level: 'error', verbose: true });
|
|
328
|
+
}
|
|
329
|
+
} else if (chunk.type === 'exit') {
|
|
330
|
+
exitCode = chunk.code;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
335
|
+
|
|
336
|
+
if (exitCode === 0) {
|
|
337
|
+
await log(` ā
Reviewer ${reviewerId} completed ${prUrl} (${duration}s)`);
|
|
338
|
+
} else {
|
|
339
|
+
throw new Error(`review.mjs exited with code ${exitCode}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Small delay between multiple reviews for same PR
|
|
344
|
+
if (reviewNum < argv.reviewsPerPr) {
|
|
345
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
await log(` ā Reviewer ${reviewerId} failed on ${prUrl}: ${error.message}`, { level: 'error' });
|
|
349
|
+
prQueue.markFailed(prUrl);
|
|
350
|
+
break; // Stop trying more reviews for this PR
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
prQueue.markCompleted(prUrl);
|
|
355
|
+
|
|
356
|
+
// Show queue stats
|
|
357
|
+
const stats = prQueue.getStats();
|
|
358
|
+
await log(` š Queue: ${stats.queued} waiting, ${stats.processing} reviewing, ${stats.completed} completed, ${stats.failed} failed`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await log(`š Reviewer ${reviewerId} stopped`, { verbose: true });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Function to check if a PR already has approvals
|
|
365
|
+
async function hasApprovals(prUrl) {
|
|
366
|
+
try {
|
|
367
|
+
const { exec } = await import('child_process');
|
|
368
|
+
const { promisify } = await import('util');
|
|
369
|
+
const execAsync = promisify(exec);
|
|
370
|
+
|
|
371
|
+
// Extract owner, repo, and PR number from URL
|
|
372
|
+
const urlMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
373
|
+
if (!urlMatch) return false;
|
|
374
|
+
|
|
375
|
+
const [, prOwner, prRepo, prNumber] = urlMatch;
|
|
376
|
+
|
|
377
|
+
// Check for reviews using GitHub API
|
|
378
|
+
const cmd = `gh api repos/${prOwner}/${prRepo}/pulls/${prNumber}/reviews --jq '[.[] | select(.state == "APPROVED")] | length'`;
|
|
379
|
+
|
|
380
|
+
const { stdout } = await execAsync(cmd, { encoding: 'utf8', env: process.env });
|
|
381
|
+
const approvalCount = parseInt(stdout.trim()) || 0;
|
|
382
|
+
|
|
383
|
+
if (approvalCount > 0) {
|
|
384
|
+
await log(` ā³ Skipping (has ${approvalCount} approval${approvalCount > 1 ? 's' : ''})`, { verbose: true });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return false;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
// If we can't check, assume no approvals
|
|
391
|
+
await log(` ā³ Could not check for approvals: ${error.message.split('\n')[0]}`, { verbose: true });
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Function to fetch pull requests from GitHub
|
|
397
|
+
async function fetchPullRequests() {
|
|
398
|
+
if (argv.allPrs) {
|
|
399
|
+
await log('\nš Fetching ALL open pull requests...');
|
|
400
|
+
} else {
|
|
401
|
+
await log(`\nš Fetching pull requests with label "${argv.reviewLabel}"...`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
let prs = [];
|
|
406
|
+
|
|
407
|
+
if (argv.allPrs) {
|
|
408
|
+
// Fetch all open PRs without label filter
|
|
409
|
+
let searchCmd;
|
|
410
|
+
if (scope === 'repository') {
|
|
411
|
+
searchCmd = `gh pr list --repo ${owner}/${repo} --state open --limit 100 --json url,title,number,isDraft`;
|
|
412
|
+
} else if (scope === 'organization') {
|
|
413
|
+
searchCmd = `gh search prs org:${owner} is:open --limit 100 --json url,title,number,repository,isDraft`;
|
|
414
|
+
} else {
|
|
415
|
+
// User scope
|
|
416
|
+
searchCmd = `gh search prs user:${owner} is:open --limit 100 --json url,title,number,repository,isDraft`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await log(` š Command: ${searchCmd}`, { verbose: true });
|
|
420
|
+
|
|
421
|
+
// Use async exec to avoid escaping issues
|
|
422
|
+
const { exec } = await import('child_process');
|
|
423
|
+
const { promisify } = await import('util');
|
|
424
|
+
const execAsync = promisify(exec);
|
|
425
|
+
const { stdout } = await execAsync(searchCmd, { encoding: 'utf8', env: process.env });
|
|
426
|
+
prs = JSON.parse(stdout || '[]');
|
|
427
|
+
|
|
428
|
+
} else {
|
|
429
|
+
// Use label filter
|
|
430
|
+
const { exec } = await import('child_process');
|
|
431
|
+
const { promisify } = await import('util');
|
|
432
|
+
const execAsync = promisify(exec);
|
|
433
|
+
|
|
434
|
+
// For repositories, use gh pr list which works better
|
|
435
|
+
if (scope === 'repository') {
|
|
436
|
+
const listCmd = `gh pr list --repo ${owner}/${repo} --state open --label "${argv.reviewLabel}" --limit 100 --json url,title,number,isDraft`;
|
|
437
|
+
await log(` š Command: ${listCmd}`, { verbose: true });
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const { stdout } = await execAsync(listCmd, { encoding: 'utf8', env: process.env });
|
|
441
|
+
prs = JSON.parse(stdout || '[]');
|
|
442
|
+
} catch (listError) {
|
|
443
|
+
await log(` ā ļø List failed: ${listError.message.split('\n')[0]}`, { verbose: true });
|
|
444
|
+
prs = [];
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
// For organizations and users, use search
|
|
448
|
+
let baseQuery;
|
|
449
|
+
if (scope === 'organization') {
|
|
450
|
+
baseQuery = `org:${owner} is:pr is:open`;
|
|
451
|
+
} else {
|
|
452
|
+
baseQuery = `user:${owner} is:pr is:open`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Handle label with potential spaces
|
|
456
|
+
let searchQuery;
|
|
457
|
+
let searchCmd;
|
|
458
|
+
|
|
459
|
+
if (argv.reviewLabel.includes(' ')) {
|
|
460
|
+
searchQuery = `${baseQuery} label:"${argv.reviewLabel}"`;
|
|
461
|
+
searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
|
|
462
|
+
} else {
|
|
463
|
+
searchQuery = `${baseQuery} label:${argv.reviewLabel}`;
|
|
464
|
+
searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await log(` š Search query: ${searchQuery}`, { verbose: true });
|
|
468
|
+
await log(` š Command: ${searchCmd}`, { verbose: true });
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const { stdout } = await execAsync(searchCmd, { encoding: 'utf8', env: process.env });
|
|
472
|
+
prs = JSON.parse(stdout || '[]');
|
|
473
|
+
} catch (searchError) {
|
|
474
|
+
await log(` ā ļø Search failed: ${searchError.message.split('\n')[0]}`, { verbose: true });
|
|
475
|
+
prs = [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (prs.length === 0) {
|
|
481
|
+
if (argv.allPrs) {
|
|
482
|
+
await log(' ā¹ļø No open pull requests found');
|
|
483
|
+
} else {
|
|
484
|
+
await log(` ā¹ļø No pull requests found with label "${argv.reviewLabel}"`);
|
|
485
|
+
}
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (argv.allPrs) {
|
|
490
|
+
await log(` š Found ${prs.length} open pull request(s)`);
|
|
491
|
+
} else {
|
|
492
|
+
await log(` š Found ${prs.length} pull request(s) with label "${argv.reviewLabel}"`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Filter out draft PRs if option is enabled
|
|
496
|
+
if (argv.skipDraft) {
|
|
497
|
+
const nonDraftPrs = prs.filter(pr => !pr.isDraft);
|
|
498
|
+
const draftCount = prs.length - nonDraftPrs.length;
|
|
499
|
+
if (draftCount > 0) {
|
|
500
|
+
await log(` āļø Filtered out ${draftCount} draft PR(s)`);
|
|
501
|
+
}
|
|
502
|
+
prs = nonDraftPrs;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Apply max PRs limit if set
|
|
506
|
+
let prsToProcess = prs;
|
|
507
|
+
if (argv.maxPrs > 0 && prs.length > argv.maxPrs) {
|
|
508
|
+
prsToProcess = prs.slice(0, argv.maxPrs);
|
|
509
|
+
await log(` š¢ Limiting to first ${argv.maxPrs} PRs`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Filter out PRs with approvals if option is enabled
|
|
513
|
+
if (argv.skipApproved) {
|
|
514
|
+
await log(' š Checking for existing approvals...');
|
|
515
|
+
const filteredPrs = [];
|
|
516
|
+
|
|
517
|
+
for (const pr of prsToProcess) {
|
|
518
|
+
const hasApproval = await hasApprovals(pr.url);
|
|
519
|
+
if (hasApproval) {
|
|
520
|
+
await log(` āļø Skipping (approved): ${pr.title || 'Untitled'} (${pr.url})`, { verbose: true });
|
|
521
|
+
} else {
|
|
522
|
+
filteredPrs.push(pr);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const skippedCount = prsToProcess.length - filteredPrs.length;
|
|
527
|
+
if (skippedCount > 0) {
|
|
528
|
+
await log(` āļø Skipped ${skippedCount} PR(s) with existing approvals`);
|
|
529
|
+
}
|
|
530
|
+
prsToProcess = filteredPrs;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// In dry-run mode, show the PRs that would be reviewed
|
|
534
|
+
if (argv.dryRun && prsToProcess.length > 0) {
|
|
535
|
+
await log('\n š PRs that would be reviewed:');
|
|
536
|
+
for (const pr of prsToProcess) {
|
|
537
|
+
await log(` - ${pr.title || 'Untitled'} (${pr.url})`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return prsToProcess.map(pr => pr.url);
|
|
542
|
+
|
|
543
|
+
} catch (error) {
|
|
544
|
+
await log(` ā Error fetching pull requests: ${error.message}`, { level: 'error' });
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Main monitoring loop
|
|
550
|
+
async function monitor() {
|
|
551
|
+
await log('\nš Starting Reviewers Hive Mind monitoring system...');
|
|
552
|
+
|
|
553
|
+
// Start reviewers
|
|
554
|
+
await log(`\nš Starting ${argv.concurrency} reviewers...`);
|
|
555
|
+
for (let i = 1; i <= argv.concurrency; i++) {
|
|
556
|
+
prQueue.workers.push(reviewer(i));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Main monitoring loop
|
|
560
|
+
let iteration = 0;
|
|
561
|
+
while (true) {
|
|
562
|
+
iteration++;
|
|
563
|
+
await log(`\nš Monitoring iteration ${iteration} at ${new Date().toISOString()}`);
|
|
564
|
+
|
|
565
|
+
// Fetch PRs
|
|
566
|
+
const prUrls = await fetchPullRequests();
|
|
567
|
+
|
|
568
|
+
// Add new PRs to queue
|
|
569
|
+
let newPrs = 0;
|
|
570
|
+
for (const url of prUrls) {
|
|
571
|
+
if (prQueue.enqueue(url)) {
|
|
572
|
+
newPrs++;
|
|
573
|
+
await log(` ā Added to review queue: ${url}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (newPrs > 0) {
|
|
578
|
+
await log(` š„ Added ${newPrs} new PR(s) to review queue`);
|
|
579
|
+
} else {
|
|
580
|
+
await log(' ā¹ļø No new PRs to add (all already reviewed or in queue)');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Show current stats
|
|
584
|
+
const stats = prQueue.getStats();
|
|
585
|
+
await log('\nš Current Status:');
|
|
586
|
+
await log(` š Queued: ${stats.queued}`);
|
|
587
|
+
await log(` āļø Reviewing: ${stats.processing}`);
|
|
588
|
+
await log(` ā
Completed: ${stats.completed}`);
|
|
589
|
+
await log(` ā Failed: ${stats.failed}`);
|
|
590
|
+
|
|
591
|
+
// If running once, wait for queue to empty then exit
|
|
592
|
+
if (argv.once) {
|
|
593
|
+
await log('\nš Single run mode - waiting for review queue to empty...');
|
|
594
|
+
|
|
595
|
+
while (stats.queued > 0 || stats.processing > 0) {
|
|
596
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
597
|
+
const currentStats = prQueue.getStats();
|
|
598
|
+
if (currentStats.queued !== stats.queued || currentStats.processing !== stats.processing) {
|
|
599
|
+
await log(` ā³ Waiting... Queue: ${currentStats.queued}, Reviewing: ${currentStats.processing}`);
|
|
600
|
+
}
|
|
601
|
+
Object.assign(stats, currentStats);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
await log('\nā
All PRs reviewed!');
|
|
605
|
+
await log(` Completed: ${stats.completed}`);
|
|
606
|
+
await log(` Failed: ${stats.failed}`);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Wait for next iteration
|
|
611
|
+
await log(`\nā° Next check in ${argv.interval} seconds...`);
|
|
612
|
+
await new Promise(resolve => setTimeout(resolve, argv.interval * 1000));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Stop reviewers
|
|
616
|
+
prQueue.stop();
|
|
617
|
+
await Promise.all(prQueue.workers);
|
|
618
|
+
|
|
619
|
+
await log('\nš Reviewers Hive Mind monitoring stopped');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Handle graceful shutdown
|
|
623
|
+
process.on('SIGINT', async () => {
|
|
624
|
+
await log('\n\nš Received interrupt signal, shutting down gracefully...');
|
|
625
|
+
prQueue.stop();
|
|
626
|
+
await Promise.all(prQueue.workers);
|
|
627
|
+
process.exit(0);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
process.on('SIGTERM', async () => {
|
|
631
|
+
await log('\n\nš Received termination signal, shutting down gracefully...');
|
|
632
|
+
prQueue.stop();
|
|
633
|
+
await Promise.all(prQueue.workers);
|
|
634
|
+
process.exit(0);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Start monitoring
|
|
638
|
+
try {
|
|
639
|
+
await monitor();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
await log(`\nā Fatal error: ${error.message}`, { level: 'error' });
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|