skill

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package skill loads invokable playbooks ("skills") from Markdown files. A skill is a named, described prompt body the model can invoke via the run_skill tool (or the user via "/<name>"): an "inline" skill folds its body into the turn as a tool result, a "subagent" skill runs in an isolated child loop and returns only its final answer. Project scope wins over global; only names+descriptions enter the cache-stable system-prompt index (see index.go) — bodies load on demand. Discovery scans several conventions (.fairpeer / .agents / .agent / .claude under the project root and the home dir — see config.ConventionDirs) so skills authored for other agent tools migrate in unchanged, and follows symlinks, so a linked skill directory or flat <name>.md is picked up like a real one.

Index

Constants

View Source
const (
	// SkillsDirname is the directory under each root that holds skills.
	SkillsDirname = "skills"
	// SkillFile is the canonical filename inside a directory-layout skill.
	SkillFile = "SKILL.md"
)
View Source
const IndexMaxChars = 4000

IndexMaxChars caps the pinned skills-index block so it can't bloat the cache-stable system-prompt prefix; bodies never enter the prefix. (Some providers currently do not report cache tokens; the prefix stability still helps.)

Variables

This section is empty.

Functions

func ApplyIndex

func ApplyIndex(basePrompt string, skills []Skill) string

ApplyIndex appends the skills index to basePrompt, or returns it unchanged when there are no skills. Only names + descriptions (+ a subagent tag) are listed; bodies load on demand via run_skill.

func BuiltinNames

func BuiltinNames() []string

BuiltinNames returns the built-in skill names, used by callers that wire dedicated subagent tools for the subagent built-ins.

func BuiltinSubagentTools

func BuiltinSubagentTools(store *Store, runner SubagentRunner, profileResolver ...ProfileResolver) []tool.Tool

BuiltinSubagentTools returns top-level wrapper tools for the built-in subagent skills, named after the verb so the model picks them naturally (affordance > prompt rules). Each is skipped when its underlying skill isn't present (e.g. a user disabled it), so the tool set never advertises a phantom skill.

func IsValidName

func IsValidName(name string) bool

IsValidName reports whether name is a usable skill identifier.

func NewInstallSkillTool

func NewInstallSkillTool(store *Store, onInstalled InstalledHook) tool.Tool

NewInstallSkillTool builds the skill-authoring tool. onInstalled may be nil.

func NewReadSkillTool

func NewReadSkillTool(store *Store) tool.Tool

NewReadSkillTool builds a read-only inline-skill loader. Unlike run_skill it stays available in plan mode, so a plan can consult inline playbooks.

func NewRunSkillTool

func NewRunSkillTool(store *Store, runner SubagentRunner, profileResolver ...ProfileResolver) tool.Tool

NewRunSkillTool builds the general skill-invocation tool. runner may be nil (subagent skills then error).

func NewRunSkillToolWithIndex

func NewRunSkillToolWithIndex(store, allStore *Store, runner SubagentRunner, profileResolver ...ProfileResolver) tool.Tool

RunSkillToolWithIndex is the Option pattern entrypoint; see NewRunSkillTool. allStore may be nil (disables the "disabled vs unknown" hint).

func Render

func Render(sk Skill, args string) string

Render builds a skill's invocation text: a header (name, description, source) followed by the body and any arguments. Used directly when a user invokes a skill via "/<name>" (sent as a turn); the run_skill tool wraps the same text in a skill-pin sentinel (see renderInline).

func SetExtraReadTools

func SetExtraReadTools(names []string)

SetExtraReadTools registers additional read-only tool names that subagent skills (explore, research, review, security-review) are allowed to use. Call from boot after plugin tools are registered.

Types

type InstalledHook

type InstalledHook func(name, path string, scope Scope)

InstalledHook fires after install_skill writes a new file, so a host can refresh UI (e.g. a skills sidebar) without a reload. nil is fine.

type Options

type Options struct {
	HomeDir         string
	ProjectRoot     string
	CustomPaths     []string
	ExcludedPaths   []string
	DisabledNames   []string
	MaxDepth        int
	DisableBuiltins bool // suppress shipped built-ins (test-only knob)
	// Stderr is the writer for diagnostic warnings. When nil, defaults to
	// os.Stderr. Set to io.Discard to suppress output (e.g. during model
	// switch inside a bubbletea session).
	Stderr io.Writer
	// StateDir is the directory holding skill_usage.json (cross-session usage
	// tracking for cold-skill retirement). Empty disables usage tracking —
	// Store.Usage() then returns a no-op tracker.
	StateDir string
	// LegacyStatePath is a pre-profile-partition fallback location read when
	// StateDir's file is absent, so usage recorded before the session dir moved
	// isn't lost. Empty = no fallback.
	LegacyStatePath string
}

Options configure a Store. ProjectRoot "" reads only the global + custom scopes. HomeDir "" resolves to the OS home dir (tests point it at a tmpdir).

type PathStatus

type PathStatus string

PathStatus describes a root directory's readability, surfaced by `/skill paths`.

const (
	StatusOK           PathStatus = "ok"
	StatusMissing      PathStatus = "missing"
	StatusNotDirectory PathStatus = "not-directory"
	StatusUnreadable   PathStatus = "unreadable"
)

type ProfileResolver

type ProfileResolver func(sk Skill) *event.Profile

ProfileResolver returns the model/effort profile a subagent skill will use. It is optional; without one, skill frontmatter still supplies display metadata.

type Root

type Root struct {
	Dir      string
	Scope    Scope
	Priority int
	Status   PathStatus
}

Root is one discovery directory with its scope, priority, and status.

type RunAs

type RunAs string

RunAs selects how an invoked skill executes. Inline folds the body into the parent turn; subagent spawns an isolated child loop and returns only the final answer (its tool calls and reasoning never enter the parent context).

const (
	RunInline   RunAs = "inline"
	RunSubagent RunAs = "subagent"
)

type Scope

type Scope string

Scope records where a skill was loaded from. Higher-priority scopes win on a name collision: project > custom > global > builtin.

const (
	ScopeProject Scope = "project"
	ScopeCustom  Scope = "custom"
	ScopeGlobal  Scope = "global"
	ScopeBuiltin Scope = "builtin"
)

type Skill

type Skill struct {
	Name        string // canonical identifier; matches the directory / filename stem
	Description string // one-liner shown in the pinned index
	Body        string // full markdown body (post-frontmatter), loaded eagerly
	Scope       Scope  // where it came from
	Path        string // absolute path to the SKILL.md / <name>.md, or "(builtin)"
	// AllowedTools, when non-empty, scopes a subagent skill's tool registry to
	// these literal tool names (from the `allowed-tools` frontmatter).
	AllowedTools []string
	RunAs        RunAs  // inline | subagent
	Model        string // optional model override for runAs=subagent (frontmatter `model:`)
	Effort       string // optional effort for runAs=subagent (frontmatter `effort:`)
	// Disabled marks a skill the user turned off. It stays in the pinned skills
	// index (so the model knows it exists and can suggest re-enabling) but is
	// not callable via run_skill until re-enabled. Set by callers building the
	// index from the full (unfiltered) store; the filtered store still omits
	// disabled skills entirely, keeping them uncallable.
	Disabled bool
	// ProfileHidden marks a skill the active product profile's whitelist hides
	// (e.g. the dev/coding profile hides office skills), NOT a user choice.
	// Unlike Disabled, a profile-hidden skill is fully OMITTED from the pinned
	// index: the model neither sees it nor suggests re-enabling it, since the
	// user didn't turn it off — switching profile brings it back automatically.
	// This keeps the coding model's prompt free of office-skill descriptions the
	// user never asked to see. It is still uncallable (the live store filters it
	// via the profile-disabled name set). Set by callers (boot) when applying a
	// profile whitelist; mutually exclusive with Disabled for the same skill.
	ProfileHidden bool
	// Cold marks a skill whose last use is older than the configured retirement
	// threshold, so the index tags it [休眠]. Set by callers (boot) from a
	// UsageTracker; built-in skills are never marked cold. Cosmetic only — a
	// cold skill is still callable, just de-emphasized in the prompt index.
	Cold bool
}

Skill is a loaded playbook.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store resolves skills across the configured roots.

func New

func New(opts Options) *Store

New builds a Store. Relative custom paths and a relative project root are made absolute; "~" in a custom path expands to the home dir.

func (*Store) Create

func (s *Store) Create(name string, scope Scope) (string, error)

Create scaffolds a new skill stub at the chosen scope. Refuses to overwrite.

func (*Store) CreateWithContent

func (s *Store) CreateWithContent(name string, scope Scope, content string) (string, error)

CreateWithContent writes caller-supplied file contents as a canonical <name>/SKILL.md skill, refusing to clobber an existing directory-layout or legacy flat skill of the same name. Returns the written path.

func (*Store) HasProjectScope

func (s *Store) HasProjectScope() bool

HasProjectScope reports whether the store was configured with a project root.

func (*Store) List

func (s *Store) List() []Skill

List returns every discoverable skill, deduped by name (first/highest-priority root wins) with built-ins folded in last, sorted by name so the prefix index stays stable and cacheable.

func (*Store) Read

func (s *Store) Read(name string) (Skill, bool)

Read resolves one skill by name, scanning the roots in priority order then the built-ins. ok is false when no such skill exists or the file is unreadable.

func (*Store) Roots

func (s *Store) Roots() []Root

Roots exposes the discovery directories with their status for `/skill paths`.

func (*Store) Usage

func (s *Store) Usage() *UsageTracker

Usage returns the skill usage tracker backed by this store's StateDir, or a no-op tracker when StateDir is empty. boot.go uses it to detect cold (long- unused) skills for retirement from the prompt index.

type SubagentRunOptions

type SubagentRunOptions struct {
	ContinueFrom string
	ForkFrom     string
}

SubagentRunner runs a runAs=subagent skill: it spawns an isolated child loop with the skill body as system prompt and `task` as its only input, returning the final answer. boot wires this over the agent's sub-agent machinery; nil means subagent skills are unavailable in this session (they error rather than silently inlining, which would lose the isolation the author asked for).

type SubagentRunner

type SubagentRunner func(ctx context.Context, sk Skill, task string, opts SubagentRunOptions) (string, error)

type UsageTracker

type UsageTracker struct {
	// contains filtered or unexported fields
}

UsageTracker records the last-used timestamp of each invoked skill, so long-unused skills can be retired from the prompt index (mirroring memory's dormant/archive decay). It is best-effort throughout: a missing or corrupt state file is not an error — the tracker simply reports "never used", so no skill is retired until real usage data accumulates.

The zero value (path == "") is a no-op: Record/LastUsed/ColdSkillNames all return without touching disk, so callers that don't pass a StateDir (tests, older code paths) are unaffected.

func NewUsageTracker

func NewUsageTracker(stateDir string, legacyPath ...string) *UsageTracker

NewUsageTracker builds a tracker backed by <stateDir>/skill_usage.json. A "" stateDir returns a no-op tracker (all methods are safe but do nothing). legacyPath, when non-empty, is read as a fallback when the primary file is absent — used to recover pre-profile-partition skill usage (the old file sat at <userDir>/skill_usage.json; sessions then moved under <profile>/sessions, shifting the state dir by one level).

func (*UsageTracker) ColdSkillNames

func (u *UsageTracker) ColdSkillNames(threshold time.Duration, includeNeverUsed bool, known []string) []string

ColdSkillNames returns the names of skills whose last use is older than threshold (or that have never been recorded, when includeNeverUsed is true). A skill used within the threshold is excluded. The never-used policy is configurable: built-in skills (explore/research/…) should not be retired just because they haven't been called yet, so callers pass includeNeverUsed only for user-authored skills.

func (*UsageTracker) LastUsed

func (u *UsageTracker) LastUsed(name string) time.Time

LastUsed returns the most recent invocation time of name, or the zero Time when the skill has never been recorded (or tracking is disabled).

func (*UsageTracker) Record

func (u *UsageTracker) Record(name string)

Record logs one invocation of name: bumps the count and stamps LastUsed to now, then writes the file. Best-effort — a write failure is swallowed because losing one usage sample must never break the skill call that triggered it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL