Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/capdisc/scope/inventory/roots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down