Skip to content

get_health: "Repository not found" for any workspace alias that differs from the directory basename #855

Description

@modF

Summary

In workspace mode, get_health fails with Repository not found: <alias> for any repo whose workspace alias differs from its directory basename — even though list_repos advertises that exact alias and every other workspace tool (get_answer, get_dead_code, search_codebase) accepts it.

Version: repowise 0.31.0 (workspace mode, stdio transport).

Reproduce

Register two repos in a workspace where the alias does not equal the folder name:

alias directory get_health(repo=<alias>)
myrepo myrepo/ ✅ works
myrepo MyRepo/ (case differs) Repository not found: myrepo
nickname some-other-dir/ (aliased) Repository not found: nickname
list_repos() -> {"workspace": true, "repos": [{"alias": "nickname", ...}]}
get_health(repo="nickname", targets=["some/file.py"])
  -> {"error": "Repository not found: nickname"}

get_answer(repo="nickname", question="...")      -> works
get_dead_code(repo="nickname")                    -> works

I confirmed this across 6 workspace repos: the 3 whose alias exactly equals their directory basename work; the 3 where it differs (two by letter-case only, one aliased to an unrelated folder name) all fail. 6/6 consistent with the cause below.

Cause

There are two repo-resolution paths, and get_health uses the one that is not alias-aware.

server/mcp_server/_helpers.py:55_get_repo() resolves only by local_path, then primary-key ID, then Repository.name:

async def _get_repo(session: AsyncSession, repo: str | None = None) -> Repository:
    """Resolve a repository — by path, by ID, or return the first one."""
    if repo:
        # Try by path
        result = await session.execute(select(Repository).where(Repository.local_path == repo))
        ...
        # Try by ID
        obj = await session.get(Repository, repo)
        ...
        # Try by name
        result = await session.execute(select(Repository).where(Repository.name == repo))
        ...
        raise LookupError(f"Repository not found: {repo}")

It never consults the workspace alias registry. Repository.name is the directory basename, so a workspace alias only resolves here by coincidence — when the alias happens to equal the basename. The match is also case-sensitive, so an alias differing only in case fails.

The alias-aware path is core/workspace/registry.py:110 (resolve()), which validates against the workspace config and raises a different error:

Unknown repo 'X'. Available: ['a', 'b', ...]

Two distinct errors (worth noting for anyone debugging this)

These look similar but mean opposite things, which makes the bug confusing to diagnose:

  • Unknown repo 'X'. Available: [...] — alias resolution ran and X is genuinely not a registered alias.
  • Repository not found: Xthe alias IS valid and resolved fine; _get_repo then failed to find it by name/path.

So Repository not found on an alias that list_repos just returned is always this bug.

Suggested fix

Have _get_repo() consult the workspace registry first (mapping alias → RepoContext/local_path) before falling back to path/ID/name lookup, so alias resolution is consistent across every workspace-aware tool. A case-insensitive comparison on the name fallback would additionally fix the case-only mismatches.

Workaround

None from the tool surface — passing the directory basename instead of the alias fails earlier in registry.resolve() with Unknown repo, since the basename is not a registered alias. Affected repos currently have no way to call get_health at all.

Related: #435

#435 ("Chat MCP tools fail with 'Repository not found' for non-primary repos in workspace mode") lands on the same final line (_helpers.py:72) and the same underlying weakness — _get_repo is not workspace/alias-aware. But this report is a different surface and a different trigger, so I've filed it separately rather than commenting there:

If the fix proposed in #435 (thread a per-request RepoContext instead of mutating MCP module globals) is applied to the shared _get_repo resolution rather than only the chat path, it would likely close both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions