diff --git a/src/capdisc/scope/inventory/roots.py b/src/capdisc/scope/inventory/roots.py index 4d4be5d..c8f6d65 100644 --- a/src/capdisc/scope/inventory/roots.py +++ b/src/capdisc/scope/inventory/roots.py @@ -153,7 +153,16 @@ def discover( kinds=_STANDALONE_KINDS, ) ) - roots += [ScanRoot(scope=ScopeKind.project, base=base) for base in _project_bases(start)] + # Launched from the home directory itself (cwd = ~), the walk-up finds `~/.claude` + # and would label it project scope while the same physical directory is also added + # as the user root below — double-capturing every artifact it holds. The user root + # is the true owner; drop the project-scope alias. + user_claude = (home_dir / ".claude").resolve() if home_dir is not None else None + roots += [ + ScanRoot(scope=ScopeKind.project, base=base) + for base in _project_bases(start) + if base.resolve() != user_claude + ] roots += [ ScanRoot(scope=ScopeKind.project, base=base, kinds=frozenset({ArtifactKind.skill})) for base in _nested_skill_bases(start) @@ -164,6 +173,8 @@ def discover( seen_project_bases = { root.base.resolve() for root in roots if root.scope == ScopeKind.project } + if user_claude is not None: + seen_project_bases.add(user_claude) for add in add_dirs: claude_dir = (add / ".claude").resolve() if claude_dir.is_dir() and claude_dir not in seen_project_bases: diff --git a/tests/test_scope.py b/tests/test_scope.py index f95348b..0407dec 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -24,6 +24,24 @@ def test_precedence_flips_by_kind(tmp_path: Path) -> None: assert _effective_scope(inv, ArtifactKind.agent, "foo") is ScopeKind.project # subagents invert +def test_home_launch_does_not_double_scan_user_claude(tmp_path: Path) -> None: + # Launched from the home directory itself (cwd = ~): `~/.claude` is the user root and + # must not also be captured as project scope, or every artifact in it lists twice. + home = tmp_path / "home" + standalone_set(home / ".claude") + roots = ScopeRoots.discover(start=home, home_dir=home) + + target = (home / ".claude").resolve() + labelled = [(root.scope, Path(root.base).resolve()) for root in roots.roots] + assert (ScopeKind.user, target) in labelled + assert (ScopeKind.project, target) not in labelled + + inv = ScopeInventory.scan(roots) + commands = [c for c in inv.artifacts if c.kind is ArtifactKind.command and c.name == "foo"] + assert len(commands) == 1 + assert commands[0].scope is ScopeKind.user + + def test_command_has_no_managed_scope(tmp_path: Path) -> None: repo, managed = tmp_path / "repo", tmp_path / "managed" (repo / ".git").mkdir(parents=True)