@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,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Branch Protection Script
|
|
4
|
+
*
|
|
5
|
+
* Enables branch protection rules on the default branch of a GitHub repository
|
|
6
|
+
* to require pull requests before merging.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ./protect-branch.mjs <owner>/<repo>
|
|
10
|
+
* ./protect-branch.mjs <owner>/<repo> <branch-name>
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* ./protect-branch.mjs konard/my-repo
|
|
14
|
+
* ./protect-branch.mjs konard/my-repo main
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
18
|
+
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
19
|
+
|
|
20
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
21
|
+
const { $ } = await use('command-stream');
|
|
22
|
+
|
|
23
|
+
// Parse command line arguments
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
|
|
26
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
27
|
+
console.log('Branch Protection Tool');
|
|
28
|
+
console.log('Usage: ./protect-branch.mjs <owner>/<repo> [branch-name]');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log('Examples:');
|
|
31
|
+
console.log(' ./protect-branch.mjs konard/my-repo');
|
|
32
|
+
console.log(' ./protect-branch.mjs konard/my-repo main');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse repository argument
|
|
37
|
+
const repoArg = args[0];
|
|
38
|
+
if (!repoArg.includes('/')) {
|
|
39
|
+
console.error('Error: Repository must be in format owner/repo');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [owner, repo] = repoArg.split('/');
|
|
44
|
+
let branchName = args[1];
|
|
45
|
+
|
|
46
|
+
console.log('๐ Branch Protection Tool');
|
|
47
|
+
console.log(`๐ฆ Repository: ${owner}/${repo}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// If branch name not provided, get the default branch
|
|
51
|
+
if (!branchName) {
|
|
52
|
+
console.log('๐ Detecting default branch...');
|
|
53
|
+
const defaultBranchResult = await $`gh api repos/${owner}/${repo} --jq .default_branch`;
|
|
54
|
+
|
|
55
|
+
if (defaultBranchResult.code !== 0) {
|
|
56
|
+
console.error('Error: Failed to get repository information');
|
|
57
|
+
console.error(defaultBranchResult.stderr ? defaultBranchResult.stderr.toString() : 'Unknown error');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
branchName = defaultBranchResult.stdout.toString().trim();
|
|
62
|
+
console.log(`โ
Default branch: ${branchName}`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`๐ฏ Target branch: ${branchName}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if branch exists
|
|
68
|
+
console.log('๐ Verifying branch exists...');
|
|
69
|
+
const branchCheckResult = await $`gh api repos/${owner}/${repo}/branches/${branchName} --silent`;
|
|
70
|
+
|
|
71
|
+
if (branchCheckResult.code !== 0) {
|
|
72
|
+
console.error(`Error: Branch '${branchName}' not found in repository`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log('โ
Branch verified');
|
|
76
|
+
|
|
77
|
+
// Enable branch protection with PR requirement
|
|
78
|
+
console.log(`๐ Enabling branch protection for '${branchName}'...`);
|
|
79
|
+
|
|
80
|
+
// Create the protection rules JSON
|
|
81
|
+
const protectionRules = {
|
|
82
|
+
required_status_checks: null,
|
|
83
|
+
enforce_admins: false,
|
|
84
|
+
required_pull_request_reviews: {
|
|
85
|
+
dismiss_stale_reviews: false,
|
|
86
|
+
require_code_owner_reviews: false,
|
|
87
|
+
required_approving_review_count: 0,
|
|
88
|
+
require_last_push_approval: false
|
|
89
|
+
},
|
|
90
|
+
restrictions: null,
|
|
91
|
+
allow_force_pushes: false,
|
|
92
|
+
allow_deletions: false,
|
|
93
|
+
block_creations: false,
|
|
94
|
+
required_conversation_resolution: false,
|
|
95
|
+
lock_branch: false,
|
|
96
|
+
allow_fork_syncing: false
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Apply branch protection using GitHub API
|
|
100
|
+
const protectResult = await $`gh api \
|
|
101
|
+
--method PUT \
|
|
102
|
+
repos/${owner}/${repo}/branches/${branchName}/protection \
|
|
103
|
+
--input - << 'EOF'
|
|
104
|
+
${JSON.stringify(protectionRules, null, 2)}
|
|
105
|
+
EOF`;
|
|
106
|
+
|
|
107
|
+
if (protectResult.code !== 0) {
|
|
108
|
+
// Check if it's already protected
|
|
109
|
+
if (protectResult.stderr && protectResult.stderr.toString().includes('Branch protection is disabled')) {
|
|
110
|
+
console.warn('โ ๏ธ Branch protection might require admin permissions or a paid plan');
|
|
111
|
+
} else {
|
|
112
|
+
console.error('Error: Failed to enable branch protection');
|
|
113
|
+
console.error(protectResult.stderr ? protectResult.stderr.toString() : 'Unknown error');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Try a simpler approach for public repos
|
|
117
|
+
console.log('๐ Trying alternative method...');
|
|
118
|
+
|
|
119
|
+
// For public repos, we can at least try to update settings
|
|
120
|
+
const updateResult = await $`gh api \
|
|
121
|
+
--method PATCH \
|
|
122
|
+
repos/${owner}/${repo} \
|
|
123
|
+
--field allow_merge_commit=true \
|
|
124
|
+
--field allow_squash_merge=true \
|
|
125
|
+
--field allow_rebase_merge=true \
|
|
126
|
+
--field delete_branch_on_merge=false`;
|
|
127
|
+
|
|
128
|
+
if (updateResult.code === 0) {
|
|
129
|
+
console.log('โ
Repository settings updated (PR workflow encouraged)');
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.log('โ
Branch protection enabled successfully!');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Verify the protection status
|
|
136
|
+
console.log('\n๐ Verifying protection status...');
|
|
137
|
+
const statusResult = await $`gh api repos/${owner}/${repo}/branches/${branchName}/protection --silent 2>/dev/null || echo "not-protected"`;
|
|
138
|
+
|
|
139
|
+
if (statusResult.stdout.toString().trim() === 'not-protected') {
|
|
140
|
+
console.log('โ ๏ธ Branch protection not fully active (may require admin rights or paid plan)');
|
|
141
|
+
console.log('\n๐ก Alternative: Configure protection manually in repository settings:');
|
|
142
|
+
console.log(` https://github.com/${owner}/${repo}/settings/branches`);
|
|
143
|
+
} else {
|
|
144
|
+
console.log('โ
Branch protection is active');
|
|
145
|
+
console.log('\n๐ฏ Protection rules applied:');
|
|
146
|
+
console.log(' โข Pull requests required before merging');
|
|
147
|
+
console.log(' โข Force pushes disabled');
|
|
148
|
+
console.log(' โข Branch deletion disabled');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('\nโจ Done! The branch is configured to require pull requests.');
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('\nโ Error:', error.message);
|
|
155
|
+
if (error.stderr) {
|
|
156
|
+
console.error('Details:', error.stderr.toString());
|
|
157
|
+
}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
package/src/review.mjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Early exit paths - handle these before loading all modules to speed up testing
|
|
4
|
+
const earlyArgs = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (earlyArgs.includes('--version')) {
|
|
7
|
+
const { getVersion } = await import('./version.lib.mjs');
|
|
8
|
+
try {
|
|
9
|
+
const version = await getVersion();
|
|
10
|
+
console.log(version);
|
|
11
|
+
} catch {
|
|
12
|
+
console.error('Error: Unable to determine version');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
19
|
+
// Show help and exit
|
|
20
|
+
console.log('Usage: review.mjs <pr-url> [options]');
|
|
21
|
+
console.log('\nOptions:');
|
|
22
|
+
console.log(' --version Show version number');
|
|
23
|
+
console.log(' --help, -h Show help');
|
|
24
|
+
console.log(' --resume, -r Resume from a previous session ID');
|
|
25
|
+
console.log(' --dry-run, -n Prepare everything but do not execute Claude');
|
|
26
|
+
console.log(' --model, -m Model to use (opus, sonnet, or full model ID) [default: opus]');
|
|
27
|
+
console.log(' --focus, -f Focus areas for review [default: all]');
|
|
28
|
+
console.log(' --approve If review passes, approve the PR');
|
|
29
|
+
console.log(' --verbose, -v Enable verbose logging');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
34
|
+
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
35
|
+
|
|
36
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
37
|
+
const { $ } = await use('command-stream');
|
|
38
|
+
|
|
39
|
+
const yargs = (await use('yargs@latest')).default;
|
|
40
|
+
const os = (await use('os')).default;
|
|
41
|
+
const path = (await use('path')).default;
|
|
42
|
+
const fs = (await use('fs')).promises;
|
|
43
|
+
|
|
44
|
+
// Import shared functions from lib.mjs to follow DRY principle
|
|
45
|
+
import { log, setLogFile, getLogFile, formatAligned } from './lib.mjs';
|
|
46
|
+
import { reportError } from './sentry.lib.mjs';
|
|
47
|
+
import * as memoryCheck from './memory-check.mjs';
|
|
48
|
+
|
|
49
|
+
// Import Claude execution functions
|
|
50
|
+
import { executeClaudeCommand } from './claude.lib.mjs';
|
|
51
|
+
|
|
52
|
+
// Configure command line arguments - GitHub PR URL as positional argument
|
|
53
|
+
// Use yargs().parse(args) instead of yargs(args).argv to ensure .strict() mode works
|
|
54
|
+
const argv = yargs()
|
|
55
|
+
.usage('Usage: $0 <pr-url> [options]')
|
|
56
|
+
.positional('pr-url', {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'The GitHub pull request URL to review'
|
|
59
|
+
})
|
|
60
|
+
.option('resume', {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Resume from a previous session ID (when limit was reached)',
|
|
63
|
+
alias: 'r'
|
|
64
|
+
})
|
|
65
|
+
.option('dry-run', {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Prepare everything but do not execute Claude',
|
|
68
|
+
alias: 'n'
|
|
69
|
+
})
|
|
70
|
+
.option('model', {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Model to use (opus, sonnet, or full model ID like claude-sonnet-4-5-20250929)',
|
|
73
|
+
alias: 'm',
|
|
74
|
+
default: 'opus',
|
|
75
|
+
choices: ['opus', 'sonnet', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101']
|
|
76
|
+
})
|
|
77
|
+
.option('focus', {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'Focus areas for review (security, performance, logic, style, tests)',
|
|
80
|
+
alias: 'f',
|
|
81
|
+
default: 'all'
|
|
82
|
+
})
|
|
83
|
+
.option('approve', {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description: 'If review passes, approve the PR',
|
|
86
|
+
default: false
|
|
87
|
+
})
|
|
88
|
+
.option('verbose', {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
description: 'Enable verbose logging for debugging',
|
|
91
|
+
alias: 'v',
|
|
92
|
+
default: false
|
|
93
|
+
})
|
|
94
|
+
.demandCommand(1, 'The GitHub pull request URL is required')
|
|
95
|
+
.parserConfiguration({
|
|
96
|
+
'boolean-negation': true
|
|
97
|
+
})
|
|
98
|
+
.help('h')
|
|
99
|
+
.alias('h', 'help')
|
|
100
|
+
// Use yargs built-in strict mode to reject unrecognized options
|
|
101
|
+
// This prevents issues like #453 and #482 where unknown options are silently ignored
|
|
102
|
+
.strict()
|
|
103
|
+
.parse(process.argv.slice(2));
|
|
104
|
+
|
|
105
|
+
const prUrl = argv['pr-url'] || argv._[0];
|
|
106
|
+
|
|
107
|
+
// Set global verbose mode for log function
|
|
108
|
+
global.verboseMode = argv.verbose;
|
|
109
|
+
|
|
110
|
+
// Create permanent log file immediately with timestamp
|
|
111
|
+
const scriptDir = path.dirname(process.argv[1]);
|
|
112
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
113
|
+
const logFilePath = path.join(scriptDir, `review-${timestamp}.log`);
|
|
114
|
+
setLogFile(logFilePath);
|
|
115
|
+
|
|
116
|
+
// Create the log file immediately
|
|
117
|
+
await fs.writeFile(logFilePath, `# Review.mjs Log - ${new Date().toISOString()}\n\n`);
|
|
118
|
+
await log(`๐ Log file: ${logFilePath}`);
|
|
119
|
+
await log(' (All output will be logged here)\n');
|
|
120
|
+
|
|
121
|
+
// Validate GitHub PR URL format
|
|
122
|
+
if (!prUrl.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/)) {
|
|
123
|
+
await log('Error: Please provide a valid GitHub pull request URL (e.g., https://github.com/owner/repo/pull/123)', { level: 'error' });
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const claudePath = process.env.CLAUDE_PATH || 'claude';
|
|
128
|
+
|
|
129
|
+
// Extract repository and PR number from URL
|
|
130
|
+
const urlParts = prUrl.split('/');
|
|
131
|
+
const owner = urlParts[3];
|
|
132
|
+
const repo = urlParts[4];
|
|
133
|
+
const prNumber = urlParts[6];
|
|
134
|
+
|
|
135
|
+
// Create or find temporary directory for cloning the repository
|
|
136
|
+
let tempDir;
|
|
137
|
+
let isResuming = argv.resume;
|
|
138
|
+
|
|
139
|
+
if (isResuming) {
|
|
140
|
+
// When resuming, try to find existing directory or create a new one
|
|
141
|
+
const scriptDir = path.dirname(process.argv[1]);
|
|
142
|
+
const sessionLogPattern = path.join(scriptDir, `${argv.resume}.log`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Check if session log exists to verify session is valid
|
|
146
|
+
await fs.access(sessionLogPattern);
|
|
147
|
+
await log(`๐ Resuming session ${argv.resume} (session log found)`);
|
|
148
|
+
|
|
149
|
+
// For resumed sessions, create new temp directory since old one may be cleaned up
|
|
150
|
+
tempDir = path.join(os.tmpdir(), `gh-pr-reviewer-resume-${argv.resume}-${Date.now()}`);
|
|
151
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
152
|
+
await log(`Creating new temporary directory for resumed session: ${tempDir}`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
reportError(err, {
|
|
155
|
+
context: 'resume_session_lookup',
|
|
156
|
+
sessionId: argv.resume
|
|
157
|
+
});
|
|
158
|
+
await log(`Warning: Session log for ${argv.resume} not found, but continuing with resume attempt`);
|
|
159
|
+
tempDir = path.join(os.tmpdir(), `gh-pr-reviewer-resume-${argv.resume}-${Date.now()}`);
|
|
160
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
161
|
+
await log(`Creating temporary directory for resumed session: ${tempDir}`);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
tempDir = path.join(os.tmpdir(), `gh-pr-reviewer-${Date.now()}`);
|
|
165
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
166
|
+
await log(`Creating temporary directory: ${tempDir}\n`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let limitReached = false;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Get PR details first
|
|
173
|
+
await log('๐ Getting pull request details...');
|
|
174
|
+
const prDetailsResult = await $`gh pr view ${prUrl} --json title,body,headRefName,baseRefName,author,number,state,files`;
|
|
175
|
+
|
|
176
|
+
if (prDetailsResult.code !== 0) {
|
|
177
|
+
await log('Error: Failed to get PR details', { level: 'error' });
|
|
178
|
+
await log(prDetailsResult.stderr ? prDetailsResult.stderr.toString() : 'Unknown error', { level: 'error' });
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const prDetails = JSON.parse(prDetailsResult.stdout.toString());
|
|
183
|
+
|
|
184
|
+
await log(`\n๐ Pull Request: #${prDetails.number} - ${prDetails.title}`);
|
|
185
|
+
await log(`๐ค Author: ${prDetails.author.login}`);
|
|
186
|
+
await log(`๐ฟ Branch: ${prDetails.headRefName} โ ${prDetails.baseRefName}`);
|
|
187
|
+
await log(`๐ State: ${prDetails.state}`);
|
|
188
|
+
await log(`๐ Files changed: ${prDetails.files.length}`);
|
|
189
|
+
|
|
190
|
+
// Clone the repository using gh tool with authentication
|
|
191
|
+
await log(`\nCloning repository ${owner}/${repo} using gh tool...\n`);
|
|
192
|
+
const cloneResult = await $`gh repo clone ${owner}/${repo} ${tempDir}`;
|
|
193
|
+
|
|
194
|
+
// Verify clone was successful
|
|
195
|
+
if (cloneResult.code !== 0) {
|
|
196
|
+
await log('Error: Failed to clone repository', { level: 'error' });
|
|
197
|
+
await log(cloneResult.stderr ? cloneResult.stderr.toString() : 'Unknown error', { level: 'error' });
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await log(`โ
Repository cloned successfully to ${tempDir}\n`);
|
|
202
|
+
|
|
203
|
+
// Set up git authentication using gh
|
|
204
|
+
const authSetupResult = await $`cd ${tempDir} && gh auth setup-git 2>&1`;
|
|
205
|
+
if (authSetupResult.code !== 0) {
|
|
206
|
+
await log('Note: gh auth setup-git had issues, continuing anyway\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fetch and checkout the PR branch
|
|
210
|
+
await log(`๐ Fetching and checking out PR branch: ${prDetails.headRefName}`);
|
|
211
|
+
const fetchResult = await $`cd ${tempDir} && gh pr checkout ${prNumber}`;
|
|
212
|
+
|
|
213
|
+
if (fetchResult.code !== 0) {
|
|
214
|
+
await log('Error: Failed to checkout PR branch', { level: 'error' });
|
|
215
|
+
await log(fetchResult.stderr ? fetchResult.stderr.toString() : 'Unknown error', { level: 'error' });
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await log('โ
Successfully checked out PR branch\n');
|
|
220
|
+
|
|
221
|
+
// Get the diff for the PR
|
|
222
|
+
await log('๐ Getting PR diff...');
|
|
223
|
+
const diffResult = await $`gh pr diff ${prUrl}`;
|
|
224
|
+
|
|
225
|
+
if (diffResult.code !== 0) {
|
|
226
|
+
await log('Error: Failed to get PR diff', { level: 'error' });
|
|
227
|
+
await log(diffResult.stderr ? diffResult.stderr.toString() : 'Unknown error', { level: 'error' });
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const prDiff = diffResult.stdout.toString();
|
|
232
|
+
await log(`โ
Got PR diff (${prDiff.length} characters)\n`);
|
|
233
|
+
|
|
234
|
+
// Save diff to a file for reference
|
|
235
|
+
const diffFile = path.join(tempDir, 'pr-diff.patch');
|
|
236
|
+
await fs.writeFile(diffFile, prDiff);
|
|
237
|
+
await log(`๐ Diff saved to: ${diffFile}\n`);
|
|
238
|
+
|
|
239
|
+
const prompt = `Pull request to review: ${prUrl}
|
|
240
|
+
PR Number: ${prNumber}
|
|
241
|
+
Repository: ${owner}/${repo}
|
|
242
|
+
Working directory: ${tempDir}
|
|
243
|
+
Diff file: ${diffFile}
|
|
244
|
+
Focus areas: ${argv.focus}
|
|
245
|
+
Auto-approve if passes: ${argv.approve}
|
|
246
|
+
|
|
247
|
+
Review this pull request thoroughly.`;
|
|
248
|
+
|
|
249
|
+
const systemPrompt = `You are an expert code reviewer for pull requests.
|
|
250
|
+
|
|
251
|
+
0. General guidelines.
|
|
252
|
+
- When you execute commands, always save their logs to files for easy reading if the output gets large.
|
|
253
|
+
- When running commands, do not set a timeout yourself โ let them run as long as needed.
|
|
254
|
+
- When a code or log file has more than 2500 lines, read it in chunks of 2500 lines.
|
|
255
|
+
- When reviewing, be thorough but constructive.
|
|
256
|
+
- When suggesting improvements, provide specific code examples.
|
|
257
|
+
|
|
258
|
+
1. Initial analysis.
|
|
259
|
+
- When you start, read the PR description using gh pr view ${prUrl}.
|
|
260
|
+
- When you need the diff, read it from ${diffFile} or use gh pr diff ${prNumber}.
|
|
261
|
+
- When you need file context, explore files in ${tempDir}.
|
|
262
|
+
- When you check tests, run them if possible using existing test commands.
|
|
263
|
+
- When you review commits, use gh pr view ${prNumber} --json commits.
|
|
264
|
+
|
|
265
|
+
2. Review focus areas.
|
|
266
|
+
${argv.focus === 'all' ? '- Review all aspects: logic, security, performance, style, tests, documentation.' : `- Focus specifically on: ${argv.focus}`}
|
|
267
|
+
- When reviewing logic, check for edge cases and error handling.
|
|
268
|
+
- When reviewing security, look for vulnerabilities and unsafe patterns.
|
|
269
|
+
- When reviewing performance, identify bottlenecks and inefficiencies.
|
|
270
|
+
- When reviewing style, ensure consistency with project conventions.
|
|
271
|
+
- When reviewing tests, verify coverage and test quality.
|
|
272
|
+
|
|
273
|
+
3. Providing feedback.
|
|
274
|
+
- When you find issues, create review comments using gh pr review ${prNumber} --comment.
|
|
275
|
+
- When suggesting changes, use gh pr review ${prNumber} --comment with specific line references.
|
|
276
|
+
- When code needs changes, provide suggestions with exact code snippets.
|
|
277
|
+
- When adding line comments, use the format: path/to/file.ext:LINE_NUMBER
|
|
278
|
+
- When creating suggestions, use GitHub's suggestion format in comments:
|
|
279
|
+
\`\`\`suggestion
|
|
280
|
+
improved code here
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
4. Review submission.
|
|
284
|
+
- When review is complete, submit it using gh pr review ${prNumber} --${argv.approve ? 'approve' : 'comment'} --body "review summary".
|
|
285
|
+
- When requesting changes, use gh pr review ${prNumber} --request-changes --body "summary of required changes".
|
|
286
|
+
- When approving, only do so if code meets all quality standards.
|
|
287
|
+
- When commenting, be specific about line numbers and files.
|
|
288
|
+
|
|
289
|
+
5. Line-specific comments.
|
|
290
|
+
- When adding comments to specific lines, use gh api to post review comments.
|
|
291
|
+
- When referencing lines, use the commit SHA from the PR.
|
|
292
|
+
- When suggesting code changes, include the suggestion block format.
|
|
293
|
+
- Example for line comment:
|
|
294
|
+
gh api repos/${owner}/${repo}/pulls/${prNumber}/comments \\
|
|
295
|
+
--method POST \\
|
|
296
|
+
--field path="file.js" \\
|
|
297
|
+
--field line=42 \\
|
|
298
|
+
--field body="Comment text with suggestion" \\
|
|
299
|
+
--field commit_id="SHA"
|
|
300
|
+
|
|
301
|
+
6. Best practices.
|
|
302
|
+
- When reviewing, check for breaking changes.
|
|
303
|
+
- When examining dependencies, verify versions and security.
|
|
304
|
+
- When looking at tests, ensure they actually test the changes.
|
|
305
|
+
- When reviewing documentation, verify it matches the code.
|
|
306
|
+
- When finding issues, prioritize them by severity.
|
|
307
|
+
- When suggesting improvements, explain why they're beneficial.`;
|
|
308
|
+
|
|
309
|
+
// Properly escape prompts for shell usage - escape quotes and preserve newlines
|
|
310
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
311
|
+
const escapedSystemPrompt = systemPrompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
312
|
+
|
|
313
|
+
// Execute claude command from the cloned repository directory
|
|
314
|
+
await log(`\n๐ค Executing Claude (${argv.model.toUpperCase()}) for PR review...`);
|
|
315
|
+
|
|
316
|
+
// If dry-run, exit here
|
|
317
|
+
if (argv.dryRun) {
|
|
318
|
+
await log('โ
Command preparation complete');
|
|
319
|
+
await log(`๐ Repository cloned to: ${tempDir}`);
|
|
320
|
+
await log(`๐ PR branch checked out: ${prDetails.headRefName}`);
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Change to the temporary directory before executing
|
|
325
|
+
process.chdir(tempDir);
|
|
326
|
+
|
|
327
|
+
// Use the shared executeClaudeCommand function from claude.lib.mjs
|
|
328
|
+
// This ensures consistent NDJSON parsing, error handling, and retry logic across all commands
|
|
329
|
+
const result = await executeClaudeCommand({
|
|
330
|
+
tempDir,
|
|
331
|
+
branchName: prDetails.headRefName,
|
|
332
|
+
prompt,
|
|
333
|
+
systemPrompt,
|
|
334
|
+
escapedPrompt,
|
|
335
|
+
escapedSystemPrompt,
|
|
336
|
+
argv,
|
|
337
|
+
log,
|
|
338
|
+
setLogFile,
|
|
339
|
+
getLogFile,
|
|
340
|
+
formatAligned,
|
|
341
|
+
getResourceSnapshot: memoryCheck.getResourceSnapshot,
|
|
342
|
+
forkedRepo: null,
|
|
343
|
+
feedbackLines: [],
|
|
344
|
+
claudePath,
|
|
345
|
+
$
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const { success: commandSuccess, sessionId, limitReached: limitReachedResult } = result;
|
|
349
|
+
limitReached = limitReachedResult;
|
|
350
|
+
|
|
351
|
+
// Handle command failure
|
|
352
|
+
if (!commandSuccess) {
|
|
353
|
+
await log('\nโ Command execution failed. Check the log file for details.');
|
|
354
|
+
await log(`๐ Log file: ${getLogFile()}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Show summary of session and log file
|
|
359
|
+
await log('\n=== Review Summary ===');
|
|
360
|
+
|
|
361
|
+
if (sessionId) {
|
|
362
|
+
await log(`โ
Session ID: ${sessionId}`);
|
|
363
|
+
await log(`โ
Complete log file: ${getLogFile()}`);
|
|
364
|
+
|
|
365
|
+
if (limitReached) {
|
|
366
|
+
await log('\nโฐ LIMIT REACHED DETECTED!');
|
|
367
|
+
await log('\n๐ To resume when limit resets, use:\n');
|
|
368
|
+
await log(`./review.mjs "${prUrl}" --resume ${sessionId}`);
|
|
369
|
+
await log('\n This will continue from where it left off with full context.\n');
|
|
370
|
+
} else {
|
|
371
|
+
// Check if review was submitted
|
|
372
|
+
await log('\n๐ Checking for submitted review...');
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
// Get reviews for the PR
|
|
376
|
+
const reviewsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --jq '.[] | select(.user.login == "'$(gh api user --jq .login)'") | {state, submitted_at}'`;
|
|
377
|
+
|
|
378
|
+
if (reviewsResult.code === 0 && reviewsResult.stdout.toString().trim()) {
|
|
379
|
+
await log(`โ
Review has been submitted to PR #${prNumber}`);
|
|
380
|
+
await log(`๐ View at: ${prUrl}`);
|
|
381
|
+
} else {
|
|
382
|
+
await log('โน๏ธ Review may be pending or saved as draft');
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
reportError(error, {
|
|
386
|
+
context: 'verify_review_status',
|
|
387
|
+
prNumber,
|
|
388
|
+
level: 'warning'
|
|
389
|
+
});
|
|
390
|
+
await log('โ ๏ธ Could not verify review status');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Show command to resume session in interactive mode
|
|
394
|
+
await log('\n๐ก To continue this session in Claude Code interactive mode:\n');
|
|
395
|
+
await log(` (cd ${tempDir} && claude --resume ${sessionId})`);
|
|
396
|
+
await log('');
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
await log('โ No session ID extracted');
|
|
400
|
+
await log(`๐ Log file available: ${getLogFile()}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await log('\nโจ Review process complete. Check the PR for review comments.');
|
|
404
|
+
await log(`๐ Pull Request: ${prUrl}`);
|
|
405
|
+
|
|
406
|
+
} catch (error) {
|
|
407
|
+
reportError(error, {
|
|
408
|
+
context: 'review_execution',
|
|
409
|
+
prUrl: prUrl
|
|
410
|
+
});
|
|
411
|
+
await log('Error executing review:', error.message, { level: 'error' });
|
|
412
|
+
process.exit(1);
|
|
413
|
+
} finally {
|
|
414
|
+
// Clean up temporary directory (but not when resuming or when limit reached)
|
|
415
|
+
if (!argv.resume && !limitReached) {
|
|
416
|
+
try {
|
|
417
|
+
process.stdout.write('\n๐งน Cleaning up...');
|
|
418
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
419
|
+
await log(' โ
');
|
|
420
|
+
} catch (cleanupError) {
|
|
421
|
+
reportError(cleanupError, {
|
|
422
|
+
context: 'cleanup_temp_dir',
|
|
423
|
+
level: 'warning',
|
|
424
|
+
tempDir
|
|
425
|
+
});
|
|
426
|
+
await log(' โ ๏ธ (failed)');
|
|
427
|
+
}
|
|
428
|
+
} else if (argv.resume) {
|
|
429
|
+
await log(`\n๐ Keeping directory for resumed session: ${tempDir}`);
|
|
430
|
+
} else if (limitReached) {
|
|
431
|
+
await log(`\n๐ Keeping directory for future resume: ${tempDir}`);
|
|
432
|
+
}
|
|
433
|
+
}
|