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
32 changes: 30 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1965,7 +1965,7 @@ Jobs can be edited by hand — `fuseraft schedule run` reads the YAML fresh on e

## `fuseraft skills`

Install, list, and remove global skills available to all agent sessions. Skills are stored in `~/.fuseraft/skills/` and registered in an FTS5 search index so fuseraft can automatically identify which ones are relevant to a given task.
Install, list, remove, and validate global skills available to all agent sessions. Skills are stored in `~/.fuseraft/skills/` and registered in an FTS5 search index so fuseraft can automatically identify which ones are relevant to a given task.

See [Skills](skills.md) for an overview of how skills work and how to write them.

Expand Down Expand Up @@ -2008,7 +2008,7 @@ List all installed global skills.
fuseraft skills list
```

Displays a table with the slug and description for each skill found under `~/.fuseraft/skills/`.
Displays a table with the slug, description, `compatibility` field (if any), and Agent Skills specification conformance (`✓`/`✗`) for each skill found under `~/.fuseraft/skills/`. Run `fuseraft skills validate` for details on any `✗` entries.

**Examples**

Expand Down Expand Up @@ -2074,6 +2074,34 @@ See [Configuration → Skill curation](configuration.md#skill-curation) for the

---

### `fuseraft skills validate`

Validate a `SKILL.md`'s frontmatter against the [Agent Skills specification](https://agentskills.io/specification) — fuseraft's equivalent of the spec's own recommended `skills-ref validate` tool. Checks the `name` field's format, length, and match against its parent directory name; the `description` field's presence and length; and the `compatibility` field's length. Uses the same validator fuseraft's orchestration skills provider applies at load time, so a skill that passes here is guaranteed to load identically in both the REPL and `fuseraft run` sessions.

```
fuseraft skills validate [path]
```

**Arguments**

| Argument | Description |
|----------|-------------|
| `[path]` | Path to a skill directory to validate. Omitted: validates every skill under `~/.fuseraft/skills/`. |

Exits with status `0` when every checked skill is fully conformant, `1` otherwise.

**Examples**

```bash
# Validate every installed skill
fuseraft skills validate

# Validate a skill before installing it
fuseraft skills validate ../skills/sandbox-test
```

---

## `fuseraft log`

View fuseraft log files. Orchestration session logs (`fuseraft log events`) are read from the global `~/.fuseraft/logs/sessions/` directory. REPL and application logs are read from the current project's `.fuseraft/logs/` directory.
Expand Down
3 changes: 2 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,8 @@ If `fuseraft run --work-dir` points at a directory you did not author, any skill

- Only run `fuseraft` in working directories you trust. Treat `.agents/skills/` and `.fuseraft/skills/` in a cloned repo the same way you would treat a `Makefile` or `package.json` postinstall script.
- For higher assurance, run fuseraft inside a Docker container (`CodeExecution` plugin) where the host environment is not exposed.
- `UseScriptApproval` support is planned — when enabled it will require explicit user confirmation before any skill script executes. Until then, script execution is automatic once a skill is loaded.
- Microsoft Agent Framework's skills provider supports gating `load_skill`/`read_skill_resource`/`run_skill_script` behind an approval step (`AgentSkillsProviderOptions`), but fuseraft explicitly disables it today, since neither the REPL nor orchestration has a pipeline that resolves an approval request — leaving it enabled would make the tools non-functional rather than gated. Script execution is therefore automatic once a skill is loaded; wiring real approval (REPL: a confirmation prompt; orchestration: `IHumanApprovalService`) is a known future improvement, not yet implemented.
- `read_skill_resource` and `run_skill_script` resolve the model-supplied path against the skill directory and reject anything that resolves outside it, including via a symlinked file or subdirectory planted inside the skill folder — this narrows path-based escape from *within* a loaded skill, but a fully malicious skill script still runs as an OS subprocess with the full process environment; it isn't a substitute for only loading trusted skills.

---

Expand Down
25 changes: 19 additions & 6 deletions docs/skills.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Skills

Skills give agents specialized knowledge and step-by-step procedures for specific types of tasks. At REPL startup fuseraft scans your skill directories, injects a catalog of available skills into the system prompt, and exposes two tools the model can call to use them.
Skills give agents specialized knowledge and step-by-step procedures for specific types of tasks, following the [Agent Skills specification](https://agentskills.io/specification). At session start fuseraft scans your skill directories, injects a catalog of available skills into the system prompt, and exposes tools the model can call to use them. Discovery, frontmatter parsing/validation, and the skill tools themselves all come from the [Microsoft Agent Framework](https://github.com/microsoft/agent-framework)'s `AgentFileSkillsSource`/`AgentSkillsProvider` — the REPL and `fuseraft run` orchestration sessions share the exact same implementation, so a skill is treated identically in both.

---

Expand Down Expand Up @@ -36,9 +36,11 @@ At startup, the skill count appears in the compact info line alongside the activ
| `read_skill_resource` | Read a supplementary file bundled with a skill (e.g. a file under `references/`), by path relative to the skill directory. |
| `run_skill_script` | Run a script bundled with a skill (`.sh`, `.py`, `.js`). |

`read_skill_resource` and `run_skill_script` reject a path that resolves outside the skill directory, including via a symlinked file or subdirectory planted inside it.

If `--no-tools` is passed, skills are disabled for that session.

`fuseraft run` orchestration sessions use the same five discovery locations and the same three tools (`load_skill`, `read_skill_resource`, `run_skill_script`), wired onto every agent automatically whenever at least one skill directory exists — there is no need to add `Skills` to an agent's `Plugins:` list, though doing so as a declaration of intent is harmless.
`fuseraft run` orchestration sessions use the same five discovery locations and the same three tools (`load_skill`, `read_skill_resource`, `run_skill_script`), wired onto every agent automatically whenever at least one skill directory exists — there is no need to add `Skills` to an agent's `Plugins:` list, though doing so as a declaration of intent is harmless. This is the same discovery pipeline the REPL uses, not a separate implementation — a skill either works identically in both, or (if its frontmatter is invalid) in neither.

---

Expand Down Expand Up @@ -182,6 +184,8 @@ The command accepts a path to a skill directory (containing `SKILL.md`) or direc

You can also install skills by placing them directly under `~/.fuseraft/skills/` without using the CLI — skills are loaded from that directory at session start regardless of how they got there.

`fuseraft skills add` canonicalizes the frontmatter as it installs: if the raw `name:` field doesn't already equal the slug it's being installed under (e.g. it had spaces or uppercase letters), the installed copy's `name:` line is rewritten to match. This guarantees an installed skill's `name:` and directory always agree, which orchestration requires (see below).

---

## Writing a skill
Expand All @@ -203,13 +207,22 @@ description: What this skill does and when to use it.
Step-by-step guidance for the agent...
```

The `name` field is used by `fuseraft skills add` to derive the destination directory name when installing a skill globally, so keeping it in sync with the directory name is strongly recommended. The `description` is what fuseraft uses to decide whether the skill is relevant to the current task — write it so it covers both what the skill does and the kinds of tasks that should trigger it.
fuseraft follows the [Agent Skills specification](https://agentskills.io/specification) for `SKILL.md` frontmatter:

| Field | Required | Notes |
|-------|----------|-------|
| `name` | Yes | Lowercase letters, digits, and single hyphens only (no leading/trailing/double hyphens); max 64 characters; must match the parent directory name exactly. |
| `description` | Yes | 1–1024 characters. What fuseraft uses to decide whether the skill is relevant to the current task — write it so it covers both what the skill does and the kinds of tasks that should trigger it. |
| `license` | No | License name, or a reference to a bundled license file. |
| `compatibility` | No | Max 500 characters. Environment requirements (e.g. `Requires docker and jq`) — shown in the REPL's skill catalog as a `[requires: ...]` hint. |
| `metadata` | No | Arbitrary string-to-string map for your own bookkeeping (author, version, etc.). Not surfaced to the model. |
| `allowed-tools` | No | Space-separated list of pre-approved tools (experimental, per spec — fuseraft parses but does not currently act on this field). |

If your instructions are long, move reference material into a `references/` subdirectory inside the skill folder. The agent loads those files on demand — with `read_skill_resource` — rather than all at once.
If your instructions are long, move reference material into a `references/` subdirectory inside the skill folder. The agent loads those files on demand — with `read_skill_resource` — rather than all at once. `scripts/` and `assets/` are supported the same way.

**If two installed skills share the same name**, the one in the higher-precedence location wins and a warning is logged.
**If two installed skills share the same name**, the one in the higher-precedence location wins.

> **Keep `name:` and the directory name identical.** The REPL loader uses the directory name as the slug and never reads `name:` at load time, so a mismatch is harmless there. `fuseraft run` orchestration sessions use a stricter loader that requires `name:` to match the directory name **exactly** (case-sensitive), to be non-empty lowercase kebab-case (letters, digits, single hyphensno leading/trailing/double hyphens), and requires a non-empty `description:`. A skill that violates any of these is silently dropped from the orchestration catalog — it works fine in the REPL but an agent in a `fuseraft run` session never sees it. Follow the frontmatter format above exactly and both surfaces will pick up the skill identically.
> **`name:` must match the directory name exactly.** Both the REPL and `fuseraft run` require `name:` to match its parent directory name **exactly** (case-sensitive), to be valid lowercase kebab-case, and require a non-empty, correctly-sized `description:`they use the identical discovery pipeline, so there is no REPL-specific leniency here. A skill that violates any of these is silently excluded from the catalog in **both** surfaces, with the reason logged as a warning or error (visible by default — no `--verbose` needed). Run `fuseraft skills validate [path]` to check a skill (or every installed skill) against the full specification before relying on it. The one exception is `fuseraft skills add`, which stays deliberately lenient — see [Installing skills](#for-all-your-projects) above.

---

Expand Down
14 changes: 11 additions & 3 deletions skills/skill-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ description: <one or two sentences>
---
```

**`name`:** A short, lowercase kebab-case slug (e.g. `debug-session`, `mcp-setup`) — letters, digits, and single hyphens only, no leading/trailing/double hyphens. This is used as the install directory name when running `fuseraft skills add`. Keep it to 1–3 words, and **make it identical to the skill's directory name**: the REPL loader ignores `name:` and uses the directory name as the slug, but `fuseraft run` orchestration sessions use a stricter loader that silently drops the skill from the catalog if `name:` doesn't exactly match the directory name (or isn't valid kebab-case, or `description:` is empty). Matching them keeps the skill working identically in both surfaces.
**`name`:** A short, lowercase kebab-case slug (e.g. `debug-session`, `mcp-setup`) — letters, digits, and single hyphens only, no leading/trailing/double hyphens, max 64 characters. This is used as the install directory name when running `fuseraft skills add`. Keep it to 1–3 words, and **make it identical to the skill's directory name**: the REPL and `fuseraft run` orchestration sessions both use the same discovery pipeline and silently exclude the skill from the catalog if `name:` doesn't exactly match the directory name (or isn't valid kebab-case, or `description:` is empty or too long) — there is no REPL-specific leniency once a skill directory exists somewhere fuseraft scans. Run `fuseraft skills validate <path>` to confirm before installing.

**Optional fields**, per the [Agent Skills specification](https://agentskills.io/specification) — add only when they earn their keep:
- **`license`:** a license name or reference to a bundled license file. Only relevant for skills you intend to share/distribute.
- **`compatibility`:** environment requirements, max 500 characters (e.g. `Requires docker and jq`, `Designed for fuseraft REPL sessions`). Shown to the agent in the REPL catalog as a `[requires: ...]` hint — add it when the skill assumes a tool or platform that isn't universally available.
- **`metadata`:** a string-to-string map for your own bookkeeping (e.g. `author`, `version`). Not shown to the agent.
- **`allowed-tools`:** experimental per spec; fuseraft parses it but doesn't currently act on it. Skip it.

**`description`:** This is the most important field — fuseraft injects only the name and description into the agent's catalog at session start. The agent reads this to decide whether the skill is relevant. Write it so it covers:
- What the skill produces or accomplishes
Expand Down Expand Up @@ -168,7 +174,9 @@ Or write directly to `~/.fuseraft/skills/<slug>/SKILL.md` — fuseraft loads fro

### Step 7: Verify

For **REPL sessions**, start or restart fuseraft and run `/tools`. The skill should appear under the `Skills` category with its name and description.
First, run `fuseraft skills validate <path-to-skill-directory>` (or `fuseraft skills validate` with no argument once installed, to check it alongside every other installed skill). This checks the frontmatter against the full specification — name format and directory match, description presence/length, compatibility length — with the same validator both the REPL and orchestration use, before you burn a session on it.

For **REPL sessions**, start or restart fuseraft and run `/tools`. The skill should appear under the `Skills` category with its name and description. Watch the startup output for an `[ERR]`/`[WRN]` line naming the SKILL.md path — that means the frontmatter is invalid (most often a name/directory mismatch) and the skill did not load.

For **orchestration sessions**, run `fuseraft validate` on the config first, then do a one-turn dry run:

Expand All @@ -179,7 +187,7 @@ fuseraft run --config <path> --max-iterations 1 "List your available skills."
The agent should name the skill in its response. If it does not appear, check:
- `SKILL.md` is directly inside the skill directory (not nested deeper)
- The install path is one of the five recognized locations (project `.fuseraft/skills/`, project `.agents/skills/`, user `.fuseraft/skills/`, user `.agents/skills/`, or shipped built-in)
- **Orchestration-only:** `name:` in the frontmatter exactly matches the directory name (case-sensitive), is valid lowercase kebab-case, and `description:` is non-empty — a mismatch here loads fine in the REPL but is silently dropped by `fuseraft run`'s stricter loader with no error to the user, only a log entry
- `fuseraft skills validate` passes — a violation it reports means the skill is silently excluded from both the REPL and `fuseraft run` catalogs, with no error to the user beyond a log entry

### Step 8: Refine the Description

Expand Down
34 changes: 22 additions & 12 deletions src/Cli/Commands/Repl/ReplCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Spectre.Console;
Expand Down Expand Up @@ -182,7 +183,7 @@ protected override async Task<int> ExecuteAsync(
var toolsByCategory = new Dictionary<string, List<AIFunction>>(StringComparer.OrdinalIgnoreCase);
using ShellPlugin? shellPlugin = settings.NoTools ? null : new ShellPlugin(shellPolicy: TryLoadDefaultShellPolicy());
SubAgentPlugin? subAgent = null;
SkillsPlugin? skillsPlugin = null;
IReadOnlyList<AgentSkill> discoveredSkills = [];
string? skillsCatalog = null;
List<AIFunction>? explorerTools = null;
TodoPlugin? todoPlugin = null;
Expand Down Expand Up @@ -220,10 +221,6 @@ protected override async Task<int> ExecuteAsync(
toolsByCategory["FileSystem"] = fsFunctions.Where(f => CoreFileSystemTools.Contains(f.Name)).ToList();
toolsByCategory["Shell"] = shellFunctions.Where(f => CoreShellTools.Contains(f.Name)).ToList();
toolsByCategory["Git"] = gitFunctions.Where(f => CoreGitTools.Contains(f.Name)).ToList();

(skillsPlugin, skillsCatalog) = ReplSkillsLoader.BuildSkills();
if (skillsPlugin is not null)
toolsByCategory["Skills"] = PluginRegistry.GetFunctionsFromObject(skillsPlugin).ToList();
}

var initialTools = toolsByCategory.Values.SelectMany(v => v).ToList();
Expand All @@ -238,6 +235,19 @@ protected override async Task<int> ExecuteAsync(
return 1;
}

if (!settings.NoTools)
{
// Skill discovery/parsing/validation and the load_skill/read_skill_resource/
// run_skill_script tools all come from Microsoft.Agents.AI's AgentFileSkillsSource/
// AgentSkillsProvider — the same classes orchestration uses — via a throwaway
// ChatClientAgent wrapping the client just built above.
var skillsResult = await ReplSkillsLoader.BuildAsync(client, loggerFactory, cancellationToken);
discoveredSkills = skillsResult.Skills;
skillsCatalog = skillsResult.CatalogInstructions;
if (skillsResult.Tools.Count > 0)
toolsByCategory["Skills"] = skillsResult.Tools.ToList();
}

var cwd = Directory.GetCurrentDirectory();
var eventsPath = FuseraftPaths.ExpandProjectPaths(FuseraftPaths.LocalReplEventsLog, FuseraftPaths.ProjectSlug(cwd));

Expand Down Expand Up @@ -373,7 +383,7 @@ protected override async Task<int> ExecuteAsync(
MessageRenderer.RenderReplHeader(
modelId, cwd, pluginNames, sessionId,
memoryCount: memoryEntries.Count,
skillCount: skillsPlugin?.Count ?? 0,
skillCount: discoveredSkills.Count,
branch: TryGetGitBranch(cwd),
eventsPath: settings.Verbose ? eventsPath : null);
}
Expand All @@ -384,10 +394,10 @@ protected override async Task<int> ExecuteAsync(
memoryStore, toolsByCategory, systemPrompt, pendingSave,
verbose: settings.Verbose, subAgent: subAgent)
{
JsonMode = jsonMode,
SkillsPlugin = skillsPlugin,
Todo = todoPlugin,
KeyStored = keyStored,
JsonMode = jsonMode,
Skills = discoveredSkills,
Todo = todoPlugin,
KeyStored = keyStored,
};

if (!settings.NoTools)
Expand All @@ -406,8 +416,8 @@ protected override async Task<int> ExecuteAsync(
.FirstOrDefault();
}

if (skillsPlugin is not null)
ctx.LineReader.SetSkillSlugs([.. skillsPlugin.Slugs]);
if (discoveredSkills.Count > 0)
ctx.LineReader.SetSkillSlugs([.. discoveredSkills.Select(s => s.Frontmatter.Name)]);

// Wire the compact_context and get_context_status tools now that ctx is available.
replSessionPlugin?.SetCompactDelegate(async (focus, ct) =>
Expand Down
Loading