@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Work session management functionality for solve.mjs
3
+ * Handles starting and ending work sessions, PR status changes, and session comments
4
+ */
5
+
6
+ export async function startWorkSession({
7
+ isContinueMode,
8
+ prNumber,
9
+ argv,
10
+ log,
11
+ formatAligned,
12
+ $
13
+ }) {
14
+ // Record work start time and convert PR to draft if in continue/watch mode
15
+ const workStartTime = new Date();
16
+ if (isContinueMode && prNumber && (argv.watch || argv.autoContinue)) {
17
+ await log(`\n${formatAligned('🚀', 'Starting work session:', workStartTime.toISOString())}`);
18
+
19
+ // Convert PR back to draft if not already
20
+ try {
21
+ const prStatusResult = await $`gh pr view ${prNumber} --repo ${global.owner}/${global.repo} --json isDraft --jq .isDraft`;
22
+ if (prStatusResult.code === 0) {
23
+ const isDraft = prStatusResult.stdout.toString().trim() === 'true';
24
+ if (!isDraft) {
25
+ await log(formatAligned('📝', 'Converting PR:', 'Back to draft mode...', 2));
26
+ const convertResult = await $`gh pr ready ${prNumber} --repo ${global.owner}/${global.repo} --undo`;
27
+ if (convertResult.code === 0) {
28
+ await log(formatAligned('✅', 'PR converted:', 'Now in draft mode', 2));
29
+ } else {
30
+ await log('Warning: Could not convert PR to draft', { level: 'warning' });
31
+ }
32
+ } else {
33
+ await log(formatAligned('✅', 'PR status:', 'Already in draft mode', 2));
34
+ }
35
+ }
36
+ } catch (error) {
37
+ const sentryLib = await import('./sentry.lib.mjs');
38
+ const { reportError } = sentryLib;
39
+ reportError(error, {
40
+ context: 'convert_pr_to_draft',
41
+ prNumber,
42
+ operation: 'pr_status_change'
43
+ });
44
+ await log('Warning: Could not check/convert PR draft status', { level: 'warning' });
45
+ }
46
+
47
+ // Post a comment marking the start of work session
48
+ try {
49
+ const startComment = `🤖 **AI Work Session Started**\n\nStarting automated work session at ${workStartTime.toISOString()}\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This comment marks the beginning of an AI work session. Please wait working session to finish, and provide your feedback._`;
50
+ const commentResult = await $`gh pr comment ${prNumber} --repo ${global.owner}/${global.repo} --body ${startComment}`;
51
+ if (commentResult.code === 0) {
52
+ await log(formatAligned('💬', 'Posted:', 'Work session start comment', 2));
53
+ }
54
+ } catch (error) {
55
+ const sentryLib = await import('./sentry.lib.mjs');
56
+ const { reportError } = sentryLib;
57
+ reportError(error, {
58
+ context: 'post_start_comment',
59
+ prNumber,
60
+ operation: 'create_pr_comment'
61
+ });
62
+ await log('Warning: Could not post work start comment', { level: 'warning' });
63
+ }
64
+ }
65
+
66
+ return workStartTime;
67
+ }
68
+
69
+ export async function endWorkSession({
70
+ isContinueMode,
71
+ prNumber,
72
+ argv,
73
+ log,
74
+ formatAligned,
75
+ $,
76
+ logsAttached = false
77
+ }) {
78
+ // Post end work session comment and convert PR back to ready if in continue mode
79
+ if (isContinueMode && prNumber && (argv.watch || argv.autoContinue)) {
80
+ const workEndTime = new Date();
81
+ await log(`\n${formatAligned('🏁', 'Ending work session:', workEndTime.toISOString())}`);
82
+
83
+ // Only post end comment if logs were NOT already attached
84
+ // The attachLogToGitHub comment already serves as finishing status with "Now working session is ended" text
85
+ if (!logsAttached) {
86
+ // Post a comment marking the end of work session
87
+ try {
88
+ const endComment = `🤖 **AI Work Session Completed**\n\nWork session ended at ${workEndTime.toISOString()}\n\nThe PR will be converted back to ready for review.\n\n_This comment marks the end of an AI work session. New comments after this time will be considered as feedback._`;
89
+ const commentResult = await $`gh pr comment ${prNumber} --repo ${global.owner}/${global.repo} --body ${endComment}`;
90
+ if (commentResult.code === 0) {
91
+ await log(formatAligned('💬', 'Posted:', 'Work session end comment', 2));
92
+ }
93
+ } catch (error) {
94
+ const sentryLib = await import('./sentry.lib.mjs');
95
+ const { reportError } = sentryLib;
96
+ reportError(error, {
97
+ context: 'post_end_comment',
98
+ prNumber,
99
+ operation: 'create_pr_comment'
100
+ });
101
+ await log('Warning: Could not post work end comment', { level: 'warning' });
102
+ }
103
+ } else {
104
+ await log(formatAligned('ℹ️', 'Skipping:', 'End comment (logs already attached with session end message)', 2));
105
+ }
106
+
107
+ // Convert PR back to ready for review
108
+ try {
109
+ const prStatusResult = await $`gh pr view ${prNumber} --repo ${global.owner}/${global.repo} --json isDraft --jq .isDraft`;
110
+ if (prStatusResult.code === 0) {
111
+ const isDraft = prStatusResult.stdout.toString().trim() === 'true';
112
+ if (isDraft) {
113
+ await log(formatAligned('🔀', 'Converting PR:', 'Back to ready for review...', 2));
114
+ const convertResult = await $`gh pr ready ${prNumber} --repo ${global.owner}/${global.repo}`;
115
+ if (convertResult.code === 0) {
116
+ await log(formatAligned('✅', 'PR converted:', 'Ready for review', 2));
117
+ } else {
118
+ await log('Warning: Could not convert PR to ready', { level: 'warning' });
119
+ }
120
+ } else {
121
+ await log(formatAligned('✅', 'PR status:', 'Already ready for review', 2));
122
+ }
123
+ }
124
+ } catch (error) {
125
+ const sentryLib = await import('./sentry.lib.mjs');
126
+ const { reportError } = sentryLib;
127
+ reportError(error, {
128
+ context: 'convert_pr_to_ready',
129
+ prNumber,
130
+ operation: 'pr_status_change'
131
+ });
132
+ await log('Warning: Could not convert PR to ready status', { level: 'warning' });
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Validation 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
+ const path = (await use('path')).default;
15
+ const fs = (await use('fs')).promises;
16
+
17
+ // Import memory check functions (RAM, swap, disk)
18
+ const memoryCheck = await import('./memory-check.mjs');
19
+
20
+ // Import shared library functions
21
+ const lib = await import('./lib.mjs');
22
+ const {
23
+ log,
24
+ setLogFile
25
+ // getLogFile - not currently used
26
+ } = lib;
27
+
28
+ // Import GitHub-related functions
29
+ const githubLib = await import('./github.lib.mjs');
30
+ const {
31
+ checkGitHubPermissions,
32
+ parseGitHubUrl
33
+ // isGitHubUrlType - not currently used
34
+ } = githubLib;
35
+
36
+ // Import Claude-related functions
37
+ const claudeLib = await import('./claude.lib.mjs');
38
+ // Import Sentry integration
39
+ const sentryLib = await import('./sentry.lib.mjs');
40
+ const { reportError } = sentryLib;
41
+
42
+ const {
43
+ validateClaudeConnection
44
+ } = claudeLib;
45
+
46
+ // Wrapper function for disk space check using imported module
47
+ const checkDiskSpace = async (minSpaceMB = 500) => {
48
+ const result = await memoryCheck.checkDiskSpace(minSpaceMB, { log });
49
+ return result.success;
50
+ };
51
+
52
+ // Wrapper function for memory check using imported module
53
+ const checkMemory = async (minMemoryMB = 256) => {
54
+ const result = await memoryCheck.checkMemory(minMemoryMB, { log });
55
+ return result.success;
56
+ };
57
+
58
+ // Validate GitHub issue or pull request URL format
59
+ export const validateGitHubUrl = (issueUrl) => {
60
+ if (!issueUrl) {
61
+ return { isValid: false, isIssueUrl: null, isPrUrl: null };
62
+ }
63
+
64
+ // Use the universal GitHub URL parser
65
+ const parsedUrl = parseGitHubUrl(issueUrl);
66
+
67
+ if (!parsedUrl.valid) {
68
+ console.error('Error: Invalid GitHub URL format');
69
+ if (parsedUrl.error) {
70
+ console.error(` ${parsedUrl.error}`);
71
+ }
72
+ console.error(' Please provide a valid GitHub issue or pull request URL');
73
+ console.error(' Examples:');
74
+ console.error(' https://github.com/owner/repo/issues/123 (issue)');
75
+ console.error(' https://github.com/owner/repo/pull/456 (pull request)');
76
+ console.error(' You can also use:');
77
+ console.error(' http://github.com/owner/repo/issues/123 (will be converted to https)');
78
+ console.error(' github.com/owner/repo/issues/123 (will add https://)');
79
+ console.error(' owner/repo/issues/123 (will be converted to full URL)');
80
+ return { isValid: false, isIssueUrl: null, isPrUrl: null };
81
+ }
82
+
83
+ // Check if it's an issue or pull request
84
+ const isIssueUrl = parsedUrl.type === 'issue';
85
+ const isPrUrl = parsedUrl.type === 'pull';
86
+
87
+ if (!isIssueUrl && !isPrUrl) {
88
+ console.error('Error: Invalid GitHub URL for solve command');
89
+ console.error(` URL type '${parsedUrl.type}' is not supported`);
90
+ console.error(' Please provide a valid GitHub issue or pull request URL');
91
+ console.error(' Examples:');
92
+ console.error(' https://github.com/owner/repo/issues/123 (issue)');
93
+ console.error(' https://github.com/owner/repo/pull/456 (pull request)');
94
+ return { isValid: false, isIssueUrl: null, isPrUrl: null };
95
+ }
96
+
97
+ return {
98
+ isValid: true,
99
+ isIssueUrl,
100
+ isPrUrl,
101
+ normalizedUrl: parsedUrl.normalized,
102
+ owner: parsedUrl.owner,
103
+ repo: parsedUrl.repo,
104
+ number: parsedUrl.number
105
+ };
106
+ };
107
+
108
+ // Show security warning for attach-logs option
109
+ export const showAttachLogsWarning = async (shouldAttachLogs) => {
110
+ if (!shouldAttachLogs) return;
111
+
112
+ await log('');
113
+ await log('⚠️ SECURITY WARNING: --attach-logs is ENABLED', { level: 'warning' });
114
+ await log('');
115
+ await log(' This option will upload the complete solution draft log file to the Pull Request.');
116
+ await log(' The log may contain sensitive information such as:');
117
+ await log(' • API keys, tokens, or secrets');
118
+ await log(' • File paths and directory structures');
119
+ await log(' • Command outputs and error messages');
120
+ await log(' • Internal system information');
121
+ await log('');
122
+ await log(' ⚠️ DO NOT use this option with public repositories or if the log');
123
+ await log(' might contain sensitive data that should not be shared publicly.');
124
+ await log('');
125
+ await log(' Continuing in 5 seconds... (Press Ctrl+C to abort)');
126
+ await log('');
127
+
128
+ // Give user time to abort if they realize this might be dangerous
129
+ for (let i = 5; i > 0; i--) {
130
+ process.stdout.write(`\r Countdown: ${i} seconds remaining...`);
131
+ await new Promise(resolve => setTimeout(resolve, 1000));
132
+ }
133
+ process.stdout.write('\r Proceeding with log attachment enabled. \n');
134
+ await log('');
135
+ };
136
+
137
+ // Create and initialize log file
138
+ export const initializeLogFile = async (logDir = null) => {
139
+ // Determine log directory:
140
+ // 1. Use provided logDir if specified
141
+ // 2. Otherwise use current working directory (not script directory)
142
+ let targetDir = logDir || process.cwd();
143
+
144
+ // Verify the directory exists
145
+ try {
146
+ await fs.access(targetDir);
147
+ } catch (error) {
148
+ reportError(error, {
149
+ context: 'create_log_directory',
150
+ operation: 'mkdir_log_dir'
151
+ });
152
+ // If directory doesn't exist, try to create it
153
+ try {
154
+ await fs.mkdir(targetDir, { recursive: true });
155
+ } catch (mkdirError) {
156
+ reportError(mkdirError, {
157
+ context: 'create_log_directory_fallback',
158
+ targetDir,
159
+ operation: 'mkdir_recursive'
160
+ });
161
+ await log(`⚠️ Unable to create log directory: ${targetDir}`, { level: 'error' });
162
+ await log(' Falling back to current working directory', { level: 'error' });
163
+ // Fall back to current working directory
164
+ targetDir = process.cwd();
165
+ }
166
+ }
167
+
168
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
169
+ const logFile = path.join(targetDir, `solve-${timestamp}.log`);
170
+ setLogFile(logFile);
171
+
172
+ // Create the log file immediately
173
+ await fs.writeFile(logFile, `# Solve.mjs Log - ${new Date().toISOString()}\n\n`);
174
+ // Always use absolute path for log file display
175
+ const absoluteLogPath = path.resolve(logFile);
176
+ await log(`📁 Log file: ${absoluteLogPath}`);
177
+ await log(' (All output will be logged here)');
178
+
179
+ return logFile;
180
+ };
181
+
182
+ // Validate GitHub URL requirement
183
+ export const validateUrlRequirement = async (issueUrl) => {
184
+ if (!issueUrl) {
185
+ await log('❌ GitHub issue URL is required', { level: 'error' });
186
+ await log(' Usage: solve <github-issue-url> [options]', { level: 'error' });
187
+ return false;
188
+ }
189
+ return true;
190
+ };
191
+
192
+ // Validate --continue-only-on-feedback option requirements
193
+ export const validateContinueOnlyOnFeedback = async (argv, isPrUrl, isIssueUrl) => {
194
+ if (argv.continueOnlyOnFeedback) {
195
+ if (!isPrUrl && !(isIssueUrl && argv.autoContinue)) {
196
+ await log('❌ --continue-only-on-feedback option requirements not met', { level: 'error' });
197
+ await log(' This option works only with:', { level: 'error' });
198
+ await log(' • Pull request URL, OR', { level: 'error' });
199
+ await log(' • Issue URL with --auto-continue option', { level: 'error' });
200
+ await log(` Current: ${isPrUrl ? 'PR URL' : 'Issue URL'} ${argv.autoContinue ? 'with --auto-continue' : 'without --auto-continue'}`, { level: 'error' });
201
+ return false;
202
+ }
203
+ }
204
+ return true;
205
+ };
206
+
207
+ // Perform all system checks (disk space, memory, tool connection, GitHub permissions)
208
+ // Note: skipToolConnection only skips the connection check, not model validation
209
+ // Model validation should be done separately before calling this function
210
+ export const performSystemChecks = async (minDiskSpace = 500, skipToolConnection = false, model = 'sonnet', argv = {}) => {
211
+ // Check disk space before proceeding
212
+ const hasEnoughSpace = await checkDiskSpace(minDiskSpace);
213
+ if (!hasEnoughSpace) {
214
+ return false;
215
+ }
216
+
217
+ // Check memory before proceeding (early check to prevent Claude kills)
218
+ const hasEnoughMemory = await checkMemory(256);
219
+ if (!hasEnoughMemory) {
220
+ return false;
221
+ }
222
+
223
+ // Skip tool connection validation if in dry-run mode or explicitly requested
224
+ if (!skipToolConnection) {
225
+ let isToolConnected = false;
226
+ if (argv.tool === 'opencode') {
227
+ // Validate OpenCode connection
228
+ const opencodeLib = await import('./opencode.lib.mjs');
229
+ isToolConnected = await opencodeLib.validateOpenCodeConnection(model);
230
+ if (!isToolConnected) {
231
+ await log('❌ Cannot proceed without OpenCode connection', { level: 'error' });
232
+ return false;
233
+ }
234
+ } else if (argv.tool === 'codex') {
235
+ // Validate Codex connection
236
+ const codexLib = await import('./codex.lib.mjs');
237
+ isToolConnected = await codexLib.validateCodexConnection(model);
238
+ if (!isToolConnected) {
239
+ await log('❌ Cannot proceed without Codex connection', { level: 'error' });
240
+ return false;
241
+ }
242
+ } else if (argv.tool === 'agent') {
243
+ // Validate Agent connection
244
+ const agentLib = await import('./agent.lib.mjs');
245
+ isToolConnected = await agentLib.validateAgentConnection(model);
246
+ if (!isToolConnected) {
247
+ await log('❌ Cannot proceed without Agent connection', { level: 'error' });
248
+ return false;
249
+ }
250
+ } else {
251
+ // Validate Claude CLI connection (default)
252
+ const isClaudeConnected = await validateClaudeConnection(model);
253
+ if (!isClaudeConnected) {
254
+ await log('❌ Cannot proceed without Claude CLI connection', { level: 'error' });
255
+ return false;
256
+ }
257
+ isToolConnected = true;
258
+ }
259
+
260
+ // Check GitHub permissions (only when tool check is not skipped)
261
+ // Skip in dry-run mode to allow CI tests without authentication
262
+ const hasValidAuth = await checkGitHubPermissions();
263
+ if (!hasValidAuth) {
264
+ return false;
265
+ }
266
+ } else {
267
+ await log('⏩ Skipping tool connection validation (dry-run mode or skip-tool-connection-check enabled)', { verbose: true });
268
+ await log('⏩ Skipping GitHub authentication check (dry-run mode or skip-tool-connection-check enabled)', { verbose: true });
269
+ }
270
+
271
+ return true;
272
+ };
273
+
274
+ // Parse URL components
275
+ export const parseUrlComponents = (issueUrl) => {
276
+ const urlParts = issueUrl.split('/');
277
+ return {
278
+ owner: urlParts[3],
279
+ repo: urlParts[4],
280
+ urlNumber: urlParts[6] // Could be issue or PR number
281
+ };
282
+ };
283
+
284
+ // Helper function to parse time string and calculate wait time
285
+ export const parseResetTime = (timeStr) => {
286
+ // Normalize and parse time formats like:
287
+ // "5:30am", "11:45pm", "12:16 PM", "07:05 Am", "5am", "5 AM"
288
+ const normalized = (timeStr || '').toString().trim();
289
+
290
+ // Accept both HH:MM am/pm and HH am/pm
291
+ let match = normalized.match(/^(\d{1,2})(?::(\d{2}))?\s*([ap]m)$/i);
292
+ if (!match) {
293
+ throw new Error(`Invalid time format: ${timeStr}`);
294
+ }
295
+
296
+ const [, hourStr, minuteMaybe, ampm] = match;
297
+ const minuteStr = minuteMaybe || '00';
298
+ let hour = parseInt(hourStr);
299
+ const minute = parseInt(minuteStr);
300
+
301
+ // Convert to 24-hour format
302
+ if (ampm.toLowerCase() === 'pm' && hour !== 12) {
303
+ hour += 12;
304
+ } else if (ampm.toLowerCase() === 'am' && hour === 12) {
305
+ hour = 0;
306
+ }
307
+
308
+ return { hour, minute };
309
+ };
310
+
311
+ // Calculate milliseconds until the next occurrence of the specified time
312
+ export const calculateWaitTime = (resetTime) => {
313
+ const { hour, minute } = parseResetTime(resetTime);
314
+
315
+ const now = new Date();
316
+ const today = new Date(now);
317
+ today.setHours(hour, minute, 0, 0);
318
+
319
+ // If the time has already passed today, schedule for tomorrow
320
+ if (today <= now) {
321
+ today.setDate(today.getDate() + 1);
322
+ }
323
+
324
+ return today.getTime() - now.getTime();
325
+ };