@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,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Watch mode module for solve.mjs
|
|
5
|
+
* Monitors for feedback continuously and restarts when changes are detected
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
9
|
+
// If not, fetch it (when running standalone)
|
|
10
|
+
if (typeof globalThis.use === 'undefined') {
|
|
11
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
12
|
+
}
|
|
13
|
+
const use = globalThis.use;
|
|
14
|
+
|
|
15
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
16
|
+
const { $ } = await use('command-stream');
|
|
17
|
+
|
|
18
|
+
// Import shared library functions
|
|
19
|
+
const lib = await import('./lib.mjs');
|
|
20
|
+
const { log, cleanErrorMessage, formatAligned } = lib;
|
|
21
|
+
|
|
22
|
+
// Import feedback detection functions
|
|
23
|
+
const feedbackLib = await import('./solve.feedback.lib.mjs');
|
|
24
|
+
// Import Sentry integration
|
|
25
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
26
|
+
const { reportError } = sentryLib;
|
|
27
|
+
|
|
28
|
+
const { detectAndCountFeedback } = feedbackLib;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if PR has been merged
|
|
32
|
+
*/
|
|
33
|
+
const checkPRMerged = async (owner, repo, prNumber) => {
|
|
34
|
+
try {
|
|
35
|
+
const prResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.merged'`;
|
|
36
|
+
if (prResult.code === 0) {
|
|
37
|
+
return prResult.stdout.toString().trim() === 'true';
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reportError(error, {
|
|
41
|
+
context: 'check_pr_merged',
|
|
42
|
+
owner,
|
|
43
|
+
repo,
|
|
44
|
+
prNumber,
|
|
45
|
+
operation: 'check_merge_status'
|
|
46
|
+
});
|
|
47
|
+
// If we can't check, assume not merged
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if there are uncommitted changes in the repository
|
|
55
|
+
*/
|
|
56
|
+
const checkForUncommittedChanges = async (tempDir, $) => {
|
|
57
|
+
try {
|
|
58
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
59
|
+
if (gitStatusResult.code === 0) {
|
|
60
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
61
|
+
return statusOutput.length > 0;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
reportError(error, {
|
|
65
|
+
context: 'check_pr_closed',
|
|
66
|
+
tempDir,
|
|
67
|
+
operation: 'check_close_status'
|
|
68
|
+
});
|
|
69
|
+
// If we can't check, assume no uncommitted changes
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Monitor for feedback in a loop and trigger restart when detected
|
|
76
|
+
*/
|
|
77
|
+
export const watchForFeedback = async (params) => {
|
|
78
|
+
const {
|
|
79
|
+
issueUrl,
|
|
80
|
+
owner,
|
|
81
|
+
repo,
|
|
82
|
+
issueNumber,
|
|
83
|
+
prNumber,
|
|
84
|
+
prBranch,
|
|
85
|
+
branchName,
|
|
86
|
+
tempDir,
|
|
87
|
+
argv
|
|
88
|
+
} = params;
|
|
89
|
+
|
|
90
|
+
const watchInterval = argv.watchInterval || 60; // seconds
|
|
91
|
+
const isTemporaryWatch = argv.temporaryWatch || false;
|
|
92
|
+
const maxAutoRestartIterations = argv.autoRestartMaxIterations || 3;
|
|
93
|
+
|
|
94
|
+
// Track latest session data across all iterations for accurate pricing
|
|
95
|
+
let latestSessionId = null;
|
|
96
|
+
let latestAnthropicCost = null;
|
|
97
|
+
|
|
98
|
+
// Track consecutive API errors for retry limit
|
|
99
|
+
const MAX_API_ERROR_RETRIES = 3;
|
|
100
|
+
let consecutiveApiErrors = 0;
|
|
101
|
+
let currentBackoffSeconds = watchInterval;
|
|
102
|
+
|
|
103
|
+
await log('');
|
|
104
|
+
if (isTemporaryWatch) {
|
|
105
|
+
await log(formatAligned('🔄', 'AUTO-RESTART MODE ACTIVE', ''));
|
|
106
|
+
await log(formatAligned('', 'Purpose:', 'Complete unfinished work from previous run', 2));
|
|
107
|
+
await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
|
|
108
|
+
await log(formatAligned('', 'Mode:', 'Auto-restart (NOT --watch mode)', 2));
|
|
109
|
+
await log(formatAligned('', 'Stop conditions:', 'All changes committed OR PR merged OR max iterations reached', 2));
|
|
110
|
+
await log(formatAligned('', 'Max iterations:', `${maxAutoRestartIterations}`, 2));
|
|
111
|
+
await log(formatAligned('', 'Note:', 'No wait time between iterations in auto-restart mode', 2));
|
|
112
|
+
} else {
|
|
113
|
+
await log(formatAligned('👁️', 'WATCH MODE ACTIVATED', ''));
|
|
114
|
+
await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds`, 2));
|
|
115
|
+
await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
|
|
116
|
+
await log(formatAligned('', 'Stop condition:', 'PR merged by maintainer', 2));
|
|
117
|
+
}
|
|
118
|
+
await log('');
|
|
119
|
+
await log('Press Ctrl+C to stop watching manually');
|
|
120
|
+
await log('');
|
|
121
|
+
|
|
122
|
+
// let lastCheckTime = new Date(); // Not currently used
|
|
123
|
+
let iteration = 0;
|
|
124
|
+
let autoRestartCount = 0;
|
|
125
|
+
let firstIterationInTemporaryMode = isTemporaryWatch;
|
|
126
|
+
|
|
127
|
+
while (true) {
|
|
128
|
+
iteration++;
|
|
129
|
+
const currentTime = new Date();
|
|
130
|
+
|
|
131
|
+
// Check if PR is merged
|
|
132
|
+
const isMerged = await checkPRMerged(owner, repo, prNumber);
|
|
133
|
+
if (isMerged) {
|
|
134
|
+
await log('');
|
|
135
|
+
await log(formatAligned('🎉', 'PR MERGED!', 'Stopping watch mode'));
|
|
136
|
+
await log(formatAligned('', 'Pull request:', `#${prNumber} has been merged`, 2));
|
|
137
|
+
await log('');
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// In temporary watch mode, check if all changes have been committed
|
|
142
|
+
if (isTemporaryWatch && !firstIterationInTemporaryMode) {
|
|
143
|
+
const hasUncommitted = await checkForUncommittedChanges(tempDir, $);
|
|
144
|
+
if (!hasUncommitted) {
|
|
145
|
+
await log('');
|
|
146
|
+
await log(formatAligned('✅', 'CHANGES COMMITTED!', 'Exiting auto-restart mode'));
|
|
147
|
+
await log(formatAligned('', 'All uncommitted changes have been resolved', '', 2));
|
|
148
|
+
await log('');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if we've reached max iterations
|
|
153
|
+
if (autoRestartCount >= maxAutoRestartIterations) {
|
|
154
|
+
await log('');
|
|
155
|
+
await log(formatAligned('⚠️', 'MAX ITERATIONS REACHED', `Exiting auto-restart mode after ${autoRestartCount} attempts`));
|
|
156
|
+
await log(formatAligned('', 'Some uncommitted changes may remain', '', 2));
|
|
157
|
+
await log(formatAligned('', 'Please review and commit manually if needed', '', 2));
|
|
158
|
+
await log('');
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check for feedback or handle initial uncommitted changes
|
|
164
|
+
if (firstIterationInTemporaryMode) {
|
|
165
|
+
await log(formatAligned('🔄', 'Initial restart:', 'Handling uncommitted changes...'));
|
|
166
|
+
} else {
|
|
167
|
+
await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Get PR merge state status
|
|
172
|
+
const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
|
|
173
|
+
const mergeStateStatus = prStateResult.code === 0 ? prStateResult.stdout.toString().trim() : null;
|
|
174
|
+
|
|
175
|
+
// Detect feedback using existing function
|
|
176
|
+
let { feedbackLines } = await detectAndCountFeedback({
|
|
177
|
+
prNumber,
|
|
178
|
+
branchName: prBranch || branchName,
|
|
179
|
+
owner,
|
|
180
|
+
repo,
|
|
181
|
+
issueNumber,
|
|
182
|
+
isContinueMode: true,
|
|
183
|
+
argv: { ...argv, verbose: false }, // Reduce verbosity in watch mode
|
|
184
|
+
mergeStateStatus,
|
|
185
|
+
workStartTime: null, // In watch mode, we want to count all comments as potential feedback
|
|
186
|
+
log,
|
|
187
|
+
formatAligned,
|
|
188
|
+
cleanErrorMessage,
|
|
189
|
+
$
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Check if there's any feedback or if it's the first iteration in temporary mode
|
|
193
|
+
const hasFeedback = feedbackLines && feedbackLines.length > 0;
|
|
194
|
+
|
|
195
|
+
// In temporary watch mode, also check for uncommitted changes as a restart trigger
|
|
196
|
+
let hasUncommittedInTempMode = false;
|
|
197
|
+
if (isTemporaryWatch && !firstIterationInTemporaryMode) {
|
|
198
|
+
hasUncommittedInTempMode = await checkForUncommittedChanges(tempDir, $);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const shouldRestart = hasFeedback || firstIterationInTemporaryMode || hasUncommittedInTempMode;
|
|
202
|
+
|
|
203
|
+
if (shouldRestart) {
|
|
204
|
+
// Handle uncommitted changes in temporary watch mode (first iteration or subsequent)
|
|
205
|
+
if (firstIterationInTemporaryMode || hasUncommittedInTempMode) {
|
|
206
|
+
await log(formatAligned('📝', 'UNCOMMITTED CHANGES:', '', 2));
|
|
207
|
+
// Get uncommitted changes for display
|
|
208
|
+
try {
|
|
209
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
210
|
+
if (gitStatusResult.code === 0) {
|
|
211
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
212
|
+
for (const line of statusOutput.split('\n')) {
|
|
213
|
+
await log(formatAligned('', `• ${line}`, '', 4));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
reportError(e, {
|
|
218
|
+
context: 'check_claude_file_exists',
|
|
219
|
+
owner,
|
|
220
|
+
repo,
|
|
221
|
+
branchName,
|
|
222
|
+
operation: 'check_file_in_branch'
|
|
223
|
+
});
|
|
224
|
+
// Ignore errors
|
|
225
|
+
}
|
|
226
|
+
await log('');
|
|
227
|
+
|
|
228
|
+
// Increment auto-restart counter and log restart number
|
|
229
|
+
autoRestartCount++;
|
|
230
|
+
const restartLabel = firstIterationInTemporaryMode ? 'Initial restart' : `Restart ${autoRestartCount}/${maxAutoRestartIterations}`;
|
|
231
|
+
await log(formatAligned('🔄', `${restartLabel}:`, `Running ${argv.tool.toUpperCase()} to handle uncommitted changes...`));
|
|
232
|
+
|
|
233
|
+
// Post a comment to PR about auto-restart
|
|
234
|
+
if (prNumber) {
|
|
235
|
+
try {
|
|
236
|
+
const remainingIterations = maxAutoRestartIterations - autoRestartCount;
|
|
237
|
+
|
|
238
|
+
// Get uncommitted files list for the comment
|
|
239
|
+
let uncommittedFilesList = '';
|
|
240
|
+
try {
|
|
241
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
242
|
+
if (gitStatusResult.code === 0) {
|
|
243
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
244
|
+
if (statusOutput) {
|
|
245
|
+
uncommittedFilesList = '\n\n**Uncommitted files:**\n```\n' + statusOutput + '\n```';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// If we can't get the file list, continue without it
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const commentBody = `## 🔄 Auto-restart ${autoRestartCount}/${maxAutoRestartIterations}\n\nDetected uncommitted changes from previous run. Starting new session to review and commit them.${uncommittedFilesList}\n\n---\n*Auto-restart will stop after changes are committed or after ${remainingIterations} more iteration${remainingIterations !== 1 ? 's' : ''}. Please wait until working session will end and give your feedback.*`;
|
|
253
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
254
|
+
await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
|
|
255
|
+
} catch (commentError) {
|
|
256
|
+
reportError(commentError, {
|
|
257
|
+
context: 'post_auto_restart_comment',
|
|
258
|
+
owner,
|
|
259
|
+
repo,
|
|
260
|
+
prNumber,
|
|
261
|
+
operation: 'comment_on_pr'
|
|
262
|
+
});
|
|
263
|
+
// Don't fail if comment posting fails
|
|
264
|
+
await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Add uncommitted changes info to feedbackLines for the run
|
|
269
|
+
if (!feedbackLines) {
|
|
270
|
+
feedbackLines = [];
|
|
271
|
+
}
|
|
272
|
+
feedbackLines.push('');
|
|
273
|
+
feedbackLines.push(`⚠️ UNCOMMITTED CHANGES DETECTED (Auto-restart ${autoRestartCount}/${maxAutoRestartIterations}):`);
|
|
274
|
+
feedbackLines.push('The following uncommitted changes were found in the repository:');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
278
|
+
if (gitStatusResult.code === 0) {
|
|
279
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
280
|
+
feedbackLines.push('');
|
|
281
|
+
for (const line of statusOutput.split('\n')) {
|
|
282
|
+
feedbackLines.push(` ${line}`);
|
|
283
|
+
}
|
|
284
|
+
feedbackLines.push('');
|
|
285
|
+
feedbackLines.push('IMPORTANT: You MUST handle these uncommitted changes by either:');
|
|
286
|
+
feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
|
|
287
|
+
feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
|
|
288
|
+
feedbackLines.push('');
|
|
289
|
+
feedbackLines.push('DO NOT leave uncommitted changes behind. The session will auto-restart until all changes are resolved.');
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
reportError(e, {
|
|
293
|
+
context: 'recheck_claude_file',
|
|
294
|
+
owner,
|
|
295
|
+
repo,
|
|
296
|
+
branchName,
|
|
297
|
+
operation: 'verify_file_in_branch'
|
|
298
|
+
});
|
|
299
|
+
// Ignore errors
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
await log(formatAligned('📢', 'FEEDBACK DETECTED!', '', 2));
|
|
303
|
+
feedbackLines.forEach(async line => {
|
|
304
|
+
await log(formatAligned('', `• ${line}`, '', 4));
|
|
305
|
+
});
|
|
306
|
+
await log('');
|
|
307
|
+
await log(formatAligned('🔄', 'Restarting:', `Re-running ${argv.tool.toUpperCase()} to handle feedback...`));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Import necessary modules for tool execution
|
|
311
|
+
const memoryCheck = await import('./memory-check.mjs');
|
|
312
|
+
const { getResourceSnapshot } = memoryCheck;
|
|
313
|
+
|
|
314
|
+
let toolResult;
|
|
315
|
+
if (argv.tool === 'opencode') {
|
|
316
|
+
// Use OpenCode
|
|
317
|
+
const opencodeExecLib = await import('./opencode.lib.mjs');
|
|
318
|
+
const { executeOpenCode } = opencodeExecLib;
|
|
319
|
+
|
|
320
|
+
// Get opencode path
|
|
321
|
+
const opencodePath = argv.opencodePath || 'opencode';
|
|
322
|
+
|
|
323
|
+
toolResult = await executeOpenCode({
|
|
324
|
+
issueUrl,
|
|
325
|
+
issueNumber,
|
|
326
|
+
prNumber,
|
|
327
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
328
|
+
branchName,
|
|
329
|
+
tempDir,
|
|
330
|
+
isContinueMode: true,
|
|
331
|
+
mergeStateStatus,
|
|
332
|
+
forkedRepo: argv.fork,
|
|
333
|
+
feedbackLines,
|
|
334
|
+
owner,
|
|
335
|
+
repo,
|
|
336
|
+
argv,
|
|
337
|
+
log,
|
|
338
|
+
formatAligned,
|
|
339
|
+
getResourceSnapshot,
|
|
340
|
+
opencodePath,
|
|
341
|
+
$
|
|
342
|
+
});
|
|
343
|
+
} else if (argv.tool === 'codex') {
|
|
344
|
+
// Use Codex
|
|
345
|
+
const codexExecLib = await import('./codex.lib.mjs');
|
|
346
|
+
const { executeCodex } = codexExecLib;
|
|
347
|
+
|
|
348
|
+
// Get codex path
|
|
349
|
+
const codexPath = argv.codexPath || 'codex';
|
|
350
|
+
|
|
351
|
+
toolResult = await executeCodex({
|
|
352
|
+
issueUrl,
|
|
353
|
+
issueNumber,
|
|
354
|
+
prNumber,
|
|
355
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
356
|
+
branchName,
|
|
357
|
+
tempDir,
|
|
358
|
+
isContinueMode: true,
|
|
359
|
+
mergeStateStatus,
|
|
360
|
+
forkedRepo: argv.fork,
|
|
361
|
+
feedbackLines,
|
|
362
|
+
forkActionsUrl: null,
|
|
363
|
+
owner,
|
|
364
|
+
repo,
|
|
365
|
+
argv,
|
|
366
|
+
log,
|
|
367
|
+
setLogFile: () => {},
|
|
368
|
+
getLogFile: () => '',
|
|
369
|
+
formatAligned,
|
|
370
|
+
getResourceSnapshot,
|
|
371
|
+
codexPath,
|
|
372
|
+
$
|
|
373
|
+
});
|
|
374
|
+
} else if (argv.tool === 'agent') {
|
|
375
|
+
// Use Agent
|
|
376
|
+
const agentExecLib = await import('./agent.lib.mjs');
|
|
377
|
+
const { executeAgent } = agentExecLib;
|
|
378
|
+
|
|
379
|
+
// Get agent path
|
|
380
|
+
const agentPath = argv.agentPath || 'agent';
|
|
381
|
+
|
|
382
|
+
toolResult = await executeAgent({
|
|
383
|
+
issueUrl,
|
|
384
|
+
issueNumber,
|
|
385
|
+
prNumber,
|
|
386
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
387
|
+
branchName,
|
|
388
|
+
tempDir,
|
|
389
|
+
isContinueMode: true,
|
|
390
|
+
mergeStateStatus,
|
|
391
|
+
forkedRepo: argv.fork,
|
|
392
|
+
feedbackLines,
|
|
393
|
+
forkActionsUrl: null,
|
|
394
|
+
owner,
|
|
395
|
+
repo,
|
|
396
|
+
argv,
|
|
397
|
+
log,
|
|
398
|
+
formatAligned,
|
|
399
|
+
getResourceSnapshot,
|
|
400
|
+
agentPath,
|
|
401
|
+
$
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
// Use Claude (default)
|
|
405
|
+
const claudeExecLib = await import('./claude.lib.mjs');
|
|
406
|
+
const { executeClaude } = claudeExecLib;
|
|
407
|
+
|
|
408
|
+
// Get claude path
|
|
409
|
+
const claudePath = argv.claudePath || 'claude';
|
|
410
|
+
|
|
411
|
+
toolResult = await executeClaude({
|
|
412
|
+
issueUrl,
|
|
413
|
+
issueNumber,
|
|
414
|
+
prNumber,
|
|
415
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
416
|
+
branchName,
|
|
417
|
+
tempDir,
|
|
418
|
+
isContinueMode: true,
|
|
419
|
+
mergeStateStatus,
|
|
420
|
+
forkedRepo: argv.fork,
|
|
421
|
+
feedbackLines,
|
|
422
|
+
owner,
|
|
423
|
+
repo,
|
|
424
|
+
argv,
|
|
425
|
+
log,
|
|
426
|
+
formatAligned,
|
|
427
|
+
getResourceSnapshot,
|
|
428
|
+
claudePath,
|
|
429
|
+
$
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!toolResult.success) {
|
|
434
|
+
// Check if this is an API error (404, 401, 400, etc.) from the result
|
|
435
|
+
const isApiError = toolResult.result &&
|
|
436
|
+
(toolResult.result.includes('API Error:') ||
|
|
437
|
+
toolResult.result.includes('not_found_error') ||
|
|
438
|
+
toolResult.result.includes('authentication_error') ||
|
|
439
|
+
toolResult.result.includes('invalid_request_error'));
|
|
440
|
+
|
|
441
|
+
if (isApiError) {
|
|
442
|
+
consecutiveApiErrors++;
|
|
443
|
+
await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, `API error detected (${consecutiveApiErrors}/${MAX_API_ERROR_RETRIES})`, 2));
|
|
444
|
+
|
|
445
|
+
if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
|
|
446
|
+
await log('');
|
|
447
|
+
await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
|
|
448
|
+
await log(formatAligned('', 'Error details:', toolResult.result || 'Unknown API error', 2));
|
|
449
|
+
await log(formatAligned('', 'Consecutive failures:', `${consecutiveApiErrors}`, 2));
|
|
450
|
+
await log(formatAligned('', 'Action:', 'Exiting watch mode to prevent infinite loop', 2));
|
|
451
|
+
await log('');
|
|
452
|
+
await log('Please check:');
|
|
453
|
+
await log(' 1. The model name is valid for the selected tool');
|
|
454
|
+
await log(' 2. You have proper authentication configured');
|
|
455
|
+
await log(' 3. The API endpoint is accessible');
|
|
456
|
+
await log('');
|
|
457
|
+
break; // Exit the watch loop
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Apply exponential backoff for API errors
|
|
461
|
+
currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 300); // Cap at 5 minutes
|
|
462
|
+
await log(formatAligned('', 'Backing off:', `Will retry after ${currentBackoffSeconds} seconds`, 2));
|
|
463
|
+
} else {
|
|
464
|
+
// Non-API error, reset consecutive counter
|
|
465
|
+
consecutiveApiErrors = 0;
|
|
466
|
+
currentBackoffSeconds = watchInterval;
|
|
467
|
+
await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, 'Will retry in next check', 2));
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
// Success - reset error counters
|
|
471
|
+
consecutiveApiErrors = 0;
|
|
472
|
+
currentBackoffSeconds = watchInterval;
|
|
473
|
+
|
|
474
|
+
// Capture latest session data from successful execution for accurate pricing
|
|
475
|
+
if (toolResult.sessionId) {
|
|
476
|
+
latestSessionId = toolResult.sessionId;
|
|
477
|
+
latestAnthropicCost = toolResult.anthropicTotalCostUSD;
|
|
478
|
+
if (argv.verbose) {
|
|
479
|
+
await log(` 📊 Session data captured: ${latestSessionId}`, { verbose: true });
|
|
480
|
+
if (latestAnthropicCost !== null && latestAnthropicCost !== undefined) {
|
|
481
|
+
await log(` 💰 Anthropic cost: $${latestAnthropicCost.toFixed(6)}`, { verbose: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await log('');
|
|
487
|
+
if (isTemporaryWatch) {
|
|
488
|
+
await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking for remaining changes...'));
|
|
489
|
+
} else {
|
|
490
|
+
await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Resuming watch mode...'));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Note: lastCheckTime tracking removed as it was not being used
|
|
495
|
+
|
|
496
|
+
// Clear the first iteration flag after handling initial uncommitted changes
|
|
497
|
+
if (firstIterationInTemporaryMode) {
|
|
498
|
+
firstIterationInTemporaryMode = false;
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
await log(formatAligned('', 'No feedback detected', 'Continuing to watch...', 2));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
} catch (error) {
|
|
505
|
+
reportError(error, {
|
|
506
|
+
context: 'watch_pr_general',
|
|
507
|
+
prNumber,
|
|
508
|
+
owner,
|
|
509
|
+
repo,
|
|
510
|
+
operation: 'watch_pull_request'
|
|
511
|
+
});
|
|
512
|
+
await log(formatAligned('⚠️', 'Check failed:', cleanErrorMessage(error), 2));
|
|
513
|
+
if (!isTemporaryWatch) {
|
|
514
|
+
await log(formatAligned('', 'Will retry in:', `${watchInterval} seconds`, 2));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Wait for next interval (skip wait entirely in temporary watch mode / auto-restart)
|
|
519
|
+
if (!isTemporaryWatch && !firstIterationInTemporaryMode) {
|
|
520
|
+
// Use backoff interval if we have consecutive API errors
|
|
521
|
+
const actualWaitSeconds = consecutiveApiErrors > 0 ? currentBackoffSeconds : watchInterval;
|
|
522
|
+
const actualWaitMs = actualWaitSeconds * 1000;
|
|
523
|
+
await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
|
|
524
|
+
await log(''); // Blank line for readability
|
|
525
|
+
await new Promise(resolve => setTimeout(resolve, actualWaitMs));
|
|
526
|
+
} else if (isTemporaryWatch && !firstIterationInTemporaryMode) {
|
|
527
|
+
// In auto-restart mode, check immediately without waiting
|
|
528
|
+
await log(formatAligned('', 'Checking immediately for uncommitted changes...', '', 2));
|
|
529
|
+
await log(''); // Blank line for readability
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Return latest session data for accurate pricing in log uploads
|
|
534
|
+
return {
|
|
535
|
+
latestSessionId,
|
|
536
|
+
latestAnthropicCost
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Start watch mode after initial execution
|
|
542
|
+
*/
|
|
543
|
+
export const startWatchMode = async (params) => {
|
|
544
|
+
const { argv } = params;
|
|
545
|
+
|
|
546
|
+
if (argv.verbose) {
|
|
547
|
+
await log('');
|
|
548
|
+
await log('📊 startWatchMode called with:', { verbose: true });
|
|
549
|
+
await log(` argv.watch: ${argv.watch}`, { verbose: true });
|
|
550
|
+
await log(` params.prNumber: ${params.prNumber || 'null'}`, { verbose: true });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!argv.watch) {
|
|
554
|
+
if (argv.verbose) {
|
|
555
|
+
await log(' Watch mode not enabled - exiting startWatchMode', { verbose: true });
|
|
556
|
+
}
|
|
557
|
+
return null; // Watch mode not enabled
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!params.prNumber) {
|
|
561
|
+
await log('');
|
|
562
|
+
await log(formatAligned('⚠️', 'Watch mode:', 'Requires a pull request'));
|
|
563
|
+
await log(formatAligned('', 'Note:', 'Watch mode only works with existing PRs', 2));
|
|
564
|
+
if (argv.verbose) {
|
|
565
|
+
await log(' prNumber is missing - cannot start watch mode', { verbose: true });
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Start the watch loop and return session data
|
|
571
|
+
return await watchForFeedback(params);
|
|
572
|
+
};
|