Documentation
¶
Overview ¶
Package skill loads and parses Agent Skills ("SKILL.md") files — the unified loader shared by exegesis and skillsaw. A skill is a directory containing a SKILL.md in the Anthropic Agent Skills format: YAML frontmatter (name and description, optionally other keys) delimited by "---" lines, followed by a Markdown body. Parsing is pure once the bytes are read; only Load and the Discover functions touch the filesystem.
Index ¶
Constants ¶
const ( MetadataKey = "metadata" LineageKey = "lineage" )
Frontmatter keys the lineage declaration lives under.
**Nested under `metadata`, not top-level**, because speclint's allowlist is the agentskills.io published set and its own rule is that anything outside the spec belongs under `metadata`. A top-level `lineage:` would be a second deliberate deviation beside `tags`, and the value of "one deviation, for the key 163 corpora already use" is that there is one.
const FileName = "SKILL.md"
FileName is the required filename inside a skill directory.
const LineagePath = MetadataKey + "." + LineageKey
LineagePath is the dotted path a diagnostic names, so a reader is sent to the right line rather than to a top-level key that would be rejected.
Variables ¶
This section is empty.
Functions ¶
func DefaultRoots ¶
func DefaultRoots() []string
DefaultRoots returns the runtime-neutral directories a multi-root discovery scans. The darwin source hard-codes .claude/skills, but runtime neutrality requires covering every skills-compatible runtime.
func Discover ¶
Discover returns every immediate subdirectory of tree that contains a SKILL.md, sorted by name. It errors if tree cannot be read. This is the single-tree walk the exegesis gates iterate over.
func DiscoverRoots ¶
DiscoverRoots returns every skill directory under any of roots (relative to base), skipping roots absent in this environment and deduplicating. This is the multi-runtime scan skillsaw's "--all" performs.
func Slug ¶
Slug normalizes a string to the Agent Skills slug form: lowercase, every run of non-[a-z0-9] becomes one hyphen, leading and trailing hyphens are trimmed, and the result is capped at 64 runes; an empty result becomes "skill". It is idempotent: Slug(Slug(x)) == Slug(x).
It does not NFKD-fold accents (é stays a separator, not "e"), avoiding an x/text dependency; skill names are ASCII kebab-case in practice.
Types ¶
type Lineage ¶ added in v0.23.0
type Lineage string
Lineage is how a skill's content came to exist: distilled from a source text, or authored directly.
**It is declared, never derived**, and the distinction is the whole point. Content cannot tell you a skill's lineage: a hand-written skill has no RIA segments because it never claimed the format, and a book-derived one that has lost its segments is malformed — the two look identical from the outside and want opposite treatment. So a predicate over the current shape would answer the wrong question, however carefully it were tuned.
This is the opposite epistemics from the executes-anything predicate skillsaw and adh use to scope their boundary checks, and the resemblance is a trap. That one *must* be derived, because nobody declares whether an artifact runs commands. This one *must not* be, because the document is the only witness to its own origin. A shared mechanism would force one of the two to be wrong.
Closed and typed, unlike finding.Category, which stays a bare string because its drift is cosmetic and nothing branches on it. A lineage typo changes **which checks run**, so where the kernel owns the rules a value selects, it owns the value's vocabulary.
const ( // LineageUnset is the zero value: the skill declares nothing. // // **It is treated as strictly as a declared book lineage, and that is deliberate.** // The tempting reading is "undeclared means hand-written, so skip the format // checks", and it is the reading to refuse: it hands every skill an escape from the // RIA-TV++ contract by omission, which is the same hole as a book skill shedding // headings until it no longer looks like one. A contract you leave by saying // nothing is not a contract. // // The cost is honest and bounded: a corpus predating this field keeps its // diagnostics until each hand-written skill adds one line. That is a migration // rather than a false positive, which is the difference this field buys. LineageUnset Lineage = "" // BookDerived is distilled from a source text, so the RIA-TV++ segments are the // contract it was produced against. BookDerived Lineage = "book-derived" // HandWritten is authored directly. It never claimed the RIA-TV++ format, so // grading it against those segments reports a defect in a format the document does // not use. HandWritten Lineage = "hand-written" )
Lineage values, closed. A skill declares one; nothing infers it.
func ParseLineage ¶ added in v0.23.0
ParseLineage reads a declared lineage, reporting whether the value is recognised.
**An unrecognised value is not silently accepted and not silently rejected.** ok is false and the returned Lineage is LineageUnset, so a caller gets the strictest treatment *and* can report the value it did not understand. That pairing is the point: `hand-written` must not quietly buy lenient treatment, and it must not fail closed without saying why either.
There are no aliases. A typo and a newer vocabulary want the same handling, and an alias table manufactures synonyms that never shrink; vocabulary evolution belongs to a format version.
func (Lineage) ClaimsRIASegments ¶ added in v0.23.0
ClaimsRIASegments reports whether a skill of this lineage was produced against the RIA-TV++ segment contract, and so should be graded on it.
Only a declared hand-written lineage is exempt. Unset and unrecognised are both graded, which is what stops the exemption from being reachable by omission or by typo.
type Skill ¶
type Skill struct {
Dir string // skill directory
Path string // <Dir>/SKILL.md
Name string // frontmatter name
Description string // frontmatter description
FrontmatterKeys []string // top-level frontmatter keys, sorted (for lint)
Frontmatter string // raw YAML frontmatter block (between the --- lines)
Body string // markdown body after the frontmatter
// Lineage is the recognised `lineage:` declaration, LineageUnset when absent *or*
// unrecognised, so a caller that ignores LineageRaw gets the strictest treatment.
//
// LineageRaw is the value as written, kept so an unrecognised one can be reported
// verbatim rather than described. Unrecognised is therefore
// `LineageRaw != "" && Lineage == LineageUnset` — derived rather than a second
// boolean, because a bool's zero value would say "unrecognised" on the
// FrontmatterErr path, where nothing was parsed and there is nothing to report.
// That is the trap the note on FrontmatterErr describes, one field over.
//
// A bad value is not a load failure, for the same reason FrontmatterErr is not: one
// malformed skill must not stop a caller walking a tree.
Lineage Lineage
LineageRaw string
Raw string // full file contents
Bytes int // byte size of Raw
// FrontmatterErr is the error from parsing the YAML frontmatter, or nil when it
// parsed. When it is non-nil, Name, Description and FrontmatterKeys are zero
// because nothing could be read out of the block — they are not evidence that
// those fields are absent from the file. A linter that reports "description is
// empty" without checking this sends the reader to the wrong line.
//
// The underlying YAML error is kept verbatim: it locates the offending line and
// column, which is the part a reader needs, and nothing classifies on it.
FrontmatterErr error
}
Skill is a parsed SKILL.md and its location.
func Load ¶
Load reads and parses <dir>/SKILL.md. A missing SKILL.md is translated at this boundary to an errs.Error with code ENOTFOUND (classify it via errs.ErrorCode); the external os.ErrNotExist is not propagated. Any other read error is wrapped with Op "skill.Load".
func (*Skill) Hash ¶
Hash is the content identity of the skill: identity.Hash of its raw bytes. It is byte-identical to the hash exegesis and skillsaw pin manifests with.
func (*Skill) UnrecognisedLineage ¶ added in v0.23.0
UnrecognisedLineage reports whether a skill declared a lineage that is not in the vocabulary, and returns it verbatim so a diagnostic can quote what was written.
A caller that never asks still behaves safely -- Lineage is LineageUnset in that case, which is graded strictly. What asking adds is the report, and the report is what stops a typo from being indistinguishable from silence.