Skip to content

Commit 42dcfbd

Browse files
authored
feat(skills): Add Agent Skills discovery (#487)
## Summary - Adds an Agent Skills system: discovers `SKILL.md` files (with YAML frontmatter) under `.infer/skills/` (project) and `~/.infer/skills/` (user-global), with project scope taking precedence on name collisions. - New `infer skills list` command surfaces discovered skills, their scope, description, and path — plus validation errors for skipped entries — and works regardless of the enable flag. - Injects skill metadata (name + description) into the agent system prompt; skill bodies are read on demand by the model via the existing `Read` tool (progressive disclosure). Disabled by default — enable via `agent.skills.enabled` or `INFER_AGENT_SKILLS_ENABLED=true`.
1 parent 952f941 commit 42dcfbd

18 files changed

Lines changed: 1829 additions & 5 deletions

File tree

.flox/env/manifest.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.flox/env/manifest.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ docker-compose.pkg-path = "docker-compose"
3939
docker-compose.version = "^5.1.3"
4040
gopls.pkg-path = "gopls"
4141
gopls.version = "0.21.1"
42+
43+
[hook]
44+
on-activate = '''
45+
export PATH="$HOME/go/bin:$PATH"
46+
'''

.infer/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ agent:
170170
git_context_enabled: true
171171
working_dir_enabled: true
172172
git_context_refresh_turns: 10
173+
skills:
174+
enabled: false
173175
verbose_tools: false
174176
max_turns: 50
175177
max_tokens: 8192

.infer/shortcuts/skills.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
# Agent Skills Shortcuts
3+
# Manage Agent Skills from within chat
4+
#
5+
# Usage:
6+
# - /skills list - List discovered skills
7+
# - /skills install <github-url> - Install a skill from a public GitHub repo
8+
# - /skills uninstall <name> - Uninstall a skill by name
9+
10+
shortcuts:
11+
- name: skills
12+
description: "Manage Agent Skills"
13+
command: infer
14+
args:
15+
- skills
16+
subcommands:
17+
- name: list
18+
description: "List discovered skills"
19+
- name: install
20+
description: "Install a skill from a public GitHub repo (usage: <github-url> [--user] [--overwrite])"
21+
- name: uninstall
22+
description: "Uninstall a skill by name (usage: <name> [--user])"

cmd/init.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ func init() {
3434
rootCmd.AddCommand(initCmd)
3535
}
3636

37-
func initializeProject(cmd *cobra.Command) error { //nolint:funlen
37+
func initializeProject(cmd *cobra.Command) error { //nolint:funlen,gocyclo,cyclop
3838
overwrite, _ := cmd.Flags().GetBool("overwrite")
3939
userspace, _ := cmd.Flags().GetBool("userspace")
4040
skipMigrations, _ := cmd.Flags().GetBool("skip-migrations")
4141

42-
var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath string
42+
var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath,
43+
mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath,
44+
a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath,
45+
channelsPath, heartbeatPath, computerUsePath, agentsPath, skillsDirPath string
4346

4447
if userspace {
4548
homeDir, err := os.UserHomeDir()
@@ -54,13 +57,15 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen
5457
shellsShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "shells.yaml")
5558
exportShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "export.yaml")
5659
a2aShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "a2a.yaml")
60+
skillsShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "skills.yaml")
5761
mcpPath = filepath.Join(homeDir, config.ConfigDirName, config.MCPFileName)
5862
keybindingsPath = filepath.Join(homeDir, config.ConfigDirName, config.KeybindingsFileName)
5963
promptsPath = filepath.Join(homeDir, config.ConfigDirName, config.PromptsFileName)
6064
channelsPath = filepath.Join(homeDir, config.ConfigDirName, config.ChannelsFileName)
6165
heartbeatPath = filepath.Join(homeDir, config.ConfigDirName, config.HeartbeatFileName)
6266
computerUsePath = filepath.Join(homeDir, config.ConfigDirName, config.ComputerUseFileName)
6367
agentsPath = filepath.Join(homeDir, config.ConfigDirName, config.AgentsFileName)
68+
skillsDirPath = filepath.Join(homeDir, config.ConfigDirName, "skills")
6469
} else {
6570
configPath = config.DefaultConfigPath
6671
gitignorePath = filepath.Join(config.ConfigDirName, config.GitignoreFileName)
@@ -70,17 +75,19 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen
7075
shellsShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "shells.yaml")
7176
exportShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "export.yaml")
7277
a2aShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "a2a.yaml")
78+
skillsShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "skills.yaml")
7379
mcpPath = filepath.Join(config.ConfigDirName, config.MCPFileName)
7480
keybindingsPath = config.DefaultKeybindingsPath
7581
promptsPath = config.DefaultPromptsPath
7682
channelsPath = config.DefaultChannelsPath
7783
heartbeatPath = config.DefaultHeartbeatPath
7884
computerUsePath = config.DefaultComputerUsePath
7985
agentsPath = config.DefaultAgentsPath
86+
skillsDirPath = filepath.Join(config.ConfigDirName, "skills")
8087
}
8188

8289
if !overwrite {
83-
if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath); err != nil {
90+
if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath); err != nil {
8491
return err
8592
}
8693
}
@@ -128,6 +135,10 @@ plans/
128135
return fmt.Errorf("failed to create A2A shortcuts file: %w", err)
129136
}
130137

138+
if err := createSkillsShortcutsFile(skillsShortcutsPath); err != nil {
139+
return fmt.Errorf("failed to create Skills shortcuts file: %w", err)
140+
}
141+
131142
if err := createMCPConfigFile(mcpPath); err != nil {
132143
return fmt.Errorf("failed to create MCP config file: %w", err)
133144
}
@@ -158,6 +169,10 @@ plans/
158169
return fmt.Errorf("failed to create agents config file: %w", err)
159170
}
160171

172+
if err := createSkillsDir(skillsDirPath); err != nil {
173+
return fmt.Errorf("failed to create skills directory: %w", err)
174+
}
175+
161176
var scopeDesc string
162177
if userspace {
163178
scopeDesc = "userspace"
@@ -174,13 +189,15 @@ plans/
174189
fmt.Printf(" Created: %s\n", shellsShortcutsPath)
175190
fmt.Printf(" Created: %s\n", exportShortcutsPath)
176191
fmt.Printf(" Created: %s\n", a2aShortcutsPath)
192+
fmt.Printf(" Created: %s\n", skillsShortcutsPath)
177193
fmt.Printf(" Created: %s\n", mcpPath)
178194
fmt.Printf(" Created: %s\n", keybindingsPath)
179195
fmt.Printf(" Created: %s\n", promptsPath)
180196
fmt.Printf(" Created: %s\n", channelsPath)
181197
fmt.Printf(" Created: %s\n", heartbeatPath)
182198
fmt.Printf(" Created: %s\n", computerUsePath)
183199
fmt.Printf(" Created: %s\n", agentsPath)
200+
fmt.Printf(" Created: %s/\n", skillsDirPath)
184201
if migrated {
185202
fmt.Printf("\n%s Migrated legacy `channels:` block from config.yaml into %s.\n", icons.CheckMarkStyle.Render(icons.CheckMark), channelsPath)
186203
fmt.Printf(" You can now remove the `channels:` block from %s.\n", configPath)
@@ -517,6 +534,15 @@ func createAgentsConfigFile(path string) error {
517534
return config.SaveAgents(path, config.DefaultAgentsConfig())
518535
}
519536

537+
// createSkillsDir creates an empty skills directory. Skills are authored by
538+
// dropping a folder containing SKILL.md into this directory; see docs/skills.md for the format.
539+
func createSkillsDir(dir string) error {
540+
if err := os.MkdirAll(dir, 0755); err != nil {
541+
return fmt.Errorf("failed to create skills directory: %w", err)
542+
}
543+
return nil
544+
}
545+
520546
// createMCPConfigFile creates the MCP configuration YAML file
521547
func createMCPConfigFile(path string) error {
522548
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
@@ -667,6 +693,39 @@ shortcuts:
667693
return os.WriteFile(path, []byte(a2aShortcutsContent), 0644)
668694
}
669695

696+
// createSkillsShortcutsFile creates the Agent Skills shortcuts YAML file
697+
func createSkillsShortcutsFile(path string) error {
698+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
699+
return fmt.Errorf("failed to create shortcuts directory: %w", err)
700+
}
701+
702+
skillsShortcutsContent := `---
703+
# Agent Skills Shortcuts
704+
# Manage Agent Skills from within chat
705+
#
706+
# Usage:
707+
# - /skills list - List discovered skills
708+
# - /skills install <github-url> - Install a skill from a public GitHub repo
709+
# - /skills uninstall <name> - Uninstall a skill by name
710+
711+
shortcuts:
712+
- name: skills
713+
description: "Manage Agent Skills"
714+
command: infer
715+
args:
716+
- skills
717+
subcommands:
718+
- name: list
719+
description: "List discovered skills"
720+
- name: install
721+
description: "Install a skill from a public GitHub repo (usage: <github-url> [--user] [--overwrite])"
722+
- name: uninstall
723+
description: "Uninstall a skill by name (usage: <name> [--user])"
724+
`
725+
726+
return os.WriteFile(path, []byte(skillsShortcutsContent), 0644)
727+
}
728+
670729
// handleMigrations handles the migration logic for the init command
671730
func handleMigrations() {
672731
defaultConfig := config.DefaultConfig()

0 commit comments

Comments
 (0)