Skip to content

Commit 5d26909

Browse files
authored
feat(skills): SKILL.md loader, validator, discovery + gaia skill CLI core (#2669)
Nothing in GAIA could read a `SKILL.md` before this. An agent gained capability only by shipping new Python, so a user who wanted a new one had no path short of forking the agent — and the large library of skills already published for Claude Code and friends was unreachable. Now a folder with a `SKILL.md` drops into `~/.gaia/skills/` and any agent loads it at runtime with `agent.load_skill("name")`, and an existing `.claude/skills/` library is discovered read-only with zero migration (this repo's own 11 skills load unchanged). This is Phase 1 of #1019 — the layer #2466, #2467, #2468, #893, and #692 are all blocked on. Scoped deliberately so no sandbox is needed to ship it: a skill may add instructions and reach connector-backed services, and one declaring a local-capability permission (`filesystem`/`shell`/`database`/`desktop`/`env`) is **refused with an actionable error** rather than loaded without enforcement. `network:*` and `mcp:connect` resolve to the existing `ConnectorRequirement` — no second grant ledger. ### Decisions taken where the specs were silent - `mcp:connect` must be scoped to a catalog connector id (`mcp:connect:mcp-tavily`); a bare one fails loudly listing the available ids. A `network:*` permission that names no catalog connector resolves against a reserved `network` pseudo-id — a **declaration** surface only, since Phase 1 does not enforce egress. - `<domain>:none` is inert in both directions: it grants nothing, so it neither refuses nor produces a requirement. - `tools_required` names that aren't in the current agent's registry are logged, not fatal — the tool universe is assembled dynamically from mixins, so absence is scoping, not a manifest defect (the open question in `skill-format.mdx`). - Discovery trims the spec's five roots to three; project-local `./.gaia/skills/` and the registry-lock root are marked deferred in the spec, not deleted. - `src/gaia/schedule/runner.py` and the `gaia schedule add --skill` gate told users their problem was "blocked on #888" — a claim this PR makes false on merge. Reworded to point at #1019, where wiring the scheduler to the skills runtime actually lives; that is why those files (and their two tests) are in the diff. - Three of `skill-format.mdx`'s own example skills (`rag-search`, `file-operations`, migrated `git-status`) declare local-capability permissions and are therefore refused today. The doc now marks which examples load rather than quietly rewriting their permissions. ### Test plan - [ ] `python -m pytest tests/unit/test_skills_format.py tests/unit/test_skills_manager.py tests/unit/test_skills_cli.py -q` — 150 tests, one per acceptance-criterion bullet - [ ] Round-trip identity: `parse → write → parse` on every fixture, and byte-stability on a second write - [ ] A bare agentskills.io skill (only `name` + `description`) loads instruction-only at `security_tier: experimental`; writing it back does **not** stamp a `metadata.gaia` block into it - [ ] `load_skill` registers tools as `<skill>/<tool>` and injects the body — including when the skill's unqualified tool name collides with one the framework already registered - [ ] A manifest that contradicts `tools.py` (missing tool, undeclared extra, parameter/requiredness/type mismatch, raising module) leaves `_TOOL_REGISTRY` byte-identical — assert on the whole dict, not just the absent key - [ ] A `filesystem:write` skill is refused via `Agent.load_skill` **and** via the lower-level `register_skill_tools` - [ ] Precedence: the same skill in all three roots resolves to the agent-bundled copy; the shadowed copies stay visible via `SkillManager.shadowed()` and in `gaia skill list` - [ ] Real CLI, cold `GAIA_CONFIG_DIR`: `gaia skill create` → `list` → `info` → `export` → `import` into a second home, plus `--json` stdout parsing cleanly with logs on stderr - [ ] `python -m pytest tests/unit/ -q` — the 16 failures on this machine reproduce identically on an untouched `main` checkout (macOS `/private/var` sandbox paths, network-dependent hub installer, missing faiss, venv-not-on-PATH) - [ ] `python util/lint.py --all` clean Closes #888
1 parent 0ae019c commit 5d26909

30 files changed

Lines changed: 4844 additions & 154 deletions

File tree

docs/plans/skill-format.mdx

Lines changed: 236 additions & 85 deletions
Large diffs are not rendered by default.

docs/reference/cli.mdx

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2065,6 +2065,162 @@ Manage agent memory: run day-zero onboarding and view memory statistics.
20652065

20662066
---
20672067

2068+
## Skills
2069+
2070+
<Card title="Agent Skills Spec" icon="puzzle-piece" href="/spec/agent-skills">
2071+
Portable `SKILL.md` capabilities — instructions, optional typed tools, and resources an agent loads only when relevant
2072+
</Card>
2073+
2074+
Author and manage agent skills. A skill is a directory whose only required file is `SKILL.md`; GAIA implements the [Agent Skills](https://agentskills.io) open standard, so a Claude Code skill loads unchanged.
2075+
2076+
```bash
2077+
gaia skill {list,info,create,import,export} [OPTIONS]
2078+
```
2079+
2080+
Skills are discovered from three roots, highest precedence first:
2081+
2082+
| Root label | Location | Writable? |
2083+
|------------|----------|:---------:|
2084+
| `agent-bundled` | An agent package's own `skills/` directory | No |
2085+
| `user` | `~/.gaia/skills/` (honors `GAIA_CONFIG_DIR`) | **Yes** — where `create` and `import` install |
2086+
| `claude-import` | `./.claude/skills/` then `~/.claude/skills/` | No — read-only Claude Code import |
2087+
2088+
### Subcommands
2089+
2090+
| Action | Description |
2091+
|--------|-------------|
2092+
| `list` | List every discovered skill with its version, tier, root, and provided tools |
2093+
| `info <name>` | Show one skill's manifest in detail |
2094+
| `create <name>` | Scaffold a new skill directory |
2095+
| `import <source>` | Copy a skill folder, `.zip`, or URL into `~/.gaia/skills/` |
2096+
| `export <name>` | Export a skill to a `.zip` bundle |
2097+
2098+
<Tabs>
2099+
<Tab title="list">
2100+
```bash
2101+
gaia skill list [--json] [--root ROOT]
2102+
```
2103+
2104+
| Option | Description |
2105+
|--------|-------------|
2106+
| `--json` | Emit JSON instead of a table (includes roots, shadowed copies, and parse errors) |
2107+
| `--root` | Only show skills from one root: `agent-bundled` \| `user` \| `claude-import` |
2108+
2109+
```
2110+
NAME VERSION TIER ROOT TOOLS
2111+
demo-skill 0.1.0 experimental user example_tool
2112+
gaia-testing - experimental claude-import -
2113+
```
2114+
2115+
A skill shadowed by a higher-precedence copy of the same name is reported on stderr rather than hidden. A skill directory that fails to parse is listed with its error and the command exits `4`.
2116+
</Tab>
2117+
2118+
<Tab title="info">
2119+
```bash
2120+
gaia skill info <name> [--json] [--body]
2121+
```
2122+
2123+
| Option | Description |
2124+
|--------|-------------|
2125+
| `--json` | Emit JSON (including the full frontmatter) instead of text |
2126+
| `--body` | Also print the Markdown instructions |
2127+
2128+
Shows path, root, license, security tier, declared permissions, the tools the skill **provides** (namespaced `<skill>/<tool>`), the registry tools it **consumes** (`tools_required`), and any lower-precedence copies it shadows.
2129+
</Tab>
2130+
2131+
<Tab title="create">
2132+
```bash
2133+
gaia skill create <name> [OPTIONS]
2134+
```
2135+
2136+
| Option | Description |
2137+
|--------|-------------|
2138+
| `--dir` | Parent directory for the new skill (default: `~/.gaia/skills`) |
2139+
| `--description` | Description / trigger signal — the text the model reads to decide relevance |
2140+
| `--with-tools` | Also scaffold `tools.py` with an example `@tool` function |
2141+
| `--force` | Overwrite an existing skill directory |
2142+
2143+
Names must be lowercase-with-hyphens and match the directory name. The scaffold is validated through the real parser before it is written, so `create` can never emit a `SKILL.md` that `info` rejects.
2144+
</Tab>
2145+
2146+
<Tab title="import">
2147+
```bash
2148+
gaia skill import <folder|zip|https-url> [OPTIONS]
2149+
```
2150+
2151+
| Option | Description |
2152+
|--------|-------------|
2153+
| `--name` | Install under this name instead of the source's |
2154+
| `--force` | Overwrite an existing installed skill |
2155+
2156+
Copies into `~/.gaia/skills/`. **Imported skills re-earn trust:** the security tier is reset to `experimental` regardless of what the source claims, and the reset is reported. `.zip` bundles are checked for path traversal before extraction.
2157+
</Tab>
2158+
2159+
<Tab title="export">
2160+
```bash
2161+
gaia skill export <name> [--output FILE.zip]
2162+
```
2163+
2164+
| Option | Description |
2165+
|--------|-------------|
2166+
| `--output` | Destination `.zip` (default: `./<name>.zip`) |
2167+
2168+
Bundles the whole skill directory. Import it elsewhere with `gaia skill import <file>.zip`.
2169+
</Tab>
2170+
</Tabs>
2171+
2172+
**Examples:**
2173+
2174+
<CodeGroup>
2175+
```bash Scaffold a skill with tools
2176+
gaia skill create web-research --with-tools \
2177+
--description "Search the web. Use when the user asks about current events."
2178+
gaia skill info web-research --body
2179+
```
2180+
2181+
```bash See what's discoverable
2182+
gaia skill list
2183+
gaia skill list --root user # only your own skills
2184+
gaia skill list --json | jq '.roots' # where GAIA looked
2185+
```
2186+
2187+
```bash Adopt a Claude Code skill
2188+
gaia skill list --root claude-import # what's already visible read-only
2189+
gaia skill import ~/.claude/skills/my-skill # take ownership, resets to experimental
2190+
```
2191+
2192+
```bash Share a skill
2193+
gaia skill export web-research --output ./web-research.zip
2194+
gaia skill import ./web-research.zip --name web-research-copy
2195+
```
2196+
</CodeGroup>
2197+
2198+
**Exit codes:** `0` success · `2` missing/unknown subcommand · `3` skill not found · `4` invalid skill (bad manifest, name collision without `--force`, or a discovery root containing a malformed skill).
2199+
2200+
### Loading a skill into an agent
2201+
2202+
Skills are never globally active — an agent loads one explicitly, and its tools register under a `<skill-name>/<tool>` namespace:
2203+
2204+
```python
2205+
class WebAgent(Agent):
2206+
SKILL_DIRS = ["<package>/skills"] # optional: bundle your own skills
2207+
2208+
def _register_tools(self):
2209+
self.load_skill("web-research") # tools land under 'web-research/'
2210+
```
2211+
2212+
<Warning>
2213+
**A skill declaring a local-capability permission is refused, not sandboxed.** `network` and `mcp` permissions bridge to the connector grant model and load fine. `filesystem`, `shell`, `database`, `desktop`, and `env` need the permission sandbox, which is deferred ([#1019](https://github.com/amd/gaia/issues/1019)) — until then `load_skill` fails with an actionable error rather than loading the skill unenforced.
2214+
</Warning>
2215+
2216+
<Note>
2217+
`gaia skill install`, `remove`, `search`, `publish`, and `migrate` are **not implemented** and do not parse — they need the skill registry and land with the marketplace ([#2467](https://github.com/amd/gaia/issues/2467)). Use `import` / `export` for local distribution.
2218+
</Note>
2219+
2220+
[→ Agent Skills Spec](/spec/agent-skills) · [→ Skill Format Reference](/plans/skill-format)
2221+
2222+
---
2223+
20682224
## Schedule
20692225

20702226
Run a prompt on a recurring cron schedule and route its output to a sink. Schedules are stored in `~/.gaia/schedules.toml` (hand-editable). The `daemon` action runs a long-lived scheduler that fires each enabled schedule when due.
@@ -2149,7 +2305,7 @@ gaia schedule add \
21492305
```
21502306

21512307
<Note>
2152-
`--skill` is not yet implemented — `gaia schedule add --skill` is rejected with an error at add time pending the skill format ([#888](https://github.com/amd/gaia/issues/888)). Use `--prompt` for now.
2308+
`--skill` is not yet implemented — `gaia schedule add --skill` is rejected with an error at add time. The [skills runtime](#skills) has shipped ([#888](https://github.com/amd/gaia/issues/888)), but the scheduler is not wired to it yet. Use `--prompt` for now.
21532309
</Note>
21542310

21552311
---
@@ -3077,6 +3233,9 @@ For more help, see:
30773233
<Card title="Agent Memory" icon="brain" href="/guides/memory">
30783234
Persistent memory across sessions
30793235
</Card>
3236+
<Card title="Agent Skills" icon="puzzle-piece" href="/spec/agent-skills">
3237+
Portable SKILL.md capabilities
3238+
</Card>
30803239
</CardGroup>
30813240

30823241
---

0 commit comments

Comments
 (0)