| package | github.com/genai-io/san/internal/skill |
|---|---|
| layer | feature |
Loads markdown-defined skills from user / project / plugin scopes, tracks
their enable state, and renders the active-skills directory that the
harness attaches to user messages via the skills-directory reminder
(see concepts/harness-channels.md).
A skill is a markdown file (with YAML frontmatter) that the model can be
made aware of (active), made invocable via slash command (enabled), or
hidden (disabled). This package:
- Discovers skills across six scopes
(
~/.claude/skills/,~/.san/plugins/*/skills/,~/.san/skills/,.claude/skills/,.san/plugins/*/skills/,.san/skills/) with project overriding user overriding Claude-compat. - Persists per-skill state in user / project state stores.
- Renders the active-skills block consumed by the
skills-directoryreminder provider ininternal/app.
For the broader extension model see
concepts/extension-model.md. A
how-to-author-a-skill guide is tracked in notes/tech-debt.md.
The package exposes *Registry directly. Skill consumers each use a
different subset of the registry surface — the TUI selector goes wide
(List / GetStatesAt / SetState), the slash-command flow uses
narrow lookups (Get / FindByPartialName / GetSkillInvocationPrompt),
the system-prompt builder uses one method (PromptSection), and the
session recorder attaches an observer (SetStateChangeObserver). No
shared narrow surface ⇒ no producer-side role interface earns its keep.
package skill
// Registry is an opaque handle to the loaded skill set plus per-scope
// enabled-state stores. The type is exported so callers can hold and
// pass *Registry values; all fields are unexported.
type Registry struct { /* internal fields */ }
// Query
func (r *Registry) Get(name string) (*Skill, bool)
func (r *Registry) FindByPartialName(name string) *Skill
func (r *Registry) List() []*Skill
func (r *Registry) GetEnabled() []*Skill
func (r *Registry) GetActive() []*Skill
func (r *Registry) Count() int
func (r *Registry) IsEnabled(name string) bool
// State (used by the TUI selector)
func (r *Registry) SetState(name string, state SkillState, userLevel bool) error
func (r *Registry) GetStatesAt(userLevel bool) map[string]SkillState
func (r *Registry) SetEnabled(name string, enabled bool, userLevel bool) error
func (r *Registry) GetDisabledAt(userLevel bool) map[string]bool
// Rendering (consumed by the skills-directory reminder provider)
func (r *Registry) PromptSection() string
func (r *Registry) GetSkillInvocationPrompt(name string) string
// Recorder observer (used by the session recorder)
func (r *Registry) SetStateChangeObserver(cb StateChangeObserver)
// Package-level access
func Initialize(opts Options)
func Default() *Registry
func DefaultIfInit() *Registry // nil if pre-Initialize
func SetDefaultRegistry(r *Registry) // test-only
func ResetDefaultRegistry() // test-only
// Skill, SkillState, SkillScope — see types.go for value types.Registry(registry.go) is the only implementation, holding:skills []*Skill— loaded byloaderuserStore,projectStore— JSON-backed persistence of per-skillSkillStatecwd— for project-scope resolution
loader.gowalks the six scopes in priority order, parsingSKILL.mdfrontmatter and bundled resource directories (scripts//references//assets/).- State (
disable→enable→active→disable) cycles viaSkillState.NextState(); the TUI's/skillflow uses this. - Active skills render through
PromptSection()and are delivered to the model via theskills-directoryreminder attached to each user message; enable-only skills surface as slash commands but stay out of the model's awareness entirely.
- Construction:
Initialize(Options{CWD})at app startup, beforeinternal/commandbuilds its slash-command list. Singleton thereafter. - Mutation:
SetEnabledwrites through to user or project store immediately; in-memoryskillsslice is updated in place. - Plugin sources are added post-init by
internal/pluginviaAddPluginSkills. - Concurrency: registry mutations are mutex-guarded; reads are RWMutex-locked.
internal/skill/skill_test.go — loader, state cycling,
scope priority, prompt rendering.
internal/skill/lazy_loading_test.go — verifies content stays on disk
until GetInstructions().
- Code:
internal/skill/ - Concepts:
concepts/extension-model.md,concepts/harness-channels.md - Related:
packages/command.md(slash-command surface),packages/plugin.md(plugin-scoped skills),packages/reminder.md(the channel that deliversPromptSection) - Layer:
feature(seereference/dependency-rules.md)