skill

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package skill ships fft's agent skill: the document an AI coding assistant reads before it drives fft on somebody's behalf.

The skill is markdown, compiled into the binary, and `fft skill install` copies it onto disk. Compiled in rather than fetched, because the skill a user installs must be the one that describes the fft they are running — and the drift spec in cmd/fft/skill_drift_test.go resolves every fft invocation in it against the real command tree, so a renamed flag fails the build instead of quietly making the skill wrong. A skill that lies is worse than no skill: an agent without one asks, and an agent with a wrong one acts.

It is documentation, not configuration and not a secret. It is written 0644 under a 0755 directory — unlike everything else fft writes — because the user, their editor and their agent all read it, and 0600 is a credential's mode.

Index

Constants

View Source
const Doc = "SKILL.md"

Doc is the entry point: the file an agent always loads, and the file whose presence in a directory is what identifies that directory as this skill's.

View Source
const ManifestName = ".fft-skill.json"

ManifestName is the file in the skill's directory that records what fft wrote there.

It is what lets an upgrade tell fft's own old text from the user's edit. Every release that changes a word of the skill changes the bytes of an installed copy against the new one, and comparing bytes alone cannot say whose change that was — so without a record, every upgrade asked for --force, and an agent, which has no terminal to be asked on, could never take one.

A sidecar rather than more frontmatter: it records every file, not only SKILL.md, and it keeps a list of hashes out of the one block an agent reads before anything else. A dotfile, so that it is not mistaken for part of the skill by anything listing the directory. It is fft's: never a stray, never a row in the plan, and not evidence that a directory is somebody else's.

View Source
const Name = "fft"

Name is the skill's name, its directory's name, and the name in SKILL.md's frontmatter. All three have to agree, and a spec asserts that they do.

Variables

View Source
var ErrMalformed = errors.New("malformed skill document")

ErrMalformed reports an installed SKILL.md whose frontmatter does not parse.

View Source
var ErrNotSkill = errors.New("not an fft skill directory")

ErrNotSkill reports a directory that fft did not put there.

It is the whole reason --force is safe to offer. --force means "replace my copy of the skill", and the directory it replaces can come from --dir — from a shell, where a typo is one keystroke. A directory with no SKILL.md in it is somebody else's, and fft will not remove a single file from it.

Functions

func Current added in v0.9.0

func Current(m Meta) bool

Current reports whether an installed skill describes the fft that is running.

A build that did not come from a release tag has no version to hold the skill to, and counts every skill as current — the same rule, for the same reason, as the update check: a developer running what they are working on does not need telling to reinstall it. A skill with no stamp at all predates stamping, and is not current: that is the install that most needs the hint.

func Document

func Document() string

Document is the whole of SKILL.md, frontmatter and version stamp included: what `fft skill show` prints, what `fft skill install` writes, and what an agent that is not Claude Code wants appended to its own context file.

func FS

func FS() fs.FS

FS is the skill's tree as it is written in the repository: SKILL.md without the version stamp that installing adds. The drift spec and the docs generator read it, and neither has any use for which fft built the binary they run in — the generated guide pages would otherwise change on every release. What is written to disk, and what `fft skill show` prints, is Document.

func ProjectDir

func ProjectDir() (string, error)

ProjectDir is ./.claude/skills: the skill for one project, which Claude Code prefers over the personal one when it is working in that directory.

func UserDir

func UserDir() (string, error)

UserDir is ~/.claude/skills: where Claude Code looks for a personal skill.

XDG_CONFIG_HOME is deliberately not consulted, unlike [config.DefaultPath]. That path is fft's to choose; this one is Claude Code's, and honouring XDG here would put the skill somewhere nothing reads it.

Types

type Change

type Change struct {
	// File is the path within the skill: "SKILL.md", "references/recipes.md".
	File string `json:"file" yaml:"file"`

	Status Status `json:"status" yaml:"status"`
}

Change is what installing does to one file. It carries its own tags because it is rendered straight to the user under -o json.

type Meta

type Meta struct {
	Name        string            `json:"name" yaml:"name"`
	Description string            `json:"description" yaml:"description"`
	Metadata    map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}

Meta is SKILL.md's YAML frontmatter: the two fields the skill format requires, and the only two an agent reads before deciding whether to open the file at all. A description that does not say when to use the skill is a skill that is never used.

Metadata is the format's own free-form map, and the one place fft writes into the frontmatter: the version of fft that installed the skill. See Meta.Version.

func Installed added in v0.9.0

func Installed(root string) (Meta, bool, error)

Installed reads the frontmatter of the skill installed under root — the SKILL.md in root/fft — and reports false when there is none there.

A SKILL.md whose frontmatter does not parse is ErrMalformed rather than an unversioned skill: fft never wrote one like that, so it is somebody's edit, and calling it "from an older fft" would send them to an install that refuses to touch it. Any other error is the file being unreadable, and is returned as the filesystem gave it, path and all.

func Parse

func Parse(doc string) (Meta, string, error)

Parse splits SKILL.md into its frontmatter and the body below it.

func (Meta) Version added in v0.9.0

func (m Meta) Version() string

Version is the fft that wrote this skill, or "" for one written before fft stamped its skills at all.

type Plan

type Plan struct {
	// Dir is the skill's own directory — <root>/fft — absolute, so that what is
	// printed is a path the user can act on.
	Dir string

	Files []Change
}

Plan is what installing into a root directory would do. Building one reads; it writes nothing, so the user can be asked before anything happens to their disk.

func NewPlan

func NewPlan(root string) (Plan, error)

NewPlan works out what installing the skill under root would do.

It refuses a directory that exists and holds no SKILL.md: see ErrNotSkill.

func (Plan) Apply

func (p Plan) Apply() (Plan, error)

Apply writes the skill. By the time it is called, whoever called it has already decided about Plan.Pending — so it overwrites and prunes without asking.

It removes exactly the files the plan named — plus the directories that removing them emptied, which held nothing to consent to. That is the invariant: every file fft deletes appeared in the plan, and every one of them but fft's own litter and fft's own untouched old files was shown to the user and agreed to (Plan.Pending is where that distinction is drawn, and why). So an install that reports no changes has removed and replaced nothing of anybody's. The one file it may still write is its own record, ManifestName, when that is missing.

An UNCHANGED file is not rewritten. Re-installing on every run of a script must not churn the mtime of a file an editor or a watcher is holding open.

func (Plan) Pending

func (p Plan) Pending() []Change

Pending are the changes that need the user's blessing: a file of theirs to be overwritten, or one to be removed. Everything else — a new file, a file already correct, a file an older fft wrote and nobody has touched since — needs nobody's permission.

fft's own crash litter is not the user's file, and asking them to consent to the removal of something they have never heard of and did not write teaches them only to say yes without reading. It is still in the plan, and still reported when it goes.

type Status

type Status string

The states a file of the skill can be in, on the way to being installed.

The distinction that earns its keep is UNCHANGED against CONFLICT. Installing twice must be silent and must ask nothing — an agent, or a provisioning script, will do it on every run — while a file the *user* has edited is a decision only they can make.

Comparing the bytes tells those two apart, and it cannot tell apart the two ways a file comes to differ: fft's own text from an older release, and the user's edit. Every release that changes a word of the skill makes an untouched install look edited — and if that asked for --force, a notice saying "run fft skill install" would send an agent, with no terminal to be asked on, straight into a refusal. So fft keeps a record of what it wrote (ManifestName), and knows every text it shipped before it kept one ([legacy]). A file that matches either is OUTDATED, or OBSOLETE if fft no longer ships it, and changes without a question. Anything else that differs is still CONFLICT or STALE.

const (
	StatusNew       Status = "NEW"       // nothing is there
	StatusUnchanged Status = "UNCHANGED" // already byte-for-byte what fft ships
	StatusOutdated  Status = "OUTDATED"  // what an older fft wrote, untouched since; replaced without asking
	StatusConflict  Status = "CONFLICT"  // something else is there; --force replaces it
	StatusObsolete  Status = "OBSOLETE"  // an older fft wrote this and fft no longer ships it; removed without asking
	StatusStale     Status = "STALE"     // not fft's, or edited since; --force removes it
	StatusWritten   Status = "WRITTEN"   // Apply created it
	StatusUpdated   Status = "UPDATED"   // Apply restamped an OUTDATED
	StatusReplaced  Status = "REPLACED"  // Apply overwrote a CONFLICT
	StatusRemoved   Status = "REMOVED"   // Apply pruned a STALE
)

Jump to

Keyboard shortcuts

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