From 5b87d7b74fd19c19098a561222e5405eb33e2b9d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 13 May 2026 12:56:24 +0300 Subject: [PATCH] feat(agent/agentcontextconfig): discover skills from ~/.coder/skills (#25271) The default skills lookup only scanned the project-relative .agents/skills directory, so personal skills had to be repeated per project or wired in via CODER_AGENT_EXP_SKILLS_DIRS. Now the default is the comma-separated list ~/.coder/skills,.agents/skills, which lets discoverSkills's existing first-occurrence-wins policy prefer home-scoped skills over project ones with the same name. The change is additive when ~/.coder/skills is absent (missing directories are silently skipped in discoverSkills) and unaffects users who set the env var explicitly. Closes CODAGT-403 --- agent/agentcontextconfig/api.go | 6 +++++- agent/agentcontextconfig/api_test.go | 31 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/agent/agentcontextconfig/api.go b/agent/agentcontextconfig/api.go index 5636f18fe3..c29a26b73e 100644 --- a/agent/agentcontextconfig/api.go +++ b/agent/agentcontextconfig/api.go @@ -60,10 +60,14 @@ var skillNamePattern = regexp.MustCompile( // Default values for agent-internal configuration. These are // used when the corresponding env vars are unset. +// +// DefaultSkillsDir is a comma-separated list so home-scoped +// skills override project-scoped ones with the same name +// (discoverSkills picks the first occurrence per skill name). const ( DefaultInstructionsDir = "~/.coder" DefaultInstructionsFile = "AGENTS.md" - DefaultSkillsDir = ".agents/skills" + DefaultSkillsDir = "~/.coder/skills,.agents/skills" DefaultSkillMetaFile = "SKILL.md" DefaultMCPConfigFile = ".mcp.json" ) diff --git a/agent/agentcontextconfig/api_test.go b/agent/agentcontextconfig/api_test.go index eadd0196d1..a04bd0e15c 100644 --- a/agent/agentcontextconfig/api_test.go +++ b/agent/agentcontextconfig/api_test.go @@ -461,6 +461,37 @@ func TestResolve(t *testing.T) { require.Len(t, skillParts, 1) require.Equal(t, "from skills1", skillParts[0].SkillDescription) }) + + //nolint:paralleltest // Uses t.Setenv to mutate HOME. + t.Run("DefaultDiscoversHomeAndProjectSkillsHomeWins", func(t *testing.T) { + fakeHome := t.TempDir() + t.Setenv("HOME", fakeHome) + t.Setenv("USERPROFILE", fakeHome) + workDir := t.TempDir() + + homeSkills := filepath.Join(fakeHome, ".coder", "skills") + writeSkillMetaFileInRoot(t, homeSkills, "home-only", "home only") + writeSkillMetaFileInRoot(t, homeSkills, "shared", "from home") + writeSkillMetaFile(t, workDir, "project-only", "project only") + writeSkillMetaFile(t, workDir, "shared", "from project") + + // Construct the Config directly with the package defaults + // to verify the default skills list (and only the defaults). + cfg, _ := agentcontextconfig.Resolve(workDir, agentcontextconfig.Config{ + SkillsDirs: agentcontextconfig.DefaultSkillsDir, + SkillMetaFile: agentcontextconfig.DefaultSkillMetaFile, + }) + + got := map[string]string{} + for _, p := range filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill) { + got[p.SkillName] = p.SkillDescription + } + require.Equal(t, map[string]string{ + "home-only": "home only", + "project-only": "project only", + "shared": "from home", + }, got) + }) } func TestNewAPI_LazyDirectory(t *testing.T) {