@link-assistant/hive-mind 1.32.2 → 1.33.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.33.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f7a2fdd: Add --auto-init-repository option to automatically initialize empty repositories by creating a simple README.md file, enabling branch creation and pull request workflows on repositories with no commits
8
+
9
+ ## 1.32.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 04cf237: fix: properly drain active handles at exit to prevent indefinite process hang (Issue #1431)
14
+
15
+ Root causes identified and fixed: process.stdin (ReadStream) was never unreferenced; undici's global connection pool (Socket×2) was never closed; surviving command-stream child processes (ChildProcess) were never unreferenced; process.stdout/stderr (WriteStream×2) were not unreferenced on non-TTY descriptors.
16
+
17
+ Added drainHandles() in exit-handler.lib.mjs that unrefs/closes all four handle types before process.exit(). Added logActiveHandles() export with per-handle detail (fd, path, pid, remoteAddress) that always logs to the log file. Added no-leaked-streams ESLint rule to catch bare createReadStream/createWriteStream calls whose return value is discarded — the stream companion to the existing no-leaked-timers rule.
18
+
3
19
  ## 1.32.2
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -341,12 +341,13 @@ solve <issue-url> [options]
341
341
 
342
342
  **Other useful options:**
343
343
 
344
- | Option | Alias | Description | Default |
345
- | --------------- | ----- | ------------------------------------------------ | ------- |
346
- | `--tool` | | AI tool (claude, opencode, codex, agent) | claude |
347
- | `--verbose` | `-v` | Enable verbose logging | false |
348
- | `--attach-logs` | | Attach logs to PR (⚠️ may expose sensitive data) | false |
349
- | `--help` | `-h` | Show all available options | - |
344
+ | Option | Alias | Description | Default |
345
+ | ------------------------ | ----- | ------------------------------------------------ | ------- |
346
+ | `--tool` | | AI tool (claude, opencode, codex, agent) | claude |
347
+ | `--verbose` | `-v` | Enable verbose logging | false |
348
+ | `--attach-logs` | | Attach logs to PR (⚠️ may expose sensitive data) | false |
349
+ | `--auto-init-repository` | | Auto-initialize empty repos (creates README.md) | false |
350
+ | `--help` | `-h` | Show all available options | - |
350
351
 
351
352
  > **📖 Full options list**: See [docs/CONFIGURATION.md](./docs/CONFIGURATION.md#solve-options) for all available options including forking, auto-continue, watch mode, and experimental features.
352
353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.32.2",
3
+ "version": "1.33.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -66,12 +66,127 @@ const showExitMessage = async (reason = 'Process exiting', code = 0) => {
66
66
  await logFunction(`📁 Full log file: ${currentLogPath}`);
67
67
  };
68
68
 
69
+ /**
70
+ * Drain and unref active Node.js handles so the event loop can exit naturally.
71
+ *
72
+ * Issue #1431: After all work completes, several handle types keep the event loop
73
+ * alive and prevent the process from exiting on its own:
74
+ *
75
+ * - ReadStream — process.stdin is never unreferenced. Node keeps it open so the
76
+ * process can receive user input, but a CLI tool is done with input
77
+ * at this point. Calling .unref() signals that this handle should
78
+ * not prevent exit.
79
+ *
80
+ * - Socket (×2) — Node 18+ built-in fetch() uses undici internally. Each HTTP
81
+ * request leaves a keep-alive socket in undici's global connection
82
+ * pool. Calling getGlobalDispatcher().close() drains and destroys
83
+ * all pooled connections.
84
+ *
85
+ * - ChildProcess — command-stream spawns child processes. The handle stays alive
86
+ * until the OS reclaims the process entry. Calling .unref() on
87
+ * each surviving child lets Node exit without waiting for them.
88
+ *
89
+ * - WriteStream (×2) — process.stdout and process.stderr are always-open writable
90
+ * streams. On non-TTY file descriptors (e.g. pipes, redirects)
91
+ * they can keep the event loop alive. Calling .unref() is safe
92
+ * because we have already finished all output at this point.
93
+ *
94
+ * All of these are "unref" fixes — the handles are not forcibly destroyed, just
95
+ * marked as non-blocking so the event loop considers the process idle once all real
96
+ * async work is done. This is the idiomatic Node.js pattern for CLI tools.
97
+ */
98
+ const drainHandles = async () => {
99
+ // 1. Unref process.stdin so a dangling ReadStream cannot block exit.
100
+ try {
101
+ if (process.stdin && !process.stdin.destroyed) {
102
+ process.stdin.unref();
103
+ }
104
+ } catch {
105
+ // Ignore — stdin may already be closed
106
+ }
107
+
108
+ // 2. Close undici's global dispatcher to drain keep-alive HTTP sockets (Socket handles).
109
+ // Node 18+ built-in fetch uses undici; each fetch() call may leave a socket in the
110
+ // pool. getGlobalDispatcher().close() is the documented way to drain them.
111
+ try {
112
+ const { getGlobalDispatcher } = await import('undici');
113
+ const dispatcher = getGlobalDispatcher();
114
+ if (dispatcher && typeof dispatcher.close === 'function') {
115
+ await Promise.race([
116
+ dispatcher.close(),
117
+ new Promise(resolve => setTimeout(resolve, 1000)), // hard 1s deadline
118
+ ]);
119
+ }
120
+ } catch {
121
+ // undici may not be available in all Node versions — safe to ignore
122
+ }
123
+
124
+ // 3. Unref surviving child processes from command-stream.
125
+ // These are typically already-exited but their OS handle entry lingers.
126
+ try {
127
+ for (const handle of process._getActiveHandles()) {
128
+ if (handle?.constructor?.name === 'ChildProcess' && typeof handle.unref === 'function') {
129
+ handle.unref();
130
+ }
131
+ }
132
+ } catch {
133
+ // _getActiveHandles is a private V8 API — safe to ignore
134
+ }
135
+
136
+ // 4. Unref stdout/stderr on non-TTY descriptors.
137
+ // On a TTY these are already non-blocking; on pipes/redirects they keep the loop alive.
138
+ try {
139
+ if (process.stdout && !process.stdout.isTTY && typeof process.stdout.unref === 'function') {
140
+ process.stdout.unref();
141
+ }
142
+ if (process.stderr && !process.stderr.isTTY && typeof process.stderr.unref === 'function') {
143
+ process.stderr.unref();
144
+ }
145
+ } catch {
146
+ // Ignore
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Log active handles and requests for diagnostics.
152
+ * Always logs if there are unexpected handles (not just in verbose mode),
153
+ * treating lingering handles as a warning-level signal.
154
+ *
155
+ * @param {Function|null} log - Optional logging function; falls back to console.warn
156
+ */
157
+ export const logActiveHandles = async (log = null) => {
158
+ try {
159
+ const handles = process._getActiveHandles();
160
+ const requests = process._getActiveRequests();
161
+ if (handles.length === 0 && requests.length === 0) return;
162
+
163
+ const emit = log || (msg => console.warn(msg));
164
+ await emit(`\n🔍 Active Node.js handles at exit (${handles.length} handles, ${requests.length} requests):`);
165
+ for (const h of handles) {
166
+ const name = h.constructor?.name || typeof h;
167
+ // Extra detail for streams: show fd and path/remoteAddress if available
168
+ const detail = [h.fd != null ? `fd=${h.fd}` : null, h.path ? `path=${h.path}` : null, h.remoteAddress ? `remote=${h.remoteAddress}:${h.remotePort}` : null, h.pid != null ? `pid=${h.pid}` : null, h.spawnfile ? `file=${h.spawnfile}` : null].filter(Boolean).join(', ');
169
+ await emit(` Handle: ${name}${detail ? ` (${detail})` : ''}`);
170
+ }
171
+ for (const r of requests) {
172
+ await emit(` Request: ${r.constructor?.name || typeof r}`);
173
+ }
174
+ } catch {
175
+ // _getActiveHandles is a private V8 API — safe to ignore if unavailable
176
+ }
177
+ };
178
+
69
179
  /**
70
180
  * Safe exit function that ensures log path is shown
71
181
  */
72
182
  export const safeExit = async (code = 0, reason = 'Process completed') => {
73
183
  await showExitMessage(reason, code);
74
184
 
185
+ // Issue #1431: Drain/unref active handles so the event loop exits naturally.
186
+ // This resolves the root causes of dangling ReadStream (stdin), Socket (undici),
187
+ // ChildProcess (command-stream), and WriteStream (stdout/stderr) handles.
188
+ await drainHandles();
189
+
75
190
  // Close Sentry to flush any pending events and allow the process to exit cleanly.
76
191
  // Use Promise.race with a hard timeout to guarantee sentry.close() never hangs
77
192
  // indefinitely — the 2000ms hint passed to sentry.close() is forwarded to internal
@@ -215,6 +215,7 @@ const KNOWN_OPTION_NAMES = [
215
215
  'prompt-examples-folder',
216
216
  'session-type',
217
217
  'working-directory',
218
+ 'auto-init-repository',
218
219
  'prompt-ensure-all-requirements-are-met',
219
220
  'finalize',
220
221
  'finalize-model',
@@ -232,15 +232,35 @@ export async function handleBranchCreationError({ branchName, errorOutput, tempD
232
232
  await log(` ${line}`);
233
233
  }
234
234
  await log('');
235
- await log(' 💡 Possible causes:');
236
- await log(' • Branch name already exists');
237
- await log(' Uncommitted changes in repository');
238
- await log(' • Git configuration issues');
239
- await log('');
240
- await log(' 🔧 How to fix:');
241
- await log(' 1. Try running the command again (uses random names)');
242
- await log(` 2. Check git status: cd ${tempDir} && git status`);
243
- await log(` 3. View existing branches: cd ${tempDir} && git branch -a`);
235
+
236
+ // Check if this is an empty repository error (no commits to branch from)
237
+ const isEmptyRepoError = errorOutput.includes('is not a commit') || errorOutput.includes('not a valid object name') || errorOutput.includes('unknown revision');
238
+ if (isEmptyRepoError) {
239
+ await log(' 💡 Root cause:');
240
+ await log(' The repository appears to be empty (no commits).');
241
+ await log(' Cannot create a branch from a non-existent commit.');
242
+ await log('');
243
+ await log(' 🔧 How to fix:');
244
+ await log(' Use the --auto-init-repository flag to automatically initialize the repository:');
245
+ if (owner && repo) {
246
+ await log(` solve https://github.com/${owner}/${repo}/issues/<number> --auto-init-repository`);
247
+ } else {
248
+ await log(' solve <issue-url> --auto-init-repository');
249
+ }
250
+ await log('');
251
+ await log(' This will create a simple README.md file to make the repository non-empty,');
252
+ await log(' allowing branch creation and pull request workflows to proceed.');
253
+ } else {
254
+ await log(' 💡 Possible causes:');
255
+ await log(' • Branch name already exists');
256
+ await log(' • Uncommitted changes in repository');
257
+ await log(' • Git configuration issues');
258
+ await log('');
259
+ await log(' 🔧 How to fix:');
260
+ await log(' 1. Try running the command again (uses random names)');
261
+ await log(` 2. Check git status: cd ${tempDir} && git status`);
262
+ await log(` 3. View existing branches: cd ${tempDir} && git branch -a`);
263
+ }
244
264
  }
245
265
 
246
266
  export async function handleBranchVerificationError({ isContinueMode, branchName, actualBranch, prNumber, owner, repo, tempDir, formatAligned, log, $ }) {
@@ -362,6 +362,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
362
362
  description: 'Guide Claude to use agent-commander CLI (start-agent) instead of native Task tool for subagent delegation. Allows using any supported agent type (claude, opencode, codex, agent) with unified API. Only works with --tool claude and requires agent-commander to be installed.',
363
363
  default: false,
364
364
  },
365
+ 'auto-init-repository': {
366
+ type: 'boolean',
367
+ description: 'Automatically initialize empty repositories by creating a simple README.md file. Only works when you have write access to the repository. This allows branch creation and pull request workflows to proceed on repositories that have no commits.',
368
+ default: false,
369
+ },
365
370
  'attach-solution-summary': {
366
371
  type: 'boolean',
367
372
  description: 'Attach the AI solution summary (from the result field) as a comment to the PR/issue after completion. The summary is extracted from the AI tool JSON output and posted under a "Solution summary" header.',
package/src/solve.mjs CHANGED
@@ -76,7 +76,7 @@ const { startWatchMode } = watchLib;
76
76
  const { startAutoRestartUntilMergeable } = await import('./solve.auto-merge.lib.mjs');
77
77
  const { runAutoEnsureRequirements } = await import('./solve.auto-ensure.lib.mjs');
78
78
  const exitHandler = await import('./exit-handler.lib.mjs');
79
- const { initializeExitHandler, installGlobalExitHandlers, safeExit } = exitHandler;
79
+ const { initializeExitHandler, installGlobalExitHandlers, safeExit, logActiveHandles } = exitHandler;
80
80
  const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
81
81
  const getResourceSnapshot = memoryCheck.getResourceSnapshot;
82
82
 
@@ -534,11 +534,16 @@ try {
534
534
  });
535
535
 
536
536
  // Verify default branch and status using the new module
537
+ // Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
537
538
  const defaultBranch = await verifyDefaultBranchAndStatus({
538
539
  tempDir,
539
540
  log,
540
541
  formatAligned,
541
542
  $,
543
+ argv,
544
+ owner,
545
+ repo,
546
+ issueUrl,
542
547
  });
543
548
  // Create or checkout branch using the new module
544
549
  const branchName = await createOrCheckoutBranch({
@@ -1450,14 +1455,14 @@ try {
1450
1455
  // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
1451
1456
  await closeSentry();
1452
1457
 
1453
- // Issue #1335: Log active handles at exit to diagnose future process hang.
1454
- if (argv.verbose) {
1455
- const handles = process._getActiveHandles();
1456
- const requests = process._getActiveRequests();
1457
- if (handles.length > 0 || requests.length > 0) {
1458
- await log(`\n🔍 Active Node.js handles at exit (${handles.length} handles, ${requests.length} requests):`, { verbose: true });
1459
- for (const h of handles) await log(` Handle: ${h.constructor?.name || typeof h}`, { verbose: true });
1460
- for (const r of requests) await log(` Request: ${r.constructor?.name || typeof r}`, { verbose: true });
1461
- }
1462
- }
1458
+ // Issue #1431: Log active handles before draining.
1459
+ // Always logged to file and console so future hangs are immediately visible in logs.
1460
+ // drainHandles() inside safeExit() will unref/close these before process.exit().
1461
+ await logActiveHandles(msg => log(msg));
1462
+
1463
+ // Issue #1431: safeExit() calls drainHandles() to unref/close known handle types
1464
+ // (process.stdin ReadStream, undici Socket pool, command-stream ChildProcess,
1465
+ // process.stdout/stderr WriteStreams) so the event loop exits naturally, then
1466
+ // calls process.exit(0) as a deterministic safety net.
1467
+ await safeExit(0, 'Process completed');
1463
1468
  }
@@ -56,7 +56,7 @@ async function setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMod
56
56
  return await setupPrForkFn(tempDir, argv, prForkOwner, repo, isContinueMode, owner);
57
57
  }
58
58
 
59
- export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $ }) {
59
+ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, argv, owner, repo, issueUrl }) {
60
60
  // Verify we're on the default branch and get its name
61
61
  const defaultBranchResult = await $({ cwd: tempDir })`git branch --show-current`;
62
62
 
@@ -66,27 +66,128 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
66
66
  throw new Error('Failed to get current branch');
67
67
  }
68
68
 
69
- const defaultBranch = defaultBranchResult.stdout.toString().trim();
69
+ let defaultBranch = defaultBranchResult.stdout.toString().trim();
70
70
  if (!defaultBranch) {
71
- await log('');
72
- await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
73
- await log('');
74
- await log(' 🔍 What happened:');
75
- await log(" Unable to determine the repository's default branch.");
76
- await log('');
77
- await log(' 💡 This might mean:');
78
- await log(' Repository is empty (no commits)');
79
- await log(' Unusual repository configuration');
80
- await log(' • Git command issues');
81
- await log('');
82
- await log(' 🔧 How to fix:');
83
- await log(' 1. Check repository status');
84
- await log(` 2. Verify locally: cd ${tempDir} && git branch`);
85
- await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
86
- await log('');
87
- throw new Error('Default branch detection failed');
71
+ // Repository is likely empty (no commits) - detect and handle
72
+ const isEmptyRepo = await detectEmptyRepository(tempDir, $);
73
+
74
+ if (isEmptyRepo && argv && argv.autoInitRepository && owner && repo) {
75
+ // --auto-init-repository is enabled, try to initialize
76
+ await log('');
77
+ await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' });
78
+ await log(`${formatAligned('', '', `Repository ${owner}/${repo} contains no commits`)}`);
79
+ await log(`${formatAligned('', '', '--auto-init-repository is enabled, attempting initialization...')}`);
80
+ await log('');
81
+
82
+ const repository = await import('./solve.repository.lib.mjs');
83
+ const { tryInitializeEmptyRepository } = repository;
84
+ const initialized = await tryInitializeEmptyRepository(owner, repo);
85
+
86
+ if (initialized) {
87
+ await log('');
88
+ await log(`${formatAligned('🔄', 'Re-fetching:', 'Pulling initialized repository...')}`);
89
+ // Wait for GitHub to process the new file
90
+ await new Promise(resolve => setTimeout(resolve, 2000));
91
+
92
+ // Re-fetch the origin to get the new commit
93
+ const fetchResult = await $({ cwd: tempDir })`git fetch origin`;
94
+ if (fetchResult.code !== 0) {
95
+ await log(`${formatAligned('❌', 'Fetch failed:', 'Could not fetch after initialization')}`, { level: 'error' });
96
+ throw new Error('Failed to fetch after empty repository initialization');
97
+ }
98
+
99
+ // Determine default branch name from the remote
100
+ const remoteHeadResult = await $({ cwd: tempDir })`git remote show origin`;
101
+ let remoteBranch = 'main'; // default fallback
102
+ if (remoteHeadResult.code === 0) {
103
+ const remoteOutput = remoteHeadResult.stdout.toString();
104
+ const headMatch = remoteOutput.match(/HEAD branch:\s*(\S+)/);
105
+ if (headMatch) {
106
+ remoteBranch = headMatch[1];
107
+ }
108
+ }
109
+
110
+ // Checkout the remote branch locally
111
+ const checkoutResult = await $({ cwd: tempDir })`git checkout -b ${remoteBranch} origin/${remoteBranch}`;
112
+ if (checkoutResult.code !== 0) {
113
+ // Try alternative: maybe the branch already exists locally somehow
114
+ const altResult = await $({ cwd: tempDir })`git checkout ${remoteBranch}`;
115
+ if (altResult.code !== 0) {
116
+ await log(`${formatAligned('❌', 'Checkout failed:', `Could not checkout ${remoteBranch} after initialization`)}`, { level: 'error' });
117
+ throw new Error('Failed to checkout branch after empty repository initialization');
118
+ }
119
+ }
120
+
121
+ defaultBranch = remoteBranch;
122
+ await log(`${formatAligned('✅', 'Repository initialized:', `Now on branch ${defaultBranch}`)}`);
123
+ await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
124
+ } else {
125
+ // Auto-init failed - provide helpful message with --auto-init-repository context
126
+ await log('');
127
+ await log(`${formatAligned('❌', 'AUTO-INIT FAILED', '')}`, { level: 'error' });
128
+ await log('');
129
+ await log(' 🔍 What happened:');
130
+ await log(` Repository ${owner}/${repo} is empty (no commits).`);
131
+ await log(' --auto-init-repository was enabled but initialization failed.');
132
+ await log(' You may not have write access to create files in the repository.');
133
+ await log('');
134
+ await log(' 💡 How to fix:');
135
+ await log(' Option 1: Ask repository owner to add initial content');
136
+ await log(' Even a simple README.md file would allow branch creation');
137
+ await log('');
138
+ await log(` Option 2: Manually initialize: gh api repos/${owner}/${repo}/contents/README.md \\`);
139
+ await log(' --method PUT --field message="Initialize repository" \\');
140
+ await log(' --field content="$(echo "# repo" | base64)"');
141
+ await log('');
142
+
143
+ // Post a comment on the issue informing about the empty repository
144
+ await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ });
145
+
146
+ throw new Error('Empty repository auto-initialization failed');
147
+ }
148
+ } else if (isEmptyRepo) {
149
+ // Empty repo detected but --auto-init-repository is not enabled
150
+ await log('');
151
+ await log(`${formatAligned('❌', 'EMPTY REPOSITORY DETECTED', '')}`, { level: 'error' });
152
+ await log('');
153
+ await log(' 🔍 What happened:');
154
+ await log(` The repository${owner && repo ? ` ${owner}/${repo}` : ''} is empty (no commits).`);
155
+ await log(' Cannot create branches or pull requests on an empty repository.');
156
+ await log('');
157
+ await log(' 💡 How to fix:');
158
+ await log(' Option 1: Use --auto-init-repository flag to automatically create a README.md');
159
+ await log(` solve <issue-url> --auto-init-repository`);
160
+ await log('');
161
+ await log(' Option 2: Ask repository owner to add initial content');
162
+ await log(' Even a simple README.md file would allow branch creation');
163
+ await log('');
164
+
165
+ // Post a comment on the issue informing about the empty repository
166
+ await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ });
167
+
168
+ throw new Error('Empty repository detected - use --auto-init-repository to initialize');
169
+ } else {
170
+ // Not an empty repo, some other issue with branch detection
171
+ await log('');
172
+ await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
173
+ await log('');
174
+ await log(' 🔍 What happened:');
175
+ await log(" Unable to determine the repository's default branch.");
176
+ await log('');
177
+ await log(' 💡 This might mean:');
178
+ await log(' • Unusual repository configuration');
179
+ await log(' • Git command issues');
180
+ await log('');
181
+ await log(' 🔧 How to fix:');
182
+ await log(' 1. Check repository status');
183
+ await log(` 2. Verify locally: cd ${tempDir} && git branch`);
184
+ await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
185
+ await log('');
186
+ throw new Error('Default branch detection failed');
187
+ }
188
+ } else {
189
+ await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
88
190
  }
89
- await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
90
191
 
91
192
  // Ensure we're on a clean default branch
92
193
  const statusResult = await $({ cwd: tempDir })`git status --porcelain`;
@@ -106,3 +207,79 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
106
207
 
107
208
  return defaultBranch;
108
209
  }
210
+
211
+ /**
212
+ * Try to post a comment on the issue informing the user about the empty repository.
213
+ * This is a non-critical operation - errors are silently ignored.
214
+ * When --auto-init-repository succeeds, no comment is posted (no action needed from the user).
215
+ */
216
+ async function tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ }) {
217
+ if (!issueUrl) return;
218
+
219
+ try {
220
+ const issueMatch = issueUrl.match(/\/issues\/(\d+)/);
221
+ if (!issueMatch) return;
222
+
223
+ const issueNumber = issueMatch[1];
224
+ await log(`${formatAligned('💬', 'Creating comment:', 'Informing about empty repository...')}`);
225
+
226
+ const commentBody = `## ⚠️ Repository Initialization Required
227
+
228
+ Hello! I attempted to work on this issue, but encountered a problem:
229
+
230
+ **Issue**: The repository is empty (no commits) and branches cannot be created.
231
+ **Reason**: Git cannot create branches in a repository with no commits.
232
+
233
+ ### 🔧 How to resolve:
234
+
235
+ **Option 1: Use \`--auto-init-repository\` flag**
236
+ Re-run the solver with the \`--auto-init-repository\` flag to automatically create a simple README.md:
237
+ \`\`\`
238
+ solve ${issueUrl} --auto-init-repository
239
+ \`\`\`
240
+
241
+ **Option 2: Initialize the repository yourself**
242
+ Please add initial content to the repository. Even a simple README.md (even if it is empty or contains just the title) file would make it possible to create branches and work on this issue.
243
+
244
+ Once the repository contains at least one commit with any file, I'll be able to proceed with solving this issue.
245
+
246
+ Thank you!`;
247
+
248
+ const commentResult = await $`gh issue comment ${issueNumber} --repo ${owner}/${repo} --body ${commentBody}`;
249
+ if (commentResult.code === 0) {
250
+ await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`);
251
+ } else {
252
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
253
+ }
254
+ } catch {
255
+ // Silently ignore comment creation errors - not critical to the process
256
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Detect if a cloned repository is empty (has no commits).
262
+ * An empty repository has no branches and no commits.
263
+ */
264
+ async function detectEmptyRepository(tempDir, $) {
265
+ // Check if there are any commits in the repository
266
+ const logResult = await $({ cwd: tempDir })`git rev-parse HEAD 2>&1`;
267
+ if (logResult.code !== 0) {
268
+ // git rev-parse HEAD fails when there are no commits
269
+ const output = (logResult.stdout || logResult.stderr || '').toString();
270
+ if (output.includes('unknown revision') || output.includes('bad default revision') || output.includes('does not have any commits')) {
271
+ return true;
272
+ }
273
+ }
274
+
275
+ // Also check if there are any remote branches
276
+ const remoteBranchResult = await $({ cwd: tempDir })`git branch -r`;
277
+ if (remoteBranchResult.code === 0) {
278
+ const branches = remoteBranchResult.stdout.toString().trim();
279
+ if (!branches) {
280
+ return true;
281
+ }
282
+ }
283
+
284
+ return false;
285
+ }
@@ -317,8 +317,9 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
317
317
  };
318
318
 
319
319
  // Try to initialize an empty repository by creating a simple README.md
320
- // This makes the repository forkable
321
- const tryInitializeEmptyRepository = async (owner, repo) => {
320
+ // This makes the repository forkable and allows branch creation
321
+ // Exported for use in solve.repo-setup.lib.mjs (direct access path for empty repos)
322
+ export const tryInitializeEmptyRepository = async (owner, repo) => {
322
323
  try {
323
324
  await log(`${formatAligned('🔧', 'Auto-fix:', 'Attempting to initialize empty repository...')}`);
324
325