@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 +16 -0
- package/README.md +7 -6
- package/package.json +1 -1
- package/src/exit-handler.lib.mjs +115 -0
- package/src/option-suggestions.lib.mjs +1 -0
- package/src/solve.branch-errors.lib.mjs +29 -9
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.mjs +16 -11
- package/src/solve.repo-setup.lib.mjs +197 -20
- package/src/solve.repository.lib.mjs +3 -2
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
|
|
345
|
-
|
|
|
346
|
-
| `--tool`
|
|
347
|
-
| `--verbose`
|
|
348
|
-
| `--attach-logs`
|
|
349
|
-
| `--
|
|
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
package/src/exit-handler.lib.mjs
CHANGED
|
@@ -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
|
|
@@ -232,15 +232,35 @@ export async function handleBranchCreationError({ branchName, errorOutput, tempD
|
|
|
232
232
|
await log(` ${line}`);
|
|
233
233
|
}
|
|
234
234
|
await log('');
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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, $ }) {
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -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 #
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
69
|
+
let defaultBranch = defaultBranchResult.stdout.toString().trim();
|
|
70
70
|
if (!defaultBranch) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|