skillgen

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 14 Imported by: 0

README

skillgen

Generate agent skill files from a cobra command tree — the way cobra itself generates shell tab completions.

skillgen emits a Markdown file with YAML frontmatter that teaches an AI coding agent (Claude Code or compatible) when the CLI is relevant and how to invoke it. The output is derived from the command tree, so it stays in sync with the CLI instead of drifting like a hand-written skill would.

Install

go get github.com/bueti/skillgen

Output layout

skillgen writes to the agentskills.io specification layout — each skill is its own directory containing a SKILL.md file:

.claude/skills/
└── mytool/
    └── SKILL.md

In split mode, one directory per leaf:

.claude/skills/
├── mytool/           # optional overview
│   └── SKILL.md
├── mytool-build/
│   └── SKILL.md
└── mytool-deploy/
    └── SKILL.md

Quick start

Add a skills subcommand to your root command — same shape as cobra's completion:

package main

import (
    "github.com/bueti/skillgen"
    "github.com/spf13/cobra"
)

func main() {
    root := &cobra.Command{
        Use:   "mytool",
        Short: "Build and deploy mytool services",
    }
    // ... register your subcommands ...

    root.AddCommand(skillgen.NewSkillsCmd(root))
    _ = root.Execute()
}

Then:

mytool skills print                       # write to stdout
mytool skills generate --dir .claude/skills  # write to disk

The generated file is deterministic, so you can check it into git and regenerate in CI.

Programmatic use

gen := skillgen.New(root,
    skillgen.WithFilenamePrefix("acme-"),
    skillgen.WithSkip(func(c *cobra.Command) bool {
        return c.Name() == "debug"
    }),
)
if err := gen.WriteTo("./.claude/skills"); err != nil {
    log.Fatal(err)
}

Enriching generated skills

Any cobra command can carry annotations that shape its output:

Annotation Effect
skill.trigger Accepts a fragment ("deploy, ship") or a full sentence ("Use when…").
skill.description Replaces the generated description entirely.
skill.name Overrides the skill name. Validated against the spec regex.
skill.skip "true" to exclude the command and its subtree.
skill.examples Free-form Markdown appended to the command's body section.
skill.avoid Renders as an Avoid section — tell the agent what not to do.
skill.prefer-over Renders as a Prefer over section — point the agent away from alternatives.
skill.license Populates the spec license frontmatter field (e.g. "Apache-2.0").
skill.compatibility Populates the spec compatibility field — max 500 chars, environment reqs only.
skill.metadata.<key> Prefix pattern that populates the spec metadata: map. Keys emitted sorted.
skill.allowed-tools Only under TargetClaudeCode — populates the allowed-tools frontmatter field.
deploy := &cobra.Command{
    Use:   "deploy <service>",
    Short: "Deploy a built artifact to an environment",
    Annotations: map[string]string{
        skillgen.AnnotationTrigger:  "deploy, promote, ship, or release a service",
        skillgen.AnnotationExamples: "Tip: pair with `--dry-run` to preview before applying.",
    },
}

Options

Option Purpose
WithSplit(mode) SplitNone (default, single skill) or SplitPerLeaf (one per leaf).
WithOverview(bool) In split mode, also emit an overview skill that lists the leaves.
WithTemplate(t) Replace the single-mode body renderer with a text/template.Template.
WithFilenamePrefix(p) Prepend a prefix to every generated filename.
WithSkip(pred) Custom predicate for excluding commands.
WithIncludeBuiltins() Keep cobra's auto-injected help / completion in the output.
WithTarget(t) TargetGeneric (default) or TargetClaudeCode for richer frontmatter.

By default, cobra's built-in help and completion subcommands are filtered out because agents don't need them. User-defined commands with those names at deeper levels are not filtered.

Example output

From the example CLI in ./example:

---
name: "mytool"
description: "Build and deploy mytool services. Use when the user asks to build, deploy, promote, ship, or release a mytool service."
---

# mytool

mytool is a small example CLI used to demonstrate agent-skill generation via the skillgen library.

## When to use

Build and deploy mytool services. Use when the user asks to build, deploy, promote, ship, or release a mytool service.

## Commands

### `mytool build`

Build an artifact of a service

Usage: `mytool build <service> [flags]`

Flags:

- `--push` — push the built image after building
- `--tag` — image tag to apply (default `latest`)

…

Try it:

go run ./example skills print

What's mined from cobra

Before consulting annotations, skillgen extracts free signal straight from the command tree so you don't have to duplicate it:

  • cmd.Aliases → rendered as Aliases: … and, when skill.trigger is unset, auto-derives trigger phrases ("Use when the user asks to deploy, ship, or release").
  • cmd.Deprecated → renders a prominent > **Deprecated:** … callout so agents know to avoid the command and see the replacement.
  • flag.Deprecated → deprecated flags are filtered from the rendered flag list entirely; the author already said the flag shouldn't be suggested.

Annotations augment this mined signal rather than replace it — an alias list alone is often enough to skip writing skill.trigger yourself.

Sibling collapse

When a parent command's visible children all accept exactly the same local flags (name, shorthand, type, required-ness, and usage text), skillgen hoists the flag table up to the parent and omits the per-child repetition:

### `mytool actions`

Shared subcommand flags (apply to every subcommand below):

- `-p, --instances` (required) — target instances
- `-r, --reason` (required) — justification for the action

#### `mytool actions cycle`
Move past a bad node.

#### `mytool actions triage`
Preserve for investigation.
…

For a CLI with six siblings taking the same two flags, that's ~45 lines of duplication gone. A single differing flag across siblings disables the collapse — the agent shouldn't ever see a flag list that's subtly wrong. Split mode renders each leaf standalone, so collapse doesn't apply there.

Targets

Default output is a minimal, interoperable frontmatter (name + description). Opt into a richer host-specific shape with WithTarget:

gen := skillgen.New(root, skillgen.WithTarget(skillgen.TargetClaudeCode))

Under TargetClaudeCode, the skill.allowed-tools annotation populates the Claude Code allowed-tools frontmatter field:

deploy.Annotations[skillgen.AnnotationAllowedTools] = "Bash, Read, Edit"
// → allowed-tools: "Bash, Read, Edit" in the generated frontmatter.

Targets other than the minimal default are additive — they never strip standard keys, only extend.

Linting

skills lint runs two passes. The first walks the cobra command tree for checks keyed by command path (more actionable than a generated file path): missing descriptions, overly short Short text, leaves without trigger hints, deprecated commands without a helpful message, operator-suffix names, deep nesting, sibling-description variance. These rules are prefixed cmd- in the output.

The second pass delegates to skilllint — skillgen generates each SKILL.md in memory and runs skilllint's built-in rules against it: spec hard limits (name format, description length, compatibility length) as errors, and spec soft limits + quality checks (body tokens, body lines, vague descriptions, heading-level jumps, trigger phrasing) as warnings. Errors produce exit code 1; --strict promotes warnings to errors too.

mytool skills lint                              # report, but don't fail on warnings
mytool skills lint --strict                     # CI-friendly: any finding is a hard fail
mytool skills lint --format=json                # machine-readable
mytool skills lint --format=github-actions      # inline annotations on GitHub PRs

A minimal GitHub Actions step:

- name: Lint skills
  run: go run ./your-cli skills lint --strict

Split mode

Default output is a single skill covering the whole CLI. Large tools with many independent subcommands may prefer split mode: one skill per leaf command so agents can load only the one that matches.

gen := skillgen.New(root,
    skillgen.WithSplit(skillgen.SplitPerLeaf),
    skillgen.WithOverview(true), // optional: also emit a root overview
)

Each skill lands in its own directory — mytool deploymytool-deploy/SKILL.md, mytool config setmytool-config-set/SKILL.md. The optional overview skill uses the root name (mytool/SKILL.md) and lists every leaf with a short description.

Rule of thumb: ≤ ~10 commands → single mode; dozens of commands or independent command groups → split mode.

Releasing

Releases are cut by pushing a semver tag to main:

git tag v0.1.0
git push origin v0.1.0

The Release workflow verifies the tag lives on main, runs the test suite, and drafts a GitHub Release with auto-generated notes. proxy.golang.org picks up the tag automatically, so go get github.com/bueti/skillgen@v0.1.0 works immediately.

Pre-release tags (e.g. v0.2.0-rc.1) are marked as pre-releases on GitHub.

Status

  • M1 — single-skill mode, cobra integration, Claude Code-compatible frontmatter ✅
  • M2 — annotations and template override hook ✅
  • M3skills generate / skills print subcommands, stable filenames, validation ✅
  • M4 — split mode (one skill per leaf + optional overview) ✅

See PRD.md for the full design and CHANGELOG.md for per-version changes.

License

MIT

Documentation

Overview

Package skillgen generates agent skill files from a cobra command tree.

The default output is a single Markdown file with YAML frontmatter that describes the whole CLI — name, description, and a section per subcommand. A large CLI can opt into split mode (one skill per leaf command) but that is not yet implemented.

Typical integration mirrors cobra's own completion command:

root.AddCommand(skillgen.NewSkillsCmd(root))

See PRD.md in the repository root for the full design.

Index

Constants

View Source
const (
	// AnnotationTrigger adds trigger phrases to the skill's description.
	//
	// Two forms are accepted:
	//
	//   - Fragment (preferred): "deploy, promote, ship a service". The
	//     library wraps it into "Use when the user asks to <fragment>."
	//   - Full sentence: "Use when the user asks to deploy the service."
	//     (case-insensitive prefix detection). Used as-is.
	//
	// Trailing punctuation is normalized so both forms produce a single
	// terminating period.
	AnnotationTrigger = "skill.trigger"

	// AnnotationDescription replaces the generated description wholesale.
	AnnotationDescription = "skill.description"

	// AnnotationName overrides the skill's name. In single-skill mode, only
	// the root command's annotation is consulted.
	AnnotationName = "skill.name"

	// AnnotationSkip, when set to "true", excludes the command and its subtree.
	AnnotationSkip = "skill.skip"

	// AnnotationExamples appends free-form example text to a command's body section.
	AnnotationExamples = "skill.examples"

	// AnnotationAvoid is free-form Markdown that appears under an "Avoid"
	// heading. Use it to tell the agent what not to do — the single highest-
	// value content in most real skills.
	AnnotationAvoid = "skill.avoid"

	// AnnotationPreferOver is free-form Markdown that appears under a "Prefer
	// over" heading. Use it to point the agent away from alternative tools or
	// commands this one supersedes (e.g. "Use instead of raw `kubectl delete`").
	AnnotationPreferOver = "skill.prefer-over"

	// AnnotationAllowedTools populates the Claude Code `allowed-tools`
	// frontmatter field (comma- or space-separated list of tool names, e.g.
	// "Bash, Read, Edit"). Only emitted under TargetClaudeCode.
	AnnotationAllowedTools = "skill.allowed-tools"

	// AnnotationLicense populates the agentskills.io `license` frontmatter
	// field. Kept short per the spec — either a SPDX name ("Apache-2.0") or a
	// reference to a bundled license file ("LICENSE.txt has complete terms").
	AnnotationLicense = "skill.license"

	// AnnotationCompatibility populates the agentskills.io `compatibility`
	// frontmatter field (max 500 chars). Only use it when the skill has
	// specific environment requirements.
	AnnotationCompatibility = "skill.compatibility"

	// AnnotationMetadataPrefix is the prefix for arbitrary metadata entries
	// that populate the agentskills.io `metadata:` frontmatter map. Set
	// cmd.Annotations["skill.metadata.<key>"] = "<value>" for each entry.
	AnnotationMetadataPrefix = "skill.metadata."
)

Annotation keys an author can set on a cobra.Command to shape its skill output.

Variables

This section is empty.

Functions

func NewSkillsCmd

func NewSkillsCmd(root *cobra.Command, opts ...Option) *cobra.Command

NewSkillsCmd returns a `skills` subcommand authors can attach to their root command, analogous to cobra's `completion`. It exposes:

  • `skills generate --dir <path>` — write skill files to disk.
  • `skills print` — write the skill(s) to stdout.

The same opts apply to both subcommands. The returned command is marked with skill.skip so it never appears in its own generated skill.

Types

type CommandData

type CommandData struct {
	Name       string // leaf name, e.g. "deploy"
	Path       string // full path, e.g. "mytool deploy"
	UseLine    string // cobra's UseLine, e.g. "mytool deploy <service>"
	Short      string
	Long       string
	Example    string
	Aliases    []string // cobra.Command.Aliases
	Deprecated string   // cobra.Command.Deprecated (empty if not deprecated)
	Flags      []FlagData
	Avoid      string // skill.avoid annotation contents
	PreferOver string // skill.prefer-over annotation contents
	Extra      string // skill.examples annotation contents
	Depth      int    // 1 for direct child of root, 2 for grandchild, ...
	HasSubs    bool   // true if this command has at least one visible subcommand

	// SharedChildrenFlags is set on a parent whose visible children all have
	// identical local flag sets. The parent's section emits these once and
	// each child's section skips its local-flags block to avoid duplication.
	SharedChildrenFlags []FlagData
	// SkipFlagsInRender is set on children whose parent owns the shared-flags
	// block — render suppresses the per-child "Flags:" list in that case.
	SkipFlagsInRender bool
}

CommandData describes one cobra command for rendering.

type FlagData

type FlagData struct {
	Name       string
	Shorthand  string
	Type       string
	DefValue   string
	Usage      string
	Required   bool
	Persistent bool
}

FlagData describes one pflag.Flag.

func (FlagData) Ref

func (f FlagData) Ref() string

Ref returns a rendered flag reference, e.g. "--env" or "-n, --name".

type FrontmatterField added in v0.4.0

type FrontmatterField struct {
	Key   string
	Value string
	Map   map[string]string // when non-nil, Value is ignored
}

FrontmatterField is a frontmatter key/value emitted alongside name + description. A scalar field uses Value; a map field (e.g. the spec `metadata:` key) uses Map. Exactly one of Value and Map should be set.

type Generator

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

Generator emits skills for a cobra command tree.

func New

func New(root *cobra.Command, opts ...Option) *Generator

New returns a Generator for the given root command.

func (*Generator) Lint added in v0.3.0

func (g *Generator) Lint() []skilllint.Issue

Lint walks the command tree and lints the resulting skills. Findings combine two passes:

  1. Cobra-tree rules ("cmd-*") that inspect cobra metadata — these produce findings keyed by command path, which is more actionable for authors than the generated skill path.
  2. Spec-compliance rules from the skilllint library, run against each generated skill's bytes. These cover name format, description length, body size, vague descriptions, and so on.

The generator's skip rules (Hidden, skill.skip, WithSkip predicate, cobra builtins) apply in both passes so lint stays in sync with what the generator would actually emit.

func (*Generator) Skills

func (g *Generator) Skills() ([]Skill, error)

Skills returns the generated skills without touching the filesystem.

func (*Generator) WriteTo

func (g *Generator) WriteTo(dir string) error

WriteTo renders skills and writes them into dir following the agentskills.io layout: each skill gets its own subdirectory named after the skill, with a SKILL.md file inside.

dir/
└── mytool/
    └── SKILL.md

type Option

type Option func(*Generator)

Option configures a Generator.

func WithFilenamePrefix

func WithFilenamePrefix(p string) Option

WithFilenamePrefix prepends a prefix to every generated filename.

func WithIncludeBuiltins

func WithIncludeBuiltins() Option

WithIncludeBuiltins keeps cobra's auto-injected `help` and `completion` commands in the output. They are excluded by default because agents don't need them to use the tool.

func WithOverview

func WithOverview(b bool) Option

WithOverview controls whether an overview skill is emitted in split mode. Has no effect in SplitNone.

func WithSkip

func WithSkip(pred func(*cobra.Command) bool) Option

WithSkip installs a predicate that excludes commands (and their subtrees). Runs in addition to Hidden and the skill.skip annotation.

func WithSplit

func WithSplit(m SplitMode) Option

WithSplit sets the output mode. Default is SplitNone.

func WithTarget added in v0.4.0

func WithTarget(t Target) Option

WithTarget selects which host's frontmatter conventions to follow. Default is TargetGeneric (strict name + description).

func WithTemplate

func WithTemplate(t *template.Template) Option

WithTemplate replaces the default body renderer with a text/template. The template receives a TemplateData value.

type Skill

type Skill struct {
	Name        string
	Description string
	Body        string
	// Path is the relative file path within the output directory, e.g.
	// "mytool/SKILL.md". Use filepath.Dir(Path) to get the skill directory.
	Path string
	// Frontmatter is spec-standard and target-specific frontmatter fields
	// emitted after name + description.
	Frontmatter []FrontmatterField
}

Skill is a single generated skill file. The spec layout is a directory named after the skill with a SKILL.md inside; Path is the relative path used by WriteTo (always `<name>/SKILL.md` with the FilenamePrefix applied to the directory name).

func (Skill) Bytes

func (s Skill) Bytes() []byte

Bytes returns the full file contents (frontmatter + body, trailing newline).

func (Skill) Dir added in v0.4.0

func (s Skill) Dir() string

Dir returns the skill directory portion of Path (e.g. "mytool" for "mytool/SKILL.md"), which by spec must match the `name` frontmatter.

type SplitMode

type SplitMode int

SplitMode selects how many skill files are emitted.

const (
	// SplitNone writes a single skill describing the whole CLI (the default).
	SplitNone SplitMode = iota
	// SplitPerLeaf writes one skill per leaf command. Enable WithOverview to
	// additionally emit a single overview skill that lists all leaves.
	SplitPerLeaf
)

type Target added in v0.4.0

type Target int

Target controls which host's conventions the generated frontmatter follows. Hosts vary in which optional fields they support; TargetGeneric stays strict (name + description only) while richer targets opt in to host-specific keys.

const (
	// TargetGeneric emits the minimal, interoperable frontmatter: name + description.
	TargetGeneric Target = iota
	// TargetClaudeCode emits Claude Code-specific keys like `allowed-tools`
	// (from the skill.allowed-tools annotation, comma-separated).
	TargetClaudeCode
)

type TemplateData

type TemplateData struct {
	Name        string
	Description string
	// TriggerClause is the "Use when the user asks to …" sentence alone,
	// separate from Description (which embeds it). Empty when no trigger
	// signal is available — renderers must omit the "When to use" section
	// in that case rather than restating Description.
	TriggerClause string
	Root          CommandData
	Commands      []CommandData // flattened, depth-first, sorted by name at each level; root not included
}

TemplateData is the value passed to a custom template via WithTemplate.

Directories

Path Synopsis
Command mytool is a runnable example of a CLI that exposes an agent skill via skillgen.
Command mytool is a runnable example of a CLI that exposes an agent skill via skillgen.

Jump to

Keyboard shortcuts

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