@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,558 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Results processing module for solve command
|
|
4
|
+
// Extracted from solve.mjs to keep files under 1500 lines
|
|
5
|
+
|
|
6
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
7
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
8
|
+
// If not, fetch it (when running standalone)
|
|
9
|
+
if (typeof globalThis.use === 'undefined') {
|
|
10
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
11
|
+
}
|
|
12
|
+
const use = globalThis.use;
|
|
13
|
+
|
|
14
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
15
|
+
const { $ } = await use('command-stream');
|
|
16
|
+
|
|
17
|
+
const path = (await use('path')).default;
|
|
18
|
+
|
|
19
|
+
// Import shared library functions
|
|
20
|
+
const lib = await import('./lib.mjs');
|
|
21
|
+
const {
|
|
22
|
+
log,
|
|
23
|
+
getLogFile,
|
|
24
|
+
formatAligned
|
|
25
|
+
} = lib;
|
|
26
|
+
|
|
27
|
+
// Import exit handler
|
|
28
|
+
import { safeExit } from './exit-handler.lib.mjs';
|
|
29
|
+
|
|
30
|
+
// Import GitHub-related functions
|
|
31
|
+
const githubLib = await import('./github.lib.mjs');
|
|
32
|
+
const {
|
|
33
|
+
sanitizeLogContent,
|
|
34
|
+
attachLogToGitHub
|
|
35
|
+
} = githubLib;
|
|
36
|
+
|
|
37
|
+
// Import auto-continue functions
|
|
38
|
+
const autoContinue = await import('./solve.auto-continue.lib.mjs');
|
|
39
|
+
const {
|
|
40
|
+
autoContinueWhenLimitResets
|
|
41
|
+
} = autoContinue;
|
|
42
|
+
|
|
43
|
+
// Import error handling functions
|
|
44
|
+
// const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
|
|
45
|
+
// Import Sentry integration
|
|
46
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
47
|
+
const { reportError } = sentryLib;
|
|
48
|
+
|
|
49
|
+
// Import GitHub linking detection library
|
|
50
|
+
const githubLinking = await import('./github-linking.lib.mjs');
|
|
51
|
+
const { hasGitHubLinkingKeyword } = githubLinking;
|
|
52
|
+
|
|
53
|
+
// Revert the CLAUDE.md commit to restore original state
|
|
54
|
+
export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
|
|
55
|
+
try {
|
|
56
|
+
// Only revert if we have the commit hash from this session
|
|
57
|
+
// This prevents reverting the wrong commit in continue mode
|
|
58
|
+
if (!claudeCommitHash) {
|
|
59
|
+
await log(' No CLAUDE.md commit to revert (not created in this session)', { verbose: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await log(formatAligned('š', 'Cleanup:', 'Reverting CLAUDE.md commit'));
|
|
64
|
+
await log(` Using saved commit hash: ${claudeCommitHash.substring(0, 7)}...`, { verbose: true });
|
|
65
|
+
|
|
66
|
+
const commitToRevert = claudeCommitHash;
|
|
67
|
+
|
|
68
|
+
// APPROACH 3: Check for modifications before reverting (proactive detection)
|
|
69
|
+
// This is the main strategy - detect if CLAUDE.md was modified after initial commit
|
|
70
|
+
await log(' Checking if CLAUDE.md was modified since initial commit...', { verbose: true });
|
|
71
|
+
const diffResult = await $({ cwd: tempDir })`git diff ${commitToRevert} HEAD -- CLAUDE.md 2>&1`;
|
|
72
|
+
|
|
73
|
+
if (diffResult.stdout && diffResult.stdout.trim()) {
|
|
74
|
+
// CLAUDE.md was modified after initial commit - use manual approach to avoid conflicts
|
|
75
|
+
await log(' CLAUDE.md was modified after initial commit, using manual cleanup...', { verbose: true });
|
|
76
|
+
|
|
77
|
+
// Get the state of CLAUDE.md from before the initial commit (parent of the commit we're reverting)
|
|
78
|
+
const parentCommit = `${commitToRevert}~1`;
|
|
79
|
+
const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:CLAUDE.md 2>&1`;
|
|
80
|
+
|
|
81
|
+
if (parentFileExists.code === 0) {
|
|
82
|
+
// CLAUDE.md existed before the initial commit - restore it to that state
|
|
83
|
+
await log(' CLAUDE.md existed before session, restoring to previous state...', { verbose: true });
|
|
84
|
+
await $({ cwd: tempDir })`git checkout ${parentCommit} -- CLAUDE.md`;
|
|
85
|
+
} else {
|
|
86
|
+
// CLAUDE.md didn't exist before the initial commit - delete it
|
|
87
|
+
await log(' CLAUDE.md was created in session, removing it...', { verbose: true });
|
|
88
|
+
await $({ cwd: tempDir })`git rm -f CLAUDE.md 2>&1`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create a manual revert commit
|
|
92
|
+
const commitResult = await $({ cwd: tempDir })`git commit -m "Revert: Remove CLAUDE.md changes from initial commit" 2>&1`;
|
|
93
|
+
|
|
94
|
+
if (commitResult.code === 0) {
|
|
95
|
+
await log(formatAligned('š¦', 'Committed:', 'CLAUDE.md revert (manual)'));
|
|
96
|
+
|
|
97
|
+
// Push the revert
|
|
98
|
+
const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
|
|
99
|
+
if (pushRevertResult.code === 0) {
|
|
100
|
+
await log(formatAligned('š¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
|
|
101
|
+
} else {
|
|
102
|
+
await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
await log(' Warning: Could not create manual revert commit', { verbose: true });
|
|
106
|
+
await log(` Commit output: ${commitResult.stderr || commitResult.stdout}`, { verbose: true });
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// No modifications detected - safe to use git revert (standard approach)
|
|
110
|
+
await log(' No modifications detected, using standard git revert...', { verbose: true });
|
|
111
|
+
|
|
112
|
+
// FALLBACK 1: Standard git revert
|
|
113
|
+
const revertResult = await $({ cwd: tempDir })`git revert ${commitToRevert} --no-edit 2>&1`;
|
|
114
|
+
if (revertResult.code === 0) {
|
|
115
|
+
await log(formatAligned('š¦', 'Committed:', 'CLAUDE.md revert'));
|
|
116
|
+
|
|
117
|
+
// Push the revert
|
|
118
|
+
const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
|
|
119
|
+
if (pushRevertResult.code === 0) {
|
|
120
|
+
await log(formatAligned('š¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
|
|
121
|
+
} else {
|
|
122
|
+
await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// FALLBACK 2: Handle unexpected conflicts (three-way merge with automatic resolution)
|
|
126
|
+
const revertOutput = revertResult.stderr || revertResult.stdout || '';
|
|
127
|
+
const hasConflict = revertOutput.includes('CONFLICT') || revertOutput.includes('conflict');
|
|
128
|
+
|
|
129
|
+
if (hasConflict) {
|
|
130
|
+
await log(' Unexpected conflict detected, attempting automatic resolution...', { verbose: true });
|
|
131
|
+
|
|
132
|
+
// Check git status to see what files are in conflict
|
|
133
|
+
const statusResult = await $({ cwd: tempDir })`git status --short 2>&1`;
|
|
134
|
+
const statusOutput = statusResult.stdout || '';
|
|
135
|
+
|
|
136
|
+
// Check if CLAUDE.md is in the conflict
|
|
137
|
+
if (statusOutput.includes('CLAUDE.md')) {
|
|
138
|
+
await log(' Resolving CLAUDE.md conflict by restoring pre-session state...', { verbose: true });
|
|
139
|
+
|
|
140
|
+
// Get the state of CLAUDE.md from before the initial commit (parent of the commit we're reverting)
|
|
141
|
+
const parentCommit = `${commitToRevert}~1`;
|
|
142
|
+
const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:CLAUDE.md 2>&1`;
|
|
143
|
+
|
|
144
|
+
if (parentFileExists.code === 0) {
|
|
145
|
+
// CLAUDE.md existed before the initial commit - restore it to that state
|
|
146
|
+
await log(' CLAUDE.md existed before session, restoring to previous state...', { verbose: true });
|
|
147
|
+
await $({ cwd: tempDir })`git checkout ${parentCommit} -- CLAUDE.md`;
|
|
148
|
+
// Stage the resolved CLAUDE.md
|
|
149
|
+
await $({ cwd: tempDir })`git add CLAUDE.md 2>&1`;
|
|
150
|
+
} else {
|
|
151
|
+
// CLAUDE.md didn't exist before the initial commit - delete it
|
|
152
|
+
await log(' CLAUDE.md was created in session, removing it...', { verbose: true });
|
|
153
|
+
await $({ cwd: tempDir })`git rm -f CLAUDE.md 2>&1`;
|
|
154
|
+
// No need to git add since git rm stages the deletion
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Complete the revert with the resolved conflict
|
|
158
|
+
const continueResult = await $({ cwd: tempDir })`git revert --continue --no-edit 2>&1`;
|
|
159
|
+
|
|
160
|
+
if (continueResult.code === 0) {
|
|
161
|
+
await log(formatAligned('š¦', 'Committed:', 'CLAUDE.md revert (conflict resolved)'));
|
|
162
|
+
|
|
163
|
+
// Push the revert
|
|
164
|
+
const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
|
|
165
|
+
if (pushRevertResult.code === 0) {
|
|
166
|
+
await log(formatAligned('š¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
|
|
167
|
+
} else {
|
|
168
|
+
await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
await log(' Warning: Could not complete revert after conflict resolution', { verbose: true });
|
|
172
|
+
await log(` Continue output: ${continueResult.stderr || continueResult.stdout}`, { verbose: true });
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Conflict in some other file, not CLAUDE.md - this is unexpected
|
|
176
|
+
await log(' Warning: Revert conflict in unexpected file(s), aborting revert', { verbose: true });
|
|
177
|
+
await $({ cwd: tempDir })`git revert --abort 2>&1`;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Non-conflict error
|
|
181
|
+
await log(' Warning: Could not revert CLAUDE.md commit', { verbose: true });
|
|
182
|
+
await log(` Revert output: ${revertOutput}`, { verbose: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
reportError(e, {
|
|
188
|
+
context: 'cleanup_claude_file',
|
|
189
|
+
tempDir,
|
|
190
|
+
operation: 'revert_claude_md_commit'
|
|
191
|
+
});
|
|
192
|
+
// If revert fails, that's okay - the task is still complete
|
|
193
|
+
await log(' CLAUDE.md revert failed or not needed', { verbose: true });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Show session summary and handle limit reached scenarios
|
|
198
|
+
export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs = false) => {
|
|
199
|
+
await log('\n=== Session Summary ===');
|
|
200
|
+
|
|
201
|
+
if (sessionId) {
|
|
202
|
+
await log(`ā
Session ID: ${sessionId}`);
|
|
203
|
+
// Always use absolute path for log file display
|
|
204
|
+
const path = (await use('path'));
|
|
205
|
+
const absoluteLogPath = path.resolve(getLogFile());
|
|
206
|
+
await log(`ā
Complete log file: ${absoluteLogPath}`);
|
|
207
|
+
|
|
208
|
+
if (limitReached) {
|
|
209
|
+
await log('\nā° LIMIT REACHED DETECTED!');
|
|
210
|
+
|
|
211
|
+
if (argv.autoContinueOnLimitReset && global.limitResetTime) {
|
|
212
|
+
await log(`\nš AUTO-CONTINUE ON LIMIT RESET ENABLED - Will resume at ${global.limitResetTime}`);
|
|
213
|
+
await autoContinueWhenLimitResets(issueUrl, sessionId, argv, shouldAttachLogs);
|
|
214
|
+
} else {
|
|
215
|
+
// Only show resume recommendation if --no-auto-cleanup was passed
|
|
216
|
+
if (argv.autoCleanup === false) {
|
|
217
|
+
await log('\nš To resume when limit resets, use:\n');
|
|
218
|
+
await log(`./solve.mjs "${issueUrl}" --resume ${sessionId}`);
|
|
219
|
+
|
|
220
|
+
if (global.limitResetTime) {
|
|
221
|
+
await log(`\nš” Or enable auto-continue-on-limit-reset to wait until ${global.limitResetTime}:\n`);
|
|
222
|
+
await log(`./solve.mjs "${issueUrl}" --resume ${sessionId} --auto-continue-on-limit-reset`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await log('\n This will continue from where it left off with full context.\n');
|
|
226
|
+
} else {
|
|
227
|
+
await log('\nā ļø Note: Temporary directory will be automatically cleaned up.');
|
|
228
|
+
await log(' To keep the directory for debugging or resuming, use --no-auto-cleanup');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Show command to resume session in interactive mode only if --no-auto-cleanup was passed
|
|
233
|
+
if (argv.autoCleanup === false) {
|
|
234
|
+
await log('\nš” To continue this session in Claude Code interactive mode:\n');
|
|
235
|
+
await log(` (cd ${tempDir} && claude --resume ${sessionId})`);
|
|
236
|
+
await log('');
|
|
237
|
+
} else {
|
|
238
|
+
await log('\nā ļø Note: Temporary directory will be automatically cleaned up.');
|
|
239
|
+
await log(' To keep the directory for debugging or resuming, use --no-auto-cleanup');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Don't show log preview, it's too technical
|
|
244
|
+
} else {
|
|
245
|
+
await log('ā No session ID extracted');
|
|
246
|
+
// Always use absolute path for log file display
|
|
247
|
+
const logFilePath = path.resolve(getLogFile());
|
|
248
|
+
await log(`š Log file available: ${logFilePath}`);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Verify results by searching for new PRs and comments
|
|
253
|
+
export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null) => {
|
|
254
|
+
await log('\nš Searching for created pull requests or comments...');
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Get the current user's GitHub username
|
|
258
|
+
const userResult = await $`gh api user --jq .login`;
|
|
259
|
+
|
|
260
|
+
if (userResult.code !== 0) {
|
|
261
|
+
throw new Error(`Failed to get current user: ${userResult.stderr ? userResult.stderr.toString() : 'Unknown error'}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const currentUser = userResult.stdout.toString().trim();
|
|
265
|
+
if (!currentUser) {
|
|
266
|
+
throw new Error('Unable to determine current GitHub user');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Search for pull requests created from our branch
|
|
270
|
+
await log('\nš Checking for pull requests from branch ' + branchName + '...');
|
|
271
|
+
|
|
272
|
+
// First, get all PRs from our branch
|
|
273
|
+
const allBranchPrsResult = await $`gh pr list --repo ${owner}/${repo} --head ${branchName} --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
|
|
274
|
+
|
|
275
|
+
if (allBranchPrsResult.code !== 0) {
|
|
276
|
+
await log(' ā ļø Failed to check pull requests');
|
|
277
|
+
// Continue with empty list
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const allBranchPrs = allBranchPrsResult.stdout.toString().trim() ? JSON.parse(allBranchPrsResult.stdout.toString().trim()) : [];
|
|
281
|
+
|
|
282
|
+
// Check if we have any PRs from our branch
|
|
283
|
+
// If auto-PR was created, it should be the one we're working on
|
|
284
|
+
if (allBranchPrs.length > 0) {
|
|
285
|
+
const pr = allBranchPrs[0]; // Get the most recent PR from our branch
|
|
286
|
+
|
|
287
|
+
// If we created a PR earlier in this session, it would be prNumber
|
|
288
|
+
// Or if the PR was updated during the session (updatedAt > referenceTime)
|
|
289
|
+
const isPrFromSession = (prNumber && pr.number.toString() === prNumber) ||
|
|
290
|
+
(prUrl && pr.url === prUrl) ||
|
|
291
|
+
new Date(pr.updatedAt) > referenceTime ||
|
|
292
|
+
new Date(pr.createdAt) > referenceTime;
|
|
293
|
+
|
|
294
|
+
if (isPrFromSession) {
|
|
295
|
+
await log(` ā
Found pull request #${pr.number}: "${pr.title}"`);
|
|
296
|
+
|
|
297
|
+
// Check if PR body has proper issue linking keywords
|
|
298
|
+
const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
|
|
299
|
+
if (prBodyResult.code === 0) {
|
|
300
|
+
const prBody = prBodyResult.stdout.toString();
|
|
301
|
+
const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
|
|
302
|
+
|
|
303
|
+
// Use the new GitHub linking detection library to check for valid keywords
|
|
304
|
+
// This ensures we only detect actual GitHub-recognized linking keywords
|
|
305
|
+
// (fixes, closes, resolves and their variants) in proper format
|
|
306
|
+
// See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
|
|
307
|
+
const hasLinkingKeyword = hasGitHubLinkingKeyword(
|
|
308
|
+
prBody,
|
|
309
|
+
issueNumber,
|
|
310
|
+
argv.fork ? owner : null,
|
|
311
|
+
argv.fork ? repo : null
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!hasLinkingKeyword) {
|
|
315
|
+
await log(` š Updating PR body to link issue #${issueNumber}...`);
|
|
316
|
+
|
|
317
|
+
// Add proper issue reference to the PR body
|
|
318
|
+
const linkingText = `\n\nFixes ${issueRef}`;
|
|
319
|
+
const updatedBody = prBody + linkingText;
|
|
320
|
+
|
|
321
|
+
// Use --body-file instead of --body to avoid command-line length limits
|
|
322
|
+
// and special character escaping issues that can cause hangs/timeouts
|
|
323
|
+
const fs = (await use('fs')).promises;
|
|
324
|
+
const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
|
|
325
|
+
await fs.writeFile(tempBodyFile, updatedBody);
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
|
|
329
|
+
|
|
330
|
+
// Clean up temp file
|
|
331
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
332
|
+
|
|
333
|
+
if (updateResult.code === 0) {
|
|
334
|
+
await log(` ā
Updated PR body to include "Fixes ${issueRef}"`);
|
|
335
|
+
} else {
|
|
336
|
+
await log(` ā ļø Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
|
|
337
|
+
}
|
|
338
|
+
} catch (updateError) {
|
|
339
|
+
// Clean up temp file on error
|
|
340
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
341
|
+
throw updateError;
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
await log(' ā
PR body already contains issue reference');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check if PR is ready for review (convert from draft if necessary)
|
|
349
|
+
if (pr.isDraft) {
|
|
350
|
+
await log(' š Converting PR from draft to ready for review...');
|
|
351
|
+
const readyResult = await $`gh pr ready ${pr.number} --repo ${owner}/${repo}`;
|
|
352
|
+
if (readyResult.code === 0) {
|
|
353
|
+
await log(' ā
PR converted to ready for review');
|
|
354
|
+
} else {
|
|
355
|
+
await log(` ā ļø Could not convert PR to ready (${readyResult.stderr ? readyResult.stderr.toString().trim() : 'unknown error'})`);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
await log(' ā
PR is already ready for review', { verbose: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Upload log file to PR if requested
|
|
362
|
+
let logUploadSuccess = false;
|
|
363
|
+
if (shouldAttachLogs) {
|
|
364
|
+
await log('\nš Uploading solution draft log to Pull Request...');
|
|
365
|
+
logUploadSuccess = await attachLogToGitHub({
|
|
366
|
+
logFile: getLogFile(),
|
|
367
|
+
targetType: 'pr',
|
|
368
|
+
targetNumber: pr.number,
|
|
369
|
+
owner,
|
|
370
|
+
repo,
|
|
371
|
+
$,
|
|
372
|
+
log,
|
|
373
|
+
sanitizeLogContent,
|
|
374
|
+
verbose: argv.verbose,
|
|
375
|
+
sessionId,
|
|
376
|
+
tempDir,
|
|
377
|
+
anthropicTotalCostUSD,
|
|
378
|
+
// Pass agent tool pricing data when available
|
|
379
|
+
publicPricingEstimate,
|
|
380
|
+
pricingInfo
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await log('\nš SUCCESS: A solution draft has been prepared as a pull request');
|
|
385
|
+
await log(`š URL: ${pr.url}`);
|
|
386
|
+
if (shouldAttachLogs && logUploadSuccess) {
|
|
387
|
+
await log('š Solution draft log has been attached to the Pull Request');
|
|
388
|
+
} else if (shouldAttachLogs && !logUploadSuccess) {
|
|
389
|
+
await log('ā ļø Solution draft log upload was requested but failed');
|
|
390
|
+
}
|
|
391
|
+
await log('\n⨠Please review the pull request for the proposed solution draft.');
|
|
392
|
+
// Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
|
|
393
|
+
if (!argv.watch && !shouldRestart) {
|
|
394
|
+
await safeExit(0, 'Process completed successfully');
|
|
395
|
+
}
|
|
396
|
+
return; // Return normally for watch mode or auto-restart
|
|
397
|
+
} else {
|
|
398
|
+
await log(` ā¹ļø Found pull request #${pr.number} but it appears to be from a different session`);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
await log(` ā¹ļø No pull requests found from branch ${branchName}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If no PR found, search for recent comments on the issue
|
|
405
|
+
await log('\nš Checking for new comments on issue #' + issueNumber + '...');
|
|
406
|
+
|
|
407
|
+
// Get all comments and filter them
|
|
408
|
+
const allCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments`;
|
|
409
|
+
|
|
410
|
+
if (allCommentsResult.code !== 0) {
|
|
411
|
+
await log(' ā ļø Failed to check comments');
|
|
412
|
+
// Continue with empty list
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const allComments = JSON.parse(allCommentsResult.stdout.toString().trim() || '[]');
|
|
416
|
+
|
|
417
|
+
// Filter for new comments by current user
|
|
418
|
+
const newCommentsByUser = allComments.filter(comment =>
|
|
419
|
+
comment.user.login === currentUser && new Date(comment.created_at) > referenceTime
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
if (newCommentsByUser.length > 0) {
|
|
423
|
+
const lastComment = newCommentsByUser[newCommentsByUser.length - 1];
|
|
424
|
+
await log(` ā
Found new comment by ${currentUser}`);
|
|
425
|
+
|
|
426
|
+
// Upload log file to issue if requested
|
|
427
|
+
if (shouldAttachLogs) {
|
|
428
|
+
await log('\nš Uploading solution draft log to issue...');
|
|
429
|
+
await attachLogToGitHub({
|
|
430
|
+
logFile: getLogFile(),
|
|
431
|
+
targetType: 'issue',
|
|
432
|
+
targetNumber: issueNumber,
|
|
433
|
+
owner,
|
|
434
|
+
repo,
|
|
435
|
+
$,
|
|
436
|
+
log,
|
|
437
|
+
sanitizeLogContent,
|
|
438
|
+
verbose: argv.verbose,
|
|
439
|
+
sessionId,
|
|
440
|
+
tempDir,
|
|
441
|
+
anthropicTotalCostUSD,
|
|
442
|
+
// Pass agent tool pricing data when available
|
|
443
|
+
publicPricingEstimate,
|
|
444
|
+
pricingInfo
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
await log('\nš¬ SUCCESS: Comment posted on issue');
|
|
449
|
+
await log(`š URL: ${lastComment.html_url}`);
|
|
450
|
+
if (shouldAttachLogs) {
|
|
451
|
+
await log('š Solution draft log has been attached to the issue');
|
|
452
|
+
}
|
|
453
|
+
await log('\n⨠A clarifying comment has been added to the issue.');
|
|
454
|
+
// Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
|
|
455
|
+
if (!argv.watch && !shouldRestart) {
|
|
456
|
+
await safeExit(0, 'Process completed successfully');
|
|
457
|
+
}
|
|
458
|
+
return; // Return normally for watch mode or auto-restart
|
|
459
|
+
} else if (allComments.length > 0) {
|
|
460
|
+
await log(` ā¹ļø Issue has ${allComments.length} existing comment(s)`);
|
|
461
|
+
} else {
|
|
462
|
+
await log(' ā¹ļø No comments found on issue');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If neither found, it might not have been necessary to create either
|
|
466
|
+
await log('\nš No new pull request or comment was created.');
|
|
467
|
+
await log(' The issue may have been resolved differently or required no action.');
|
|
468
|
+
await log('\nš” Review the session log for details:');
|
|
469
|
+
// Always use absolute path for log file display
|
|
470
|
+
const reviewLogPath = path.resolve(getLogFile());
|
|
471
|
+
await log(` ${reviewLogPath}`);
|
|
472
|
+
// Don't exit if watch mode is enabled - it needs to continue monitoring
|
|
473
|
+
if (!argv.watch) {
|
|
474
|
+
await safeExit(0, 'Process completed successfully');
|
|
475
|
+
}
|
|
476
|
+
return; // Return normally for watch mode
|
|
477
|
+
|
|
478
|
+
} catch (searchError) {
|
|
479
|
+
reportError(searchError, {
|
|
480
|
+
context: 'verify_pr_creation',
|
|
481
|
+
issueNumber,
|
|
482
|
+
operation: 'search_for_pr'
|
|
483
|
+
});
|
|
484
|
+
await log('\nā ļø Could not verify results:', searchError.message);
|
|
485
|
+
await log('\nš” Check the log file for details:');
|
|
486
|
+
// Always use absolute path for log file display
|
|
487
|
+
const checkLogPath = path.resolve(getLogFile());
|
|
488
|
+
await log(` ${checkLogPath}`);
|
|
489
|
+
// Don't exit if watch mode is enabled - it needs to continue monitoring
|
|
490
|
+
if (!argv.watch) {
|
|
491
|
+
await safeExit(0, 'Process completed successfully');
|
|
492
|
+
}
|
|
493
|
+
return; // Return normally for watch mode
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Handle execution errors with log attachment
|
|
498
|
+
export const handleExecutionError = async (error, shouldAttachLogs, owner, repo, argv = {}) => {
|
|
499
|
+
const { cleanErrorMessage } = await import('./lib.mjs');
|
|
500
|
+
await log('Error executing command:', cleanErrorMessage(error));
|
|
501
|
+
await log(`Stack trace: ${error.stack}`, { verbose: true });
|
|
502
|
+
|
|
503
|
+
// If --attach-logs is enabled, try to attach failure logs
|
|
504
|
+
if (shouldAttachLogs && getLogFile()) {
|
|
505
|
+
await log('\nš Attempting to attach failure logs...');
|
|
506
|
+
|
|
507
|
+
// Try to attach to existing PR first
|
|
508
|
+
if (global.createdPR && global.createdPR.number) {
|
|
509
|
+
try {
|
|
510
|
+
const logUploadSuccess = await attachLogToGitHub({
|
|
511
|
+
logFile: getLogFile(),
|
|
512
|
+
targetType: 'pr',
|
|
513
|
+
targetNumber: global.createdPR.number,
|
|
514
|
+
owner,
|
|
515
|
+
repo,
|
|
516
|
+
$,
|
|
517
|
+
log,
|
|
518
|
+
sanitizeLogContent,
|
|
519
|
+
verbose: argv.verbose || false,
|
|
520
|
+
errorMessage: cleanErrorMessage(error)
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
if (logUploadSuccess) {
|
|
524
|
+
await log('š Failure log attached to Pull Request');
|
|
525
|
+
}
|
|
526
|
+
} catch (attachError) {
|
|
527
|
+
reportError(attachError, {
|
|
528
|
+
context: 'attach_success_log',
|
|
529
|
+
prNumber: global.createdPR?.number,
|
|
530
|
+
operation: 'attach_log_to_pr'
|
|
531
|
+
});
|
|
532
|
+
await log(`ā ļø Could not attach failure log: ${attachError.message}`, { level: 'warning' });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// If --auto-close-pull-request-on-fail is enabled, close the PR
|
|
538
|
+
if (argv.autoClosePullRequestOnFail && global.createdPR && global.createdPR.number) {
|
|
539
|
+
await log('\nš Auto-closing pull request due to failure...');
|
|
540
|
+
try {
|
|
541
|
+
const result = await $`gh pr close ${global.createdPR.number} --repo ${owner}/${repo} --comment "Auto-closed due to execution failure. Logs have been attached for debugging."`;
|
|
542
|
+
if (result.exitCode === 0) {
|
|
543
|
+
await log('ā
Pull request closed successfully');
|
|
544
|
+
} else {
|
|
545
|
+
await log(`ā ļø Could not close pull request: ${result.stderr}`, { level: 'warning' });
|
|
546
|
+
}
|
|
547
|
+
} catch (closeError) {
|
|
548
|
+
reportError(closeError, {
|
|
549
|
+
context: 'close_success_pr',
|
|
550
|
+
prNumber: global.createdPR?.number,
|
|
551
|
+
operation: 'close_pull_request'
|
|
552
|
+
});
|
|
553
|
+
await log(`ā ļø Could not close pull request: ${closeError.message}`, { level: 'warning' });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
await safeExit(1, 'Execution error');
|
|
558
|
+
};
|