NousResearch/hermes-agent

Handle interrupted or incomplete install/clone

Cluster 40998 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #41941 on 8 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #41941. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #41941. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address interrupted checkouts that leave a commit-less state, triggering re-cloning rather than an error, alongside a reported Windows installation issue. The common thread is install-time errors and recovery behavior.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #41941 fix(install): re-clone interrupted (commit-less) checkout instead of failing update

    merged to main

    files: scripts/install.ps1, scripts/install.sh, tests/test_install_no_initial_commit.py

    Show the diff
    • scripts/install.ps1
      @@ -1060,9 +1060,10 @@ function Install-Repository {
               # directory OR a symlink OR a submodule-style gitfile -- and also when
               # it's a broken stub left over from a failed previous install (e.g.
               # a partial Remove-Item that couldn't delete a locked index.lock).
      -        # Validate the repo properly by asking git itself.  Two checks
      -        # belt-and-braces: rev-parse AND git status.  If either fails the
      -        # repo is broken and we fall through to a fresh clone.
      +        # Validate the repo properly by asking git itself.  Three checks
      +        # belt-and-braces: rev-parse (work tree), git status, and a resolvable
      +        # HEAD (an initial commit).  If any fails the repo is broken and we
      +        # fall through to a fresh clone.
               $repoValid = $false
               if (Test-Path "$InstallDir\.git") {
                   Push-Location $InstallDir
      @@ -1077,7 +1078,17 @@ function Install-Repository {
                       $null = & git -c windows.appendAtomically=false status --short 2>&1
                       $statusOk = ($LASTEXITCODE -eq 0)
       
      -                if ($revParseOk -and $statusOk) {
      +                # An interrupted previous clone leaves a repo with NO initial
      +                # commit. rev-parse/status still succeed there, but the update
      +                # path's `git stash` (and later `git checkout`) abort with
      +                # "You do not have the initial commit yet" and fail the install
      +                # (#40998). Require a resolvable HEAD so such partial checkouts
      +                # are treated as broken and re-cloned fresh below.
      +                $global:LASTEXITCODE = 0
      +                $null = & git -c windows.appendAtomically=false rev-parse --verify HEAD 2>&1
      +                $hasCommit = ($LASTEXITCODE -eq 0)
      +
      +                if ($revParseOk -and $statusOk -and $hasCommit) {
                           $repoValid = $true
                       }
                   } catch {}
      @@ -1198,16 +1209,19 @@ function Install-Repository {
                   }
                   $didUpdate = $true
               } else {
      -            # Directory exists but isn't a usable git repo.  Wipe it and
      -            # fall through to a fresh clone.  A leftover ``.git`` stub from
      -            # a partial uninstall used to lock the installer into the
      -            # "update" branch forever, emitting three ``fatal: not a git
      -            # repository`` errors and failing with "not in a git directory".
      -            Write-Warn "Existing directory at $InstallDir is not a valid git repo -- replacing it."
      +            # Directory exists but isn't a usable git repo -- e.g. an
      +            # interrupted clone with no initial commit (#40998), or a leftover
      +            # ``.git`` stub from a partial uninstall that used to lock the
      +            # installer into the "update" branch forever. Move it aside rather
      +            # than deleting it -- never destroy a directory the user might still
      +            # want -- and fall through to a fresh clone.
      +            $backupDir = "$InstallDir.broken-" + (Get-Date -Format "yyyyMMdd-HHmmss")
      +            Write-Warn "Existing directory at $InstallDir is not a valid git repo."
      +            Write-Warn "Moving it aside to $backupDir before re-cloning."
                   try {
      -                Remove-Item -Recurse -Force $InstallDir -ErrorAction Stop
      +                Move-Item -LiteralPath $InstallDir -Destination $backupDir -ErrorAction Stop
                   } catch {
      -                Write-Err "Could not remove $InstallDir : $_"
      +                Write-Err "Could not move $InstallDir aside : $_"
                       Write-Info "Close any programs that might be using files in $InstallDir (editors,"
                       Write-Info "terminals, running hermes processes) and try again."
                       throw
    • scripts/install.sh
      @@ -1092,6 +1092,18 @@ show_manual_install_hint() {
       clone_repo() {
           log_info "Installing to $INSTALL_DIR..."
       
      +    # An interrupted previous clone leaves a .git with no initial commit, where
      +    # the update path's `git stash` / `git checkout` abort with "You do not
      +    # have the initial commit yet" and fail the install (#40998). Move such a
      +    # partial checkout aside -- never delete it, in case it holds something the
      +    # user wants -- so the fresh-clone path below can proceed.
      +    if [ -d "$INSTALL_DIR/.git" ] && ! git -C "$INSTALL_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
      +        backup_dir="${INSTALL_DIR}.broken-$(date -u +%Y%m%d-%H%M%S)"
      +        log_warn "Existing checkout at $INSTALL_DIR has no commits (interrupted clone)."
      +        log_warn "Moving it aside to $backup_dir before re-cloning."
      +        mv "$INSTALL_DIR" "$backup_dir"
      +    fi
      +
           if [ -d "$INSTALL_DIR" ]; then
               if [ -d "$INSTALL_DIR/.git" ]; then
                   log_info "Existing installation found, updating..."
    • tests/test_install_no_initial_commit.py
      @@ -0,0 +1,136 @@
      +"""Regression for #40998: installer fails on an interrupted prior clone.
      +
      +A previous clone that died before its first commit leaves ``$INSTALL_DIR/.git``
      +present but with no resolvable ``HEAD``. ``git rev-parse --is-inside-work-tree``
      +and ``git status`` both still succeed there, so the installer treated it as a
      +valid checkout and tried to *update* it -- but ``git stash``/``git checkout``
      +abort with "You do not have the initial commit yet", failing the install at the
      +"Cloning Hermes repository" stage.
      +
      +Both installers must instead treat a commit-less checkout as broken and
      +re-clone fresh.
      +"""
      +
      +from __future__ import annotations
      +
      +import re
      +import shlex
      +import shutil
      +import subprocess
      +from pathlib import Path
      +
      +import pytest
      +
      +REPO_ROOT = Path(__file__).resolve().parent.parent
      +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
      +INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1"
      +
      +pytestmark = pytest.mark.skipif(
      +    shutil.which("git") is None or shutil.which("bash") is None,
      +    reason="needs git and bash",
      +)
      +
      +
      +def _git(cwd: Path, *args: str) -> None:
      +    subprocess.run(
      +        ["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
      +        cwd=cwd,
      +        check=True,
      +        capture_output=True,
      +    )
      +
      +
      +def _extract_no_commit_guard() -> str:
      +    """Pull the clone_repo() guard that drops a commit-less checkout."""
      +    text = INSTALL_SH.read_text()
      +    m = re.search(
      +        r'if \[ -d "\$INSTALL_DIR/\.git" \] && ! git -C "\$INSTALL_DIR" '
      +        r"rev-parse --verify HEAD.*?\n    fi",
      +        text,
      +        re.DOTALL,
      +    )
      +    assert m is not None, "no-commit guard not found in install.sh clone_repo()"
      +    return m.group(0)
      +
      +
      +def _run_guard(install_dir: Path) -> None:
      +    block = _extract_no_commit_guard()
      +    script = (
      +        "log_warn() { echo \"WARN: $*\"; }\n"
      +        f"INSTALL_DIR={shlex.quote(str(install_dir))}\n"
      +        f"{block}\n"
      +    )
      +    res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
      +    assert res.returncode == 0, res.stderr
      +
      +
      +def test_install_sh_guard_moves_commitless_checkout_aside(tmp_path: Path) -> None:
      +    install_dir = tmp_path / "hermes-agent"
      +    install_dir.mkdir()
      +    _git(install_dir, "init")
      +    (install_dir / "leftover.txt").write_text("partial download")  # untracked
      +
      +    # Sanity: this is exactly the state that breaks `git stash`.
      +    head = subprocess.run(
      +        ["git", "-C", str(install_dir), "rev-parse", "--verify", "HEAD"],
      +        capture_output=True,
      +    )
      +    assert head.returncode != 0
      +
      +    _run_guard(install_dir)
      +    # The original path is cleared so a fresh clone can proceed, but the
      +    # content is preserved in a backup (never deleted -- review feedback).
      +    assert not install_dir.exists(), "commit-less checkout should be moved aside"
      +    backups = list(install_dir.parent.glob(install_dir.name + ".broken-*"))
      +    assert len(backups) == 1, "broken checkout should be moved to one backup dir"
      +    assert (backups[0] / "leftover.txt").read_text() == "partial download"
      +
      +
      +def test_install_sh_guard_keeps_repo_with_commits(tmp_path: Path) -> None:
      +    install_dir = tmp_path / "hermes-agent"
      +    install_dir.mkdir()
      +    _git(install_dir, "init")
      +    (install_dir / "f.txt").write_text("real content")
      +    _git(install_dir, "add", "f.txt")
      +    _git(install_dir, "commit", "-m", "init")
      +
      +    _run_guard(install_dir)
      +    assert install_dir.exists()
      +    assert (install_dir / "f.txt").exists(), "a real checkout must be left intact"
      +    assert not list(install_dir.parent.glob(install_dir.name + ".broken-*")), (
      +        "a healthy checkout must not be moved aside"
      +    )
      +
      +
      +def test_install_sh_guard_ignores_non_repo_dir(tmp_path: Path) -> None:
      +    install_dir = tmp_path / "hermes-agent"
      +    install_dir.mkdir()
      +    (install_dir / "f.txt").write_text("not a repo")
      +
      +    _run_guard(install_dir)
      +    # No .git → not our concern; the existing "not a git repository" branch
      +    # still handles it. The guard must leave it untouched.
      +    assert install_dir.exists()
      +    assert (install_dir / "f.txt").exists()
      +
      +
      +def test_install_ps1_validity_requires_initial_commit() -> None:
      +    """The PowerShell repo-validity gate must also require a resolvable HEAD."""
      +    text = INSTALL_PS1.read_text()
      +    assert "rev-parse --verify HEAD" in text, (
      +        "install.ps1 must probe for an initial commit (#40998)"
      +    )
      +    # Contract: $repoValid is only set when the HEAD probe succeeded too.
      +    assert re.search(
      +        r"if \(\$revParseOk -and \$statusOk -and \$hasCommit\) \{",
      +        text,
      +    ), "repo validity must be gated on $hasCommit, not just rev-parse + status"
      +    # Cleanup must be non-destructive: move the broken checkout aside, never
      +    # `Remove-Item -Recurse -Force` it (review feedback on #40998).
      +    assert "Move-Item -LiteralPath $InstallDir" in text, (
      +        "install.ps1 must move an invalid checkout aside, not delete it"
      +    )
      +    assert "Remove-Item -Recurse -Force $InstallDir -ErrorAction Stop" not in text, (
      +        "the destructive wipe of an existing install dir must be gone "
      +        "(transient cleanup of a just-failed clone is fine)"
      +    )

1 PR closed without merging

From the close comments: merged via another change 1

  • #41208 fix(install): re-clone interrupted (commit-less) checkout instead of failing update · merged via another change → #41941, #40998
Closed issues · 1
  • Closed#40998[Bug]: windows安装包安装的时候安装失败
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#41941
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
8 Jun 2026

For your agent

The same facts, typed.