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: 32 additions & 0 deletions .skillware.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Example Skillware configuration.
# Copy to .skillware.yaml in your repository root (or use global config.yaml).
#
# Global config (optional):
# Linux/macOS: ~/.config/skillware/config.yaml
# Windows: %APPDATA%/skillware/config.yaml
#
# The bundled registry from `pip install skillware` is always available.

paths:
# auto = walk up from cwd for ./skills/ (default)
# Or set an explicit skills root directory:
project: auto
external:
# - /path/to/private-skills
[]

resolution:
order:
- project
- external
- bundled

legacy:
# When true, SKILLWARE_SKILL_PATH is merged as external roots (default).
honor_skillware_skill_path: true

# Reserved for future releases (ignored today; preserved by skillware config show):
# theme:
# preset: default
# chains:
# default: []
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta

### Added

- **Config:** Persistent YAML configuration (global `config.yaml` and project `.skillware.yaml`); `paths` section drives skill root discovery when present; bundled registry always included (#246).
- **CLI:** `skillware config show` prints merged configuration (read-only); `skillware paths` tips updated for config files (#246).
- **Loader:** `SkillLoader.load_skill(..., execute_module=False)` inspect-only load (manifest, instructions, card, requirement pre-flight) without executing `skill.py`; clearer `ImportError` when `skill.py` import fails after pre-flight (#235).
- **CLI:** `skillware doctor` checks manifest deps and `skill.py` import readiness per skill (`DEPS` / `LOAD` table); optional skill ID, `--category`, and `--skills-root` (#235).

Expand Down
2 changes: 1 addition & 1 deletion docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ A skill is a folder on disk. The loader turns the manifest into whatever tool sc
When you run `SkillLoader.load_skill("category/skill_name")`, a complex orchestration happens behind the scenes:

### Step 1: Discovery & Loading
The loader resolves `category/skill_name` to a skill directory by checking, in order: an existing path on disk, roots in `SKILLWARE_SKILL_PATH`, a `skills/` folder in the current working directory (or its parents), then bundled skills installed with the package. Run `skillware paths` for a live view of resolved roots, tiers, and shadowing. Each bundle is a directory containing `manifest.yaml` and `skill.py`.
The loader resolves `category/skill_name` to a skill directory by checking, in order: an existing path on disk, configured skill roots (`.skillware.yaml`, global config, or legacy `SKILLWARE_SKILL_PATH` + cwd `skills/` walk), then bundled skills installed with the package. Run `skillware paths` and `skillware config show` for a live view. Each bundle is a directory containing `manifest.yaml` and `skill.py`.
* It dynamically imports the `skill.py` module and auto-discovers the single `BaseSkill` subclass as `bundle["class"]` (no hardcoded class names required).
* It parses the `manifest.yaml` (including `issuer` for attribution, separate from tool-calling fields). Registry skills set `name` to the full ID (`category/skill_name`), which Claude uses as the tool name; Gemini, OpenAI, and DeepSeek receive a sanitized variant (slashes → underscores). For registry-layout paths (`<skill_root>/<category>/<skill_name>/`), the loader warns when `name` does not match the folder path; flat private layouts (`<skill_root>/<skill_name>/`) skip this check. Loaded bundles expose `registry_id` when validation applies.
* It reads `instructions.md` and, when present, optional `card.json`.
Expand Down
12 changes: 8 additions & 4 deletions docs/security/skill-trust-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@ The trust tiers in this document describe how much you should trust a skill's or

## 2. How skills are resolved on disk

When you pass a registry id (for example finance/wallet_screening) rather than a path that already exists, the loader searches a fixed set of roots and uses the first match it finds, in this order:
When you pass a registry id (for example finance/wallet_screening) rather than a path that already exists, the loader searches skill roots and uses the first match it finds.

**Default (no config file):**

1. SKILLWARE_SKILL_PATH — one or more roots, separated by your OS path separator.
2. ./skills/ in the current working directory, and its parent directories — the loader walks up to six levels of parents looking for a skills/ directory.
2. `./skills/` in the current working directory, and its parent directories (walk up to six levels).
3. Bundled skills shipped inside the installed skillware package (for example under site-packages/skills/).

**With config (`.skillware.yaml` or global `config.yaml`):** tiers follow `resolution.order` (default: project → external → bundled). Persist private roots under `paths.external`; set `paths.project` to `auto` or an explicit directory. Bundled registry skills are always included. See `skillware config show` and [CLI config](../usage/cli.md#skillware-config).

If you pass a path that already points at a skill directory (absolute or relative to the current directory), the loader uses it directly and skips the search entirely.

### Shadowing

Because the search stops at the first matching id, a skill earlier in the order shadows any skill with the same id later in the order. If a finance/wallet_screening exists under SKILLWARE_SKILL_PATH or in a local ./skills/, it is loaded instead of the bundled, maintainer-reviewed copy of the same id — and the bundled copy never runs.
Because the search stops at the first matching id, a skill earlier in the order shadows any skill with the same id later in the order. If a finance/wallet_screening exists under project or external paths before bundled, it is loaded instead of the bundled, maintainer-reviewed copy — and the bundled copy never runs.

Run `skillware paths` to see which roots are active and which IDs shadow bundled registry skills.
Run `skillware paths` and `skillware config show` to see which roots are active and which IDs shadow bundled registry skills.

The practical consequence: placing a skill with the same id as an official one, anywhere earlier in the search order, silently replaces the official skill. Shadowing is a normal feature of the resolution order, but it means the id you ask for does not by itself tell you which code will run — the location does.

Expand Down
12 changes: 8 additions & 4 deletions docs/usage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ How to load Skillware skills and connect them to language models. Each guide cov

## Finding skills on disk

`SkillLoader.load_skill()` accepts an absolute path to a skill directory, or a registry id such as `compliance/tos_evaluator`. When the id is not already a path on disk, the loader searches in order:
`SkillLoader.load_skill()` accepts an absolute path to a skill directory, or a registry id such as `compliance/tos_evaluator`. When the id is not already a path on disk, the loader searches configured skill roots in resolution order.

**Default (no config file):**

1. Roots listed in `SKILLWARE_SKILL_PATH` (OS path separator between multiple roots)
2. A `skills/` directory in the current working directory or its parents
3. Bundled skills installed with the `skillware` package (for example under `site-packages/skills/`)

For pip-installed apps, keep project skills in `./skills/<category>/<name>/` or set `SKILLWARE_SKILL_PATH` to your skills root.
**With config:** copy [`.skillware.yaml.example`](../.skillware.yaml.example) to `.skillware.yaml` (or use global `~/.config/skillware/config.yaml`) to persist project and external paths. Default order is project → external → bundled; the bundled registry is always available. See [CLI — config](cli.md#skillware-config) and `skillware config show`.

For pip-installed apps, bundled maintainer skills are the default; add private skills under `./skills/<category>/<name>/`, config `paths.external`, or `SKILLWARE_SKILL_PATH`.

By default, `SkillLoader.load_skill()` validates manifest `requirements` before loading `skill.py`: unpinned deps must be importable; pinned specifiers (for example `web3>=6.0.0`) must match the installed version. See [Install extras — Loader behavior](install_extras.md#loader-behavior).

> **Security:** Loading a skill executes its `skill.py` in your process — there is no sandbox, and the first matching id in the search order wins (a local skill can shadow a bundled one). Only load skills you trust, and see the [skill trust model](../security/skill-trust-model.md) before loading external skills.

To list locally available skills, inspect path resolution, check load readiness, or run bundle tests from the terminal, see the [CLI reference](cli.md) (`skillware list`, `skillware paths`, `skillware doctor`, `skillware test`).
To list locally available skills, inspect path resolution, show config, check load readiness, or run bundle tests from the terminal, see the [CLI reference](cli.md) (`skillware list`, `skillware paths`, `skillware config show`, `skillware doctor`, `skillware test`).

| Provider | Adapter | Guide | Agent API key (typical) |
| :--- | :--- | :--- | :--- |
Expand All @@ -26,7 +30,7 @@ To list locally available skills, inspect path resolution, check load readiness,
| OpenAI-compatible hosts | `to_openai_tool()` | [openai_compatible.md](openai_compatible.md) | Host-specific key |
| DeepSeek | `to_deepseek_tool()` | [deepseek.md](deepseek.md) | `DEEPSEEK_API_KEY` |
| Ollama (prompt mode) | `to_ollama_prompt()` | [ollama.md](ollama.md) | (local; no cloud key) |
| CLI | `skillware list`, `skillware paths`, `skillware doctor`, `skillware test`, `skillware examples` | [cli.md](cli.md) | pytest in `[dev]` for `test` |
| CLI | `skillware list`, `skillware paths`, `skillware config`, `skillware doctor`, `skillware test`, `skillware examples` | [cli.md](cli.md) | pytest in `[dev]` for `test` |
| Install extras | Category, skill, SDK, and meta `pip install` targets | [install_extras.md](install_extras.md) | See guide for `[all]`, `[agents]`, per-skill extras |

Skill-specific **Usage Examples** (sample prompts and execute payloads) live on each [skill catalog page](../skills/README.md).
Expand Down
48 changes: 44 additions & 4 deletions docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ After installation, the `skillware` command is available directly:
skillware
skillware list
skillware doctor
skillware config show
skillware test
skillware examples
skillware --version
Expand Down Expand Up @@ -208,7 +209,7 @@ Show where Skillware looks for skills — same order as `SkillLoader.load_skill(
| :--- | :--- |
| `--skills-root <path>` | Override the skills directory for this command only (shows a single override root). |

Read-only in v0.4.x; persist project/external paths via config is tracked in #246. Interactive menu: **`4` / `paths`**.
Read-only in v0.4.x. Interactive menu: **`4` / `paths`**.

### skillware doctor

Expand All @@ -234,21 +235,60 @@ Exit code is non-zero when any skill fails **DEPS** or **LOAD**. For full bundle

Interactive menu: **`5` / `doctor`**.

### skillware config

Show merged global + project Skillware configuration (read-only). The `paths` section is active today; other top-level keys are preserved for future settings (themes, chains, etc.).

skillware config show

**Global config:** `~/.config/skillware/config.yaml` (Linux/macOS), `%APPDATA%/skillware/config.yaml` (Windows), or override with `SKILLWARE_CONFIG_DIR`.

**Project config:** `.skillware.yaml` in the repository root (walks up from cwd). See [`.skillware.yaml.example`](../../.skillware.yaml.example).

Example project file:

```yaml
paths:
project: auto
external:
- /path/to/private-skills
resolution:
order:
- project
- external
- bundled
legacy:
honor_skillware_skill_path: true
```

When no config file exists, resolution stays **legacy**: `SKILLWARE_SKILL_PATH` → `./skills/` walk → bundled. When config exists, `resolution.order` applies (default: project → external → bundled). The **bundled** registry from `pip install skillware` is always included and cannot be disabled.

## Path resolution

`skillware list` searches for skills in the same order as `SkillLoader`:
`skillware list`, `load_skill`, `test`, and `doctor` share the same roots as `SkillLoader`.

**Without config files (default):**

1. Roots listed in `SKILLWARE_SKILL_PATH` (OS path separator between multiple entries)
2. A `skills/` directory under the current working directory and its parents
3. Bundled skills installed with the `skillware` package

Run `skillware paths` for a live view of resolved roots, tiers, and shadowing.
**With `.skillware.yaml` and/or global config:**

To point the CLI at a persistent custom root, set the environment variable:
1. Tiers in `resolution.order` (default: project → external → bundled)
2. `paths.project`: `auto` (same walk as above) or an explicit directory
3. `paths.external`: persisted private/proprietary skill roots
4. Bundled registry always last-resort fallback (always on)

Run `skillware paths` for a live view of resolved roots, tiers, and shadowing. Run `skillware config show` for merged YAML settings.

To point the CLI at custom roots without config files:

export SKILLWARE_SKILL_PATH=/path/to/my/skills
skillware list

Or copy `.skillware.yaml.example` to `.skillware.yaml` and list paths under `paths.external`.

Only skills with both `manifest.yaml` and `skill.py` present are shown —
the same condition `SkillLoader` requires to load a skill successfully.

Expand Down
97 changes: 93 additions & 4 deletions skillware/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
import importlib.metadata

from skillware.core.loader import SkillLoader
from skillware.core.config import (
GLOBAL_CONFIG_FILENAME,
PROJECT_CONFIG_FILENAME,
format_config_sources,
global_config_path,
load_merged_config,
)
from skillware.core.discovery import (
SKILLWARE_SKILL_PATH_ENV,
find_shadow_conflicts,
Expand Down Expand Up @@ -478,7 +485,7 @@ def cmd_paths(
skills_root_override: Optional[Path] = None,
console=None,
) -> int:
"""Show skill root resolution order, tiers, and shadowing (read-only; #246 adds config)."""
"""Show skill root resolution order, tiers, and shadowing (read-only config via skillware config show)."""
if console is None:
console = Console()

Expand Down Expand Up @@ -550,21 +557,85 @@ def cmd_paths(
style=MENU_STYLE,
)
console.print(
f" • Persistent external roots: export {SKILLWARE_SKILL_PATH_ENV}=/path/to/skills",
f" • Persistent paths: {PROJECT_CONFIG_FILENAME} or {global_config_path()}",
style=MENU_STYLE,
)
console.print(
f" • Legacy env override: export {SKILLWARE_SKILL_PATH_ENV}=/path/to/skills",
style=MENU_STYLE,
)
console.print(" • Inspect merged config: skillware config show", style=MENU_STYLE)
console.print(
" • Trust tiers: docs/security/skill-trust-model.md",
style=f"dim {SPLASH_STYLE}",
)
console.print(
" • Persist project/external paths in config: tracked in #246",
" • Flat-layout skills (<root>/<name>/) load but may not appear in list",
style="dim",
)
return 0


def cmd_config_show(console=None) -> int:
"""Print merged global + project configuration (read-only)."""
if console is None:
console = Console()

config = load_merged_config(refresh=True)
paths = config.paths
console.print(Text("Skillware config", style=f"bold {TABLE_STYLE}"))
console.print()

console.print(Text("Config files", style=f"bold {TABLE_STYLE}"))
console.print(f" Global (default): {global_config_path()}", style="dim")
for line in format_config_sources(config):
console.print(f" Loaded: {line}", style=MENU_STYLE if config.layers else "dim")
console.print()

if not config.has_config_files:
console.print(
"No config files found — using legacy resolution "
f"({SKILLWARE_SKILL_PATH_ENV} → ./skills/ walk → bundled).",
style="dim",
)
console.print(
f"Create {PROJECT_CONFIG_FILENAME} or {GLOBAL_CONFIG_FILENAME} to persist settings.",
style="dim",
)
console.print(
" docs/usage/cli.md#skillware-config", style=f"dim {SPLASH_STYLE}"
)
return 0

console.print(Text("paths (active)", style=f"bold {TABLE_STYLE}"))
project_label = paths.project if paths.project is not None else "auto"
console.print(f" project: {project_label}", style=MENU_STYLE)
if paths.external:
console.print(" external:", style=MENU_STYLE)
for entry in paths.external:
console.print(f" - {entry}", style="dim")
else:
console.print(" external: []", style=MENU_STYLE)

order = " → ".join(paths.resolution_order)
console.print(f" resolution.order: {order}", style=MENU_STYLE)
console.print(
f" legacy.honor_skillware_skill_path: {paths.honor_skillware_skill_path}",
style=MENU_STYLE,
)
console.print()

if config.extra:
console.print(Text("Other sections (reserved)", style=f"bold {TABLE_STYLE}"))
for key in sorted(config.extra):
console.print(f" {key}: (present, not applied yet)", style="dim")
console.print()

console.print(
" • Flat-layout skills (<root>/<name>/) load but may not appear in list",
"Bundled registry is always included and cannot be removed via config.",
style="dim",
)
console.print("Edit YAML manually to change settings.", style="dim")
return 0


Expand Down Expand Up @@ -743,6 +814,7 @@ def cmd_help(console=None) -> None:
console.print(" skillware test <category/name> — run one skill bundle test")
console.print(" skillware test --category <n> — run tests for a category")
console.print(" skillware paths — show skill root resolution")
console.print(" skillware config show — show merged configuration")
console.print(" skillware doctor — check deps and skill.py import")
console.print(" skillware doctor <id> — diagnose one skill")
console.print(" skillware doctor --category — diagnose a category")
Expand All @@ -754,6 +826,7 @@ def cmd_help(console=None) -> None:
console.print(" examples available now", style=ID_STYLE)
console.print(" test available now", style=ID_STYLE)
console.print(" paths available now", style=ID_STYLE)
console.print(" config available now (read-only)", style=ID_STYLE)
console.print(" doctor available now", style=ID_STYLE)
console.print()

Expand All @@ -771,6 +844,7 @@ def cmd_help(console=None) -> None:
console.print(" skillware examples compliance/tos_evaluator", style=MENU_STYLE)
console.print(" skillware test finance/wallet_screening", style=MENU_STYLE)
console.print(" skillware paths", style=MENU_STYLE)
console.print(" skillware config show", style=MENU_STYLE)
console.print(" skillware doctor --category compliance", style=MENU_STYLE)
console.print()

Expand Down Expand Up @@ -1041,6 +1115,16 @@ def main() -> None:
help="Diagnose all skills in a category.",
)

config_parser = subparsers.add_parser(
"config",
help="Show merged Skillware configuration (read-only).",
)
config_subparsers = config_parser.add_subparsers(dest="config_command")
config_subparsers.add_parser(
"show",
help="Print merged global and project YAML settings.",
)

args = parser.parse_args()

if args.help and args.command is None:
Expand Down Expand Up @@ -1076,6 +1160,11 @@ def main() -> None:
category=args.category,
)
)
elif args.command == "config":
if args.config_command == "show":
raise SystemExit(cmd_config_show())
config_parser.print_help()
raise SystemExit(2)
else:
cmd_interactive(parser=parser)

Expand Down
Loading
Loading