Integrate Skill discovery in a Host

Discover and materialize Skills from files or Environments, and make them available to a Run.

a13n-harness keeps Skill discovery reusable outside Agent execution. A Host chooses one of two explicit modes:

Host situationAPIResultConsistency owner
A CLI or embedded process directly controls one FileOperatorSkillManager.scan(files=...)tuple[SkillCatalogItem, ...]The caller keeps the operator's namespace stable
A Host uses an entered EnvironmentSkillManager.scan_environment(environment=...)BoundSkillCatalogThe manager pins mount incarnations; the Host checks catalog currency before later path use

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 workspace implicitly.

Design

The design separates four responsibilities:

  • FileSkillSource discovers bounded metadata below explicitly configured FileOperator roots.
  • SkillMaterializer is trusted Host code that publishes managed packages beneath one declared root.
  • SkillManager performs materialization, discovery, validation, conflict resolution, and optional Environment mount binding without depending on an Agent run.
  • SkillsCapability adapts one bound catalog into run selection, model instructions, resolved SkillPath values, 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 unpinned environment.files facade or direct scan mode;
  • a source reads only its declared roots and never tries the process working directory, home directory, package locations, or alternate workspace paths;
  • a relevant route change raises skill_catalog_stale; it never triggers an automatic rescan or retarget;
  • FileSkillSource, SkillSource.roots, and SkillManager.roots are 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 file discovery and final document validation use at most eight read workers per operation. Materializers, sources, and roots remain sequential; parallel reads do not change source precedence, skipped-entry diagnostic order, or the name-sorted model catalog. With unchanged source configuration and content, read completion order does not change the Skill instruction prefix. Cancellation joins the workers before releasing their file scopes.

Existing size limits remain independent of concurrency: FileSkillSource.max_entries_per_root defaults to 256 listed directory entries, and SkillsPolicy.max_skills defaults to 512 entries per source and in the final resolved catalog. Oversized catalogs fail explicitly rather than silently selecting the first entries. Concurrency is internal; no new Host configuration or cross-run cache is required.

Skill Package Layout

A selected 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.md

Each 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

Use direct scanning when the Host already owns a non-virtual FileOperator and controls its lifetime and retargeting. Roots are canonical absolute paths in that operator's namespace: repeated separators, traversal segments, and a trailing slash other than / are rejected. For example, a root-confined local operator whose / is the project directory 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)

This mode operates directly in the supplied FileOperator namespace and has no Environment mount-routing semantics. Keep that backing namespace stable until every path derived from the returned catalog has been consumed. If another process can replace the backing directory concurrently, provide an operator with the snapshot or locking behavior your Host requires, or use an entered Environment instead.

SkillManager.default() is designed for Environment-backed runs and contains the canonical /workspace/.agents/skills source. A direct FileOperator Host normally constructs an explicit manager with roots in its own 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 import Environment
from a13n_harness.capabilities import (
    BoundSkillCatalog,
    SkillManager,
)


async def scan_environment_skills(
    environment: Environment,
) -> 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 catalog

scan_environment():

  1. captures every configured root with Environment.select_files() before awaiting provider I/O;
  2. opens mount-incarnation-pinned file scopes for those roots;
  3. runs materialization, listing, frontmatter reads, and final SKILL.md validation through the pinned scopes;
  4. resolves every final item to exact directory and document EnvironmentPath values;
  5. verifies that every configured scan route, including empty and conflict-overridden roots, is still current before returning.

A BoundSkillCatalogItem contains:

  • name, description, path, and source_id;
  • directory, the exact resolved Skill directory;
  • document, the exact resolved SKILL.md path;
  • mount_id, the opaque Harness mount incarnation held during scanning;
  • observed_generation, the provider generation held during scanning.

BoundSkillCatalog.require_current(environment) reselects only paths represented by catalog items. Adding or replacing an unrelated Environment mount does not invalidate the catalog. Changing a relevant mount selection, opaque mount ID, provider generation, default route, or resolved provider path raises DefinitionError with code skill_catalog_stale.

Do not persist EnvironmentPath values as durable authority. They describe one entered Environment and are useful only while that Environment remains active. A Host that imports Skill packages should copy and validate package content into its own immutable revision format.

Add Explicit Sources

Retain the canonical workspace 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,
        ),
    )
)

Pass an explicit SkillManager(...) when the Host wants to replace the default composition completely. Source order is deterministic. Configure SkillsPolicy(conflict="error") when duplicate final names must fail rather than use 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 declared 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`.
        ...

target_root must equal one configured source root. It is a composition and provenance contract, not a sandbox wrapper. The materializer is trusted Host code and must stay beneath that root; the supplied FileOperator or Environment remains the actual authority boundary.

Use Skills in a Harness Run

Pass the manager to the definition-selected SkillsCapability. The Capability uses Environment-aware scanning, publishes exact SkillPath values, injects bounded routing instructions, observes ordinary SKILL.md reads, and fences the selected catalog at model and tool boundaries.

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=None to 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 Harness scanner deliberately does not own:

  • source CRUD or enablement;
  • native path authorization;
  • package manifests, digests, signatures, or immutable revisions;
  • symlink and traversal policy for imported package copies;
  • persistence or refresh history;
  • durable execution or retry;
  • credentials, plugins, Capabilities, or tools.

An importing Host should scan through scan_environment(), select one exact item, verify the catalog remains current, enumerate and validate the package through the same entered Environment, copy it into Host-owned immutable storage, and validate the copied package again before publication.

On this page