Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ test isolation.
Every platform is one row in `Specs` (`internal/platform/spec.go`) driving a
single generic `Adapter` — adding a platform means a `Spec` row plus a
`Type` constant, never a per-platform Go file. Installing is a directory
copy *today*, but pure-copy is **not** an invariant: a `Spec` may grow
declarative post-install behavior (companion files, per-platform
destination overrides) where a platform's loader demands it. Any such
copy filtered by the skill's own `install.exclude` ([#103]) *today*, but
pure-copy is **not** an invariant: a `Spec` may grow declarative
post-install behavior (companion files, per-platform destination
overrides) where a platform's loader demands it. Any such
behavior belongs in the spec table as data, not as a hand-written
per-platform code path, and `Uninstall` must reverse whatever `Install`
produced. Open work depending on this: [#99], [#101], [#47].
Expand Down Expand Up @@ -155,6 +156,8 @@ metadata:
type: agent
platform: codex-cli
date: "2025-07-15T10:30:00Z"
install: # optional; shapes the platform copy only
exclude: [eval, fixtures/*] # kept in the registry, not installed
---

## Overview
Expand Down Expand Up @@ -199,6 +202,18 @@ hand-edited tags still match. `skill list`, `skill install`, and
semantics for all three; on install/uninstall the filter is mutually
exclusive with positional names.

**`install.exclude`** ([#103]) lists `path.Match` globs, relative to the skill
directory, that `skill install` leaves out of the platform copy (the registry
keeps everything). A pattern matches a path or any leading directory of it,
so a bare directory name excludes its subtree; `**` is rejected; `SKILL.md`
is never excluded (so `*` is legal). Matching lives in `skill.MatchExclude`
(`internal/skill/folder.go`) and is applied by `platform.copyDir` via
`InstallOptions.Exclude`; `skill validate` errors on malformed patterns and
warns on no-match / excluded-but-referenced files; `skill install` refuses a
skill whose patterns fail validation; `skill diff` reports a changed list
under `install.exclude`. Other keys under `install:` pass through via
`InstallConfig.Extra`.

**Unmodeled keys pass through.** The fields above are the keys skern models;
any other key — top-level, `metadata.*`, or nested in `metadata.author` /
a `modified-by` entry — lands in the matching `Extra` inline map
Expand Down Expand Up @@ -327,10 +342,6 @@ request.

Everything planned is a tracked issue; this list is a map, not a commitment.

**Correctness and ergonomics (near-term)**

- [#103] — exclude companion directories (eval corpora, fixtures) from install

**Adapter model** — all three need the declarative-hook mechanism from design
decision 3; settle the mechanism once rather than special-casing each.

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
swallowed). `uninstall --tag` narrows the group to what is actually
installed on the platform and skips the rest. `--enforce-budget` counts
the resolved group. ([#102])
- **`install.exclude` frontmatter — keep companion files out of platform
copies.** A skill can now list `path.Match` globs (relative to its
directory) under `install.exclude` for assets that belong in the
registry but not in an agent's context — evaluation corpora, fixtures,
scratch. `skern skill install` skips them; the registry keeps everything.
A bare directory name excludes its subtree (`eval`), `fixtures/*` its
children, `*.draft.md` top-level files by suffix; `**` is rejected;
`SKILL.md` is never excluded (so `*` means "everything else").
`skill validate` errors on malformed, absolute, `..`, or `**` patterns and
warns when a pattern matches nothing or excludes a file the body
references; `skill install` refuses a skill whose patterns fail
validation; `skill diff` reports a changed exclude list. Platform adapters
take an `InstallOptions` argument on `Install`. Compatibility: `install`
is now a modeled frontmatter key — a top-level `install:` that is not a
mapping fails to parse, and other keys under `install:` pass through like
any unmodeled key. ([#103])

### Changed

Expand Down Expand Up @@ -91,6 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#98]: https://github.com/devrimcavusoglu/skern/pull/98
[#100]: https://github.com/devrimcavusoglu/skern/issues/100
[#102]: https://github.com/devrimcavusoglu/skern/issues/102
[#103]: https://github.com/devrimcavusoglu/skern/issues/103
[#104]: https://github.com/devrimcavusoglu/skern/issues/104

## [v0.3.1] — 2026-05-13
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/platform-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Platform adapters bridge the skill registry with agent runtimes. Each adapter kn

When you run `skern skill install`, the adapter:

1. Reads the `SKILL.md` from the skern registry
1. Reads the skill from the skern registry (its `SKILL.md` and companion files)
2. Creates the platform-specific skill directory
3. Copies the `SKILL.md` into the target location
3. Copies the skill directory into the target location — minus any paths the skill's frontmatter lists under [`install.exclude`](/concepts/skill-format#install-time-exclusions) (eval corpora, fixtures, scratch), which stay in the registry only
4. The agent runtime discovers the skill on its next invocation

## Supported Platforms
Expand Down
24 changes: 23 additions & 1 deletion docs/concepts/skill-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The main technique or pattern (before/after for techniques).
| `metadata.author.platform` | No | Platform name (e.g. `claude-code`) — used when `type: agent` |
| `metadata.version` | No | Semantic version (e.g. `1.0.0`); defaults to `0.0.1` on `skill create` |
| `metadata.modified-by` | No | Append-only modification history (set via `skern skill edit --modified-by`) |
| `install.exclude` | No | Glob patterns (relative to the skill directory) for files and directories that stay in the registry but are **not** copied on `skern skill install`. See [Install-time exclusions](#install-time-exclusions). |

`name` and `description` are the only hard requirements. The rest help discovery, validation, and provenance.

Expand Down Expand Up @@ -98,10 +99,31 @@ my-skill/
└── template.json
```

When a skill is installed to a platform, the **entire directory** is copied. The `scripts/` directory is language-agnostic — skills can include Python, shell, JavaScript, or any other scripts. The agent decides which language is appropriate.
When a skill is installed to a platform, the directory is copied — minus anything the skill's `install.exclude` rules out (below). The `scripts/` directory is language-agnostic — skills can include Python, shell, JavaScript, or any other scripts. The agent decides which language is appropriate.

`skern skill validate <name>` checks that files referenced in the body (via backticks or markdown links) actually exist in the skill directory. Missing references produce **warnings**, not errors. `skern skill show <name>` lists every file bundled with the skill.

### Install-time exclusions

A skill directory often carries assets that belong in the registry but not in a platform's skill directory — evaluation corpora, fixtures, development scratch. Because host agents load or index everything under an installed skill, that material is dead weight in the agent's context on every project. The skill author lists what to leave behind in frontmatter:

```yaml
install:
exclude:
- eval # a directory: excludes eval/ and everything beneath it
- fixtures/* # direct children of fixtures/ (and their subtrees)
- "*.draft.md" # top-level files by suffix (globs do not cross "/")
```

Rules:

- Patterns are slash-separated paths relative to the skill directory, using Go `path.Match` syntax (`*`, `?`, `[…]`; `\` escapes a metacharacter). `*` does not cross `/`, and `**` is **not** supported (it is rejected, not silently treated as `*`); a pattern matches a path when it matches the full relative path **or any leading directory of it**, which is what makes a bare directory name exclude its whole subtree. A trailing `/` or leading `./` is ignored. `exclude: eval` (a bare string) is accepted as shorthand for a one-element list.
- `SKILL.md` is never excluded, whatever the patterns say — so `*` or `*.md` are legal and mean "everything except `SKILL.md`". Naming `SKILL.md` literally, absolute paths, `..` segments, `**`, and malformed globs are validation **errors**, and `skern skill install` refuses a skill whose patterns fail validation rather than silently copying what the author meant to leave out.
- The registry always keeps the full directory — `skill create --from-template` and `skill import` copy everything, and `skill show` lists every file. Only the copy made by `skern skill install` is trimmed. (A pattern such as `fixtures/*` prunes the directory's contents; the now-empty `fixtures/` itself is still created.)
- `skern skill validate` **warns** when a pattern matches no file in the skill directory, and when a file the body references (`` `references/guide.md` ``) is excluded — it would exist in the registry but be missing from every installed copy.
- `skern skill diff` compares manifests (frontmatter and body), not file trees, so excluded files never show up as drift — but a change to the `install.exclude` list itself *is* reported (`install.exclude`), since it changes what the next install copies.
- `install` is a skern-modeled key: other keys under it (`install.mode`, …) round-trip like any other unmodeled key, but a top-level `install:` that is not a mapping (e.g. `install: pip install foo`) is a parse error.

## Creating Skills

Use `skern skill create` to scaffold a new skill:
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ Instead of names, select a group with the same filters [`skill list`](#skern-ski

The response includes a top-level `capacity` block reporting the platform's installed-skill count after the operation, the threshold for that scope, and remaining headroom.

The whole skill directory is copied except paths the skill's frontmatter lists under `install.exclude` — see [Skill Format › Install-time exclusions](/concepts/skill-format#install-time-exclusions). The registry copy is never trimmed.

**Flags:**

| Flag | Default | Description |
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ If `allowed-tools` is set, no entry may be empty.

`metadata.version` must be a valid semver string (`MAJOR.MINOR.PATCH`).

### Install Exclusions

Every `install.exclude` entry must be a non-empty, relative, well-formed glob (`path.Match` syntax) with no `..` or `**` segment, and must not name `SKILL.md` literally (wildcards that merely also match it are fine — `SKILL.md` is never excluded). With more than one entry the field is reported as `install.exclude[N]`. `skern skill install` refuses a skill whose patterns fail these checks. See [Skill Format › Install-time exclusions](/concepts/skill-format#install-time-exclusions).

## Warnings

### Folder Integrity
Expand All @@ -50,6 +54,10 @@ When the body references files (backtick-enclosed paths like `` `scripts/run.py`

Inline code spans and URLs are excluded from this check (no false positives from `` `--flag` `` or `https://…`).

### Install Exclusions

Two warnings relate to `install.exclude`: a pattern that matches no file in the skill directory (usually a typo, or stale after a rename), and a body-referenced file that an exclude pattern would drop — it exists in the registry but would be missing from every installed copy.

## Hints

Stylistic suggestions adapted from [writing-skills](/writing-skills). They never block creation or validation.
Expand Down
9 changes: 9 additions & 0 deletions internal/cli/skill_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,18 @@ func compareSkills(a *skill.Skill, nameA, sourceA string, b *skill.Skill, nameB,
fields = append(fields, output.FieldDiff{Field: "modified-by", Left: modA, Right: modB})
}

// install.exclude decides what an install copies; a change here is the
// signal to reinstall, so it must show up.
exA := strings.Join(a.Install.Exclude, ", ")
exB := strings.Join(b.Install.Exclude, ", ")
if exA != exB {
fields = append(fields, output.FieldDiff{Field: "install.exclude", Left: exA, Right: exB})
}

// Unmodeled keys round-trip through skern (see skill.Skill.Extra), so
// they are part of what an install or edit can change — diff them too.
fields = append(fields, extraDiffs("", a.Extra, b.Extra)...)
fields = append(fields, extraDiffs("install.", a.Install.Extra, b.Install.Extra)...)
fields = append(fields, extraDiffs("metadata.", a.Metadata.Extra, b.Metadata.Extra)...)
fields = append(fields, extraDiffs("author.", a.Metadata.Author.Extra, b.Metadata.Author.Extra)...)

Expand Down
17 changes: 14 additions & 3 deletions internal/cli/skill_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/devrimcavusoglu/skern/internal/output"
"github.com/devrimcavusoglu/skern/internal/platform"
"github.com/devrimcavusoglu/skern/internal/registry"
"github.com/devrimcavusoglu/skern/internal/skill"
"github.com/spf13/cobra"
)

Expand All @@ -26,7 +27,8 @@ func newSkillInstallCmd() *cobra.Command {

Each skill must already exist in skern's registry — create it with 'skern skill
create' or pull it in with 'skern skill import' before installing. Install copies
the registered skill into the platform's skill directory; the registry copy is
the registered skill directory into the platform's skill directory, minus any
paths the skill's frontmatter lists under install.exclude; the registry copy is
left untouched.

Each invocation targets exactly one platform — agents are expected to specify
Expand Down Expand Up @@ -98,19 +100,28 @@ installed-skill count would meet or exceed the per-platform threshold (see
for _, name := range names {
entry := output.SkillActionEntry{Skill: name}

_, skillDir, getErr := reg.Get(name, scopeVal)
s, skillDir, getErr := reg.Get(name, scopeVal)
if getErr != nil {
entry.Error = fmt.Sprintf("not registered in skern (%s scope) — run 'skern skill create' or 'skern skill import' first, or 'skern skill list' to see available skills", scope)
entries = append(entries, entry)
continue
}

// A malformed exclude pattern would silently install what the
// author meant to leave out; refuse this skill instead.
if vErr := skill.ValidateExcludePatterns(s.Install.Exclude); vErr != nil {
entry.Error = fmt.Sprintf("%v (fix the skill's frontmatter; 'skern skill validate %s' lists every problem)", vErr, name)
entries = append(entries, entry)
continue
}

if force {
// Best-effort cleanup: skill may not be installed yet.
_ = p.Uninstall(name, scopeVal)
}

if installErr := p.Install(skillDir, name, scopeVal); installErr != nil {
opts := platform.InstallOptions{Exclude: s.Install.Exclude}
if installErr := p.Install(skillDir, name, scopeVal, opts); installErr != nil {
entry.Error = installErr.Error()
} else {
entry.Success = true
Expand Down
Loading