From 10fb005803b1de9ed769d05b60302267f10c1772 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 6 Feb 2026 22:54:18 +0100 Subject: [PATCH 1/5] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-assistant/hive-mind/issues/1230 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..4487f2d6a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-assistant/hive-mind/issues/1230 +Your prepared branch: issue-1230-f88cfda7aae8 +Your prepared working directory: /tmp/gh-issue-solver-1770414852260 + +Proceed. From f7a2fddf86dd2b3d66c3b7cc88b22dc4c9d645ac Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 6 Feb 2026 23:03:20 +0100 Subject: [PATCH 2/5] feat: add --auto-init-repository option for empty repositories (#1230) When the solver encounters an empty repository (no commits) with direct write access, branch creation fails because origin/main has no commits. The existing empty repo handling only covered the fork path. This adds: - New --auto-init-repository CLI option (default: false) - Empty repository detection in verifyDefaultBranchAndStatus() - Auto-initialization using existing tryInitializeEmptyRepository() - Improved error messages suggesting --auto-init-repository - Case study documentation in docs/case-studies/issue-1230/ - Tests and experiment scripts Fixes #1230 Co-Authored-By: Claude Opus 4.6 --- .changeset/auto-init-repository.md | 5 + docs/case-studies/issue-1230/README.md | 87 +++++++++++ docs/case-studies/issue-1230/solve-log.txt | 85 +++++++++++ experiments/test-auto-init-repository.mjs | 112 ++++++++++++++ src/option-suggestions.lib.mjs | 1 + src/solve.branch-errors.lib.mjs | 38 +++-- src/solve.config.lib.mjs | 5 + src/solve.mjs | 4 + src/solve.repo-setup.lib.mjs | 160 +++++++++++++++++--- src/solve.repository.lib.mjs | 5 +- tests/test-auto-init-repository.mjs | 168 +++++++++++++++++++++ 11 files changed, 639 insertions(+), 31 deletions(-) create mode 100644 .changeset/auto-init-repository.md create mode 100644 docs/case-studies/issue-1230/README.md create mode 100644 docs/case-studies/issue-1230/solve-log.txt create mode 100644 experiments/test-auto-init-repository.mjs create mode 100644 tests/test-auto-init-repository.mjs diff --git a/.changeset/auto-init-repository.md b/.changeset/auto-init-repository.md new file mode 100644 index 000000000..f49cff4d4 --- /dev/null +++ b/.changeset/auto-init-repository.md @@ -0,0 +1,5 @@ +--- +'@link-assistant/hive-mind': minor +--- + +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 diff --git a/docs/case-studies/issue-1230/README.md b/docs/case-studies/issue-1230/README.md new file mode 100644 index 000000000..6f5b984a7 --- /dev/null +++ b/docs/case-studies/issue-1230/README.md @@ -0,0 +1,87 @@ +# Case Study: Issue #1230 - Empty Repository Branch Creation Failure + +## Summary + +When the solve command attempted to work on an issue in an empty repository (no commits), it failed silently during branch creation instead of providing actionable guidance or automatically resolving the problem. + +## Timeline of Events + +1. **User runs solve command** against `https://github.com/DeepYV/Healora/issues/1` +2. **Repository access check passes** - User has write access to the private repository +3. **Clone succeeds** - Empty repository is cloned (a valid operation) +4. **Default branch detection succeeds** - Returns `main` (from HEAD symbolic ref) +5. **Branch creation fails** - `git checkout -b issue-1-xxx origin/main` fails because `origin/main` is not a valid commit (no commits exist) +6. **Error message is misleading** - Suggests branch name conflicts, uncommitted changes, or git config issues, none of which are the actual problem + +## Root Cause Analysis + +### Primary Root Cause + +The repository `DeepYV/Healora` was completely empty (no commits, no files). When attempting to create a branch from `origin/main`, git fails with: + +``` +fatal: 'origin/main' is not a commit and a branch 'issue-1-4529e36b433e' cannot be created from it +``` + +### Contributing Factors + +1. **Existing empty repo handling was fork-only**: The codebase already had `tryInitializeEmptyRepository()` in `solve.repository.lib.mjs`, but it was only triggered during the fork creation path (HTTP 403 "Empty repositories cannot be forked"). When the user has direct write access, the fork path is skipped entirely. + +2. **No empty repo detection in the direct access path**: The `verifyDefaultBranchAndStatus()` function checks for empty default branch but doesn't distinguish between "empty repository" and "other git issues." + +3. **Error handler lacks empty repo context**: `handleBranchCreationError()` suggests generic causes (branch exists, uncommitted changes) without detecting the specific "is not a commit" pattern. + +## Code Flow (Before Fix) + +``` +solve.mjs: + ├── checkRepositoryWritePermission() → PASS (has write access) + ├── setupRepositoryAndClone() + │ ├── setupRepository() → No fork needed (direct access) + │ └── cloneRepository() → SUCCESS (cloning empty repo is valid) + ├── verifyDefaultBranchAndStatus() + │ └── git branch --show-current → "" (empty - no commits!) + │ └── THROWS: "Default branch detection failed" + └── createOrCheckoutBranch() → NEVER REACHED + └── git checkout -b ... origin/main → Would fail (no commits) +``` + +## Solution + +### New Feature: `--auto-init-repository` + +Added a new CLI option `--auto-init-repository` that: + +1. **Detects empty repositories** via `detectEmptyRepository()` - checks for absence of commits and remote branches +2. **Auto-initializes** using the existing `tryInitializeEmptyRepository()` function (creates README.md via GitHub API) +3. **Re-fetches and continues** - After initialization, fetches the new commit, checks out the default branch, and proceeds normally + +### Improved Error Messages + +- When `--auto-init-repository` is NOT enabled: Clear message suggesting the flag +- When branch creation fails due to empty repo: Specific "is not a commit" pattern detection with actionable fix suggestion +- Reuses existing `tryInitializeEmptyRepository()` code (DRY principle) + +## Files Changed + +| File | Change | +| --------------------------------- | ---------------------------------------------------------------------------- | +| `src/solve.config.lib.mjs` | Added `--auto-init-repository` option definition | +| `src/option-suggestions.lib.mjs` | Added to `KNOWN_OPTION_NAMES` for typo detection | +| `src/solve.repository.lib.mjs` | Exported `tryInitializeEmptyRepository` for reuse | +| `src/solve.repo-setup.lib.mjs` | Added empty repo detection and auto-init in `verifyDefaultBranchAndStatus()` | +| `src/solve.mjs` | Pass `argv`, `owner`, `repo` to `verifyDefaultBranchAndStatus()` | +| `src/solve.branch-errors.lib.mjs` | Improved error message for empty repo branch creation failures | + +## Artifacts + +- [Original solve log](./solve-log.txt) - Full log of the failed solve attempt +- [GitHub Issue](https://github.com/link-assistant/hive-mind/issues/1230) +- [Related Issue](https://github.com/DeepYV/Healora/issues/1) - The issue that triggered this failure +- [PR #361](https://github.com/link-assistant/hive-mind/pull/361) - Previous fix for empty repo handling (fork path only) + +## Lessons Learned + +1. **Edge cases need end-to-end coverage**: The empty repo case was handled in the fork path but not the direct access path. Both code paths need the same level of resilience. +2. **Error messages should diagnose, not just report**: The original error message listed generic causes. Pattern-matching on error output (`"is not a commit"`) enables specific, actionable suggestions. +3. **Reuse existing solutions**: `tryInitializeEmptyRepository()` already existed and worked correctly - it just needed to be exported and called from the right place. diff --git a/docs/case-studies/issue-1230/solve-log.txt b/docs/case-studies/issue-1230/solve-log.txt new file mode 100644 index 000000000..2e7adadf8 --- /dev/null +++ b/docs/case-studies/issue-1230/solve-log.txt @@ -0,0 +1,85 @@ +Log file: solve-2026-02-06T21-35-13-888Z.log + +# Solve.mjs Log - 2026-02-06T21:35:13.889Z + +[2026-02-06T21:35:13.890Z] [INFO] 📁 Log file: /home/hive/solve-2026-02-06T21-35-13-888Z.log +[2026-02-06T21:35:13.891Z] [INFO] (All output will be logged here) +[2026-02-06T21:35:14.373Z] [INFO] +[2026-02-06T21:35:14.374Z] [INFO] 🚀 solve v1.16.0 +[2026-02-06T21:35:14.374Z] [INFO] 🔧 Raw command executed: +[2026-02-06T21:35:14.374Z] [INFO] /home/hive/.nvm/versions/node/v20.20.0/bin/node /home/hive/.bun/bin/solve https://github.com/DeepYV/Healora/issues/1 --model opus --attach-logs --verbose --no-tool-check --auto-resume-on-limit-reset --tokens-budget-stats +[2026-02-06T21:35:14.374Z] [INFO] +[2026-02-06T21:35:14.388Z] [INFO] +[2026-02-06T21:35:14.389Z] [WARNING] ⚠️ SECURITY WARNING: --attach-logs is ENABLED +[2026-02-06T21:35:14.389Z] [INFO] +[2026-02-06T21:35:14.389Z] [INFO] This option will upload the complete solution draft log file to the Pull Request. +[2026-02-06T21:35:14.389Z] [INFO] The log may contain sensitive information such as: +[2026-02-06T21:35:14.390Z] [INFO] • API keys, tokens, or secrets +[2026-02-06T21:35:14.390Z] [INFO] • File paths and directory structures +[2026-02-06T21:35:14.390Z] [INFO] • Command outputs and error messages +[2026-02-06T21:35:14.390Z] [INFO] • Internal system information +[2026-02-06T21:35:14.390Z] [INFO] +[2026-02-06T21:35:14.390Z] [INFO] ⚠️ DO NOT use this option with public repositories or if the log +[2026-02-06T21:35:14.391Z] [INFO] might contain sensitive data that should not be shared publicly. +[2026-02-06T21:35:14.391Z] [INFO] +[2026-02-06T21:35:14.391Z] [INFO] Continuing in 5 seconds... (Press Ctrl+C to abort) +[2026-02-06T21:35:14.391Z] [INFO] +[2026-02-06T21:35:19.399Z] [INFO] +[2026-02-06T21:35:19.432Z] [INFO] 💾 Disk space check: 25268MB available (2048MB required) ✅ +[2026-02-06T21:35:19.435Z] [INFO] 🧠 Memory check: 10084MB available, swap: 4095MB (255MB used), total: 13924MB (256MB required) ✅ +[2026-02-06T21:35:19.453Z] [INFO] ⏩ Skipping tool connection validation (dry-run mode or skip-tool-connection-check enabled) +[2026-02-06T21:35:19.453Z] [INFO] ⏩ Skipping GitHub authentication check (dry-run mode or skip-tool-connection-check enabled) +[2026-02-06T21:35:19.453Z] [INFO] 📋 URL validation: +[2026-02-06T21:35:19.453Z] [INFO] Input URL: https://github.com/DeepYV/Healora/issues/1 +[2026-02-06T21:35:19.453Z] [INFO] Is Issue URL: true +[2026-02-06T21:35:19.453Z] [INFO] Is PR URL: false +[2026-02-06T21:35:19.454Z] [INFO] 🔍 Checking repository access for auto-fork... +[2026-02-06T21:35:20.183Z] [INFO] Repository visibility: private +[2026-02-06T21:35:20.184Z] [INFO] ✅ Auto-fork: Write access detected to private repository, working directly on repository +[2026-02-06T21:35:20.185Z] [INFO] 🔍 Checking repository write permissions... +[2026-02-06T21:35:20.583Z] [INFO] ✅ Repository write access: Confirmed +[2026-02-06T21:35:20.990Z] [INFO] Repository visibility: private +[2026-02-06T21:35:20.990Z] [INFO] Auto-cleanup default: true (repository is private) +[2026-02-06T21:35:20.991Z] [INFO] 🔍 Auto-continue enabled: Checking for existing PRs for issue #1... +[2026-02-06T21:35:20.992Z] [INFO] 🔍 Checking for existing branches in DeepYV/Healora... +[2026-02-06T21:35:21.769Z] [INFO] 📝 No existing PRs found for issue #1 - creating new PR +[2026-02-06T21:35:21.769Z] [INFO] 📝 Issue mode: Working with issue #1 +[2026-02-06T21:35:21.770Z] [INFO] +Creating temporary directory: /tmp/gh-issue-solver-1770413721770 +[2026-02-06T21:35:21.773Z] [INFO] +📥 Cloning repository: DeepYV/Healora +[2026-02-06T21:35:22.678Z] [INFO] ✅ Cloned to: /tmp/gh-issue-solver-1770413721770 +[2026-02-06T21:35:22.853Z] [INFO] +📌 Default branch: main +[2026-02-06T21:35:22.904Z] [INFO] +🌿 Creating branch: issue-1-4529e36b433e from main (default) +[2026-02-06T21:35:22.950Z] [INFO] +[2026-02-06T21:35:22.953Z] [ERROR] ❌ BRANCH CREATION FAILED +[2026-02-06T21:35:22.953Z] [INFO] +[2026-02-06T21:35:22.953Z] [INFO] 🔍 What happened: +[2026-02-06T21:35:22.953Z] [INFO] Unable to create branch 'issue-1-4529e36b433e'. +[2026-02-06T21:35:22.954Z] [INFO] Repository: https://github.com/DeepYV/Healora +[2026-02-06T21:35:22.954Z] [INFO] +[2026-02-06T21:35:22.954Z] [INFO] 📦 Git output: +[2026-02-06T21:35:22.954Z] [INFO] fatal: 'origin/main' is not a commit and a branch 'issue-1-4529e36b433e' cannot be created from it +[2026-02-06T21:35:22.954Z] [INFO] +[2026-02-06T21:35:22.954Z] [INFO] 💡 Possible causes: +[2026-02-06T21:35:22.954Z] [INFO] • Branch name already exists +[2026-02-06T21:35:22.955Z] [INFO] • Uncommitted changes in repository +[2026-02-06T21:35:22.955Z] [INFO] • Git configuration issues +[2026-02-06T21:35:22.955Z] [INFO] +[2026-02-06T21:35:22.955Z] [INFO] 🔧 How to fix: +[2026-02-06T21:35:22.955Z] [INFO] 1. Try running the command again (uses random names) +[2026-02-06T21:35:22.955Z] [INFO] 2. Check git status: cd /tmp/gh-issue-solver-1770413721770 && git status +[2026-02-06T21:35:22.955Z] [INFO] 3. View existing branches: cd /tmp/gh-issue-solver-1770413721770 && git branch -a +[2026-02-06T21:35:22.956Z] [INFO] +[2026-02-06T21:35:22.956Z] [INFO] 📂 Working directory: /tmp/gh-issue-solver-1770413721770 +[2026-02-06T21:35:22.961Z] [INFO] Error executing command: +[2026-02-06T21:35:22.963Z] [INFO] Stack trace: Error: Branch operation failed + at createOrCheckoutBranch (file:///home/hive/.bun/install/global/node_modules/@link-assistant/hive-mind/src/solve.branch.lib.mjs:166:11) + at async file:///home/hive/.bun/install/global/node_modules/@link-assistant/hive-mind/src/solve.mjs:549:22 +[2026-02-06T21:35:22.964Z] [ERROR] 📁 Full log file: /home/hive/solve-2026-02-06T21-35-13-888Z.log +[2026-02-06T21:35:23.292Z] [WARNING] ⚠️ Could not determine GitHub user. Cannot create error report issue. +[2026-02-06T21:35:23.295Z] [INFO] +[2026-02-06T21:35:23.295Z] [ERROR] ❌ Error occurred +[2026-02-06T21:35:23.296Z] [INFO] 📁 Full log file: /home/hive/solve-2026-02-06T21-35-13-888Z.log diff --git a/experiments/test-auto-init-repository.mjs b/experiments/test-auto-init-repository.mjs new file mode 100644 index 000000000..c61e6eb2b --- /dev/null +++ b/experiments/test-auto-init-repository.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +// Experiment script to test the --auto-init-repository feature (Issue #1230) +// This script verifies the empty repository detection logic and auto-init flow + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = join(__dirname, '..', 'src'); + +const log = msg => console.log(`[TEST] ${msg}`); + +async function testAutoInitRepository() { + log('Starting --auto-init-repository feature verification...\n'); + + // Test 1: Verify empty repo detection patterns + log('=== Test 1: Empty Repository Detection Patterns ==='); + const detectionPatterns = [ + { input: "fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree", expected: true }, + { input: "fatal: bad default revision 'HEAD'", expected: true }, + { input: 'warning: you appear to have cloned an empty repository. this repository does not have any commits', expected: true }, + { input: "fatal: 'origin/main' is not a commit and a branch 'issue-1-xxx' cannot be created from it", expected: false }, + { input: "Already on 'main'", expected: false }, + ]; + + for (const pattern of detectionPatterns) { + const isEmptyRepo = pattern.input.includes('unknown revision') || pattern.input.includes('bad default revision') || pattern.input.includes('does not have any commits'); + const result = isEmptyRepo === pattern.expected ? '✅ PASS' : '❌ FAIL'; + log(` ${result}: "${pattern.input.substring(0, 60)}..." → ${isEmptyRepo ? 'empty repo' : 'not empty'}`); + } + + // Test 2: Verify branch creation error detection patterns + log('\n=== Test 2: Branch Creation Error Detection ==='); + const branchErrors = [ + { input: "fatal: 'origin/main' is not a commit and a branch 'issue-1-4529e36b433e' cannot be created from it", isEmptyRepo: true }, + { input: "fatal: 'main' is not a valid object name", isEmptyRepo: true }, + { input: "fatal: ambiguous argument 'HEAD': unknown revision", isEmptyRepo: true }, + { input: "fatal: A branch named 'issue-1-xxx' already exists", isEmptyRepo: false }, + { input: "error: pathspec 'issue-1-xxx' did not match any file(s) known to git", isEmptyRepo: false }, + ]; + + for (const test of branchErrors) { + const detected = test.input.includes('is not a commit') || test.input.includes('not a valid object name') || test.input.includes('unknown revision'); + const result = detected === test.isEmptyRepo ? '✅ PASS' : '❌ FAIL'; + log(` ${result}: "${test.input.substring(0, 70)}..." → ${detected ? 'empty repo error' : 'other error'}`); + } + + // Test 3: Verify implementation in source files + log('\n=== Test 3: Implementation Verification ==='); + + // Check solve.config.lib.mjs + const configContent = readFileSync(join(srcDir, 'solve.config.lib.mjs'), 'utf-8'); + if (configContent.includes("'auto-init-repository'")) { + log(' ✅ Option defined in solve.config.lib.mjs'); + } else { + log(' ❌ Option NOT found in solve.config.lib.mjs'); + } + + // Check option-suggestions.lib.mjs + const suggestionsContent = readFileSync(join(srcDir, 'option-suggestions.lib.mjs'), 'utf-8'); + if (suggestionsContent.includes("'auto-init-repository'")) { + log(' ✅ Option in KNOWN_OPTION_NAMES'); + } else { + log(' ❌ Option NOT in KNOWN_OPTION_NAMES'); + } + + // Check solve.repository.lib.mjs export + const repoContent = readFileSync(join(srcDir, 'solve.repository.lib.mjs'), 'utf-8'); + if (repoContent.includes('export const tryInitializeEmptyRepository')) { + log(' ✅ tryInitializeEmptyRepository is exported'); + } else { + log(' ❌ tryInitializeEmptyRepository is NOT exported'); + } + + // Check solve.repo-setup.lib.mjs + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + if (repoSetupContent.includes('detectEmptyRepository') && repoSetupContent.includes('autoInitRepository')) { + log(' ✅ Empty repo detection and auto-init flow implemented'); + } else { + log(' ❌ Empty repo detection and auto-init flow incomplete'); + } + + // Check solve.mjs passes new parameters + const solveContent = readFileSync(join(srcDir, 'solve.mjs'), 'utf-8'); + if (solveContent.includes('argv,') && solveContent.includes('owner,') && solveContent.includes('repo,')) { + log(' ✅ solve.mjs passes argv, owner, repo to verifyDefaultBranchAndStatus'); + } else { + log(' ❌ solve.mjs may not pass all required parameters'); + } + + // Check solve.branch-errors.lib.mjs + const branchErrorsContent = readFileSync(join(srcDir, 'solve.branch-errors.lib.mjs'), 'utf-8'); + if (branchErrorsContent.includes('--auto-init-repository') && branchErrorsContent.includes('is not a commit')) { + log(' ✅ Branch error handler detects empty repo pattern and suggests --auto-init-repository'); + } else { + log(' ❌ Branch error handler may be incomplete'); + } + + log('\n=== Summary ==='); + log('The --auto-init-repository feature:'); + log('1. ✅ Adds new CLI option --auto-init-repository (default: false)'); + log('2. ✅ Detects empty repositories via git rev-parse HEAD and git branch -r'); + log('3. ✅ Reuses existing tryInitializeEmptyRepository() from solve.repository.lib.mjs'); + log('4. ✅ Re-fetches and continues after successful initialization'); + log('5. ✅ Provides clear error messages when auto-init is disabled or fails'); + log('6. ✅ Improves branch creation error messages for empty repo cases'); + log('7. ✅ Maintains backward compatibility (default: false, opt-in only)'); +} + +testAutoInitRepository().catch(console.error); diff --git a/src/option-suggestions.lib.mjs b/src/option-suggestions.lib.mjs index dda1b35dd..8b1927c85 100644 --- a/src/option-suggestions.lib.mjs +++ b/src/option-suggestions.lib.mjs @@ -215,6 +215,7 @@ const KNOWN_OPTION_NAMES = [ 'prompt-examples-folder', 'session-type', 'working-directory', + 'auto-init-repository', ]; /** diff --git a/src/solve.branch-errors.lib.mjs b/src/solve.branch-errors.lib.mjs index bee0c3b21..c4459686a 100644 --- a/src/solve.branch-errors.lib.mjs +++ b/src/solve.branch-errors.lib.mjs @@ -232,15 +232,35 @@ export async function handleBranchCreationError({ branchName, errorOutput, tempD await log(` ${line}`); } await log(''); - await log(' 💡 Possible causes:'); - await log(' • Branch name already exists'); - await log(' • Uncommitted changes in repository'); - await log(' • Git configuration issues'); - await log(''); - await log(' 🔧 How to fix:'); - await log(' 1. Try running the command again (uses random names)'); - await log(` 2. Check git status: cd ${tempDir} && git status`); - await log(` 3. View existing branches: cd ${tempDir} && git branch -a`); + + // Check if this is an empty repository error (no commits to branch from) + const isEmptyRepoError = errorOutput.includes('is not a commit') || errorOutput.includes('not a valid object name') || errorOutput.includes('unknown revision'); + if (isEmptyRepoError) { + await log(' 💡 Root cause:'); + await log(' The repository appears to be empty (no commits).'); + await log(' Cannot create a branch from a non-existent commit.'); + await log(''); + await log(' 🔧 How to fix:'); + await log(' Use the --auto-init-repository flag to automatically initialize the repository:'); + if (owner && repo) { + await log(` solve https://github.com/${owner}/${repo}/issues/ --auto-init-repository`); + } else { + await log(' solve --auto-init-repository'); + } + await log(''); + await log(' This will create a simple README.md file to make the repository non-empty,'); + await log(' allowing branch creation and pull request workflows to proceed.'); + } else { + await log(' 💡 Possible causes:'); + await log(' • Branch name already exists'); + await log(' • Uncommitted changes in repository'); + await log(' • Git configuration issues'); + await log(''); + await log(' 🔧 How to fix:'); + await log(' 1. Try running the command again (uses random names)'); + await log(` 2. Check git status: cd ${tempDir} && git status`); + await log(` 3. View existing branches: cd ${tempDir} && git branch -a`); + } } export async function handleBranchVerificationError({ isContinueMode, branchName, actualBranch, prNumber, owner, repo, tempDir, formatAligned, log, $ }) { diff --git a/src/solve.config.lib.mjs b/src/solve.config.lib.mjs index 19ae0e7cb..10f18b8e7 100644 --- a/src/solve.config.lib.mjs +++ b/src/solve.config.lib.mjs @@ -359,6 +359,11 @@ export const SOLVE_OPTION_DEFINITIONS = { 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.', default: false, }, + 'auto-init-repository': { + type: 'boolean', + 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.', + default: false, + }, }; // Function to create yargs configuration - avoids duplication diff --git a/src/solve.mjs b/src/solve.mjs index e50a7e872..a61a0280c 100755 --- a/src/solve.mjs +++ b/src/solve.mjs @@ -539,11 +539,15 @@ try { }); // Verify default branch and status using the new module + // Pass argv, owner, repo for empty repository auto-initialization (--auto-init-repository) const defaultBranch = await verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, + argv, + owner, + repo, }); // Create or checkout branch using the new module const branchName = await createOrCheckoutBranch({ diff --git a/src/solve.repo-setup.lib.mjs b/src/solve.repo-setup.lib.mjs index 98aa229e9..7f30a3a92 100644 --- a/src/solve.repo-setup.lib.mjs +++ b/src/solve.repo-setup.lib.mjs @@ -56,7 +56,7 @@ async function setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMod return await setupPrForkFn(tempDir, argv, prForkOwner, repo, isContinueMode, owner); } -export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $ }) { +export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, argv, owner, repo }) { // Verify we're on the default branch and get its name const defaultBranchResult = await $({ cwd: tempDir })`git branch --show-current`; @@ -66,27 +66,120 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned throw new Error('Failed to get current branch'); } - const defaultBranch = defaultBranchResult.stdout.toString().trim(); + let defaultBranch = defaultBranchResult.stdout.toString().trim(); if (!defaultBranch) { - await log(''); - await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' }); - await log(''); - await log(' 🔍 What happened:'); - await log(" Unable to determine the repository's default branch."); - await log(''); - await log(' 💡 This might mean:'); - await log(' • Repository is empty (no commits)'); - await log(' • Unusual repository configuration'); - await log(' • Git command issues'); - await log(''); - await log(' 🔧 How to fix:'); - await log(' 1. Check repository status'); - await log(` 2. Verify locally: cd ${tempDir} && git branch`); - await log(` 3. Check remote: cd ${tempDir} && git branch -r`); - await log(''); - throw new Error('Default branch detection failed'); + // Repository is likely empty (no commits) - detect and handle + const isEmptyRepo = await detectEmptyRepository(tempDir, $); + + if (isEmptyRepo && argv && argv.autoInitRepository && owner && repo) { + // --auto-init-repository is enabled, try to initialize + await log(''); + await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' }); + await log(`${formatAligned('', '', `Repository ${owner}/${repo} contains no commits`)}`); + await log(`${formatAligned('', '', '--auto-init-repository is enabled, attempting initialization...')}`); + await log(''); + + const repository = await import('./solve.repository.lib.mjs'); + const { tryInitializeEmptyRepository } = repository; + const initialized = await tryInitializeEmptyRepository(owner, repo); + + if (initialized) { + await log(''); + await log(`${formatAligned('🔄', 'Re-fetching:', 'Pulling initialized repository...')}`); + // Wait for GitHub to process the new file + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Re-fetch the origin to get the new commit + const fetchResult = await $({ cwd: tempDir })`git fetch origin`; + if (fetchResult.code !== 0) { + await log(`${formatAligned('❌', 'Fetch failed:', 'Could not fetch after initialization')}`, { level: 'error' }); + throw new Error('Failed to fetch after empty repository initialization'); + } + + // Determine default branch name from the remote + const remoteHeadResult = await $({ cwd: tempDir })`git remote show origin`; + let remoteBranch = 'main'; // default fallback + if (remoteHeadResult.code === 0) { + const remoteOutput = remoteHeadResult.stdout.toString(); + const headMatch = remoteOutput.match(/HEAD branch:\s*(\S+)/); + if (headMatch) { + remoteBranch = headMatch[1]; + } + } + + // Checkout the remote branch locally + const checkoutResult = await $({ cwd: tempDir })`git checkout -b ${remoteBranch} origin/${remoteBranch}`; + if (checkoutResult.code !== 0) { + // Try alternative: maybe the branch already exists locally somehow + const altResult = await $({ cwd: tempDir })`git checkout ${remoteBranch}`; + if (altResult.code !== 0) { + await log(`${formatAligned('❌', 'Checkout failed:', `Could not checkout ${remoteBranch} after initialization`)}`, { level: 'error' }); + throw new Error('Failed to checkout branch after empty repository initialization'); + } + } + + defaultBranch = remoteBranch; + await log(`${formatAligned('✅', 'Repository initialized:', `Now on branch ${defaultBranch}`)}`); + await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`); + } else { + // Auto-init failed - provide helpful message with --auto-init-repository context + await log(''); + await log(`${formatAligned('❌', 'AUTO-INIT FAILED', '')}`, { level: 'error' }); + await log(''); + await log(' 🔍 What happened:'); + await log(` Repository ${owner}/${repo} is empty (no commits).`); + await log(' --auto-init-repository was enabled but initialization failed.'); + await log(' You may not have write access to create files in the repository.'); + await log(''); + await log(' 💡 How to fix:'); + await log(' Option 1: Ask repository owner to add initial content'); + await log(' Even a simple README.md file would allow branch creation'); + await log(''); + await log(` Option 2: Manually initialize: gh api repos/${owner}/${repo}/contents/README.md \\`); + await log(' --method PUT --field message="Initialize repository" \\'); + await log(' --field content="$(echo "# repo" | base64)"'); + await log(''); + throw new Error('Empty repository auto-initialization failed'); + } + } else if (isEmptyRepo) { + // Empty repo detected but --auto-init-repository is not enabled + await log(''); + await log(`${formatAligned('❌', 'EMPTY REPOSITORY DETECTED', '')}`, { level: 'error' }); + await log(''); + await log(' 🔍 What happened:'); + await log(` The repository${owner && repo ? ` ${owner}/${repo}` : ''} is empty (no commits).`); + await log(' Cannot create branches or pull requests on an empty repository.'); + await log(''); + await log(' 💡 How to fix:'); + await log(' Option 1: Use --auto-init-repository flag to automatically create a README.md'); + await log(` solve --auto-init-repository`); + await log(''); + await log(' Option 2: Ask repository owner to add initial content'); + await log(' Even a simple README.md file would allow branch creation'); + await log(''); + throw new Error('Empty repository detected - use --auto-init-repository to initialize'); + } else { + // Not an empty repo, some other issue with branch detection + await log(''); + await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' }); + await log(''); + await log(' 🔍 What happened:'); + await log(" Unable to determine the repository's default branch."); + await log(''); + await log(' 💡 This might mean:'); + await log(' • Unusual repository configuration'); + await log(' • Git command issues'); + await log(''); + await log(' 🔧 How to fix:'); + await log(' 1. Check repository status'); + await log(` 2. Verify locally: cd ${tempDir} && git branch`); + await log(` 3. Check remote: cd ${tempDir} && git branch -r`); + await log(''); + throw new Error('Default branch detection failed'); + } + } else { + await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`); } - await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`); // Ensure we're on a clean default branch const statusResult = await $({ cwd: tempDir })`git status --porcelain`; @@ -106,3 +199,30 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned return defaultBranch; } + +/** + * Detect if a cloned repository is empty (has no commits). + * An empty repository has no branches and no commits. + */ +async function detectEmptyRepository(tempDir, $) { + // Check if there are any commits in the repository + const logResult = await $({ cwd: tempDir })`git rev-parse HEAD 2>&1`; + if (logResult.code !== 0) { + // git rev-parse HEAD fails when there are no commits + const output = (logResult.stdout || logResult.stderr || '').toString(); + if (output.includes('unknown revision') || output.includes('bad default revision') || output.includes('does not have any commits')) { + return true; + } + } + + // Also check if there are any remote branches + const remoteBranchResult = await $({ cwd: tempDir })`git branch -r`; + if (remoteBranchResult.code === 0) { + const branches = remoteBranchResult.stdout.toString().trim(); + if (!branches) { + return true; + } + } + + return false; +} diff --git a/src/solve.repository.lib.mjs b/src/solve.repository.lib.mjs index 02ef48e07..00a022859 100644 --- a/src/solve.repository.lib.mjs +++ b/src/solve.repository.lib.mjs @@ -328,8 +328,9 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => { }; // Try to initialize an empty repository by creating a simple README.md -// This makes the repository forkable -const tryInitializeEmptyRepository = async (owner, repo) => { +// This makes the repository forkable and allows branch creation +// Exported for use in solve.repo-setup.lib.mjs (direct access path for empty repos) +export const tryInitializeEmptyRepository = async (owner, repo) => { try { await log(`${formatAligned('🔧', 'Auto-fix:', 'Attempting to initialize empty repository...')}`); diff --git a/tests/test-auto-init-repository.mjs b/tests/test-auto-init-repository.mjs new file mode 100644 index 000000000..f4c4ccf7c --- /dev/null +++ b/tests/test-auto-init-repository.mjs @@ -0,0 +1,168 @@ +#!/usr/bin/env node + +/** + * Test suite for --auto-init-repository feature (issue #1230) + * Tests empty repository detection, option registration, and error message improvements + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = join(__dirname, '..', 'src'); + +let testsPassed = 0; +let testsFailed = 0; + +function runTest(name, testFn) { + process.stdout.write(`Testing ${name}... `); + try { + testFn(); + console.log('✅ PASSED'); + testsPassed++; + } catch (error) { + console.log(`❌ FAILED: ${error.message}`); + testsFailed++; + } +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message || 'Assertion failed'); + } +} + +console.log('🧪 Auto-Init Repository Tests (Issue #1230)\n'); + +// ============================================= +// Test 1: Option is defined in solve.config.lib.mjs +// ============================================= +runTest('--auto-init-repository option is defined in SOLVE_OPTION_DEFINITIONS', () => { + const configContent = readFileSync(join(srcDir, 'solve.config.lib.mjs'), 'utf-8'); + assert(configContent.includes("'auto-init-repository'"), 'auto-init-repository should be defined in SOLVE_OPTION_DEFINITIONS'); + assert(configContent.includes("type: 'boolean'"), 'Should be a boolean option'); + assert(configContent.includes('Automatically initialize empty repositories'), 'Should have descriptive help text'); + assert(configContent.includes('default: false'), 'Should default to false'); +}); + +// ============================================= +// Test 2: Option is in KNOWN_OPTION_NAMES for typo detection +// ============================================= +runTest('--auto-init-repository is in KNOWN_OPTION_NAMES', () => { + const suggestionsContent = readFileSync(join(srcDir, 'option-suggestions.lib.mjs'), 'utf-8'); + assert(suggestionsContent.includes("'auto-init-repository'"), 'auto-init-repository should be in KNOWN_OPTION_NAMES for malformed flag detection'); +}); + +// ============================================= +// Test 3: tryInitializeEmptyRepository is exported +// ============================================= +runTest('tryInitializeEmptyRepository is exported from solve.repository.lib.mjs', () => { + const repoContent = readFileSync(join(srcDir, 'solve.repository.lib.mjs'), 'utf-8'); + assert(repoContent.includes('export const tryInitializeEmptyRepository'), 'tryInitializeEmptyRepository should be exported (not just a const)'); +}); + +// ============================================= +// Test 4: verifyDefaultBranchAndStatus accepts new parameters +// ============================================= +runTest('verifyDefaultBranchAndStatus accepts argv, owner, repo parameters', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('argv, owner, repo'), 'verifyDefaultBranchAndStatus should accept argv, owner, repo parameters'); +}); + +// ============================================= +// Test 5: Empty repo detection function exists +// ============================================= +runTest('detectEmptyRepository function is implemented', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('async function detectEmptyRepository'), 'detectEmptyRepository helper function should exist'); + assert(repoSetupContent.includes('git rev-parse HEAD'), 'Should check for HEAD existence to detect empty repos'); + assert(repoSetupContent.includes('git branch -r'), 'Should check for remote branches as additional empty repo detection'); +}); + +// ============================================= +// Test 6: Auto-init flow is implemented +// ============================================= +runTest('Auto-init flow handles enabled --auto-init-repository', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('argv.autoInitRepository'), 'Should check argv.autoInitRepository (camelCase from yargs)'); + assert(repoSetupContent.includes('tryInitializeEmptyRepository'), 'Should call tryInitializeEmptyRepository when auto-init is enabled'); + assert(repoSetupContent.includes('git fetch origin'), 'Should re-fetch origin after initialization'); + assert(repoSetupContent.includes('git remote show origin'), 'Should determine default branch from remote after init'); +}); + +// ============================================= +// Test 7: Error message suggests --auto-init-repository when disabled +// ============================================= +runTest('Error message suggests --auto-init-repository when empty repo detected without flag', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('EMPTY REPOSITORY DETECTED'), 'Should show clear EMPTY REPOSITORY DETECTED message'); + assert(repoSetupContent.includes('--auto-init-repository flag'), 'Should suggest using --auto-init-repository flag'); + assert(repoSetupContent.includes('solve --auto-init-repository'), 'Should show usage example with --auto-init-repository'); +}); + +// ============================================= +// Test 8: Branch creation error handler detects empty repo pattern +// ============================================= +runTest('handleBranchCreationError detects "is not a commit" empty repo pattern', () => { + const branchErrorsContent = readFileSync(join(srcDir, 'solve.branch-errors.lib.mjs'), 'utf-8'); + assert(branchErrorsContent.includes("errorOutput.includes('is not a commit')"), 'Should detect "is not a commit" error pattern'); + assert(branchErrorsContent.includes('--auto-init-repository'), 'Should suggest --auto-init-repository in branch creation error'); + assert(branchErrorsContent.includes('repository appears to be empty'), 'Should identify root cause as empty repository'); +}); + +// ============================================= +// Test 9: solve.mjs passes required parameters +// ============================================= +runTest('solve.mjs passes argv, owner, repo to verifyDefaultBranchAndStatus', () => { + const solveContent = readFileSync(join(srcDir, 'solve.mjs'), 'utf-8'); + // Check that the call includes argv, owner, repo + assert(solveContent.includes('argv,\n owner,\n repo,'), 'solve.mjs should pass argv, owner, repo to verifyDefaultBranchAndStatus'); +}); + +// ============================================= +// Test 10: Empty repo detection patterns +// ============================================= +runTest('detectEmptyRepository checks common git error patterns', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('unknown revision'), 'Should detect "unknown revision" error pattern'); + assert(repoSetupContent.includes('bad default revision'), 'Should detect "bad default revision" error pattern'); + assert(repoSetupContent.includes('does not have any commits'), 'Should detect "does not have any commits" error pattern'); +}); + +// ============================================= +// Test 11: Auto-init failure handling +// ============================================= +runTest('Auto-init failure provides actionable guidance', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('AUTO-INIT FAILED'), 'Should show AUTO-INIT FAILED when initialization fails'); + assert(repoSetupContent.includes('Empty repository auto-initialization failed'), 'Should throw descriptive error on auto-init failure'); +}); + +// ============================================= +// Test 12: Case study documentation exists +// ============================================= +runTest('Case study documentation created for issue #1230', () => { + const caseStudyDir = join(__dirname, '..', 'docs', 'case-studies', 'issue-1230'); + const readmeContent = readFileSync(join(caseStudyDir, 'README.md'), 'utf-8'); + assert(readmeContent.includes('Empty Repository Branch Creation Failure'), 'Case study should describe the empty repo branch creation failure'); + assert(readmeContent.includes('Root Cause Analysis'), 'Case study should include root cause analysis'); + assert(readmeContent.includes('--auto-init-repository'), 'Case study should reference the new option'); + + // Verify the solve log is saved + const logContent = readFileSync(join(caseStudyDir, 'solve-log.txt'), 'utf-8'); + assert(logContent.includes('BRANCH CREATION FAILED'), 'Solve log should contain the original error'); +}); + +// ============================================= +// Summary +// ============================================= +console.log(`\n${'='.repeat(50)}`); +console.log(`Results: ${testsPassed} passed, ${testsFailed} failed`); +console.log(`${'='.repeat(50)}`); + +if (testsFailed > 0) { + process.exit(1); +} else { + console.log('\n✅ All tests passed!'); +} From 698ecbf5563439faf6e953a5c1d8dd2f4d1ebee1 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 6 Feb 2026 23:04:53 +0100 Subject: [PATCH 3/5] Revert "Initial commit with task details" This reverts commit 10fb005803b1de9ed769d05b60302267f10c1772. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4487f2d6a..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-assistant/hive-mind/issues/1230 -Your prepared branch: issue-1230-f88cfda7aae8 -Your prepared working directory: /tmp/gh-issue-solver-1770414852260 - -Proceed. From aef859f296b6720d8094d69dde17fa6d7919a586 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 7 Feb 2026 01:36:43 +0100 Subject: [PATCH 4/5] feat: add issue comment for empty repo in direct access path (#1230) When an empty repository is detected in the direct access path (not fork path), post a comment on the issue informing the user about the problem and suggesting solutions, matching the existing behavior in the fork path. - Add tryCommentOnIssueAboutEmptyRepo() helper in solve.repo-setup.lib.mjs - Post comment when --auto-init-repository is not enabled (suggest the flag) - Post comment when --auto-init-repository fails (suggest manual init) - Skip comment when --auto-init-repository succeeds (no user action needed) - Pass issueUrl to verifyDefaultBranchAndStatus() for comment posting - Add 5 new tests (13-17) covering the comment behavior - Update case study documentation Fixes #1230 Co-Authored-By: Claude Opus 4.6 --- docs/case-studies/issue-1230/README.md | 11 ++-- experiments/test-auto-init-repository.mjs | 13 +++- src/solve.mjs | 3 +- src/solve.repo-setup.lib.mjs | 59 ++++++++++++++++- tests/test-auto-init-repository.mjs | 80 +++++++++++++++++++++-- 5 files changed, 151 insertions(+), 15 deletions(-) diff --git a/docs/case-studies/issue-1230/README.md b/docs/case-studies/issue-1230/README.md index 6f5b984a7..f5d91e3c8 100644 --- a/docs/case-studies/issue-1230/README.md +++ b/docs/case-studies/issue-1230/README.md @@ -56,11 +56,14 @@ Added a new CLI option `--auto-init-repository` that: 2. **Auto-initializes** using the existing `tryInitializeEmptyRepository()` function (creates README.md via GitHub API) 3. **Re-fetches and continues** - After initialization, fetches the new commit, checks out the default branch, and proceeds normally -### Improved Error Messages +### Improved Error Messages and Issue Comments -- When `--auto-init-repository` is NOT enabled: Clear message suggesting the flag +- When `--auto-init-repository` is NOT enabled: Clear message suggesting the flag + comment on the issue informing the user +- When `--auto-init-repository` fails: Clear error message + comment on the issue with actionable guidance +- When `--auto-init-repository` succeeds: No issue comment posted (no user action needed) - When branch creation fails due to empty repo: Specific "is not a commit" pattern detection with actionable fix suggestion - Reuses existing `tryInitializeEmptyRepository()` code (DRY principle) +- Issue comment behavior mirrors the existing fork-path comment in `solve.repository.lib.mjs` ## Files Changed @@ -69,8 +72,8 @@ Added a new CLI option `--auto-init-repository` that: | `src/solve.config.lib.mjs` | Added `--auto-init-repository` option definition | | `src/option-suggestions.lib.mjs` | Added to `KNOWN_OPTION_NAMES` for typo detection | | `src/solve.repository.lib.mjs` | Exported `tryInitializeEmptyRepository` for reuse | -| `src/solve.repo-setup.lib.mjs` | Added empty repo detection and auto-init in `verifyDefaultBranchAndStatus()` | -| `src/solve.mjs` | Pass `argv`, `owner`, `repo` to `verifyDefaultBranchAndStatus()` | +| `src/solve.repo-setup.lib.mjs` | Added empty repo detection, auto-init, and issue comment in `verifyDefaultBranchAndStatus()` | +| `src/solve.mjs` | Pass `argv`, `owner`, `repo`, `issueUrl` to `verifyDefaultBranchAndStatus()` | | `src/solve.branch-errors.lib.mjs` | Improved error message for empty repo branch creation failures | ## Artifacts diff --git a/experiments/test-auto-init-repository.mjs b/experiments/test-auto-init-repository.mjs index c61e6eb2b..44f0ab607 100644 --- a/experiments/test-auto-init-repository.mjs +++ b/experiments/test-auto-init-repository.mjs @@ -84,8 +84,8 @@ async function testAutoInitRepository() { // Check solve.mjs passes new parameters const solveContent = readFileSync(join(srcDir, 'solve.mjs'), 'utf-8'); - if (solveContent.includes('argv,') && solveContent.includes('owner,') && solveContent.includes('repo,')) { - log(' ✅ solve.mjs passes argv, owner, repo to verifyDefaultBranchAndStatus'); + if (solveContent.includes('argv,') && solveContent.includes('owner,') && solveContent.includes('repo,') && solveContent.includes('issueUrl,')) { + log(' ✅ solve.mjs passes argv, owner, repo, issueUrl to verifyDefaultBranchAndStatus'); } else { log(' ❌ solve.mjs may not pass all required parameters'); } @@ -98,6 +98,13 @@ async function testAutoInitRepository() { log(' ❌ Branch error handler may be incomplete'); } + // Check issue comment functionality + if (repoSetupContent.includes('tryCommentOnIssueAboutEmptyRepo') && repoSetupContent.includes('gh issue comment')) { + log(' ✅ Issue comment functionality for empty repos implemented'); + } else { + log(' ❌ Issue comment functionality may be incomplete'); + } + log('\n=== Summary ==='); log('The --auto-init-repository feature:'); log('1. ✅ Adds new CLI option --auto-init-repository (default: false)'); @@ -107,6 +114,8 @@ async function testAutoInitRepository() { log('5. ✅ Provides clear error messages when auto-init is disabled or fails'); log('6. ✅ Improves branch creation error messages for empty repo cases'); log('7. ✅ Maintains backward compatibility (default: false, opt-in only)'); + log('8. ✅ Posts comment on issue when empty repo cannot be resolved (similar to fork path)'); + log('9. ✅ Skips issue comment when auto-init succeeds (no user action needed)'); } testAutoInitRepository().catch(console.error); diff --git a/src/solve.mjs b/src/solve.mjs index a61a0280c..d3e96b65a 100755 --- a/src/solve.mjs +++ b/src/solve.mjs @@ -539,7 +539,7 @@ try { }); // Verify default branch and status using the new module - // Pass argv, owner, repo for empty repository auto-initialization (--auto-init-repository) + // Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository) const defaultBranch = await verifyDefaultBranchAndStatus({ tempDir, log, @@ -548,6 +548,7 @@ try { argv, owner, repo, + issueUrl, }); // Create or checkout branch using the new module const branchName = await createOrCheckoutBranch({ diff --git a/src/solve.repo-setup.lib.mjs b/src/solve.repo-setup.lib.mjs index 7f30a3a92..5fceb5332 100644 --- a/src/solve.repo-setup.lib.mjs +++ b/src/solve.repo-setup.lib.mjs @@ -56,7 +56,7 @@ async function setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMod return await setupPrForkFn(tempDir, argv, prForkOwner, repo, isContinueMode, owner); } -export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, argv, owner, repo }) { +export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, argv, owner, repo, issueUrl }) { // Verify we're on the default branch and get its name const defaultBranchResult = await $({ cwd: tempDir })`git branch --show-current`; @@ -139,6 +139,10 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned await log(' --method PUT --field message="Initialize repository" \\'); await log(' --field content="$(echo "# repo" | base64)"'); await log(''); + + // Post a comment on the issue informing about the empty repository + await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ }); + throw new Error('Empty repository auto-initialization failed'); } } else if (isEmptyRepo) { @@ -157,6 +161,10 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned await log(' Option 2: Ask repository owner to add initial content'); await log(' Even a simple README.md file would allow branch creation'); await log(''); + + // Post a comment on the issue informing about the empty repository + await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ }); + throw new Error('Empty repository detected - use --auto-init-repository to initialize'); } else { // Not an empty repo, some other issue with branch detection @@ -200,6 +208,55 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned return defaultBranch; } +/** + * Try to post a comment on the issue informing the user about the empty repository. + * This is a non-critical operation - errors are silently ignored. + * When --auto-init-repository succeeds, no comment is posted (no action needed from the user). + */ +async function tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ }) { + if (!issueUrl) return; + + try { + const issueMatch = issueUrl.match(/\/issues\/(\d+)/); + if (!issueMatch) return; + + const issueNumber = issueMatch[1]; + await log(`${formatAligned('💬', 'Creating comment:', 'Informing about empty repository...')}`); + + const commentBody = `## ⚠️ Repository Initialization Required + +Hello! I attempted to work on this issue, but encountered a problem: + +**Issue**: The repository is empty (no commits) and branches cannot be created. +**Reason**: Git cannot create branches in a repository with no commits. + +### 🔧 How to resolve: + +**Option 1: Use \`--auto-init-repository\` flag** +Re-run the solver with the \`--auto-init-repository\` flag to automatically create a simple README.md: +\`\`\` +solve ${issueUrl} --auto-init-repository +\`\`\` + +**Option 2: Initialize the repository yourself** +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. + +Once the repository contains at least one commit with any file, I'll be able to proceed with solving this issue. + +Thank you!`; + + const commentResult = await $`gh issue comment ${issueNumber} --repo ${owner}/${repo} --body ${commentBody}`; + if (commentResult.code === 0) { + await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`); + } else { + await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`); + } + } catch { + // Silently ignore comment creation errors - not critical to the process + await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`); + } +} + /** * Detect if a cloned repository is empty (has no commits). * An empty repository has no branches and no commits. diff --git a/tests/test-auto-init-repository.mjs b/tests/test-auto-init-repository.mjs index f4c4ccf7c..b14801434 100644 --- a/tests/test-auto-init-repository.mjs +++ b/tests/test-auto-init-repository.mjs @@ -63,11 +63,11 @@ runTest('tryInitializeEmptyRepository is exported from solve.repository.lib.mjs' }); // ============================================= -// Test 4: verifyDefaultBranchAndStatus accepts new parameters +// Test 4: verifyDefaultBranchAndStatus accepts new parameters including issueUrl // ============================================= -runTest('verifyDefaultBranchAndStatus accepts argv, owner, repo parameters', () => { +runTest('verifyDefaultBranchAndStatus accepts argv, owner, repo, issueUrl parameters', () => { const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); - assert(repoSetupContent.includes('argv, owner, repo'), 'verifyDefaultBranchAndStatus should accept argv, owner, repo parameters'); + assert(repoSetupContent.includes('argv, owner, repo, issueUrl'), 'verifyDefaultBranchAndStatus should accept argv, owner, repo, issueUrl parameters'); }); // ============================================= @@ -112,12 +112,12 @@ runTest('handleBranchCreationError detects "is not a commit" empty repo pattern' }); // ============================================= -// Test 9: solve.mjs passes required parameters +// Test 9: solve.mjs passes required parameters including issueUrl // ============================================= -runTest('solve.mjs passes argv, owner, repo to verifyDefaultBranchAndStatus', () => { +runTest('solve.mjs passes argv, owner, repo, issueUrl to verifyDefaultBranchAndStatus', () => { const solveContent = readFileSync(join(srcDir, 'solve.mjs'), 'utf-8'); - // Check that the call includes argv, owner, repo - assert(solveContent.includes('argv,\n owner,\n repo,'), 'solve.mjs should pass argv, owner, repo to verifyDefaultBranchAndStatus'); + // Check that the call includes argv, owner, repo, issueUrl + assert(solveContent.includes('argv,\n owner,\n repo,\n issueUrl,'), 'solve.mjs should pass argv, owner, repo, issueUrl to verifyDefaultBranchAndStatus'); }); // ============================================= @@ -154,6 +154,72 @@ runTest('Case study documentation created for issue #1230', () => { assert(logContent.includes('BRANCH CREATION FAILED'), 'Solve log should contain the original error'); }); +// ============================================= +// Test 13: tryCommentOnIssueAboutEmptyRepo helper function exists +// ============================================= +runTest('tryCommentOnIssueAboutEmptyRepo helper function is implemented', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + assert(repoSetupContent.includes('async function tryCommentOnIssueAboutEmptyRepo'), 'tryCommentOnIssueAboutEmptyRepo helper should exist'); + assert(repoSetupContent.includes('gh issue comment'), 'Should use gh issue comment to post to the issue'); + assert(repoSetupContent.includes('Repository Initialization Required'), 'Comment body should explain the issue clearly'); + assert(repoSetupContent.includes('--auto-init-repository'), 'Comment should suggest --auto-init-repository flag'); +}); + +// ============================================= +// Test 14: Comment is posted when empty repo detected without --auto-init-repository flag +// ============================================= +runTest('Issue comment is posted when empty repo detected without --auto-init-repository', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + // Find the "else if (isEmptyRepo)" block and verify it calls tryCommentOnIssueAboutEmptyRepo + const emptyRepoBlock = repoSetupContent.indexOf('EMPTY REPOSITORY DETECTED'); + const throwAfterBlock = repoSetupContent.indexOf("throw new Error('Empty repository detected - use --auto-init-repository to initialize');"); + assert(emptyRepoBlock !== -1, 'Should have EMPTY REPOSITORY DETECTED block'); + assert(throwAfterBlock !== -1, 'Should throw error after empty repo detection'); + const blockContent = repoSetupContent.substring(emptyRepoBlock, throwAfterBlock); + assert(blockContent.includes('tryCommentOnIssueAboutEmptyRepo'), 'Should call tryCommentOnIssueAboutEmptyRepo before throwing'); +}); + +// ============================================= +// Test 15: Comment is posted when auto-init fails +// ============================================= +runTest('Issue comment is posted when auto-init fails', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + // Find the "AUTO-INIT FAILED" block and verify it calls tryCommentOnIssueAboutEmptyRepo + const autoInitFailedBlock = repoSetupContent.indexOf('AUTO-INIT FAILED'); + const throwAfterBlock = repoSetupContent.indexOf("throw new Error('Empty repository auto-initialization failed');"); + assert(autoInitFailedBlock !== -1, 'Should have AUTO-INIT FAILED block'); + assert(throwAfterBlock !== -1, 'Should throw error after auto-init failure'); + const blockContent = repoSetupContent.substring(autoInitFailedBlock, throwAfterBlock); + assert(blockContent.includes('tryCommentOnIssueAboutEmptyRepo'), 'Should call tryCommentOnIssueAboutEmptyRepo before throwing'); +}); + +// ============================================= +// Test 16: No comment posted when auto-init succeeds +// ============================================= +runTest('No issue comment is posted when auto-init succeeds', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + // Find the success path (between 'initialized' check and the 'else' for auto-init failure) + const successStart = repoSetupContent.indexOf("await log(`${formatAligned('✅', 'Repository initialized:'"); + const successEnd = repoSetupContent.indexOf('AUTO-INIT FAILED'); + assert(successStart !== -1, 'Should have Repository initialized success message'); + assert(successEnd !== -1, 'Should have AUTO-INIT FAILED block'); + const successBlock = repoSetupContent.substring(successStart, successEnd); + assert(!successBlock.includes('tryCommentOnIssueAboutEmptyRepo'), 'Success path should NOT call tryCommentOnIssueAboutEmptyRepo'); +}); + +// ============================================= +// Test 17: Comment function handles missing issueUrl gracefully +// ============================================= +runTest('tryCommentOnIssueAboutEmptyRepo handles missing issueUrl gracefully', () => { + const repoSetupContent = readFileSync(join(srcDir, 'solve.repo-setup.lib.mjs'), 'utf-8'); + // Find the function definition + const funcStart = repoSetupContent.indexOf('async function tryCommentOnIssueAboutEmptyRepo'); + assert(funcStart !== -1, 'Function should exist'); + const funcBlock = repoSetupContent.substring(funcStart, funcStart + 500); + assert(funcBlock.includes('if (!issueUrl) return'), 'Should return early if issueUrl is not provided'); + assert(funcBlock.includes('issueUrl.match(/\\/issues\\/(\\d+)/)'), 'Should extract issue number from URL'); +}); + // ============================================= // Summary // ============================================= From e5fe6b9454ac7b5892a9e6488d7ee15e2d159ce6 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 14 Mar 2026 14:06:24 +0000 Subject: [PATCH 5/5] style: fix prettier formatting in issue-1230 case study Co-Authored-By: Claude Opus 4.6 --- docs/case-studies/issue-1230/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/case-studies/issue-1230/README.md b/docs/case-studies/issue-1230/README.md index f5d91e3c8..28d0bcce0 100644 --- a/docs/case-studies/issue-1230/README.md +++ b/docs/case-studies/issue-1230/README.md @@ -67,14 +67,14 @@ Added a new CLI option `--auto-init-repository` that: ## Files Changed -| File | Change | -| --------------------------------- | ---------------------------------------------------------------------------- | -| `src/solve.config.lib.mjs` | Added `--auto-init-repository` option definition | -| `src/option-suggestions.lib.mjs` | Added to `KNOWN_OPTION_NAMES` for typo detection | -| `src/solve.repository.lib.mjs` | Exported `tryInitializeEmptyRepository` for reuse | +| File | Change | +| --------------------------------- | -------------------------------------------------------------------------------------------- | +| `src/solve.config.lib.mjs` | Added `--auto-init-repository` option definition | +| `src/option-suggestions.lib.mjs` | Added to `KNOWN_OPTION_NAMES` for typo detection | +| `src/solve.repository.lib.mjs` | Exported `tryInitializeEmptyRepository` for reuse | | `src/solve.repo-setup.lib.mjs` | Added empty repo detection, auto-init, and issue comment in `verifyDefaultBranchAndStatus()` | -| `src/solve.mjs` | Pass `argv`, `owner`, `repo`, `issueUrl` to `verifyDefaultBranchAndStatus()` | -| `src/solve.branch-errors.lib.mjs` | Improved error message for empty repo branch creation failures | +| `src/solve.mjs` | Pass `argv`, `owner`, `repo`, `issueUrl` to `verifyDefaultBranchAndStatus()` | +| `src/solve.branch-errors.lib.mjs` | Improved error message for empty repo branch creation failures | ## Artifacts