Skip to content

fix(cli): resolve agent discovery against user home, not config dir - #7

Merged
eezzekl merged 1 commit into
mainfrom
fix/global-agent-discovery
Oct 2, 2026
Merged

eezzekl merged 1 commit into
mainfrom
fix/global-agent-discovery

Conversation

@eezzekl

@eezzekl eezzekl commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • init and config resolved every global agent directory against the user config directory instead of the user home, so no global agent was ever discovered. init emitted an empty targets: list on a machine with several agents installed.
  • Pi was the most visible casualty: its AgentConfig.LocalDir is "", so it has only a global path and was completely invisible.
  • Splits the overloaded seam and adds regression coverage. The pre-existing tests were also not hermetic; they are now pinned to an empty temp home.

Root cause

Both commands injected the config-directory lookup into the discovery home seam:

d.UserHomeDir = func() (string, error) { return getUserConfig() }

getUserConfig is os.UserConfigDir → ~/.config on Linux. Global paths therefore resolved to ~/.config/.pi/agent, ~/.config/.claude, ~/.config/.gemini, none of which exist.

One package-level variable was serving two distinct responsibilities: locating skill-sync.yaml and locating the user home for agent discovery.

The TUI was unaffected — internal/tui/program.go:71,86 call discovery.New() without overriding the seam. CLI and TUI had silently diverged.

Fix

getUserConfig keeps locating skill-sync.yaml; a new getUserHome (os.UserHomeDir) resolves agent directories.

A second defect this surfaced

After wiring the real home seam, the pre-existing case TestInitCmd/no_agents_found_prints_'no skills configured' started failing because it discovered the developer's actual ~/.claude, ~/.gemini and ~/.pi/agent.

Those tests had been passing only because the broken seam pointed them at a directory that never existed — the bug was acting as the test's isolation mechanism, which is why CI never caught it. Both suites now pin getUserHome to an empty t.TempDir().

Changes

File Change
internal/cli/init.go Add getUserHome seam; use it for discovery
internal/cli/config.go Use getUserHome for discovery
internal/cli/init_test.go Add table-driven TestInitDiscoversGlobalAgents (Pi, Claude Code, OpenCode); pin getUserHome in TestInitCmd
internal/cli/config_test.go Add table-driven TestConfigDiscoversGlobalAgents (Pi, Gemini CLI); pin getUserHome in TestConfigCmd
odd/tasks/fix-global-agent-discovery.md Task breakdown and recorded evidence

In the new tests the config directory is a sibling of home, never its parent, so a future collapse of the two seams cannot pass by accident.

Test plan

Built test-first; RED observed as a behavioral failure, not a compile error (the getUserHome variable was declared before being wired).

  • RED go test ./internal/cli/ -run TestInitDiscoversGlobalAgents → all three cases emitted an empty targets:
  • RED go test ./internal/cli/ -run TestConfigDiscoversGlobalAgents → both cases emitted targets: []
  • GREEN go test -race ./... → all 14 packages pass
  • go vet ./... → clean
  • gofmt -l . → empty

Manual run against a real home directory:

$ XDG_CONFIG_HOME=/tmp/pitest/cfg skill-sync init
targets:
  - /home/.../.config/opencode/skills
  - /home/.../.claude/skills
  - /home/.../.gemini/skills
  - /home/.../.copilot/skills
  - /home/.../.pi/agent/skills

Before this change the same command produced an empty targets: list.

Notes

  • ~/.pi/agent/skills need not pre-exist: Discovery.Scan matches the agent root and internal/sync/engine.go:43-56 creates missing target directories, so the first sync materializes it.
  • Pi project-local skills stay out of scope. LocalDir for Pi remains ""; whether Pi should gain a per-project directory is a separate decision.

`init` and `config` injected the user config directory (`os.UserConfigDir`,
`~/.config` on Linux) into the discovery home seam, so every global agent
directory was resolved as `~/.config/.pi/agent`, `~/.config/.claude`, etc.
None of those exist, so no global agent was ever discovered.

Pi was fully invisible because its `AgentConfig.LocalDir` is `""`: it has
only a global path. The TUI was unaffected — it calls `discovery.New()`
without overriding the seam.

Split the overloaded seam: `getUserConfig` keeps locating `skill-sync.yaml`,
and a new `getUserHome` (`os.UserHomeDir`) resolves agent directories.

The pre-existing `init`/`config` tests were passing only because the broken
seam pointed them at a directory that never existed; they now pin
`getUserHome` to an empty temporary directory so they cannot read the
developer's real home.
@eezzekl eezzekl added the bug Something isn't working label Oct 2, 2026
@eezzekl
eezzekl merged commit aca3719 into main Oct 2, 2026
3 checks passed
@eezzekl
eezzekl deleted the fix/global-agent-discovery branch October 2, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant