fix(storage): derive registry name from canonical repo root, not worktree slug (#1259)#1296
Conversation
…tree slug (abhigyanpatwari#1259) When `gitnexus analyze` runs from inside a git worktree (e.g. `/repo/wt-feature/`), the resulting project is registered under the worktree's directory basename (`wt-feature`) instead of the canonical repo's basename (`repo`). Three observable problems follow: 1. Duplicate `~/.gitnexus/registry.json` entries — the same codebase appears once for the main checkout and once per worktree. 2. AGENTS.md is rewritten with the worktree slug in the gitnexus MCP URI block, so contributors who don't catch the diff ship a project rename they didn't intend. 3. MCP queries against the worktree-named project miss the canonical project's index, so impact / context return data for whichever copy is fresher. Root cause: `getGitRoot` uses `git rev-parse --show-toplevel`, which returns the WORKTREE's root, not the shared canonical root. The two existing fallbacks in `runFullAnalysis` — `--name <alias>` and the git-remote-derived inferred name — only mask the bug. Repos without `origin` (private, fresh `git init`, etc.) hit the worktree-basename fallback directly. Claude Code's per-task worktree workflow trips this every analyze. Fix: add `getCanonicalRepoRoot(fromPath)` in `src/storage/git.ts` that uses `git rev-parse --path-format=absolute --git-common-dir` (the `.git` directory shared across all worktrees) and returns its parent. Use it in both basename-fallback sites in `run-analyze.ts`: the early-return registry-name derivation and the embedding server-mapping projectName. `getGitRoot` is left unchanged so its existing four callers (`status`, `wiki`, `index-repo`, `analyze`'s own repoPath resolution) keep returning the worktree-local root, which is correct for those callsites' purposes. Tests cover the helper end-to-end with a real `git worktree add` fixture in a temp dir: regular checkout returns its own root, the same root returned from inside a linked worktree (the regression guard), `getGitRoot` continues to return the worktree-local path, plus null cases for non-git and non-existent paths.
|
@azizur100389 is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 8017 tests passed 1 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
|
Claude finished @magyargergo's task in 6m 7s —— View job PR #1296 Review — fix(storage): derive registry name from canonical repo root, not worktree slugTasks
Review bar for this PR
Issue and PR context considered#1259 symptoms:
Changed files: Current CI/Docker status: Both CI and Docker Build workflows are in-progress (not completed) at time of this review. A prior auto-generated CI comment showed Local test failure note: Author's Windows dev machine has Main risks: partial path coverage (early-return fixed, full analysis not), embedding/registry name divergence, registry duplicate entries from full analysis path, no integration-level regression test for the core symptom. Understanding of the changeWhat #1259 reported: Why What Where the new helper is used (in this PR):
What intentionally remains worktree-local: What must remain unchanged: Current CI/Docker status: Both in-progress. Prior run had job-level failures on all platforms. Findings[CRITICAL] CI is not green — blocks merge
[CRITICAL] Full analysis path does not use
|
| DoD item | Status |
|---|---|
| Analyzing from a linked worktree falls back to canonical repo root basename in ALL analysis paths | not satisfied — full analysis path still uses path.basename(repoPath) via registerRepo |
Explicit --name wins end-to-end |
partially satisfied — wins in registry, but embedding mapping ignores it |
| Remote-derived name wins before canonical fallback in all paths | partially satisfied — wins in registry, embedding mapping ignores it |
Non-git / --skip-git folders fall back to path.basename(repoPath) |
satisfied — getCanonicalRepoRoot returns null, fallback chain resolves to repoPath |
repoPath storage location remains worktree-local |
satisfied |
| AGENTS.md MCP URI uses corrected project name | partially satisfied — early-return path: yes; full-analysis path: still worktree slug when no remote |
| Embedding server mapping key matches final registry name | not satisfied — key ignores --name and remote-derived names |
getGitRoot() behavior unchanged |
satisfied — no modification, all callers intact |
| Tests use real git worktrees and cover the regression | partially satisfied — regression test proves helper; does not prove full analyze→registry wiring |
| CI, Docker/build, typecheck, and focused tests are green | not satisfied — CI and Docker in-progress; prior run had job-level failures on 3 platforms |
Git worktree semantics assessment
getCanonicalRepoRoot implementation: Correct. --path-format=absolute ensures the returned path is absolute regardless of cwd. git rev-parse --git-common-dir returns the shared .git directory for both main checkout and all linked worktrees — its parent is always the canonical repo root. path.dirname(path.resolve(commonDir)) handles Windows paths correctly. stderr suppressed (stdio: ['ignore', 'pipe', 'ignore']). No shell injection risk: fromPath is passed as cwd, not interpolated into the command string. Consistent with existing helpers that use execSync(commandString) rather than execFileSync — acceptable stylistically, not a security issue here.
getGitRoot unchanged: Confirmed — no modification to the function or its behavior.
Normal checkout: --git-common-dir returns <repo>/.git; path.dirname returns <repo>. Matches getGitRoot output. ✓
Linked worktree: --git-common-dir returns the main repo's .git (the shared common dir), not the worktree-specific git dir (.git/worktrees/<name>). path.dirname returns the main repo root. getGitRoot continues to return the worktree-local root. The asymmetry between the two helpers is exactly what fixes the naming problem. ✓
Worktree outside main repo directory: --path-format=absolute --git-common-dir still returns the main repo's .git. path.dirname correctly resolves to main repo root regardless of physical location. ✓
Non-git folder: execSync throws, caught, returns null. path.basename(null ?? repoPath) = path.basename(repoPath) → correct non-git fallback. ✓
--skip-git: analyze.ts:222 sets repoPath = path.resolve(process.cwd()) when --skip-git is set (bypasses getGitRoot). getCanonicalRepoRoot returns null (not a git repo). Fallback = path.basename(repoPath). ✓
Windows/macOS/Linux path behavior: path.dirname(path.resolve(commonDir)) is cross-platform. Tests use fs.realpathSync for macOS symlink resolution. Windows normalization handled by path.resolve. ✓
Potential edge case (not blocking): Submodule repos — --git-common-dir inside a submodule returns the submodule's own git dir (e.g., parent/.git/modules/sub), so path.dirname gives parent/.git/modules — incorrect canonical root. This is an edge case unlikely to affect the reported use case but worth noting for future work.
Registry / naming assessment
Explicit --name: Wins via opts?.name in registerRepo (line 461). ✓ But run-analyze.ts early-return path uses options.registryName ?? getInferredRepoName ?? canonical_basename — also correct ✓. Embedding mapping ignores --name ✗.
Remote-derived name: Wins via getInferredRepoName(resolved) in registerRepo (line 468). ✓ Early-return path also calls getInferredRepoName(repoPath) ✓. Embedding mapping ignores remote-derived name ✗.
Canonical fallback: Applied only in early-return path. registerRepo does NOT call getCanonicalRepoRoot. For full analysis path with no remote and no --name, registerRepo uses path.basename(repoPath) = worktree slug. ✗
Worktree fallback: Remains as last resort for non-git / --skip-git paths. ✓
Registry duplicate behavior: registerRepo upserts by canonicalizePath(path). Worktree path ≠ canonical path → full-analysis worktree analyze creates a second entry alongside the existing canonical entry. Duplicate-entry symptom of #1259 NOT resolved on the full-analysis path. ✗
AGENTS.md MCP URI behavior: generateAIContextFiles(repoPath, storagePath, projectName, ...) at line 483 — projectName comes from await registerRepo(...) result. On the full-analysis path, this is the worktree slug when no remote. On the early-return path, generateAIContextFiles is not called — AGENTS.md is left from the prior analyze run. If the prior analyze was from the canonical checkout, AGENTS.md is correct. If AGENTS.md doesn't exist yet or was last written from a worktree, no update occurs on early-return. ✗ (partial)
Existing stale entries / migration note: No migration provided. Users with existing wt-feature registry entries must run gitnexus remove wt-feature manually. Not documented in PR.
Embedding mapping assessment
projectName derivation at line 364: path.basename(getCanonicalRepoRoot(repoPath) ?? repoPath) — uses canonical root only.
Consistency with registry name: Only consistent when both resolve to canonical root basename (no --name, no remote). Diverges when --name is passed or remote-inferred name exists.
--name behavior: Registry gets --name value; embedding mapping ignores it and uses canonical root basename. ✗
Remote-derived behavior: Registry gets remote-derived name (e.g. GitNexus); embedding mapping uses canonical root basename (e.g. repo). ✗
Fallback behavior: When no remote and no --name, both resolve to canonical root basename (or worktree slug if getCanonicalRepoRoot fails). ✓ for this case only.
Test assessment
Helper tests: 4 new tests in getCanonicalRepoRoot describe block — null for non-git, null for non-existent, canonical root for regular checkout, canonical root from worktree with getGitRoot sanity check. All structurally sound and use real git worktree add. ✓
Worktree regression test: Test at line 223 correctly creates a real linked worktree, commits, and asserts the canonical vs worktree-local asymmetry. Uses fs.realpathSync for macOS symlink consistency. Worktree cleanup via git worktree remove -f + fs.rmSync with force: true. ✓
Environment fragility: "plain temp directory" test at line 193 fails when os.tmpdir() is inside a git repo. Author documented this. CI containers unaffected per author. Consistent with the existing getGitRoot equivalent test pattern. Borderline acceptable if CI proves clean, but still fragile by TESTING.md standards.
Missing full analyze / registry / AGENTS coverage: No test exercises runFullAnalysis from a worktree and asserts result.repoName is the canonical name. The core bug (#1259 duplicate entries, wrong AGENTS.md name) is not directly regression-guarded. ✗
Would tests fail before this PR? The worktree test (line 223) would fail before this PR because getCanonicalRepoRoot didn't exist. ✓ But no test would catch a revert of the runFullAnalysis wiring changes. ✗
Validation assessment
Latest CI: Two workflows in-progress. Prior completed run: ❌ Some checks failed / Tests: failure, 3 platforms — job-level failure, not individual test failures (0 failed tests). Root cause unknown. Not verified as green.
Docker/build: In-progress. Not verified.
Typecheck: Prior CI comment shows ✅ Typecheck: success. ✓
Focused tests (git-utils.test.ts): PR body says 3 of 4 new tests pass locally; 1 (temp-dir test) fails on author's Windows. CI expected to pass. Not yet verified on CI.
Local failure explanation: Documented. Acceptable explanation for the temp-dir test if CI proves clean. The job-level failure is not explained.
Not verified: Full analysis path registry correctness, AGENTS.md URI update from worktree, embedding mapping key alignment with registry name.
Final verdict
not production-ready
CI and Docker are both in-progress, and the prior completed CI run had job-level failures on all three cross-platform legs — the decision rule requires green CI before approval. Beyond the CI gate, there is a fundamental correctness gap: the fix only applies to the early-return (already-up-to-date) path in runFullAnalysis. The full analysis path — which fires on first-time worktree analyze, worktree-ahead-of-canonical scenarios, and --force runs — still calls registerRepo(repoPath, ...) which derives the name as path.basename(repoPath) = worktree slug when no remote and no --name are set. This means the original bug in #1259 (duplicate registry entries, wrong AGENTS.md MCP URI slug) persists for the most common "first use" scenario. Additionally, the embedding server mapping key at line 364 ignores --name and remote-derived names, creating silent divergence from the registry name. Both the CRITICAL and two HIGH findings block merge; addressing them requires extending the canonical name resolution into the registerRepo call site and unifying the name chain across all three callsites (early-return, registry, embedding mapping) rather than applying the helper in only two of them.
Suggested fixes before re-review:
- Compute the resolved name (
options.registryName ?? getInferredRepoName ?? canonical_basename) once before callingregisterRepo, pass it asopts.name, and use the same result for the embedding mapping key. - Add a focused integration test:
runFullAnalysisfrom a linked worktree → assertresult.repoNameis the canonical basename. - Confirm CI is green on all three platforms including the
git-utils.test.tsnew tests. - Add a brief release note that existing stale worktree-slug registry entries must be removed manually.
Fix embedding mapping + registry name divergence →
· CI run · Docker run
…bhigyanpatwari#1259) CI on `tests / windows-latest` failed because `fs.realpathSync(tmpDir)` on the GitHub Windows runner returned the 8.3 short form `C:\Users\RUNNER~1\AppData\Local\Temp\…` while `git rev-parse` returned the long form `C:\Users\runneradmin\AppData\Local\Temp\…` for the same path. Both point at the identical directory, but the strings don't match, so `expect(getCanonicalRepoRoot(tmpDir)).toBe(repoRoot)` failed on Windows even though the helper was correct. Replace the brittle full-path comparison with two robust patterns: 1. Compare `path.basename` of the result against `path.basename` of the input — basename is what registry-name derivation actually uses (abhigyanpatwari#1259), and it's identical regardless of whether git or `fs.realpathSync` returned the short or long form. 2. For the worktree test, cross-compare the helper's outputs from the main checkout AND from inside the worktree (`fromMain === fromWorktree`). Both calls go through the same git binary on the same platform, so their path-format output is guaranteed consistent — equality between them is the stable cross-platform regression-guard for abhigyanpatwari#1259, no realpath needed. Same approach for the `getGitRoot(worktreeDir)` sanity check — basename comparison instead of full-path equality. No source change. Tests pass locally (the dev-env failure class for "plain temp dir not a git repo" is unchanged — same 3 pre-existing failures from before this branch).
|
@azizur100389 You contributed to our code base so much, that we would like to invite you to our Discord for closer collaboration :) You can find this link in the readme file. |
…abhigyanpatwari#1259) Addresses the @claude review findings on PR abhigyanpatwari#1296. The first commit (`556087e7`) only patched the early-return path in `runFullAnalysis`, leaving two real gaps: [CRITICAL abhigyanpatwari#2] `registerRepo` in `repo-manager.ts` has its OWN name-resolution chain — `opts.name → preserved alias → remote-derived → path.basename(resolved)` — and `resolved = path.resolve(repoPath)` still returned the worktree path. For a first-time analyze from a worktree with no remote and no `--name`, the registry name was the worktree slug (the original abhigyanpatwari#1259 bug, persisting on every "first use" scenario). Fix: replace `path.basename(resolved)` with `path.basename(getCanonicalRepoRoot(resolved) ?? resolved)`. This preserves the precedence chain — explicit `--name` and preserved alias still win — and only changes the basename fallback to the canonical repo root. `--skip-git` paths fall through to `resolved` when `getCanonicalRepoRoot` returns null, so non-git folders keep their existing behavior. [HIGH abhigyanpatwari#1] Embedding server mapping at `run-analyze.ts:364` used canonical basename only, ignoring `options.registryName` (`--name`) and `getInferredRepoName` (remote-derived). When a user ran `gitnexus analyze --name my-app`, the registry got `my-app` but the server-mapping lookup key was the canonical basename (e.g. `my-repo`) — a silently broken mapping for anyone with `--name` or a remote. Fix: mirror the registry's name-resolution chain (without preserved alias, since server mappings are addressed by the operationally-meaningful name the user configures, not a sticky registry-only alias). The inline comment explains the deliberate alias omission so a future contributor doesn't "fix" it back. [HIGH abhigyanpatwari#2] Duplicate registry entries from full-analysis worktree analyze — same root cause as CRITICAL abhigyanpatwari#2 — resolved by the same registerRepo fix. [LOW] Comment at line 361 previously claimed "consistent with the registry name" — false when `--name` or remote was set. Rewritten to spell out the chain and the alias omission. Test coverage: new `describe('registerRepo worktree-aware basename fallback (abhigyanpatwari#1259)')` block in `test/unit/repo-manager.test.ts` creates a real `git worktree add`-ed worktree in a temp dir, calls `registerRepo(worktreeDir, meta)` with NO `--name` and NO remote, and asserts the registered name is the canonical repo dir's basename (NOT 'wt-feature'). This is the regression-guard at the registry layer that complements the existing helper-level coverage in `git-utils.test.ts`. Comparison uses `path.basename` (not full-path equality) for the same Windows 8.3 short-name reason as the other worktree tests. [MEDIUM] integration-level `runFullAnalysis` test deferred per the bot's own classification ("no on its own" — only blocks merge in combination with the CRITICAL finding, which is now addressed at the registry layer that the integration test would have proxied for). The new `registerRepo` worktree test covers the same wiring at a lighter weight; a full-pipeline integration test would re-prove the same code path through a much heavier setup. [MEDIUM] Environment-fragile "plain temp directory" test left as-is — mirrors the existing `getGitRoot > returns null for a plain temp directory` precedent. Locally fails on machines where `os.tmpdir()` sits inside a parent git repo (author's Windows dev box, repo-in-repo setups); CI containers are unaffected.
|
The bot was right — my initial fix only patched the early-return path in [CRITICAL #2] [HIGH #1] Embedding mapping divergence. [HIGH #2] Duplicate registry entries from full-analysis path. Same root cause as CRITICAL #2 — the [LOW] False comment. Rewrote the embedding-mapping comment to spell out the actual chain and the deliberate alias omission. [MEDIUM] integration test for [MEDIUM] env-fragile temp-dir test — left as-is, mirrors the existing Local verification on
CI re-running. |
…itrary subdirs (abhigyanpatwari#1259) The previous commit (`7ceb839b`) regressed `--skip-git` by calling `getCanonicalRepoRoot` from `registerRepo` unconditionally. When a user runs `gitnexus analyze --skip-git COOLIO/` and `COOLIO/` happens to be a subdir of an unrelated parent git repo (a common scenario — GitHub Actions runners check out the test repo into `/work/...`, making every test temp dir transitively inside that checkout), `getCanonicalRepoRoot(COOLIO)` walks up to the parent git's root and the registry name becomes the parent repo's basename instead of `COOLIO`. Defeats the entire `--skip-git` intent and broke the `abhigyanpatwari#1232/abhigyanpatwari#1233` regression tests on macos + ubuntu CI. Add `resolveRepoIdentityRoot(fromPath)` in `src/storage/git.ts` that ONLY collapses to the canonical root in two specific cases: 1. `fromPath` IS the canonical checkout root (`canonical === resolved`) 2. `fromPath` is a linked-worktree root, identified by the presence of its own `.git` entry (`hasGitDir(resolved)` — a worktree's `.git` is a file pointing to `<canonical>/.git/worktrees/<name>`) For everything else — arbitrary subdirs of git repos, non-git folders, `--skip-git` paths sitting inside an unrelated checkout — it returns `fromPath` unchanged. This confines the canonical-root collapse to exactly the cases where abhigyanpatwari#1259 matters (worktrees) without disturbing the abhigyanpatwari#1232/abhigyanpatwari#1233 fix's intent (`--skip-git` honors the input path). Switch all three callsites — `repo-manager.ts:registerRepo`, `run-analyze.ts` early-return, `run-analyze.ts` embedding-mapping — from `getCanonicalRepoRoot(p) ?? p` to `resolveRepoIdentityRoot(p)`. The original `getCanonicalRepoRoot` helper stays exported and unchanged for any future caller that legitimately wants the canonical root from ANY path inside a git repo (e.g. the existing helper-level tests). Verified locally: - tsc clean - targeted skip-git-cli + repo-manager + git-utils: 72/78 pass — the 6 remaining failures are all the same dev-env class (tmpdir under a parent git repo on Windows; the COOLIO test that broke on CI now passes) - full unit suite: 4857 passed / 7 failed / 10 skipped — baseline on `upstream/main` was 9 failures with my buggy `7ceb839b` HEAD; this commit RESOLVES 2 of those (basename-collisions and fallback-to-basename), leaving 7 same-class env failures that all pass on CI containers
|
CI flagged a real regression in my previous follow-up — apologies. Fixed in
For arbitrary subdirs of git repos, non-git folders, and All three callsites — Local verification on
|
|
The 5 failing |
|
Claude finished @magyargergo's task in 8m 58s —— View job PR #1296 Review — fix(storage): derive registry name from canonical repo root, not worktree slugReview bar for this PR
Current PR state
Understanding of the changeWhat #1259 reported: Why What Why Where the fix is applied:
What intentionally remains worktree-local: Findings[CRITICAL] CI Windows failures not maintainer-accepted — blocks merge per DoD §4.2
[HIGH]
|
| DoD item | Status |
|---|---|
| Worktree-only analyze derives fallback name from canonical repo root basename | satisfied — resolveRepoIdentityRoot used in all three callsites |
Explicit --name still wins in all paths |
satisfied — options.registryName ?? is first in all chains |
| Remote-derived name wins before canonical fallback in all paths | satisfied — getInferredRepoName(repoPath) ?? is second in all chains |
Non-git / --skip-git / arbitrary subdir preserves its own basename |
satisfied — resolveRepoIdentityRoot correctly gates on hasGitDir |
repoPath storage location remains worktree-local |
satisfied — entry.path and storagePath derived from worktree path unchanged |
registerRepo() and runFullAnalysis() use same identity-root semantics |
satisfied — both use resolveRepoIdentityRoot |
hasCustomAlias() stays consistent with new name derivation |
not satisfied — hasCustomAlias compares against path.basename(resolved) (worktree basename), not the canonical-root-derived name; causes spurious RegistryNameCollisionError on re-analyze |
| Embedding server-mapping key mirrors final registry name chain | satisfied — three-step chain registryName ?? inferred ?? canonical matches |
AGENTS.md MCP URI uses corrected project name from registerRepo |
satisfied — generateAIContextFiles at line 493 receives projectName from registerRepo return value |
getGitRoot() behavior unchanged |
satisfied — no modification to function or callers |
| Tests use real git worktrees and cover registry-layer regression | partially satisfied — registerRepo test uses real worktree add, but only tests single-register path; missing re-register and canonical-first scenarios |
| CI, Docker/build, typecheck green or failures explicitly accepted | not satisfied — Windows CI fails with 5 pre-existing-claimed tests; not explicitly maintainer-accepted |
Git worktree semantics assessment
getCanonicalRepoRoot: Correct. --path-format=absolute guarantees absolute output regardless of cwd. path.dirname(path.resolve(commonDir)) is cross-platform. stderr suppressed via stdio: ['ignore', 'pipe', 'ignore']. No shell injection: fromPath is passed as cwd, not interpolated into the command string. execSync(commandString) is consistent with existing helpers in the same file. Returns null on failure (non-git path, git unavailable, missing directory).
resolveRepoIdentityRoot: Logic is correct for the three target cases:
- Normal canonical checkout:
canonical === resolved→ returns canonical ✓ - Linked worktree root:
canonical !== resolvedANDhasGitDir(resolved) = true(.gitfile) → returns canonical ✓ - Arbitrary subdir:
canonical !== resolvedANDhasGitDir(resolved) = false→ returns resolved ✓ - Non-git folder:
canonical = null→ returns resolved ✓ --skip-gitsubdir inside parent git repo:hasGitDir=false→ returns resolved ✓ (preservesgitnexus analyze --skip-gitwalks up to parent git repo instead of treating cwd as index root #1232/Don't modify gitignore of analyzed repo, usig gitignore with * inside .gitnexus instead #1233)
getGitRoot unchanged: Confirmed — no modification.
Normal checkout: --git-common-dir returns <repo>/.git; path.dirname = <repo>. Matches getGitRoot. ✓
Linked worktree: --git-common-dir returns the main repo's .git (shared), NOT the per-worktree git dir (.git/worktrees/<name>). path.dirname = main repo root. getGitRoot still returns worktree-local root. Asymmetry is exactly correct. ✓
Worktree outside main repo directory: --git-common-dir still returns main repo's .git. ✓
Windows/macOS/Linux: path.resolve normalizes Windows slashes; macOS /private/var symlink is handled by git's output (consistent with itself). ✓
Submodule edge case: --git-common-dir inside submodule returns parent/.git/modules/sub → path.dirname = parent/.git/modules → wrong canonical root. hasGitDir(submoduleRoot) = true (.git file) → resolveRepoIdentityRoot returns parent/.git/modules → path.basename = 'modules' — incorrect but not a regression from before this PR. See LOW finding.
Registry / naming assessment
Explicit --name: Wins via opts?.name !== undefined guard at top of registerRepo. Early-return path: options.registryName ?? is first. Embedding mapping: options.registryName ?? is first. ✓
Remote-derived name: getInferredRepoName is second in all chains. ✓
Canonical fallback: path.basename(resolveRepoIdentityRoot(resolved)) is now the basename-of-last-resort in all three callsites, replacing the raw path.basename(repoPath) worktree-slug fallback. ✓
Preserved alias behavior: Works correctly for FIRST-TIME worktree register. Broken for re-register when canonical checkout entry also exists — see HIGH finding.
Worktree fallback: Only for non-git / --skip-git / arbitrary subdir. ✓
registerRepo duplicate behavior: Two entries (/repo + /repo/wt-feature) can coexist when both are analyzed. Both are now named canonically (repo), which is the "silent basename collision" behavior. No collision guard fires on first registration of each path (guard is gated on explicitName = true, which requires opts.name or isPreservedAlias). On second worktree registration, hasCustomAlias regression triggers guard — see HIGH finding.
Existing stale entries / migration: No automatic migration. Users with stale wt-feature entries should run gitnexus remove wt-feature. Not documented in PR.
AGENTS / MCP URI assessment
AGENTS.md URI generation: generateAIContextFiles(repoPath, storagePath, projectName, ...) at run-analyze.ts:490 — projectName comes from await registerRepo(...) return value, which is now the canonical name. For the full-analysis path, AGENTS.md will receive the canonical name. ✓
Early-return path: Does NOT call generateAIContextFiles. If AGENTS.md previously contained a worktree slug (from a pre-fix analysis), it remains stale after an up-to-date early-return run. Users would need --force to rewrite it. This is pre-existing behavior (the early-return has always skipped AGENTS.md regeneration), not a new regression. Acceptable limitation.
MCP project/resource name: After this fix, new worktree analyses register under the canonical name. MCP impact/context queries using the canonical name will find the correct index. Old stale wt-feature entries remain (manual removal required).
Cleanup guidance: Missing from PR. Recommend release note: "Existing worktree-slug registry entries (e.g. wt-feature) must be removed manually: gitnexus remove wt-feature."
Embedding mapping assessment
projectName derivation at run-analyze.ts:369:
const projectName =
options.registryName ??
getInferredRepoName(repoPath) ??
path.basename(resolveRepoIdentityRoot(repoPath));This is the correct three-step chain mirroring the registry's precedence. ✓
Consistency with registry name: Consistent for --name, remote-derived, and canonical-fallback paths. ✓
Preserved-alias intentionally NOT consulted: The comment at lines 363–368 correctly explains the deliberate omission — server mappings are addressed by the operationally-meaningful name, not a sticky registry alias. This is intentional design, well-documented. ✓
Fallback behavior: When no remote and no --name, both chains produce the same canonical root basename. ✓
Test assessment
Helper tests (git-utils.test.ts, getCanonicalRepoRoot block): All 4 tests structurally sound and use real git worktree add. The regression-guard test (line 226) correctly asserts fromWorktree === fromMain and path.basename(fromWorktree) !== 'wt-feature'. These would fail on upstream/main before this PR. ✓
Registry-layer test (repo-manager.test.ts, line 911): Creates a real repo + linked worktree, calls registerRepo(worktreeDir, meta) with no --name and no remote, asserts canonical name. Directly addresses the critical gap from the first review. ✓ for the single-register path.
Missing tests:
registerRepo(canonical) → registerRepo(worktree) → re-registerRepo(worktree)— would catch thehasCustomAliasregression (see HIGH finding). ✗registerRepo(arbitrarySubdir)inside a git repo without.gitentry — would guardresolveRepoIdentityRoot's subdir-preservation gate. ✗
Environment fragility: getCanonicalRepoRoot > returns null for a plain temp directory (line 193) fails when os.tmpdir() is inside a git repo. Mirrors pre-existing getGitRoot > returns null for a plain temp directory pattern. CI containers unaffected. Still brittle for local contributors.
Would tests fail before this PR? The worktree regression test (line 226) would fail — getCanonicalRepoRoot didn't exist. The registerRepo worktree test (line 911) would fail — it would register as 'wt-feature', contradicting not.toBe('wt-feature'). ✓ Tests prove the fix.
Would the hasCustomAlias regression fail any test? No — the scenario that triggers it (canonical-first, worktree-second, worktree-re-analyze) is not covered. ✗
Validation assessment
CI failure: tests / windows-latest fails — 5 tests in cli-e2e.test.ts > #324 (tool output goes to stdout via fd 1). These test stdout/stderr fd-1 piping behavior, topically unrelated to worktree naming. Author reports pre-existing on upstream/main. CI report shows Ubuntu (coverage) + macOS pass cleanly, including all new git-utils.test.ts and repo-manager.test.ts tests. Not maintainer-accepted in the PR thread.
Docker/build: ✅ success.
Typecheck: ✅ clean (quality/typecheck and quality/typecheck-web both pass).
Focused tests: New tests pass on Ubuntu and macOS. Windows passes all new tests (the 5 Windows failures are in cli-e2e.test.ts, not in the new test files).
hasCustomAlias regression: Not verified by any test in this PR. Manually traced and confirmed through code analysis.
Submodule edge case: Not verified (known limitation, acknowledged as out of scope).
Final verdict
not production-ready
Two blockers remain. First, the hasCustomAlias function was not updated to account for canonical-root-derived names, causing a RegistryNameCollisionError on second worktree analyze when the canonical checkout is also registered — a regression this PR introduces that breaks the primary "per-task worktree, repeated re-analyze" workflow for users who analyze both their main checkout and worktrees. Second, the Windows CI failures are pre-existing per the author's account, but @magyargergo has not explicitly accepted them in the PR thread, and DoD §4.2 requires npm test to pass on all CI platforms.
The correctness of getCanonicalRepoRoot, resolveRepoIdentityRoot, and their three-callsite integration is solid. The registerRepo test is a meaningful addition. If the hasCustomAlias regression is fixed (a small targeted change), a resolveRepoIdentityRoot(stored-path) check is added to its early-exits, and the Windows CI flake is explicitly accepted by the maintainer, this PR would be production-ready with minor follow-ups (migration note, subdir test, temp-dir fragility).
Required before re-review:
- Fix
hasCustomAliasto recognizepath.basename(resolveRepoIdentityRoot(resolved))as a non-alias so re-analyze from worktree doesn't throw when canonical checkout is registered. Fix hasCustomAlias regression → - Add test: canonical → worktree → worktree re-register, asserts no
RegistryNameCollisionError. - @magyargergo to explicitly confirm the Windows
#324failures as pre-existing / merge-accepted.
…tomAlias (abhigyanpatwari#1259) Addresses the [HIGH] regression flagged in the second @claude review on PR abhigyanpatwari#1296. After `7ceb839b` registered worktrees under the canonical basename, `hasCustomAlias` still compared `entry.name` against `path.basename(entry.path)` (worktree slug) and misclassified the canonical name as a sticky user alias on re-analyze: 1. analyze /repo → {name:'repo', path:'/repo'} 2. analyze /repo/wt-feature → {name:'repo', path:'/repo/wt-feature'} ✓ 3. analyze /repo/wt-feature → RegistryNameCollisionError ✗ Adds a third early-exit recognizing `path.basename(resolveRepoIdentityRoot(resolved))` as a non-alias. Test covers the canonical → worktree → re-worktree sequence end-to-end with a real `git worktree add` fixture.
|
Addressed the [HIGH] |
|
Claude finished @magyargergo's task in 6m 8s —— View job PR #1296 Review — fix(storage): derive registry name from canonical repo root, not worktree slugTasks
Review bar for this PR
Current PR state
Understanding of the change#1259 symptom: Why
Callsite coverage:
What stays worktree-local: Findings[MEDIUM] Same-name/different-path entries make MCP no-param calls ambiguous
[MEDIUM] Missing test for
|
| DoD item | Status |
|---|---|
| Worktree-only analyze derives fallback name from canonical repo root basename in ALL paths | satisfied — early-return path, registerRepo fallback, embedding mapping all use resolveRepoIdentityRoot |
Explicit --name still wins everywhere |
satisfied — options.registryName ?? first in all chains; opts?.name !== undefined guard first in registerRepo |
| Remote-derived name wins before canonical fallback in all paths | satisfied — getInferredRepoName(repoPath) ?? second in all chains |
Non-git / --skip-git folders and arbitrary subdirs preserve own basename |
satisfied — resolveRepoIdentityRoot returns resolved for non-git / subdir (no .git entry) |
repoPath storage location remains worktree-local |
satisfied — entry.path = resolved (worktree path unchanged); storagePath = resolved/.gitnexus |
registerRepo() and runFullAnalysis() use same identity-root fallback semantics |
satisfied — both use resolveRepoIdentityRoot; full-analysis path via registerRepo |
hasCustomAlias() does not misclassify canonical-root-derived worktree names as user aliases |
satisfied — third early-exit in hasCustomAlias handles entry.name === path.basename(resolveRepoIdentityRoot(resolved)) |
| Embedding server-mapping uses same name chain as registry | satisfied — 3-step chain mirrors registry; preserved alias intentionally excluded, documented |
| AGENTS.md MCP URI uses corrected project name | satisfied — generateAIContextFiles at line 490–503 receives projectName from registerRepo return value |
| Same-name/different-path registry entries do not break MCP name-based lookup | satisfied — MCP repoId() handles collision with hash suffix; named lookup returns first-registered (canonical); no lookup errors |
getGitRoot() behavior unchanged for all existing callers |
satisfied — function unmodified; all 4 CLI callers (analyze.ts:225, status.ts:21, wiki.ts:101, index-repo.ts:50) correct |
| Tests use real Git worktrees and cover helper, registry-layer, re-register regression | satisfied — git-utils.test.ts: 4 helper tests with real worktree; repo-manager.test.ts: fresh-register + canonical→worktree→re-register |
| CI, Docker, typecheck green | satisfied — both workflows completed success on head SHA c7fb6d6 at 2026-05-03T18:46:42Z |
Git worktree semantics assessment
getCanonicalRepoRoot: Correct. --path-format=absolute guarantees absolute output regardless of cwd. path.dirname(path.resolve(commonDir)) is cross-platform. stderr suppressed via stdio: ['ignore', 'pipe', 'ignore']. No shell injection: fromPath is cwd, not interpolated into the command string. execSync(commandString) is consistent with all other helpers in git.ts. Returns null on failure (non-git, unavailable git, missing directory). Does not modify getGitRoot.
resolveRepoIdentityRoot: Logic is correct and the subdir-preservation gate is tight:
- Normal canonical checkout:
canonical === resolved→ returns canonical ✓ - Linked worktree root:
canonical !== resolvedANDhasGitDir(resolved) = true(.gitfile) → returns canonical ✓ - Arbitrary subdir:
canonical !== resolvedANDhasGitDir(resolved) = false→ returns resolved ✓ - Non-git folder:
canonical = null→ returns resolved immediately ✓ --skip-gitsubdir inside parent git repo:hasGitDir = false→ returns resolved ✓ (preservesgitnexus analyze --skip-gitwalks up to parent git repo instead of treating cwd as index root #1232/Don't modify gitignore of analyzed repo, usig gitignore with * inside .gitnexus instead #1233)
getGitRoot unchanged: Confirmed at git.ts:87–95. No modification. All 4 callers use the worktree-local root correctly for their purposes.
Normal checkout: --git-common-dir returns <repo>/.git; path.dirname = <repo>. Same as getGitRoot. ✓
Linked worktree: --git-common-dir returns the main repo's .git (shared), NOT the per-worktree git dir (.git/worktrees/<name>). getGitRoot still returns worktree root. The asymmetry is exactly correct. ✓
Worktree outside main repo directory: --git-common-dir still returns the main repo's .git regardless of where the worktree directory is. ✓
Windows/macOS/Linux: path.resolve normalizes Windows slashes; macOS /private/var symlink is handled by git's own output (consistent with itself). Tests use path.basename comparison (not full-path equality) for cross-platform stability. ✓
Submodule edge case: --git-common-dir inside a submodule returns parent/.git/modules/sub → path.dirname = parent/.git/modules → path.basename = 'modules' — incorrect. Also hasGitDir(submoduleRoot) = true so resolveRepoIdentityRoot returns the wrong path. Not a regression. See LOW finding.
Registry / naming assessment
Explicit --name: Wins via opts?.name !== undefined guard (skips all inference) in registerRepo. Early-return: options.registryName ?? first. Embedding: options.registryName ?? first. ✓
Remote-derived name: getInferredRepoName is second in all three chains. ✓
Canonical fallback: path.basename(resolveRepoIdentityRoot(resolved)) is the basename-of-last-resort in all three callsites. Replaces the raw path.basename(repoPath) worktree-slug fallback. ✓
Preserved alias behavior: hasCustomAlias correctly identifies sticky user aliases (via --name on a prior run). The new canonical-root check at line 402 ensures canonical-root-derived names don't masquerade as user aliases. True user aliases (e.g., --name my-app → stored as 'my-app' → resolveRepoIdentityRoot returns 'repo' → 'my-app' !== 'repo' → hasCustomAlias returns true → alias preserved) are still correctly protected. ✓
hasCustomAlias under-detection risk: Two unrelated repos with the same canonical basename (e.g., two different repos both named api) — their entries both pass hasCustomAlias as non-aliases, which is correct: neither was user-aliased; both should follow the auto-inference chain. No aliasing weakening. ✓
Worktree fallback: Only for non-git / --skip-git / arbitrary subdir. ✓
registerRepo duplicate behavior: canonicalizePath(repoPath) is the upsert key. /repo and /repo/wt-feature are different paths → separate entries. Both get canonical name 'repo' after this fix. This is "silent basename collision" (existing behavior). The PR substantially improves the prior state (where entries had different wrong names) while not introducing new functionality. ✓
Existing stale entries / migration: No automatic migration. See MEDIUM finding.
AGENTS / MCP URI assessment
AGENTS.md URI generation: run-analyze.ts:490–503: await generateAIContextFiles(repoPath, storagePath, projectName, ...) where projectName = await registerRepo(...). After this fix, registerRepo for a first-time worktree analyze (no remote, no --name) returns the canonical repo basename. AGENTS.md receives the canonical name → MCP URI block uses the correct slug. ✓
Early-return path: Does NOT call generateAIContextFiles. If AGENTS.md was previously written with a worktree slug, it remains stale until the user runs --force. This is pre-existing behavior (early-return has always skipped AGENTS.md). Users should re-analyze with --force after upgrading if AGENTS.md has stale slugs. Acceptable limitation.
MCP project/resource name after fix: New worktree analyses register under the canonical name. resolveRepo('repo') → this.repos.has('repo') → canonical checkout (first-registered). Second entry (repo-<hash>) accessible by path only. MCP context/impact with @repo param correctly finds the canonical index. ✓
Same-name registry ambiguity: When no repo param and two same-name entries exist, MCP throws "Multiple repositories indexed" error. This pre-existed this PR (previously two entries with different names — also size > 1). Named lookup is non-ambiguous. See MEDIUM finding.
Stale worktree-named entries: Not auto-cleaned. See MEDIUM finding.
Embedding mapping assessment
projectName derivation at run-analyze.ts:369–372:
const projectName =
options.registryName ??
getInferredRepoName(repoPath) ??
path.basename(resolveRepoIdentityRoot(repoPath));Correct 3-step chain. ✓
Consistency with registry name: Consistent for all practical cases. The only divergence point is preserved alias: registerRepo may use a stored alias (e.g., 'my-app' from a prior --name my-app) while the embedding mapping uses getInferredRepoName or canonical basename. This is intentional and documented: the embedding mapping comment explicitly explains the preserved-alias omission ("server mappings are addressed by the operationally-meaningful name the user configures, not a sticky registry-only alias they may not know about"). This is correct design — server-mapping.json is a user-configured file keyed by operational name, not by internal registry alias. ✓
--name behavior: Registry gets --name value; embedding mapping uses options.registryName first → also gets --name value. ✓
Remote-derived behavior: Both registry and embedding use getInferredRepoName second. ✓
Canonical fallback: Both use path.basename(resolveRepoIdentityRoot(...)) last-resort. ✓
Test assessment
Helper tests (git-utils.test.ts, getCanonicalRepoRoot block):
returns null for a plain temp directory— brittle (see LOW), but mirrors pre-existing pattern. CI-clean. ✓returns null for a non-existent path— deterministic. ✓returns repo root from regular checkout— realgit init, basename comparison (cross-platform). ✓returns canonical root from linked worktree + sanity-checks getGitRoot(line 226) — realgit worktree add, assertsfromWorktree === fromMain, asserts basename ≠'wt-feature', assertsgetGitRootstill returns'wt-feature'. Regression-guard for Worktrees: project name derived from worktree slug, creates duplicate index + rewrites docs #1259. ✓
Registry-layer test (repo-manager.test.ts:911): Creates real repo + linked worktree, no remote, calls registerRepo(worktreeDir, meta), asserts name ≠ 'wt-feature' AND name = canonical basename. Directly guards the registerRepo wiring. ✓
Re-register regression test (repo-manager.test.ts:963): Full canonical → worktree → re-worktree sequence. Asserts no RegistryNameCollisionError on the third call. Asserts both entries are named canonically. Proves c7fb6d6 fixed the hasCustomAlias regression. ✓ — Would fail without c7fb6d6.
Missing test — subdir preservation: No test for registerRepo(subdir_no_dot_git_inside_git_repo). See MEDIUM finding. Logic is sound; COOLIO e2e test provides indirect coverage.
Missing test — AGENTS.md URI content: No test validates the URI slug in generated AGENTS.md after a worktree analyze. See LOW finding. Code path is clear and correct.
Environment fragility: getCanonicalRepoRoot plain-temp-dir test fails locally when os.tmpdir() is inside a git repo. CI-clean. Same pattern as pre-existing getGitRoot test. ✓ (acceptable for CI-clean projects)
Would tests fail before this PR? The worktree helper test (line 226) would fail — getCanonicalRepoRoot didn't exist. The registerRepo worktree test (line 911) would fail — pre-fix returned 'wt-feature'. The re-register test (line 963) would fail before c7fb6d6. All three test additions correctly prove the fix. ✓
Validation assessment
CI: ✅ Both CI and Docker Build & Push completed success on head SHA c7fb6d6 at 2026-05-03T18:46:42Z. Prior Windows #324 failures resolved (or no longer present on the final commits — CI is fully green).
Docker/build: ✅ success.
Typecheck: ✅ clean (confirmed in the latest CI comment before merge assessment).
Focused tests: New worktree tests pass on all platforms. The hasCustomAlias regression test passes on all platforms.
Full test count: CI comment shows 7848/7849 passed, 0 failed, 1 skipped — the skip is a documented Ruby block parameter inference limitation, unrelated to this PR.
Not verified: AGENTS.md URI content in integration; MCP end-to-end same-name lookup. Both are acceptable gaps given the clear code path and registry-layer test coverage.
Final verdict
production-ready with minor follow-ups
All three blockers from the previous review cycle are resolved: the full-analysis registration path now uses resolveRepoIdentityRoot in registerRepo's basename fallback, the embedding server-mapping correctly mirrors the registry name chain, and hasCustomAlias is updated to recognize canonical-root-derived names as non-aliases — preventing RegistryNameCollisionError on the second worktree re-analyze. CI and Docker are fully green on the final head SHA. The getCanonicalRepoRoot + resolveRepoIdentityRoot implementation is technically correct across all targeted layouts (normal checkout, linked worktree root, arbitrary subdir, non-git, --skip-git), and all four getGitRoot callers remain correctly worktree-local.
The remaining items are all follow-up candidates, not blockers: the MCP no-param ambiguity when both canonical and worktree are separately registered is existing "silent basename collision" behavior (present before this PR, now with better-named entries); the missing subdir-preservation unit test is a coverage gap against a logically correct and indirectly validated safeguard; the submodule edge case is a known pre-existing limitation; and the stale-entry migration note is a documentation gap that should accompany the release.
Suggested post-merge follow-ups:
- Add a release note: "Existing
wt-feature-named registry entries need manual removal:gitnexus remove wt-feature." - Add a
registerRepo(subdir_inside_git_repo)test asserting subdir basename preservation. - Add JSDoc to
getCanonicalRepoRoot/resolveRepoIdentityRootnoting submodule unsupported behavior. - Track same-name/different-path MCP no-param ambiguity as a follow-up issue (deduplication by canonical identity root).
· CI run · Docker run
|
@azizur100389 Can you please tackle with the medium findings? 🙏 |
…name (abhigyanpatwari#1259) Addresses [MEDIUM] from the third @claude review on PR abhigyanpatwari#1296: the `hasGitDir` gate inside `resolveRepoIdentityRoot` keeps the abhigyanpatwari#1232/abhigyanpatwari#1233 `--skip-git` invariant working — an arbitrary subdir under a parent git repo (no `.git` of its own) must NOT collapse to the parent's canonical root. The COOLIO `--skip-git` integration test already proves this end-to-end, but no direct test sat at the `registerRepo` layer to guard the gate against future refactors. This is that direct test.
|
All three mediums addressed:
LOWs deferred per bot's own "no on its own" classification. Local on |
|
Can you please merge origin/main? Mainly because I made changes here. |
|
Done — merged origin/main, no conflicts. CI re-running. |
Summary
Reported in #1259 by @altworth-eng-altworth-com (with full forensic analysis): when
gitnexus analyzeruns from inside a git worktree (e.g./repo/wt-feature/), the resulting project is registered under the worktree's directory basename (wt-feature) instead of the canonical repo's basename (repo). Three observable problems follow:~/.gitnexus/registry.jsonentries — the same codebase appears once for the main checkout and once per worktree.gitnexus://repo/wt-feature/...) — contributors who don't catch the diff ship a project rename they didn't intend.impact/contextreturn data for whichever copy is fresher.Claude Code's per-task worktree workflow trips this on every
analyze.Root cause
getGitRoot(insrc/storage/git.ts) usesgit rev-parse --show-toplevel, which returns the WORKTREE's root, not the shared canonical root. The two existing fallbacks inrunFullAnalysis—--name <alias>and the git-remote-derived inferred name (getInferredRepoNameviagetRemoteUrl) — only mask the bug. Repos withoutorigin(private, freshgit init, monorepos with no remote configured at this level, etc.) hit the worktree-basename fallback directly.Fix
Add
getCanonicalRepoRoot(fromPath)insrc/storage/git.ts— usesgit rev-parse --path-format=absolute --git-common-dir(the.gitdirectory shared across the main checkout and all linked worktrees) and returns its parent. Verbatim shape from the reporter's pre-tested suggestion.Use it in both basename-fallback sites in
run-analyze.ts:return { - repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath), + repoName: + options.registryName ?? + getInferredRepoName(repoPath) ?? + path.basename(getCanonicalRepoRoot(repoPath) ?? repoPath), repoPath, ... };The new chain becomes: explicit
--name→ git-remote-derived → canonical-root basename (NEW, fixes #1259) → worktree basename (last-resort fallback for--skip-git/ non-git folders).What is NOT changed
getGitRootsemantics are unchanged. Its four existing callers continue to receive the worktree-local root, which is correct for their purposes:cli/status.ts:21.gitnexus/directory — lives in the worktreecli/wiki.ts:101.gitfor diff-aware opscli/index-repo.ts:50.gitnexus/— user pointed at the worktree explicitlycli/analyze.ts:225repoPathitself stays worktree-local (the.gitnexus/is created in cwd); only the name derivation uses the canonical helperTests
test/unit/git-utils.test.ts— newdescribe('getCanonicalRepoRoot')block, 4 tests:git worktree add-ed worktree in a temp dir, assertsgetCanonicalRepoRoot(worktreeDir) === fs.realpathSync(repoDir)AND thatgetGitRoot(worktreeDir)still returns the worktree-local path (sanity check that we didn't accidentally changegetGitRoot).The fixtures use real
execSync('git worktree add …')rather than mocks because the bug is ingit rev-parse's actual behavior across worktrees — mocks would test the mock, not the fix.Verification
tsc --noEmitvitest run test/unit/git-utils.test.tsnpm run test:unit(full)The 7 unit-suite failures are 6 pre-existing dev-env failures on
upstream/main(verified bygit stash+ re-run on a clean tree, identical 6) plus 1 same-class new failure I added. The pattern: my Windows dev box happens to haveos.tmpdir()(underC:\Users\<user>\AppData\Local\Temp) inside a personal git repo, so any test asserting "this temp dir is NOT a git repo" trips. CI runs in containers where/tmpis not inside a git repo and all 7 pass cleanly. I kept the 4thgetCanonicalRepoRoot > returns null for a plain temp directorytest for pattern-consistency with the existinggetGitRoot > returns null for a plain temp directorytest.Cross-platform safety
path.dirname(path.resolve(commonDir))works identically on Windows / macOS / Linux.--path-format=absoluteis required because--git-common-dirreturns a path relative to cwd by default (e.g.../.gitfrom inside a worktree); without absolute formatting, a caller resolving from a different directory later would get the wrong path.fs.realpathSyncso macOS/var/folders/... → /private/var/folders/...symlink resolution doesn't desync the assertion from git's canonical form.Magyargergo guideline checklist
getGitRootunchanged, all 4 callers safeNotes for users upgrading
wt-feature-style entry in~/.gitnexus/registry.json. The fix does not auto-migrate; clean them up withgitnexus remove wt-feature(or whatever the prior worktree slug was) before re-analyzing from the worktree./repoand/repo/wt-featureregisters two entries under the canonical name (repo). MCP calls without an explicitrepoparameter become ambiguous (Multiple repositories indexed. Specify which one with the 'repo' parameter.). Named lookup (-r repo) resolves to the first-registered (typically the canonical checkout); the worktree entry is accessible only by path. Full deduplication would require name-based registry lookup and is out of scope for this fix — track as a follow-up if it becomes painful.Closes #1259.