@link-assistant/hive-mind 0.46.1 → 0.47.1
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 -15
- package/README.md +42 -8
- package/package.json +16 -3
- package/src/agent.lib.mjs +49 -70
- package/src/agent.prompts.lib.mjs +6 -20
- package/src/buildUserMention.lib.mjs +4 -17
- package/src/claude-limits.lib.mjs +15 -15
- package/src/claude.lib.mjs +617 -626
- package/src/claude.prompts.lib.mjs +7 -22
- package/src/codex.lib.mjs +39 -71
- package/src/codex.prompts.lib.mjs +6 -20
- package/src/config.lib.mjs +3 -16
- package/src/contributing-guidelines.lib.mjs +5 -18
- package/src/exit-handler.lib.mjs +4 -4
- package/src/git.lib.mjs +7 -7
- package/src/github-issue-creator.lib.mjs +17 -17
- package/src/github-linking.lib.mjs +8 -33
- package/src/github.batch.lib.mjs +20 -16
- package/src/github.graphql.lib.mjs +18 -18
- package/src/github.lib.mjs +89 -91
- package/src/hive.config.lib.mjs +50 -50
- package/src/hive.mjs +1293 -1296
- package/src/instrument.mjs +7 -11
- package/src/interactive-mode.lib.mjs +112 -138
- package/src/lenv-reader.lib.mjs +1 -6
- package/src/lib.mjs +36 -45
- package/src/lino.lib.mjs +2 -2
- package/src/local-ci-checks.lib.mjs +15 -14
- package/src/memory-check.mjs +52 -60
- package/src/model-mapping.lib.mjs +25 -32
- package/src/model-validation.lib.mjs +31 -31
- package/src/opencode.lib.mjs +37 -62
- package/src/opencode.prompts.lib.mjs +7 -21
- package/src/protect-branch.mjs +14 -15
- package/src/review.mjs +28 -27
- package/src/reviewers-hive.mjs +64 -69
- package/src/sentry.lib.mjs +13 -10
- package/src/solve.auto-continue.lib.mjs +48 -38
- package/src/solve.auto-pr.lib.mjs +111 -69
- package/src/solve.branch-errors.lib.mjs +17 -46
- package/src/solve.branch.lib.mjs +16 -23
- package/src/solve.config.lib.mjs +263 -261
- package/src/solve.error-handlers.lib.mjs +21 -79
- package/src/solve.execution.lib.mjs +10 -18
- package/src/solve.feedback.lib.mjs +25 -46
- package/src/solve.mjs +59 -60
- package/src/solve.preparation.lib.mjs +10 -36
- package/src/solve.repo-setup.lib.mjs +4 -19
- package/src/solve.repository.lib.mjs +37 -37
- package/src/solve.results.lib.mjs +32 -46
- package/src/solve.session.lib.mjs +7 -22
- package/src/solve.validation.lib.mjs +19 -17
- package/src/solve.watch.lib.mjs +20 -33
- package/src/start-screen.mjs +24 -24
- package/src/task.mjs +38 -44
- package/src/telegram-bot.mjs +125 -121
- package/src/telegram-top-command.lib.mjs +32 -48
- package/src/usage-limit.lib.mjs +9 -13
- package/src/version-info.lib.mjs +1 -1
- package/src/version.lib.mjs +1 -1
- package/src/youtrack/solve.youtrack.lib.mjs +3 -8
- package/src/youtrack/youtrack-sync.mjs +8 -14
- package/src/youtrack/youtrack.lib.mjs +26 -28
|
@@ -24,10 +24,7 @@ const lib = await import('./lib.mjs');
|
|
|
24
24
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
25
25
|
const { reportError } = sentryLib;
|
|
26
26
|
|
|
27
|
-
const {
|
|
28
|
-
log,
|
|
29
|
-
formatAligned
|
|
30
|
-
} = lib;
|
|
27
|
+
const { log, formatAligned } = lib;
|
|
31
28
|
|
|
32
29
|
// Import exit handler
|
|
33
30
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
@@ -58,14 +55,14 @@ export const getRootRepository = async (owner, repo) => {
|
|
|
58
55
|
context: 'get_root_repository',
|
|
59
56
|
owner,
|
|
60
57
|
repo,
|
|
61
|
-
operation: 'determine_fork_root'
|
|
58
|
+
operation: 'determine_fork_root',
|
|
62
59
|
});
|
|
63
60
|
return null;
|
|
64
61
|
}
|
|
65
62
|
};
|
|
66
63
|
|
|
67
64
|
// Check if current user has a fork of the given root repository
|
|
68
|
-
export const checkExistingForkOfRoot = async
|
|
65
|
+
export const checkExistingForkOfRoot = async rootRepo => {
|
|
69
66
|
try {
|
|
70
67
|
const userResult = await $`gh api user --jq .login`;
|
|
71
68
|
if (userResult.code !== 0) {
|
|
@@ -79,7 +76,11 @@ export const checkExistingForkOfRoot = async (rootRepo) => {
|
|
|
79
76
|
return null;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
const forks = forksResult.stdout
|
|
79
|
+
const forks = forksResult.stdout
|
|
80
|
+
.toString()
|
|
81
|
+
.trim()
|
|
82
|
+
.split('\n')
|
|
83
|
+
.filter(f => f);
|
|
83
84
|
|
|
84
85
|
if (forks.length > 0) {
|
|
85
86
|
return forks[0];
|
|
@@ -90,14 +91,14 @@ export const checkExistingForkOfRoot = async (rootRepo) => {
|
|
|
90
91
|
reportError(error, {
|
|
91
92
|
context: 'check_existing_fork_of_root',
|
|
92
93
|
rootRepo,
|
|
93
|
-
operation: 'search_user_forks'
|
|
94
|
+
operation: 'search_user_forks',
|
|
94
95
|
});
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
// Create or find temporary directory for cloning the repository
|
|
100
|
-
export const setupTempDirectory = async
|
|
101
|
+
export const setupTempDirectory = async argv => {
|
|
101
102
|
let tempDir;
|
|
102
103
|
let isResuming = argv.resume;
|
|
103
104
|
|
|
@@ -119,7 +120,7 @@ export const setupTempDirectory = async (argv) => {
|
|
|
119
120
|
reportError(err, {
|
|
120
121
|
context: 'resume_session_lookup',
|
|
121
122
|
sessionId: argv.resume,
|
|
122
|
-
operation: 'find_session_log'
|
|
123
|
+
operation: 'find_session_log',
|
|
123
124
|
});
|
|
124
125
|
await log(`Warning: Session log for ${argv.resume} not found, but continuing with resume attempt`);
|
|
125
126
|
tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
|
|
@@ -169,8 +170,7 @@ const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
169
170
|
} else {
|
|
170
171
|
const errorOutput = createResult.stdout.toString() + createResult.stderr.toString();
|
|
171
172
|
// Check if it's a permission error
|
|
172
|
-
if (errorOutput.includes('403') || errorOutput.includes('Forbidden') ||
|
|
173
|
-
errorOutput.includes('not have permission') || errorOutput.includes('Resource not accessible')) {
|
|
173
|
+
if (errorOutput.includes('403') || errorOutput.includes('Forbidden') || errorOutput.includes('not have permission') || errorOutput.includes('Resource not accessible')) {
|
|
174
174
|
await log(`${formatAligned('❌', 'No access:', 'You do not have write access to this repository')}`);
|
|
175
175
|
return false;
|
|
176
176
|
} else {
|
|
@@ -184,7 +184,7 @@ const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
184
184
|
context: 'initialize_empty_repository',
|
|
185
185
|
owner,
|
|
186
186
|
repo,
|
|
187
|
-
operation: 'create_readme'
|
|
187
|
+
operation: 'create_readme',
|
|
188
188
|
});
|
|
189
189
|
await log(`${formatAligned('❌', 'Error:', 'Failed to initialize repository')}`);
|
|
190
190
|
return false;
|
|
@@ -225,7 +225,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
225
225
|
|
|
226
226
|
if (existingForkOwner === currentUser) {
|
|
227
227
|
const targetRepo = `${owner}/${repo}`;
|
|
228
|
-
const targetIsRoot =
|
|
228
|
+
const targetIsRoot = targetRepo === rootRepo;
|
|
229
229
|
|
|
230
230
|
if (!targetIsRoot) {
|
|
231
231
|
await log('');
|
|
@@ -234,7 +234,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
234
234
|
await log(' 🔍 What happened:');
|
|
235
235
|
await log(` You are trying to fork ${targetRepo}`);
|
|
236
236
|
await log(` But you already have a fork of ${rootRepo}: ${existingFork}`);
|
|
237
|
-
await log(
|
|
237
|
+
await log(" GitHub doesn't allow multiple forks of the same root repository");
|
|
238
238
|
await log('');
|
|
239
239
|
await log(' 📦 Root repository analysis:');
|
|
240
240
|
await log(` • Target repository: ${targetRepo}`);
|
|
@@ -330,8 +330,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// Always capture output to parse actual fork name
|
|
333
|
-
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') +
|
|
334
|
-
(forkResult.stdout ? forkResult.stdout.toString() : '');
|
|
333
|
+
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') + (forkResult.stdout ? forkResult.stdout.toString() : '');
|
|
335
334
|
|
|
336
335
|
// Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
|
|
337
336
|
// GitHub may create forks with modified names to avoid conflicts
|
|
@@ -354,10 +353,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
354
353
|
break;
|
|
355
354
|
} else {
|
|
356
355
|
// Fork creation failed - check if it's because fork already exists
|
|
357
|
-
if (forkOutput.includes('already exists') ||
|
|
358
|
-
forkOutput.includes('Name already exists') ||
|
|
359
|
-
forkOutput.includes('fork of') ||
|
|
360
|
-
forkOutput.includes('HTTP 422')) {
|
|
356
|
+
if (forkOutput.includes('already exists') || forkOutput.includes('Name already exists') || forkOutput.includes('fork of') || forkOutput.includes('HTTP 422')) {
|
|
361
357
|
// Fork already exists (likely created by another concurrent worker)
|
|
362
358
|
await log(`${formatAligned('ℹ️', 'Fork exists:', actualForkName)}`);
|
|
363
359
|
forkExists = true;
|
|
@@ -365,9 +361,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
365
361
|
}
|
|
366
362
|
|
|
367
363
|
// Check if it's an empty repository (HTTP 403) - try to auto-fix
|
|
368
|
-
if (forkOutput.includes('HTTP 403') &&
|
|
369
|
-
(forkOutput.includes('Empty repositories cannot be forked') ||
|
|
370
|
-
forkOutput.includes('contains no Git content'))) {
|
|
364
|
+
if (forkOutput.includes('HTTP 403') && (forkOutput.includes('Empty repositories cannot be forked') || forkOutput.includes('contains no Git content'))) {
|
|
371
365
|
// Empty repository detected - try to initialize it
|
|
372
366
|
await log('');
|
|
373
367
|
await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' });
|
|
@@ -388,11 +382,13 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
388
382
|
} else {
|
|
389
383
|
// Failed to initialize - provide helpful suggestions
|
|
390
384
|
await log('');
|
|
391
|
-
await log(`${formatAligned('❌', 'Cannot proceed:', 'Unable to initialize empty repository')}`, {
|
|
385
|
+
await log(`${formatAligned('❌', 'Cannot proceed:', 'Unable to initialize empty repository')}`, {
|
|
386
|
+
level: 'error',
|
|
387
|
+
});
|
|
392
388
|
await log('');
|
|
393
389
|
await log(' 🔍 What happened:');
|
|
394
390
|
await log(` The repository ${owner}/${repo} is empty and cannot be forked.`);
|
|
395
|
-
await log(
|
|
391
|
+
await log(" GitHub doesn't allow forking repositories with no content.");
|
|
396
392
|
await log(' Auto-fix failed: You need write access to initialize the repository.');
|
|
397
393
|
await log('');
|
|
398
394
|
await log(' 💡 How to fix:');
|
|
@@ -462,7 +458,7 @@ Thank you!`;
|
|
|
462
458
|
// Fork still doesn't exist and creation failed
|
|
463
459
|
if (attempt < maxForkRetries) {
|
|
464
460
|
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
465
|
-
await log(`${formatAligned('⏳', 'Retry:', `Attempt ${attempt}/${maxForkRetries} failed, waiting ${delay/1000}s before retry...`)}`);
|
|
461
|
+
await log(`${formatAligned('⏳', 'Retry:', `Attempt ${attempt}/${maxForkRetries} failed, waiting ${delay / 1000}s before retry...`)}`);
|
|
466
462
|
await log(` Error: ${forkOutput.split('\n')[0]}`); // Show first line of error
|
|
467
463
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
468
464
|
} else {
|
|
@@ -485,7 +481,7 @@ Thank you!`;
|
|
|
485
481
|
for (let attempt = 1; attempt <= maxVerifyRetries; attempt++) {
|
|
486
482
|
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
487
483
|
if (attempt > 1) {
|
|
488
|
-
await log(`${formatAligned('⏳', 'Verifying fork:', `Attempt ${attempt}/${maxVerifyRetries} (waiting ${delay/1000}s)...`)}`);
|
|
484
|
+
await log(`${formatAligned('⏳', 'Verifying fork:', `Attempt ${attempt}/${maxVerifyRetries} (waiting ${delay / 1000}s)...`)}`);
|
|
489
485
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
490
486
|
}
|
|
491
487
|
|
|
@@ -582,7 +578,7 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
582
578
|
}
|
|
583
579
|
await log('');
|
|
584
580
|
await log(' 💡 Common causes:');
|
|
585
|
-
await log(
|
|
581
|
+
await log(" • Repository doesn't exist or is private");
|
|
586
582
|
await log(' • No GitHub authentication');
|
|
587
583
|
await log(' • Network connectivity issues');
|
|
588
584
|
if (argv.fork) {
|
|
@@ -688,9 +684,7 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
|
|
|
688
684
|
} else {
|
|
689
685
|
// Check if it's a non-fast-forward error (fork has diverged from upstream)
|
|
690
686
|
const errorMsg = pushResult.stderr ? pushResult.stderr.toString().trim() : '';
|
|
691
|
-
const isNonFastForward = errorMsg.includes('non-fast-forward') ||
|
|
692
|
-
errorMsg.includes('rejected') ||
|
|
693
|
-
errorMsg.includes('tip of your current branch is behind');
|
|
687
|
+
const isNonFastForward = errorMsg.includes('non-fast-forward') || errorMsg.includes('rejected') || errorMsg.includes('tip of your current branch is behind');
|
|
694
688
|
|
|
695
689
|
if (isNonFastForward) {
|
|
696
690
|
// Fork has diverged from upstream
|
|
@@ -716,7 +710,9 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
|
|
|
716
710
|
// Use --force-with-lease for safer force push
|
|
717
711
|
// This will only force push if the remote hasn't changed since our last fetch
|
|
718
712
|
await log(`${formatAligned('🔄', 'Force pushing:', 'Syncing fork with upstream (--force-with-lease)')}`);
|
|
719
|
-
const forcePushResult = await $({
|
|
713
|
+
const forcePushResult = await $({
|
|
714
|
+
cwd: tempDir,
|
|
715
|
+
})`git push --force-with-lease origin ${upstreamDefaultBranch}`;
|
|
720
716
|
|
|
721
717
|
if (forcePushResult.code === 0) {
|
|
722
718
|
await log(`${formatAligned('✅', 'Fork synced:', 'Successfully force-pushed to align with upstream')}`);
|
|
@@ -724,7 +720,9 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
|
|
|
724
720
|
} else {
|
|
725
721
|
// Force push also failed - this is a more serious issue
|
|
726
722
|
await log('');
|
|
727
|
-
await log(`${formatAligned('❌', 'FATAL ERROR:', 'Failed to sync fork with upstream')}`, {
|
|
723
|
+
await log(`${formatAligned('❌', 'FATAL ERROR:', 'Failed to sync fork with upstream')}`, {
|
|
724
|
+
level: 'error',
|
|
725
|
+
});
|
|
728
726
|
await log('');
|
|
729
727
|
await log(' 🔍 What happened:');
|
|
730
728
|
await log(` Fork branch ${upstreamDefaultBranch} has diverged from upstream`);
|
|
@@ -862,12 +860,14 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
862
860
|
prForkRepoName = `${owner}-${repo}`;
|
|
863
861
|
}
|
|
864
862
|
|
|
865
|
-
await log(`${formatAligned('🔗', 'Setting up pr-fork:',
|
|
863
|
+
await log(`${formatAligned('🔗', 'Setting up pr-fork:', "Branch exists in another user's fork")}`);
|
|
866
864
|
await log(`${formatAligned('', 'PR fork owner:', prForkOwner)}`);
|
|
867
865
|
await log(`${formatAligned('', 'Current user:', currentUser)}`);
|
|
868
866
|
await log(`${formatAligned('', 'Action:', `Adding ${prForkOwner}/${prForkRepoName} as pr-fork remote`)}`);
|
|
869
867
|
|
|
870
|
-
const addRemoteResult = await $({
|
|
868
|
+
const addRemoteResult = await $({
|
|
869
|
+
cwd: tempDir,
|
|
870
|
+
})`git remote add pr-fork https://github.com/${prForkOwner}/${prForkRepoName}.git`;
|
|
871
871
|
if (addRemoteResult.code !== 0) {
|
|
872
872
|
await log(`${formatAligned('❌', 'Error:', 'Failed to add pr-fork remote')}`);
|
|
873
873
|
if (addRemoteResult.stderr) {
|
|
@@ -945,7 +945,7 @@ export const cleanupTempDirectory = async (tempDir, argv, limitReached) => {
|
|
|
945
945
|
reportError(cleanupError, {
|
|
946
946
|
context: 'cleanup_temp_directory',
|
|
947
947
|
tempDir,
|
|
948
|
-
operation: 'remove_temp_dir'
|
|
948
|
+
operation: 'remove_temp_dir',
|
|
949
949
|
});
|
|
950
950
|
await log(' ⚠️ (failed)');
|
|
951
951
|
}
|
|
@@ -18,27 +18,18 @@ const path = (await use('path')).default;
|
|
|
18
18
|
|
|
19
19
|
// Import shared library functions
|
|
20
20
|
const lib = await import('./lib.mjs');
|
|
21
|
-
const {
|
|
22
|
-
log,
|
|
23
|
-
getLogFile,
|
|
24
|
-
formatAligned
|
|
25
|
-
} = lib;
|
|
21
|
+
const { log, getLogFile, formatAligned } = lib;
|
|
26
22
|
|
|
27
23
|
// Import exit handler
|
|
28
24
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
29
25
|
|
|
30
26
|
// Import GitHub-related functions
|
|
31
27
|
const githubLib = await import('./github.lib.mjs');
|
|
32
|
-
const {
|
|
33
|
-
sanitizeLogContent,
|
|
34
|
-
attachLogToGitHub
|
|
35
|
-
} = githubLib;
|
|
28
|
+
const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
36
29
|
|
|
37
30
|
// Import auto-continue functions
|
|
38
31
|
const autoContinue = await import('./solve.auto-continue.lib.mjs');
|
|
39
|
-
const {
|
|
40
|
-
autoContinueWhenLimitResets
|
|
41
|
-
} = autoContinue;
|
|
32
|
+
const { autoContinueWhenLimitResets } = autoContinue;
|
|
42
33
|
|
|
43
34
|
// Import error handling functions
|
|
44
35
|
// const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
|
|
@@ -122,24 +113,26 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
|
|
|
122
113
|
const firstCommitLine = branchCommits[0];
|
|
123
114
|
const [firstCommitHash, firstCommitMessage] = firstCommitLine.split('|');
|
|
124
115
|
|
|
125
|
-
await log(` First commit on branch: ${firstCommitHash.substring(0, 7)} - "${firstCommitMessage}"`, {
|
|
116
|
+
await log(` First commit on branch: ${firstCommitHash.substring(0, 7)} - "${firstCommitMessage}"`, {
|
|
117
|
+
verbose: true,
|
|
118
|
+
});
|
|
126
119
|
|
|
127
120
|
// Safety check: Verify commit message matches expected pattern
|
|
128
|
-
const expectedMessagePatterns = [
|
|
129
|
-
/^Initial commit with task details/i,
|
|
130
|
-
/^Add CLAUDE\.md/i,
|
|
131
|
-
/^CLAUDE\.md/i
|
|
132
|
-
];
|
|
121
|
+
const expectedMessagePatterns = [/^Initial commit with task details/i, /^Add CLAUDE\.md/i, /^CLAUDE\.md/i];
|
|
133
122
|
|
|
134
123
|
const messageMatches = expectedMessagePatterns.some(pattern => pattern.test(firstCommitMessage));
|
|
135
124
|
if (!messageMatches) {
|
|
136
125
|
await log(' First commit message does not match expected CLAUDE.md pattern', { verbose: true });
|
|
137
|
-
await log(' Expected patterns: "Initial commit with task details...", "Add CLAUDE.md", etc.', {
|
|
126
|
+
await log(' Expected patterns: "Initial commit with task details...", "Add CLAUDE.md", etc.', {
|
|
127
|
+
verbose: true,
|
|
128
|
+
});
|
|
138
129
|
return null;
|
|
139
130
|
}
|
|
140
131
|
|
|
141
132
|
// Safety check: Verify the commit ONLY adds CLAUDE.md file (no other files)
|
|
142
|
-
const filesChangedResult = await $({
|
|
133
|
+
const filesChangedResult = await $({
|
|
134
|
+
cwd: tempDir,
|
|
135
|
+
})`git diff-tree --no-commit-id --name-only -r ${firstCommitHash} 2>&1`;
|
|
143
136
|
if (filesChangedResult.code !== 0 || !filesChangedResult.stdout) {
|
|
144
137
|
await log(' Could not get files changed in first commit', { verbose: true });
|
|
145
138
|
return null;
|
|
@@ -157,7 +150,9 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
|
|
|
157
150
|
// CRITICAL SAFETY CHECK: Only allow revert if CLAUDE.md is the ONLY file changed
|
|
158
151
|
// This prevents Issue #617 where reverting a commit deleted .gitignore, LICENSE, README.md
|
|
159
152
|
if (filesChanged.length > 1) {
|
|
160
|
-
await log(` ⚠️ First commit changes more than just CLAUDE.md (${filesChanged.length} files)`, {
|
|
153
|
+
await log(` ⚠️ First commit changes more than just CLAUDE.md (${filesChanged.length} files)`, {
|
|
154
|
+
verbose: true,
|
|
155
|
+
});
|
|
161
156
|
await log(` Files: ${filesChanged.join(', ')}`, { verbose: true });
|
|
162
157
|
await log(' Refusing to revert to prevent data loss (Issue #617 safety)', { verbose: true });
|
|
163
158
|
return null;
|
|
@@ -174,7 +169,7 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
|
|
|
174
169
|
context: 'detect_claude_md_commit',
|
|
175
170
|
tempDir,
|
|
176
171
|
branchName,
|
|
177
|
-
operation: 'detect_commit_from_branch_structure'
|
|
172
|
+
operation: 'detect_commit_from_branch_structure',
|
|
178
173
|
});
|
|
179
174
|
await log(` Error detecting CLAUDE.md commit: ${error.message}`, { verbose: true });
|
|
180
175
|
return null;
|
|
@@ -226,7 +221,9 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
|
|
|
226
221
|
}
|
|
227
222
|
|
|
228
223
|
// Create a manual revert commit
|
|
229
|
-
const commitResult = await $({
|
|
224
|
+
const commitResult = await $({
|
|
225
|
+
cwd: tempDir,
|
|
226
|
+
})`git commit -m "Revert: Remove CLAUDE.md changes from initial commit" 2>&1`;
|
|
230
227
|
|
|
231
228
|
if (commitResult.code === 0) {
|
|
232
229
|
await log(formatAligned('📦', 'Committed:', 'CLAUDE.md revert (manual)'));
|
|
@@ -324,7 +321,7 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
|
|
|
324
321
|
reportError(e, {
|
|
325
322
|
context: 'cleanup_claude_file',
|
|
326
323
|
tempDir,
|
|
327
|
-
operation: 'revert_claude_md_commit'
|
|
324
|
+
operation: 'revert_claude_md_commit',
|
|
328
325
|
});
|
|
329
326
|
// If revert fails, that's okay - the task is still complete
|
|
330
327
|
await log(' CLAUDE.md revert failed or not needed', { verbose: true });
|
|
@@ -338,7 +335,7 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
338
335
|
if (sessionId) {
|
|
339
336
|
await log(`✅ Session ID: ${sessionId}`);
|
|
340
337
|
// Always use absolute path for log file display
|
|
341
|
-
const path =
|
|
338
|
+
const path = await use('path');
|
|
342
339
|
const absoluteLogPath = path.resolve(getLogFile());
|
|
343
340
|
await log(`✅ Complete log file: ${absoluteLogPath}`);
|
|
344
341
|
|
|
@@ -423,10 +420,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
423
420
|
|
|
424
421
|
// If we created a PR earlier in this session, it would be prNumber
|
|
425
422
|
// Or if the PR was updated during the session (updatedAt > referenceTime)
|
|
426
|
-
const isPrFromSession = (prNumber && pr.number.toString() === prNumber) ||
|
|
427
|
-
(prUrl && pr.url === prUrl) ||
|
|
428
|
-
new Date(pr.updatedAt) > referenceTime ||
|
|
429
|
-
new Date(pr.createdAt) > referenceTime;
|
|
423
|
+
const isPrFromSession = (prNumber && pr.number.toString() === prNumber) || (prUrl && pr.url === prUrl) || new Date(pr.updatedAt) > referenceTime || new Date(pr.createdAt) > referenceTime;
|
|
430
424
|
|
|
431
425
|
if (isPrFromSession) {
|
|
432
426
|
await log(` ✅ Found pull request #${pr.number}: "${pr.title}"`);
|
|
@@ -441,12 +435,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
441
435
|
// This ensures we only detect actual GitHub-recognized linking keywords
|
|
442
436
|
// (fixes, closes, resolves and their variants) in proper format
|
|
443
437
|
// See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
|
|
444
|
-
const hasLinkingKeyword = hasGitHubLinkingKeyword(
|
|
445
|
-
prBody,
|
|
446
|
-
issueNumber,
|
|
447
|
-
argv.fork ? owner : null,
|
|
448
|
-
argv.fork ? repo : null
|
|
449
|
-
);
|
|
438
|
+
const hasLinkingKeyword = hasGitHubLinkingKeyword(prBody, issueNumber, argv.fork ? owner : null, argv.fork ? repo : null);
|
|
450
439
|
|
|
451
440
|
if (!hasLinkingKeyword) {
|
|
452
441
|
await log(` 📝 Updating PR body to link issue #${issueNumber}...`);
|
|
@@ -514,7 +503,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
514
503
|
anthropicTotalCostUSD,
|
|
515
504
|
// Pass agent tool pricing data when available
|
|
516
505
|
publicPricingEstimate,
|
|
517
|
-
pricingInfo
|
|
506
|
+
pricingInfo,
|
|
518
507
|
});
|
|
519
508
|
}
|
|
520
509
|
|
|
@@ -552,9 +541,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
552
541
|
const allComments = JSON.parse(allCommentsResult.stdout.toString().trim() || '[]');
|
|
553
542
|
|
|
554
543
|
// Filter for new comments by current user
|
|
555
|
-
const newCommentsByUser = allComments.filter(comment =>
|
|
556
|
-
comment.user.login === currentUser && new Date(comment.created_at) > referenceTime
|
|
557
|
-
);
|
|
544
|
+
const newCommentsByUser = allComments.filter(comment => comment.user.login === currentUser && new Date(comment.created_at) > referenceTime);
|
|
558
545
|
|
|
559
546
|
if (newCommentsByUser.length > 0) {
|
|
560
547
|
const lastComment = newCommentsByUser[newCommentsByUser.length - 1];
|
|
@@ -578,7 +565,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
578
565
|
anthropicTotalCostUSD,
|
|
579
566
|
// Pass agent tool pricing data when available
|
|
580
567
|
publicPricingEstimate,
|
|
581
|
-
pricingInfo
|
|
568
|
+
pricingInfo,
|
|
582
569
|
});
|
|
583
570
|
}
|
|
584
571
|
|
|
@@ -611,12 +598,11 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
611
598
|
await safeExit(0, 'Process completed successfully');
|
|
612
599
|
}
|
|
613
600
|
return; // Return normally for watch mode
|
|
614
|
-
|
|
615
601
|
} catch (searchError) {
|
|
616
602
|
reportError(searchError, {
|
|
617
603
|
context: 'verify_pr_creation',
|
|
618
604
|
issueNumber,
|
|
619
|
-
operation: 'search_for_pr'
|
|
605
|
+
operation: 'search_for_pr',
|
|
620
606
|
});
|
|
621
607
|
await log('\n⚠️ Could not verify results:', searchError.message);
|
|
622
608
|
await log('\n💡 Check the log file for details:');
|
|
@@ -654,7 +640,7 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
654
640
|
log,
|
|
655
641
|
sanitizeLogContent,
|
|
656
642
|
verbose: argv.verbose || false,
|
|
657
|
-
errorMessage: cleanErrorMessage(error)
|
|
643
|
+
errorMessage: cleanErrorMessage(error),
|
|
658
644
|
});
|
|
659
645
|
|
|
660
646
|
if (logUploadSuccess) {
|
|
@@ -664,7 +650,7 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
664
650
|
reportError(attachError, {
|
|
665
651
|
context: 'attach_success_log',
|
|
666
652
|
prNumber: global.createdPR?.number,
|
|
667
|
-
operation: 'attach_log_to_pr'
|
|
653
|
+
operation: 'attach_log_to_pr',
|
|
668
654
|
});
|
|
669
655
|
await log(`⚠️ Could not attach failure log: ${attachError.message}`, { level: 'warning' });
|
|
670
656
|
}
|
|
@@ -685,11 +671,11 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
685
671
|
reportError(closeError, {
|
|
686
672
|
context: 'close_success_pr',
|
|
687
673
|
prNumber: global.createdPR?.number,
|
|
688
|
-
operation: 'close_pull_request'
|
|
674
|
+
operation: 'close_pull_request',
|
|
689
675
|
});
|
|
690
676
|
await log(`⚠️ Could not close pull request: ${closeError.message}`, { level: 'warning' });
|
|
691
677
|
}
|
|
692
678
|
}
|
|
693
679
|
|
|
694
680
|
await safeExit(1, 'Execution error');
|
|
695
|
-
};
|
|
681
|
+
};
|
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
* Handles starting and ending work sessions, PR status changes, and session comments
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export async function startWorkSession({
|
|
7
|
-
isContinueMode,
|
|
8
|
-
prNumber,
|
|
9
|
-
argv,
|
|
10
|
-
log,
|
|
11
|
-
formatAligned,
|
|
12
|
-
$
|
|
13
|
-
}) {
|
|
6
|
+
export async function startWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $ }) {
|
|
14
7
|
// Record work start time and convert PR to draft if in continue/watch mode
|
|
15
8
|
const workStartTime = new Date();
|
|
16
9
|
if (isContinueMode && prNumber && (argv.watch || argv.autoContinue)) {
|
|
@@ -39,7 +32,7 @@ export async function startWorkSession({
|
|
|
39
32
|
reportError(error, {
|
|
40
33
|
context: 'convert_pr_to_draft',
|
|
41
34
|
prNumber,
|
|
42
|
-
operation: 'pr_status_change'
|
|
35
|
+
operation: 'pr_status_change',
|
|
43
36
|
});
|
|
44
37
|
await log('Warning: Could not check/convert PR draft status', { level: 'warning' });
|
|
45
38
|
}
|
|
@@ -57,7 +50,7 @@ export async function startWorkSession({
|
|
|
57
50
|
reportError(error, {
|
|
58
51
|
context: 'post_start_comment',
|
|
59
52
|
prNumber,
|
|
60
|
-
operation: 'create_pr_comment'
|
|
53
|
+
operation: 'create_pr_comment',
|
|
61
54
|
});
|
|
62
55
|
await log('Warning: Could not post work start comment', { level: 'warning' });
|
|
63
56
|
}
|
|
@@ -66,15 +59,7 @@ export async function startWorkSession({
|
|
|
66
59
|
return workStartTime;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
export async function endWorkSession({
|
|
70
|
-
isContinueMode,
|
|
71
|
-
prNumber,
|
|
72
|
-
argv,
|
|
73
|
-
log,
|
|
74
|
-
formatAligned,
|
|
75
|
-
$,
|
|
76
|
-
logsAttached = false
|
|
77
|
-
}) {
|
|
62
|
+
export async function endWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, logsAttached = false }) {
|
|
78
63
|
// Post end work session comment and convert PR back to ready if in continue mode
|
|
79
64
|
if (isContinueMode && prNumber && (argv.watch || argv.autoContinue)) {
|
|
80
65
|
const workEndTime = new Date();
|
|
@@ -96,7 +81,7 @@ export async function endWorkSession({
|
|
|
96
81
|
reportError(error, {
|
|
97
82
|
context: 'post_end_comment',
|
|
98
83
|
prNumber,
|
|
99
|
-
operation: 'create_pr_comment'
|
|
84
|
+
operation: 'create_pr_comment',
|
|
100
85
|
});
|
|
101
86
|
await log('Warning: Could not post work end comment', { level: 'warning' });
|
|
102
87
|
}
|
|
@@ -127,9 +112,9 @@ export async function endWorkSession({
|
|
|
127
112
|
reportError(error, {
|
|
128
113
|
context: 'convert_pr_to_ready',
|
|
129
114
|
prNumber,
|
|
130
|
-
operation: 'pr_status_change'
|
|
115
|
+
operation: 'pr_status_change',
|
|
131
116
|
});
|
|
132
117
|
await log('Warning: Could not convert PR to ready status', { level: 'warning' });
|
|
133
118
|
}
|
|
134
119
|
}
|
|
135
|
-
}
|
|
120
|
+
}
|
|
@@ -21,7 +21,7 @@ const memoryCheck = await import('./memory-check.mjs');
|
|
|
21
21
|
const lib = await import('./lib.mjs');
|
|
22
22
|
const {
|
|
23
23
|
log,
|
|
24
|
-
setLogFile
|
|
24
|
+
setLogFile,
|
|
25
25
|
// getLogFile - not currently used
|
|
26
26
|
} = lib;
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ const {
|
|
|
29
29
|
const githubLib = await import('./github.lib.mjs');
|
|
30
30
|
const {
|
|
31
31
|
checkGitHubPermissions,
|
|
32
|
-
parseGitHubUrl
|
|
32
|
+
parseGitHubUrl,
|
|
33
33
|
// isGitHubUrlType - not currently used
|
|
34
34
|
} = githubLib;
|
|
35
35
|
|
|
@@ -39,9 +39,7 @@ const claudeLib = await import('./claude.lib.mjs');
|
|
|
39
39
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
40
40
|
const { reportError } = sentryLib;
|
|
41
41
|
|
|
42
|
-
const {
|
|
43
|
-
validateClaudeConnection
|
|
44
|
-
} = claudeLib;
|
|
42
|
+
const { validateClaudeConnection } = claudeLib;
|
|
45
43
|
|
|
46
44
|
// Wrapper function for disk space check using imported module
|
|
47
45
|
const checkDiskSpace = async (minSpaceMB = 500) => {
|
|
@@ -56,7 +54,7 @@ const checkMemory = async (minMemoryMB = 256) => {
|
|
|
56
54
|
};
|
|
57
55
|
|
|
58
56
|
// Validate GitHub issue or pull request URL format
|
|
59
|
-
export const validateGitHubUrl =
|
|
57
|
+
export const validateGitHubUrl = issueUrl => {
|
|
60
58
|
if (!issueUrl) {
|
|
61
59
|
return { isValid: false, isIssueUrl: null, isPrUrl: null };
|
|
62
60
|
}
|
|
@@ -100,12 +98,12 @@ export const validateGitHubUrl = (issueUrl) => {
|
|
|
100
98
|
normalizedUrl: parsedUrl.normalized,
|
|
101
99
|
owner: parsedUrl.owner,
|
|
102
100
|
repo: parsedUrl.repo,
|
|
103
|
-
number: parsedUrl.number
|
|
101
|
+
number: parsedUrl.number,
|
|
104
102
|
};
|
|
105
103
|
};
|
|
106
104
|
|
|
107
105
|
// Show security warning for attach-logs option
|
|
108
|
-
export const showAttachLogsWarning = async
|
|
106
|
+
export const showAttachLogsWarning = async shouldAttachLogs => {
|
|
109
107
|
if (!shouldAttachLogs) return;
|
|
110
108
|
|
|
111
109
|
await log('');
|
|
@@ -146,7 +144,7 @@ export const initializeLogFile = async (logDir = null) => {
|
|
|
146
144
|
} catch (error) {
|
|
147
145
|
reportError(error, {
|
|
148
146
|
context: 'create_log_directory',
|
|
149
|
-
operation: 'mkdir_log_dir'
|
|
147
|
+
operation: 'mkdir_log_dir',
|
|
150
148
|
});
|
|
151
149
|
// If directory doesn't exist, try to create it
|
|
152
150
|
try {
|
|
@@ -155,7 +153,7 @@ export const initializeLogFile = async (logDir = null) => {
|
|
|
155
153
|
reportError(mkdirError, {
|
|
156
154
|
context: 'create_log_directory_fallback',
|
|
157
155
|
targetDir,
|
|
158
|
-
operation: 'mkdir_recursive'
|
|
156
|
+
operation: 'mkdir_recursive',
|
|
159
157
|
});
|
|
160
158
|
await log(`⚠️ Unable to create log directory: ${targetDir}`, { level: 'error' });
|
|
161
159
|
await log(' Falling back to current working directory', { level: 'error' });
|
|
@@ -179,7 +177,7 @@ export const initializeLogFile = async (logDir = null) => {
|
|
|
179
177
|
};
|
|
180
178
|
|
|
181
179
|
// Validate GitHub URL requirement
|
|
182
|
-
export const validateUrlRequirement = async
|
|
180
|
+
export const validateUrlRequirement = async issueUrl => {
|
|
183
181
|
if (!issueUrl) {
|
|
184
182
|
await log('❌ GitHub issue URL is required', { level: 'error' });
|
|
185
183
|
await log(' Usage: solve <github-issue-url> [options]', { level: 'error' });
|
|
@@ -263,25 +261,29 @@ export const performSystemChecks = async (minDiskSpace = 500, skipToolConnection
|
|
|
263
261
|
return false;
|
|
264
262
|
}
|
|
265
263
|
} else {
|
|
266
|
-
await log('⏩ Skipping tool connection validation (dry-run mode or skip-tool-connection-check enabled)', {
|
|
267
|
-
|
|
264
|
+
await log('⏩ Skipping tool connection validation (dry-run mode or skip-tool-connection-check enabled)', {
|
|
265
|
+
verbose: true,
|
|
266
|
+
});
|
|
267
|
+
await log('⏩ Skipping GitHub authentication check (dry-run mode or skip-tool-connection-check enabled)', {
|
|
268
|
+
verbose: true,
|
|
269
|
+
});
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
return true;
|
|
271
273
|
};
|
|
272
274
|
|
|
273
275
|
// Parse URL components
|
|
274
|
-
export const parseUrlComponents =
|
|
276
|
+
export const parseUrlComponents = issueUrl => {
|
|
275
277
|
const urlParts = issueUrl.split('/');
|
|
276
278
|
return {
|
|
277
279
|
owner: urlParts[3],
|
|
278
280
|
repo: urlParts[4],
|
|
279
|
-
urlNumber: urlParts[6] // Could be issue or PR number
|
|
281
|
+
urlNumber: urlParts[6], // Could be issue or PR number
|
|
280
282
|
};
|
|
281
283
|
};
|
|
282
284
|
|
|
283
285
|
// Helper function to parse time string and calculate wait time
|
|
284
|
-
export const parseResetTime =
|
|
286
|
+
export const parseResetTime = timeStr => {
|
|
285
287
|
// Normalize and parse time formats like:
|
|
286
288
|
// "5:30am", "11:45pm", "12:16 PM", "07:05 Am", "5am", "5 AM"
|
|
287
289
|
const normalized = (timeStr || '').toString().trim();
|
|
@@ -308,7 +310,7 @@ export const parseResetTime = (timeStr) => {
|
|
|
308
310
|
};
|
|
309
311
|
|
|
310
312
|
// Calculate milliseconds until the next occurrence of the specified time
|
|
311
|
-
export const calculateWaitTime =
|
|
313
|
+
export const calculateWaitTime = resetTime => {
|
|
312
314
|
const { hour, minute } = parseResetTime(resetTime);
|
|
313
315
|
|
|
314
316
|
const now = new Date();
|