Discover and use Skills
Discover and materialize Skills from files or Environments, and make them available to a Run.
Scan Skills directly through a FileOperator or through an entered Environment:
| Host situation | API | Result | Consistency owner |
|---|---|---|---|
A CLI or embedded process directly controls one FileOperator | SkillManager.scan(files=...) | tuple[SkillCatalogItem, ...] | The caller keeps the operator's namespace stable |
A Host uses an entered BoundEnvironment | SkillManager.scan_environment(environment=...) | BoundSkillCatalog | The manager pins mount incarnations; the Host checks that the catalog is still current before it uses a catalog path later |
Both modes use the same SkillSource, SkillMaterializer, frontmatter parser, limits, conflict policy, and path-containment checks. Neither mode scans a home directory, installed package, or sibling directory implicitly.
Design
The design separates four responsibilities:
FileSkillSourcediscovers bounded metadata below explicitly configured FileOperator roots.SkillMaterializeris trusted Host code that publishes managed packages beneath one configured root.SkillManagerperforms materialization, discovery, validation, conflict resolution, and optional Environment mount binding without depending on an Agent Run.SkillsCapabilityadapts one bound catalog into Run selection, model instructions, resolvedSkillPathvalues, access observations, and model/tool-boundary stale checks.
The two APIs have exact, non-overlapping behavior:
scan(files=...)uses exactly the supplied FileOperator and never constructs or probes an Environment;scan_environment(environment=...)uses only mount-incarnation-pinned scopes and never retries through the unpinnedenvironment.filesfacade or direct scan mode;- a source reads only its configured roots and never tries the process working directory, home directory, package locations, or alternate directories;
- a relevant route change raises
skill_catalog_stale; it never triggers an automatic rescan or retarget; FileSkillSource,SkillSource.roots, andSkillManager.rootsare the complete source/root surface.
required=False is explicit source policy rather than fallback discovery. It can omit that exact missing, unroutable, or unsupported root, but it never substitutes another path.
Read Concurrency and Limits
Built-in scans use up to eight read workers while preserving source precedence and name-sorted results.
max_entries_per_root defaults to 256; SkillsPolicy.max_skills defaults to 512 per source and final catalog. Oversized catalogs fail instead of truncating.
Skill Package Layout
A configured root can itself be a Skill package, and each immediate child directory can be one Skill package. Discovery does not recurse beyond that level.
.agents/skills/
├── SKILL.md
├── code-review/
│ ├── SKILL.md
│ └── checklist.md
└── release/
└── SKILL.mdEach SKILL.md starts with bounded YAML frontmatter:
---
name: code-review
description: Review a code change for correctness and maintainability.
---
# Code review
Follow the repository review workflow.name is the conflict and Run-selection identity. Skill content is untrusted model context; it grants no tools, credentials, filesystem access, plugin loading, or package authority.
Scan a Direct FileOperator
For direct scanning, use canonical absolute roots in the FileOperator's namespace. A local operator rooted at the project uses /.agents/skills, not /workspace/.agents/skills:
from a13n_harness.capabilities import (
FileSkillSource,
SkillCatalogItem,
SkillManager,
)
from a13n_harness.environment import FileOperator
async def scan_cli_skills(
files: FileOperator,
) -> tuple[SkillCatalogItem, ...]:
manager = SkillManager(
(
FileSkillSource(
"project",
("/.agents/skills",),
required=False,
),
)
)
return await manager.scan(files=files)Keep the operator's namespace stable until you finish using the returned paths. Use Environment-aware scanning when mounts can change.
SkillManager.default() uses /workspace/.agents/skills in Environment-backed Runs. For explicit mount roots or direct FileOperators, provide a manager with roots in that namespace.
Scan an Entered Environment
Use Environment-aware scanning when paths can route through /workspace or /environment/{name} and mounts can change while the Host is active:
from a13n_harness.capabilities import (
BoundSkillCatalog,
SkillManager,
)
from a13n_harness.environment.advanced import BoundEnvironment
async def scan_environment_skills(
environment: BoundEnvironment,
) -> BoundSkillCatalog:
manager = SkillManager.default()
catalog = await manager.scan_environment(environment=environment)
# Call again immediately before consuming catalog paths after any await.
catalog.require_current(environment)
return catalogscan_environment() pins configured routes while reading and returns exact paths. It checks that those routes remain current before returning.
A BoundSkillCatalogItem contains:
name,description,path, andsource_id;directory, the exact resolved Skill directory;document, the exact resolvedSKILL.mdpath;directory.mount_idanddocument.mount_id, the opaque mount incarnation used during scanning;observed_generation, the provider generation held during scanning.
Call catalog.require_current(environment) before reusing catalog paths. A changed relevant mount, generation, or route fails with skill_catalog_stale; unrelated mounts do not invalidate it.
EnvironmentPath is valid only in the entered Environment. Import packages into Host-owned immutable storage for durable use.
Add Explicit Sources
Retain the canonical /workspace/.agents/skills source and append Host roots with normal later-source precedence:
from a13n_harness.capabilities import (
FileSkillSource,
SkillManager,
)
manager = SkillManager.default(
additional_sources=(
FileSkillSource(
"organization",
("/environment/shared/skills",),
required=True,
),
)
)An explicit manager replaces the defaults. Set SkillsPolicy(conflict="error") to reject duplicate names instead of applying source precedence.
With required=False, each missing, unroutable, or unsupported root is skipped independently. Permission denial, malformed paths or frontmatter, provider failures, and catalog overflow remain errors.
Materialize Managed Skills
A trusted Host adapter can implement SkillMaterializer to populate one configured source root before scanning:
from a13n_harness.environment import FileOperator
class ManagedSkillMaterializer:
materializer_id = "managed-snapshot"
target_root = "/workspace/.agents/skills"
async def materialize(self, *, files: FileOperator) -> None:
# Verify the Host-owned package manifest and digest first.
# Then publish only beneath target_root through `files`.
...Set target_root to one configured source root and materialize beneath it. The FileOperator or Environment supplies access permissions.
Use Skills in a Harness Run
Pass the manager to SkillsCapability to expose the catalog and its paths to the Agent:
from a13n_harness import RunBindings
from a13n_harness.capabilities import (
SkillsCapability,
)
skills = SkillsCapability(manager)
bindings = RunBindings.embedded(
environment=environment_binding,
skill_selection=frozenset({"code-review", "release"}),
)Selection is exact and fresh for each root, resumed, or child Run:
- leave
RunBindings.skill_selection=Noneto expose the complete conflict-resolved catalog; - provide a non-empty set to expose only those names;
- provide an empty set to inject no Skill instructions or paths;
- an unknown name fails Run preparation with
skill_selection_unknown.
Selection is not portable HarnessState and grants no source or file authority. If a relevant Environment route changes, the current Run fails with skill_catalog_stale; it does not silently rescan or retarget frozen instructions.
Host Responsibilities
The Host manages source authorization, package validation, and durable storage.
To import a package:
- Scan through
scan_environment()and select an item. - Check the catalog remains current; validate content through the same Environment.
- Copy to Host-owned immutable storage and validate the copy before publication.