From 3176e945b463fe6efca91655898e6571c339f1a8 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 5 Aug 2026 01:31:22 -0700 Subject: [PATCH] Install the skill where opencode actually looks skillLocations offered "OpenCode (Global)" and "OpenCode (Project)" at `skill/`, singular. opencode reads `skills/`, plural. It has never read either path we wrote. That list is both the wizard's install targets and the refresh set, so the typo failed in both directions and silently in both. Picking OpenCode in `basecamp skill` wrote a file opencode would never load, and reported success. An opencode user who installed the skill the plural way by hand then never got it refreshed on upgrade, because the refresh loop only visits paths in this list and skips ones that don't exist. Verified against real binaries into a temp HOME, with a stale SKILL.md pre-placed at ~/.config/opencode/skills/basecamp/: the pre-fix build leaves it reading "STALE", this build rewrites it byte-identical to the embedded skill. Worth recording: the skill already worked in opencode regardless, because opencode also searches ~/.agents/skills//SKILL.md, which is the canonical target `installSkillFiles` has always written. So this fixes the wizard and the refresh loop, not first-run coverage. The new test pins the literal paths instead of deriving them. The existing refresh test built its opencode path from the same wrong spelling the code had, so it asserted the bug was working. Search paths: https://opencode.ai/docs/skills/ --- internal/commands/skill.go | 4 ++-- internal/commands/skill_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/internal/commands/skill.go b/internal/commands/skill.go index aa727ef0c..43115fa5a 100644 --- a/internal/commands/skill.go +++ b/internal/commands/skill.go @@ -30,8 +30,8 @@ var skillLocations = []skillLocation{ {Name: "Agents (Shared)", Path: "~/.agents/skills/basecamp/SKILL.md"}, {Name: "Claude Code (Global)", Path: "~/.claude/skills/basecamp/SKILL.md"}, {Name: "Claude Code (Project)", Path: ".claude/skills/basecamp/SKILL.md"}, - {Name: "OpenCode (Global)", Path: "~/.config/opencode/skill/basecamp/SKILL.md"}, - {Name: "OpenCode (Project)", Path: ".opencode/skill/basecamp/SKILL.md"}, + {Name: "OpenCode (Global)", Path: "~/.config/opencode/skills/basecamp/SKILL.md"}, + {Name: "OpenCode (Project)", Path: ".opencode/skills/basecamp/SKILL.md"}, {Name: "Codex (Global)", Path: codexGlobalSkillPath()}, } diff --git a/internal/commands/skill_test.go b/internal/commands/skill_test.go index 6422ba4b5..3d686e6f3 100644 --- a/internal/commands/skill_test.go +++ b/internal/commands/skill_test.go @@ -222,6 +222,30 @@ func TestCopySkillFilesRejectsSubdirs(t *testing.T) { } } +// skillLocations is both the wizard's install targets and the refresh set, so a +// path an agent doesn't read is a silent no-op in either direction. Pin the +// literals rather than deriving them, so a test can't mirror the same typo the +// code has. OpenCode's search paths: https://opencode.ai/docs/skills/. Codex's +// entry is computed by codexGlobalSkillPath and covered separately. +func TestSkillLocationsMatchAgentSearchPaths(t *testing.T) { + want := map[string]string{ + "Agents (Shared)": "~/.agents/skills/basecamp/SKILL.md", + "Claude Code (Global)": "~/.claude/skills/basecamp/SKILL.md", + "Claude Code (Project)": ".claude/skills/basecamp/SKILL.md", + "OpenCode (Global)": "~/.config/opencode/skills/basecamp/SKILL.md", + "OpenCode (Project)": ".opencode/skills/basecamp/SKILL.md", + } + + got := make(map[string]string, len(skillLocations)) + for _, loc := range skillLocations { + got[loc.Name] = loc.Path + } + + for name, path := range want { + assert.Equal(t, path, got[name], "install target for %s", name) + } +} + func TestNormalizeSkillPath(t *testing.T) { tests := []struct { input, want string @@ -494,7 +518,7 @@ func TestRefreshAllInstalledSkills_MultipleLocations(t *testing.T) { require.NoError(t, os.MkdirAll(claudeSkill, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(claudeSkill, "SKILL.md"), []byte("old"), 0o644)) - opencode := filepath.Join(home, ".config", "opencode", "skill", "basecamp") + opencode := filepath.Join(home, ".config", "opencode", "skills", "basecamp") require.NoError(t, os.MkdirAll(opencode, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(opencode, "SKILL.md"), []byte("old"), 0o644))