# iii-directory

> Engine introspection, workers registry proxy, filesystem-backed skills, system prompts, and agent profiles, plus lexical function search with a conditional pre-generate hint.

| field | value |
|-------|-------|
| version | 1.2.4 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, i686-pc-windows-msvc, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=iii-directory@1.2.4
```

## dependencies

- `configuration` @ `0.x`

## readme

# iii-directory

Workers registry HTTP proxy and filesystem-backed skill + prompt
reader for the [iii engine](https://github.com/iii-hq/iii). Every
public function sits under a single `directory::*` namespace, split
into five surfaces (all MCP-agnostic):

| Surface | What clients see | When to use it |
|---|---|---|
| **Skills** (`directory::skills::*`) | Enriched listing via `directory::skills::list` (`{ id, title, type, description, bytes, modified_at }` per row), a single-skill reader `directory::skills::get { id }` returning `{ id, title, type, description, path, body, modified_at }`, and `directory::skills::index` which renders a short per-worker overview document (one `## <title>` + first paragraph + `read more` link per `type: index` skill). Authored by `create`, edited by `update`, removed by `delete`. `title` prefers the YAML frontmatter `title:` (then `name:`) over the body H1; `type` is lifted from frontmatter `type:` (e.g. `index`, `how-to`, `reference`) and serialised as `null` when absent. System-installed agent skills under the read-only `agents_skills_folder` are served too (see [On-disk layout](#on-disk-layout)). | Orientation: "when and why to use my worker's tools" |
| **Prompts** (`directory::prompts::*`) | Command templates listed by `directory::prompts::list`, read by `get`, authored by `create`, edited by `update`. Stored under any `prompts/` path segment; `create` writes `<skills_folder>/prompts/<name>.md`. | Parametric command templates the *user* invokes |
| **System prompts** (`directory::system-prompts::*`) | Identity prompts with the same four verbs and the same response shapes as Prompts — including the `prompts` field name on `list`. Stored under any `system-prompts/` path segment; `create` writes `<skills_folder>/system-prompts/<name>.md`. | What the chat's system-prompt picker offers as an identity prompt (enrich or replace) |
| **Search** (`directory::search_functions`) | One to six external capabilities → compact function-id candidates (installed, plus registry workers under `installable`), with a conditional pre-generate hint pointing agents at it. | "Which functions do I call for this task?" |
| **Registry** (`directory::registry::*`) | HTTP proxy over `api.workers.iii.dev` with `workers::{list,info}`. Rows share the core `name` / `description` / `version` fields with the engine's `engine::workers::list` and add publication metadata (`type`, `config`, `supported_targets`, `total_downloads`, `dependencies`, optional `image`). `workers::list` is cursor-paginated with a server-authored page size. | "What's published in the public registry?" |

Engine introspection (functions / triggers / registered triggers /
workers) is served by the engine natively at
`engine::functions::*`, `engine::triggers::*`,
`engine::registered-triggers::*`, and `engine::workers::*`. Call the
engine ids directly. One wrapper survives for callers that can only
reach the `directory::` namespace: `directory::engine::functions::info`
proxies a single function's schema (see its row below).

Skills and prompts are sourced from a single configured folder on disk
(`skills_folder`). Writes are the **`directory::skills::download*`**
functions, which pull markdown into `skills_folder` from either the
[workers registry](https://workers.iii.dev) or a GitHub repo, plus the
per-kind single-file editors — `directory::skills::{create,update,delete}`,
`directory::prompts::{create,update,delete}` and
`directory::system-prompts::{create,update,delete}`. Once downloaded, files
belong to the developer — edit them however you want, in the editor of
your choice: a change made directly on disk fires the matching
`on-change` with `op: "external"` (see [Custom trigger
types](#custom-trigger-types)).

`directory::registry::workers::*` and the engine's `engine::workers::*`
share the core `name` / `description` / `version` fields so a parser
that touches only those keys works against either surface; the
registry view also surfaces publication metadata (`type`, `config`,
`supported_targets`, `total_downloads`, `dependencies`, optional
`image`) and the engine view adds runtime / connection state.

## Table of contents

1. [Install](#install)
2. [Configuration](#configuration)
3. [Quickstart: download some skills](#quickstart-download-some-skills)
4. [On-disk layout](#on-disk-layout)
5. [Skill ids](#skill-ids)
6. [Functions](#functions)
7. [Function search & pre-generate hint](#function-search--pre-generate-hint)
8. [Custom trigger types](#custom-trigger-types)
9. [Local development & testing](#local-development--testing)
10. [Migration from skills v0.2.x](#migration-from-skills-v02x)

---

## Install

```bash
iii worker add iii-directory
```

`iii worker add` fetches the binary, writes a config block into
`~/.iii/config.yaml`, and the engine starts the worker the next time it boots.

---

## Skills

Install the `iii-directory` agent skill for Claude Code, Cursor, and 30+ other agents:

```bash
npx skills add iii-hq/workers --skill iii-directory
```

Browse or install every worker skill at once:

```bash
npx skills add iii-hq/workers --list
npx skills add iii-hq/workers --all
```

---

## Configuration

Runtime settings live in the **`configuration` worker** under id
**`iii-directory`** (the same pattern `database` and `storage` use). At boot
the worker registers its JSON Schema, reads the live value via
`configuration::get` (the configuration worker env-expands `${VAR}`), and binds
a `configuration` trigger so it re-fetches on change.

Persisted values default to `./data/configuration/iii-directory.yaml` (fs
adapter). Edit that file directly, call `configuration::set id=iii-directory`,
or use the console Workers tab — all three propagate without a redeploy.

### Fields

```yaml
# TOPOLOGY — changing any of these requires a worker restart.
skills_folder: ~/.iii/skills          # read/write root for skills + prompts
local_skills_folder: ./.iii/skills    # project-scoped overrides (whole-namespace local-wins)
agents_skills_folder: ~/.agents/skills # READ-ONLY system-installed agent skills (shallow <skill>/SKILL.md scan)
auto_download: true                   # subscribe to worker-add + run the boot reconcile

# TUNABLE — hot-reload live on `configuration:updated`.
registry_url: https://api.workers.iii.dev   # workers registry base URL
download_timeout_ms: 60000                   # per git-clone / HTTP request timeout (ms)
registry_cache_ttl_ms: 60000                 # in-process TTL for registry::workers::* responses
filter_unregistered: true                    # hide skills whose namespace isn't an installed worker
inject_hint: true                            # bind the directory::pre-generate search-hint hook
hint_min_workers: 2                          # minimum surface width before the hint fires (0 = always)
registry_search: true                        # include installable registry workers in every search
```

The `skills_folder` is created on first download if it doesn't exist.

### Zero-config default + seed

With no seed and no stored value the worker uses built-in defaults
(`skills_folder: ~/.iii/skills`, `registry_url: https://api.workers.iii.dev`).
Pass `--config <path>` to supply a YAML seed: when present and no value is
stored yet, its contents become `initial_value` on `configuration::register`
(see [`config.yaml.example`](config.yaml.example)). Engine-managed deployments
inline the config under the worker entry; the engine delivers it via `--config`.

### Hot reload

On `configuration::set` (or an external edit to the persisted file), the worker
re-fetches the authoritative value. Tunable changes apply in place and the
registry caches are cleared so a repointed `registry_url` takes effect
immediately. Topology changes (`skills_folder` / `local_skills_folder` /
`agents_skills_folder` / `auto_download`) are refused with a "restart
required" log; the previous configuration is kept until the worker restarts.

Both folder settings are also watch roots, and the watcher creates each one at
boot if it is missing (so a fresh install is watched rather than silently
unwatched until the next restart). `agents_skills_folder` is a watch root too,
but only when it already exists — the worker never creates (or writes)
anything under it. Install your first agents skill while the worker is
running and that root stays unwatched until the next restart: reads still
serve it, since every read re-scans disk, but the live `external` doorbell
is missing until then. `local_skills_folder` defaults to the
CWD-relative `./.iii/skills`, so expect an empty `.iii/skills` directory to
appear in whatever working directory the engine launches the worker from. An
empty local root shadows nothing. Because both are restart-required, the watch
roots are fixed for the process lifetime.

---

## Quickstart: download some skills

```bash
# Pull a specific worker's skills + prompts at a fixed semver from
# the registry. Files land under `<skills_folder>/agent-memory/`.
iii trigger --function-id=directory::skills::download \
  --payload='{"worker": "agent-memory", "version": "1.2.3"}'

# Same, but always fetch whatever's tagged `latest` (also the default
# when neither version nor tag is given).
iii trigger --function-id=directory::skills::download \
  --payload='{"worker": "agent-memory"}'

# Pull a single subfolder out of a public GitHub repo via
# `git clone --depth 1 --branch main`. Files land under
# `<skills_folder>/frontend-design/`. The `branch` field defaults to
# `main`; pass `"master"` for older repos that haven't migrated.
iii trigger --function-id=directory::skills::download \
  --payload='{
    "repo": "https://github.com/anthropics/skills",
    "skill": "frontend-design"
  }'
```

The response is
`{ namespace, skills_written, prompts_written, system_prompts_written, source }`
where `skills_written`, `prompts_written`, and `system_prompts_written`
are arrays of relative paths / prompt names that were materialised in
this run.

After every successful download the worker fires the
`directory::skills::on-change`, `directory::prompts::on-change`,
and/or `directory::system-prompts::on-change` trigger types so that
subscribers like the [`mcp`](https://github.com/iii-hq/workers/tree/main/mcp) worker can
forward MCP `notifications/list_changed` to their clients.

---

## On-disk layout

The worker assumes a fixed layout under `skills_folder`:

```text
skills_folder/
  <namespace>/                 # one folder per `directory::skills::download` namespace
    index.md                   # → iii://<namespace>/index
    contacts.md                # → iii://<namespace>/contacts
    emails/send-email.md       # → iii://<namespace>/emails/send-email
    prompts/                   # ← magic marker for command templates
      send-email.md            # ← MCP slash-command (needs YAML frontmatter)
      triage.md
    system-prompts/            # ← magic marker for system prompts
      reviewer.md              # ← identity prompt (needs YAML frontmatter)
  prompts/                     # top level works too — the marker is the
    quick-note.md              #   segment, not its depth
  system-prompts/              # ← where system-prompts::create writes
    pirate.md
```

A second, READ-ONLY root — `agents_skills_folder` (default
`~/.agents/skills`) — serves system-installed agent skills. It is
scanned **shallowly**: only `<skill-dir>/SKILL.md` becomes an entry
(id `<skill-dir>/index`, displayed as `<skill-dir>`); a skill's
`reference/`, `scripts/`, or other support payload is never listed. A
missing directory is silently empty. These skills bypass
`filter_unregistered` (their namespaces are skills, not workers), stay
out of `directory::skills::index` (a per-WORKER surface), and are
refused by `update`/`delete` — edit them with their owning tool, or
copy one into `skills_folder` on disk to fork it. Precedence is
`local_skills_folder` > `skills_folder` > `agents_skills_folder`,
namespace-wise: a top-level namespace directory in a higher root
shadows the same namespace below.

A few rules:

- **Skill ids** are the relative path under `skills_folder` with `.md`
  stripped. Each segment must satisfy `[a-z0-9_-]{1,64}`.
- **Skill frontmatter is optional.** When present, the reader honours
  three keys: `title:` (used by `directory::skills::list` and
  `directory::skills::get` in preference to a body `# H1`), `name:`
  (title fallback when `title:` is absent — the `~/.agents/skills`
  convention), and `type:` (free-form classifier surfaced verbatim on
  both responses). Any other YAML keys are ignored.
- **Prompts** live under any `*/prompts/*.md` path. They must start with
  a YAML frontmatter block declaring at least `description`; `name`
  is optional and overrides the file-stem default.
- **System prompts** live under any `*/system-prompts/*.md` path, with
  the same frontmatter rule as prompts (`description` required, `name`
  optional). A path carrying both a `prompts` and a `system-prompts`
  segment, in either order, is a system prompt — `system-prompts` wins
  precedence so every path classifies as exactly one kind.
- **What a system prompt can do, by design.** The console's chat picker
  can send a selected system prompt with
  `system_prompt_strategy: "override"`, which replaces the harness's
  built-in identity prompt with that file's body verbatim. Files reach
  `system-prompts/` either by local authoring or via
  `directory::skills::download` from a git repo or the registry, so a
  downloaded bundle can supply one. This is an accepted property, not a
  hole: it takes a deliberate selection in the UI, the same UI already
  accepts arbitrary typed text, and the operator owns `skills_folder`.
  Worth knowing before you point `skills_folder` at a directory other
  people can write to.
- Files anywhere else (i.e. *not* in a `prompts/` or `system-prompts/`
  segment) are skills.

The download function namespaces by source:

| Source | Destination |
|---|---|
| `repo=URL skill=NAME branch?=main` | `<skills_folder>/<NAME>/...` |
| `worker=NAME version=…` | `<skills_folder>/<NAME>/...` |
| `worker=NAME tag=…` (default `tag=latest`) | `<skills_folder>/<NAME>/...` |

Re-pulling the same source overwrites files **file-by-file** —
existing siblings outside the response set are preserved (so
hand-edited additions survive a re-pull).

---

## Skill ids

Skills are addressed by their relative path under `skills_folder` with
`.md` stripped — e.g. `<skills_folder>/agent-memory/observe.md` →
id `"agent-memory/observe"`. The same string is what
`directory::skills::list` returns and what `directory::skills::get`
expects in `{ "id": ... }`. The legacy `iii://{id}` link form is still
accepted on `get` (the prefix is auto-stripped), but the worker no
longer parses any other `iii://` URI shape — bodies are read solely by
id, and the auto-rendered tree-shaped index that previous releases
served at `iii://directory/skills` is gone. Consumers that want a
tree-shaped picker iterate `list` rows themselves and indent by
`id.matches('/').count()`.

---

## Functions

Twenty-two functions, all under `directory::*`. All registrations are
namespace-clean; this worker is intentionally agnostic to MCP and any
other adapter.

### `directory::skills::*` (filesystem reader + editor)

| Function ID | Description |
|---|---|
| `directory::skills::download` | Pull markdown into `skills_folder`. Flexible alias accepting either source set: `{repo, skill, branch?}` (defaults `branch=main`) or `{worker, version?\|tag?}` (defaults `tag=latest`). Prefer the two explicit forms below so the source is unambiguous. |
| `directory::skills::download_from_repo` | Repo-only form: `{repo, skill, branch?}`. Copies one skill folder out of a GitHub repo, classifying each written file as a skill, a command template (`prompts/`), or a system prompt (`system-prompts/`). |
| `directory::skills::download_from_registry` | Registry-only form: `{worker, version?\|tag?}`. Installs a published worker's bundle from `api.workers.iii.dev`. |
| `directory::skills::list` | Enriched listing of every fs-backed skill: `{ id, title, type, description, bytes, modified_at }` per row. `title` prefers the YAML frontmatter `title:` over the body H1, `type` is lifted from frontmatter `type:` (`null` when absent), and `description` is the first paragraph of the body — so consumers can render a picker without a follow-up `get` per row. |
| `directory::skills::get` | Fetch one skill by id. Returns `{ id, title, type, description, path, body, modified_at }` — same shape `directory::skills::list` rows use, plus the raw markdown `body` and the absolute on-disk `path` (its parent directory is the skill's base directory, where payload like `scripts/` and `reference/` lives — meaningful only to callers on this worker's machine). Same title-resolution and `type` precedence as `list`. Accepts a bare id or the same id prefixed with `iii://`. Pass `raw: true` to additionally get the FULL on-disk file (frontmatter included) as `raw` — the round-trip form `update` takes. |
| `directory::skills::update` | Overwrite one EXISTING skill file with new full-file content: `{ id, content }` where `content` is the edited `raw` from `get { raw: true }`. Validated against the read invariants (size cap, non-empty body after frontmatter); atomic write; fans out `directory::skills::on-change` with `op: "update"`. Never creates files (use `create`). Refuses read-only system-installed skills under `agents_skills_folder` (`D116`). |
| `directory::skills::create` | Create a NEW skill file at `<skills_folder>/<id>.md` from full-file content: `{ id, content }`. Frontmatter is optional (same rules as `update`: size cap, non-empty body). Refuses an `id` that already resolves in the visible set — including the `<id>` → `<id>/index` overview alias and system-installed agents skills — or a target path that already exists on disk (`D114`); an `id` in a namespace reserved by a system-installed agents skill (`D115`); and, while `filter_unregistered` is on, an `id` the visibility filter would immediately hide (`D115`). Atomic write; fans out `directory::skills::on-change` with `op: "create"`. Returns the same shape as `update`. |
| `directory::skills::delete` | Permanently remove one EXISTING skill file by `{ id }` (same id forms as `get`). Resolves against the same visible set as `list`/`get`, refuses read-only system-installed skills under `agents_skills_folder` (`D116`), removes the file plus any parent directories left empty (so a deleted namespace can't keep shadowing a lower-precedence root), and fans out `directory::skills::on-change` with `op: "delete"`. Returns `{ id }` (the resolved on-disk id). |
| `directory::skills::index` | Render one short markdown entry per installed worker (skills with frontmatter `type: index`). Returns `{ body, workers_count }` where `body` is a ready-to-paste page: `# Skills index`, then one `## <worker title>` heading + the worker's first overview paragraph + a `Read iii://<ns>/index` pointer the agent can follow with `directory::skills::get`. Token-light by design; use `directory::skills::list` for per-skill rows. |

### `directory::prompts::*` (filesystem reader + editor)

| Function ID | Description |
|---|---|
| `directory::prompts::list` | Metadata-only listing of every fs-backed prompt. |
| `directory::prompts::get` | Fetch one prompt's body + `{name, description, modified_at}`. Plain shape, no envelope. Pass `raw: true` to additionally get the FULL on-disk file (frontmatter included) as `raw`. |
| `directory::prompts::update` | Overwrite one EXISTING prompt file with new full-file content: `{ name, content }`. The frontmatter must keep a non-empty `description` (and a valid `name` when declared) — the same rules the scanner enforces. Atomic write; fans out `directory::prompts::on-change` with `op: "update"`. Returns the prompt's effective name after the write. |
| `directory::prompts::create` | Create a NEW command-template prompt file at `<skills_folder>/prompts/<name>.md` from full-file content: `{ name, content }`, where `content` is the FULL file including frontmatter. The frontmatter must carry a non-empty `description` (and a `name` matching the request, when declared) — the same rules `update` enforces. Refuses a `name` that already exists anywhere in the merged command-prompt scan, and a target path that already exists on disk even if the scanner would skip it. Atomic write; fans out `directory::prompts::on-change` with `op: "create"`. Returns `{ name, description, bytes, modified_at }`. |
| `directory::prompts::delete` | Permanently remove one EXISTING command-template prompt file by `{ name }`. Resolves against the same merged scan as `list`/`get`, fans out `directory::prompts::on-change` with `op: "delete"`, and returns `{ name }`. |

### `directory::system-prompts::*` (filesystem reader + editor)

| Function ID | Description |
|---|---|
| `directory::system-prompts::list` | Metadata-only listing of every fs-backed system prompt. |
| `directory::system-prompts::get` | Fetch one system prompt's body + `{name, description, modified_at}`. Plain shape, no envelope. Pass `raw: true` to additionally get the FULL on-disk file (frontmatter included) as `raw`. |
| `directory::system-prompts::update` | Overwrite one EXISTING system prompt file with new full-file content: `{ name, content }`. The frontmatter must keep a non-empty `description` (and a valid `name` when declared) — the same rules the scanner enforces. Atomic write; fans out `directory::system-prompts::on-change` with `op: "update"`. Returns the system prompt's effective name after the write. |
| `directory::system-prompts::create` | Create a NEW system prompt file at `<skills_folder>/system-prompts/<name>.md` from full-file content: `{ name, content }`, where `content` is the FULL file including frontmatter. The frontmatter must carry a non-empty `description` (and a `name` matching the request, when declared) — the same rules `update` enforces. Refuses a `name` that already exists anywhere in the merged system-prompt scan, and a target path that already exists on disk even if the scanner would skip it. Atomic write; fans out `directory::system-prompts::on-change` with `op: "create"`. Returns `{ name, description, bytes, modified_at }`. |
| `directory::system-prompts::delete` | Permanently remove one EXISTING system prompt file by `{ name }`. Resolves against the same merged scan as `list`/`get`, fans out `directory::system-prompts::on-change` with `op: "delete"`, and returns `{ name }`. |

### Engine introspection (native, plus one wrapper)

Engine introspection is served natively; call these ids directly — every
one takes the same filters (`prefix`, `search`, `worker`,
`include_internal` where applicable). One wrapper is kept for callers
whose policy only admits the `directory::` namespace:

| Function ID | Description |
|---|---|
| `directory::engine::functions::info` | Thin proxy to `engine::functions::info` for a single `function_id`: request/response schemas, metadata, and registered triggers. The one `directory::engine::*` helper that still exists — reach for it only when you cannot call `engine::*` directly. |

The native ids:

| Function ID | Description |
|---|---|
| `engine::functions::list` | List functions registered with the engine. |
| `engine::functions::info` | Single-function detail: schemas, owning worker. |
| `engine::triggers::list` | List trigger TYPES (the providers, e.g. `http`, `cron`). |
| `engine::triggers::info` | Single trigger-type detail: configuration schema, return schema. |
| `engine::registered-triggers::list` | List trigger INSTANCES (subscriber rows). |
| `engine::registered-triggers::info` | Single registered-trigger detail. |
| `engine::workers::list` | List workers with an open engine WS connection. Daemon-managed providers (`http`, `cron`, `state`) won't appear — call `worker::list` from the supervisor to see those. |
| `engine::workers::info` | One worker's detail by `name`. |

### `directory::registry::*` (workers registry HTTP proxy)

| Function ID | Description |
|---|---|
| `directory::registry::workers::list` | Browse / search published workers in `api.workers.iii.dev`. Optional free-text `search` (matched fuzzy by `pg_trgm`) and opaque `cursor` for pagination; page size is server-authored. Response is `{ workers: [...], pagination: { next_cursor, has_more, page_size } }`. Shares the core `name` / `description` / `version` fields with the engine's `engine::workers::list`. |
| `directory::registry::workers::info` | Full registry detail for one worker. Fans out two parallel registry calls — `GET /w/{slug}` for the worker envelope (publication metadata + readme + functions + triggers) and `GET /w/{slug}/skills` for the skills/prompts tree — and merges them into `{ worker, readme, api_reference, skills_tree }`. The user-facing input still accepts `version:` (semver) or `tag:` (e.g. `latest`); both go on the wire as `?version=…`. |

Both `directory::registry::*` responses are cached in-process for
`registry_cache_ttl_ms` (default 60s).

There is **no** `directory::skills::register` /
`directory::prompts::register` — see
[Migration](#migration-from-skills-v02x) below.

---

## Function search & pre-generate hint

One-shot lexical function search over the live engine catalog, absorbed from
the former `discovery` worker. It returns only compact `{ function_id,
description }` candidates, grouped by worker in rank order. The model chooses
the candidates it needs, then fetches their contracts in one
`engine::functions::info { function_ids: [...] }` call instead of walking the
catalog with `engine::functions::list`.

| Function | Kind | What it does |
|---|---|---|
| `directory::search_functions` | public | `{ capabilities }` → `{ guidance, workers[], installable[]?, latency_ms }`: BM25 rank over the live engine catalog (at most 6 workers / 12 candidates) plus matching NOT-installed registry workers under `installable`. `capabilities` is a required list of one to six non-empty unmet external capability searches. Requests to summarize provided text/content are ignored. |
| `directory::pre-generate` | internal hook | Injects the conditional search hint into a harness generation (at most once per turn). |
| `directory::on-functions-change` | internal | Refreshes the search catalog on the engine's functions-available push. |
| `directory::hint-preview` | internal | The exact hint text per exposure mode, for the configuration UI. |

Ranking pipeline:

1. **Corpus**: the live engine catalog (boot snapshot + push refresh),
   slimmed to name + first description sentence + argument names. `engine::`
   ids, functions published with `metadata.internal`, and the search itself
   never participate — the worker's own public `directory::*` functions are
   searchable like any other capability.
2. **Scoring**: Okapi BM25 (k1 1.2, b 0.75) with the function name indexed at
   3× weight, camelCase segmentation (`presignUrl` → presign + url), a
   22-word grammatical stoplist, conservative plural folding, JSON-key
   stripping from capability text, and a two-distinct-terms minimum match.
3. **Capability queries**: each `capabilities` entry is ranked independently
   against its own leader and is authoritative. Include every currently unmet
   external capability once in the same call. Write every capability in
   English, translating non-English requests while preserving proper names,
   URLs, and function ids. Requests to summarize provided text or content are
   ignored. Results merge round-robin so every capability gets a candidate
   before any gets a rider.
4. **Pruning**: coverage-aware function floor (≥50% of the leader AND full
   term coverage or ≥85% score) drops same-worker family riders; a
   namespace-level floor (40% of the leader) drops trailing workers.
5. **Registry search** (`registry_search`, default on): every call also
   consults the public workers registry in-process with the same capability
   queries plus informative-term retries (all concurrent; verified authors
   only). Candidates merge round-robin across search variants, returning up to
   2 workers / 6 candidates that WOULD match if installed, with `worker::add`
   guidance.
6. **Session memory** (keyed by caller-supplied OTel baggage, fail-open):
   repeat queries omit candidates already delivered.

The pre-generate hook appends one `<discovery_assist>` block pointing the
model at `search_functions`, telling it to derive capabilities from the goal
and current execution state and to batch selected ids through
`engine::functions::info` — at most once per turn, and only when every gate
clears: the function is callable in the surface, no search result is in the
current task window yet, the surface spans at least `hint_min_workers` distinct
workers, the current task (from the latest user message) has no real function
results yet, and it does not already name a callable function id. Measured on
26 pre-existing e2e scenarios: an unconditional hint *induces* redundant
discovery on guided tasks (up to +110% tokens); the gates are what make
default-on affordable. The hook's transcript annotations (`origin.directory`)
carry only coarse outcome/reason and counts.

Knobs (`inject_hint`, `hint_min_workers`, `registry_search`) live in the
`iii-directory` configuration entry and hot-apply — see Configuration.

## Custom trigger types

| Trigger type | Fires when | Payload to subscribers |
|---|---|---|
| `directory::skills::on-change` | After a `directory::skills::download` that wrote at least one skill markdown file, a `directory::skills::update`, `create`, or `delete`, or external (file pasted/edited/deleted directly on disk — including under `agents_skills_folder`) | download: `{ "op": "download", "namespace": "<ns>", "source": "repo" \| "registry" }`; update/create/delete: `{ "op": "<operation>", "namespace": "<ns>", "id": "<id>" }`; external (file pasted/edited/deleted directly on disk): `{ "op": "external" }` |
| `directory::prompts::on-change` | After a `directory::skills::download` that wrote at least one prompt markdown file, a `directory::prompts::update`, `create`, `delete`, or external (file pasted/edited/deleted directly on disk) | download: `{ "op": "download", "namespace": "<ns>", "source": "repo" \| "registry" }`; update/create/delete: `{ "op": "<operation>", "name": "<name>" }`; external (file pasted/edited/deleted directly on disk): `{ "op": "external" }` |
| `directory::system-prompts::on-change` | After a `directory::skills::download` that wrote at least one system prompt markdown file, a `directory::system-prompts::update`, `create`, `delete`, or external file change | download: `{ "op": "download", "namespace": "<ns>", "source": "repo" \| "registry" }`; update/create/delete: `{ "op": "<operation>", "name": "<name>" }`; external: `{ "op": "external" }` |

Dispatches are fire-and-forget (Void), so the write path doesn't
block on downstream latency.

The `external` op comes from a filesystem watch over `skills_folder`,
`local_skills_folder`, and (when it already exists) the read-only
`agents_skills_folder`. It is a doorbell, not a ledger: every read re-scans
disk, so a missed event costs a stale open view until the next call, never
data. A burst coalesces into one event per kind, and this worker's own writes
are suppressed — a `create` or `update` sends its precise op and never an
extra `external`.

**Loop hazard for subscribers.** Suppression covers writes made *through this
worker*. A subscriber that reacts to `{ "op": "external" }` by writing `.md`
files under `skills_folder` by some other route — a shell or coder worker, a
script — is not suppressed and will re-trigger itself. Either write through
`directory::*::update` / `create`, or make the reaction idempotent and gated.

---

## Local development & testing

### Run from source

```bash
# --config is an optional YAML seed (see config.yaml.example); omit it to
# rely on the value stored in the `configuration` worker (or built-in defaults).
cargo run --release -- --url ws://127.0.0.1:49134 --config ./config.yaml.example
```

### Tests

```bash
# Fast, offline — exercises the pure helpers (markdown / id validators
# / fs source) without needing an iii engine.
cargo test --lib

# Full BDD suite — requires an iii engine on ws://127.0.0.1:49134
# (or III_ENGINE_WS_URL). The git-backed download scenarios spin up
# a local fixture repo via `git init`; the registry-backed scenarios
# point a wiremock server at the worker's `registry_url` config.
cargo test

# One feature group at a time. Available tags:
#   @engine  @read  @prompts  @download  @download_repo  @download_registry
cargo test --test bdd -- --tags @download
```

The BDD harness lives under [tests/](tests/). Feature files mirror the
modules in [src/functions/](src/functions/). Step definitions under
[tests/steps/](tests/steps/) drive each feature through the same
`iii.trigger` path the production binary uses.

## api reference

```json
{
  "functions": [
    {
      "description": "Internal: auto-download skills on worker add event.",
      "metadata": {
        "internal": true
      },
      "name": "directory::__on_worker_added",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "`worker` trigger payload for `directory::__on_worker_added`. Only `worker` is read; declared as a struct so the function publishes a typed schema.",
        "properties": {
          "worker": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "WorkerAddedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "WorkerAddedAck",
        "type": "object"
      }
    },
    {
      "description": "Full detail for one engine function: schemas, owning worker, and registered triggers that target it. Proxies to the engine's native engine::functions::info for the core data.",
      "metadata": {},
      "name": "directory::engine::functions::info",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_id": {
            "description": "Fully-qualified function id on the bus (e.g. `sandbox::create`).",
            "type": "string"
          }
        },
        "required": [
          "function_id"
        ],
        "title": "FunctionInfoInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "RegisteredTriggerSummary": {
            "description": "Trigger instance summary for the response envelope.",
            "properties": {
              "config": true,
              "id": {
                "type": "string"
              },
              "trigger_type": {
                "type": "string"
              }
            },
            "required": [
              "config",
              "id",
              "trigger_type"
            ],
            "type": "object"
          }
        },
        "description": "Response shape for `directory::engine::functions::info`.\n\nMirrors the shape of the old `directory::engine::functions::info` but WITHOUT the `how_guide` and `related_skills` fields.",
        "properties": {
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "function_id": {
            "type": "string"
          },
          "metadata": true,
          "registered_triggers": {
            "items": {
              "$ref": "#/definitions/RegisteredTriggerSummary"
            },
            "type": "array"
          },
          "request_schema": true,
          "response_schema": true,
          "worker_name": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "function_id",
          "registered_triggers"
        ],
        "title": "FunctionInfoOutput",
        "type": "object"
      }
    },
    {
      "description": "Internal: the exact search-hint text per exposure mode, for the configuration UI.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "directory::hint-preview",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "HintPreviewRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "The exact hint text per exposure mode, for the configuration UI. The live block's `functions_generation` attribute varies per generation; the preview renders it as 0.",
        "properties": {
          "agent_trigger": {
            "type": "string"
          },
          "native": {
            "type": "string"
          }
        },
        "required": [
          "agent_trigger",
          "native"
        ],
        "title": "HintPreviewResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: reload tunable iii-directory settings from the authoritative configuration when it changes.",
      "metadata": {
        "internal": true
      },
      "name": "directory::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Trigger payload for `directory::on-config-change`. The handler ignores it (it re-fetches from the authoritative configuration); the empty struct exists so the function publishes a typed request schema rather than AnyValue.",
        "title": "OnConfigChangeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: refresh the search catalog after the engine function set changes.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "directory::on-functions-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "event": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnFunctionsChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "AckResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: inject the conditional search hint into one harness generation.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "directory::pre-generate",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ExposeKind": {
            "oneOf": [
              {
                "enum": [
                  "agent_trigger",
                  "native"
                ],
                "type": "string"
              },
              {
                "description": "Forward compatibility: an exposure mode this build does not know gets the mode-neutral call instruction rather than a deserialization error.",
                "enum": [
                  "other"
                ],
                "type": "string"
              }
            ]
          },
          "GeneratePayload": {
            "properties": {
              "expose": {
                "allOf": [
                  {
                    "$ref": "#/definitions/ExposeKind"
                  }
                ],
                "description": "How the harness exposes functions to the model for this generation. Absent on harnesses that predate the field, which only ever exposed through agent_trigger by default."
              },
              "functions_generation": {
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "messages": {
                "items": true,
                "type": "array"
              },
              "system_prompt": {
                "type": "string"
              },
              "tools": {
                "items": {
                  "$ref": "#/definitions/ToolSchema"
                },
                "type": "array"
              }
            },
            "required": [
              "functions_generation",
              "messages",
              "system_prompt",
              "tools"
            ],
            "type": "object"
          },
          "ToolSchema": {
            "description": "One catalog entry: the contract fields search runs on.",
            "properties": {
              "description": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "parameters": true
            },
            "required": [
              "description",
              "name",
              "parameters"
            ],
            "type": "object"
          }
        },
        "properties": {
          "depth": {
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "generate": {
            "$ref": "#/definitions/GeneratePayload"
          },
          "session_id": {
            "type": "string"
          },
          "step": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "turn_id": {
            "type": "string"
          }
        },
        "required": [
          "depth",
          "generate",
          "session_id",
          "step",
          "turn_id"
        ],
        "title": "PreGenerateHookRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "DiscoveryOutcome": {
            "enum": [
              "hint_injected",
              "skipped"
            ],
            "type": "string"
          },
          "DiscoveryPassV1": {
            "description": "The durable transcript row for one hook pass. Coarse by design: outcome, reason, and counts only — no prompts, messages, tool contracts, session ids, or timings ever leave the hook.",
            "properties": {
              "allowed_functions": {
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "functions_generation": {
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "outcome": {
                "$ref": "#/definitions/DiscoveryOutcome"
              },
              "reason": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/DiscoveryReason"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "allowed_functions",
              "functions_generation",
              "outcome"
            ],
            "type": "object"
          },
          "DiscoveryReason": {
            "enum": [
              "search_unavailable",
              "already_searched",
              "narrow_surface",
              "already_operating",
              "task_guided",
              "hint_already_sent"
            ],
            "type": "string"
          },
          "DiscoveryTranscriptAnnotation": {
            "properties": {
              "data": {
                "$ref": "#/definitions/DiscoveryPassV1"
              },
              "summary": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "version": {
                "format": "uint8",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "data",
              "summary",
              "type",
              "version"
            ],
            "type": "object"
          },
          "PreGenerateAnnotations": {
            "properties": {
              "directory": {
                "$ref": "#/definitions/DiscoveryTranscriptAnnotation"
              }
            },
            "required": [
              "directory"
            ],
            "type": "object"
          },
          "PreGenerateMutations": {
            "properties": {
              "system_prompt": {
                "type": "string"
              }
            },
            "required": [
              "system_prompt"
            ],
            "type": "object"
          }
        },
        "properties": {
          "annotations": {
            "$ref": "#/definitions/PreGenerateAnnotations"
          },
          "decision": {
            "type": "string"
          },
          "mutations": {
            "anyOf": [
              {
                "$ref": "#/definitions/PreGenerateMutations"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "annotations",
          "decision"
        ],
        "title": "PreGenerateHookResponse",
        "type": "object"
      }
    },
    {
      "description": "Create a NEW command-template prompt at <skills_folder>/prompts/<name>.md from full-file markdown content (frontmatter block included; a non-empty `description` is required, and a declared frontmatter `name` must match the requested name). Rejects names that already exist anywhere in the merged command-prompt scan, or a target path that already exists on disk (even one the scanner would skip). The write is atomic and fans out directory::prompts::on-change with { op: \"create\" }. Use directory::prompts::update to edit existing prompts.",
      "metadata": {
        "tool": {
          "label": "Create prompt"
        }
      },
      "name": "directory::prompts::create",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL file content, frontmatter block included — the same form `directory::prompts::update` takes. Frontmatter must carry a non-empty `description`; a declared `name` must match the requested name.",
            "type": "string"
          },
          "name": {
            "description": "New prompt name — becomes the file stem (`<skills_folder>/prompts/<name>.md` for command templates, `<skills_folder>/system-prompts/<name>.md` for system prompts). Same charset rules as list/get names; must not collide with an existing prompt of the SAME kind (a command prompt and a system prompt may share a name).",
            "type": "string"
          }
        },
        "required": [
          "content",
          "name"
        ],
        "title": "PromptCreateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "description": {
            "description": "Description parsed from the frontmatter.",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "name": {
            "description": "The created prompt's name (as requested).",
            "type": "string"
          }
        },
        "required": [
          "bytes",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptCreateOutput",
        "type": "object"
      }
    },
    {
      "description": "Permanently delete one EXISTING filesystem-backed command-template prompt by name. Resolves against the same merged scan as directory::prompts::list, removes only that prompt's markdown file, and fans out directory::prompts::on-change with { op: \"delete\" }.",
      "metadata": {
        "tool": {
          "label": "Delete prompt"
        }
      },
      "name": "directory::prompts::delete",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "description": "Existing prompt name, as returned by the matching list function for this kind (`directory::prompts::list` / `directory::system-prompts::list`).",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptDeleteInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "description": "Name of the prompt whose file was removed.",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptDeleteOutput",
        "type": "object"
      }
    },
    {
      "description": "Fetch one filesystem-backed command-template prompt by name. Returns the raw markdown body plus name, description, and modified_at — no envelope, no templating.",
      "metadata": {},
      "name": "directory::prompts::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "type": "string"
          },
          "raw": {
            "default": null,
            "description": "When `true`, the response includes the FULL on-disk file content (frontmatter block included) as `raw`. For editors that need to round-trip the exact file (`directory::prompts::update` takes the same full-file form); agent readers should leave this unset and use `body`.",
            "type": [
              "boolean",
              "null"
            ]
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptGetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body": {
            "description": "Raw markdown body (post-frontmatter) from disk.",
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime as RFC 3339.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "raw": {
            "description": "FULL on-disk file content (frontmatter included). Present only when the request set `raw: true` — the exact string to hand back to `directory::prompts::update`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "body",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptGetOutput",
        "type": "object"
      }
    },
    {
      "description": "List filesystem-backed command-template prompts (name, description, modified_at) from skills_folder (`prompts/` path segment).",
      "metadata": {},
      "name": "directory::prompts::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ListPromptsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PromptEntry": {
            "properties": {
              "description": {
                "type": "string"
              },
              "modified_at": {
                "description": "File mtime as RFC 3339.",
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "modified_at",
              "name"
            ],
            "type": "object"
          }
        },
        "properties": {
          "prompts": {
            "items": {
              "$ref": "#/definitions/PromptEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "prompts"
        ],
        "title": "ListPromptsOutput",
        "type": "object"
      }
    },
    {
      "description": "Overwrite one EXISTING filesystem-backed prompt with new full-file markdown content. The frontmatter must keep a non-empty `description` (and a valid `name` when it declares one) — the same rules the scanner enforces, so an update can never produce a file the next directory::prompts::list would skip. The write is atomic and fans out directory::prompts::on-change with { op: \"update\" }. Returns the prompt's effective name after the write (frontmatter `name:` wins over the file stem). Command templates only — system prompts have their own directory::system-prompts::* family.",
      "metadata": {
        "tool": {
          "label": "Update prompt"
        }
      },
      "name": "directory::prompts::update",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL new file content, frontmatter block included. The frontmatter must keep a non-empty `description` — a prompt without one would be skipped by the next scan.",
            "type": "string"
          },
          "name": {
            "description": "Prompt name to overwrite, as returned by the matching list for this kind (`directory::prompts::list` for command templates, `directory::system-prompts::list` for system prompts). The target file must already exist.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "name"
        ],
        "title": "PromptUpdateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "description": {
            "description": "Description parsed from the new frontmatter.",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "name": {
            "description": "The prompt's EFFECTIVE name after the write: the new frontmatter `name:` when declared, otherwise the file stem. Differs from the input name when the update renames the prompt.",
            "type": "string"
          }
        },
        "required": [
          "bytes",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptUpdateOutput",
        "type": "object"
      }
    },
    {
      "description": "Fetch full registry metadata for one worker: worker envelope (same core fields as the engine's `engine::workers::list` row shape, plus registry-only `type` / `config` / `supported_targets` / `total_downloads` / `dependencies` / `image`), readme, full API reference (functions + triggers schemas), and the tree of skill / prompt file paths fetched from the registry's /w/{slug}/skills endpoint. Pass either `version` or `tag` (defaults to tag=\"latest\"). Results are cached for `registry_cache_ttl_ms`.",
      "metadata": {},
      "name": "directory::registry::workers::info",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "`directory::registry::workers::info` input. Pass either `version` or `tag`; if neither is provided we fall back to `tag: \"latest\"`.",
        "properties": {
          "name": {
            "description": "Worker name in the registry (e.g. `\"resend\"`).",
            "type": "string"
          },
          "tag": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "default": null,
            "description": "Mutually exclusive with `tag`. If neither is provided we fall back to `tag: \"latest\"`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "name"
        ],
        "title": "WorkerInfoInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ApiReference": {
            "properties": {
              "functions": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/ApiReferenceFunction"
                },
                "type": "array"
              },
              "triggers": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/ApiReferenceTrigger"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "ApiReferenceFunction": {
            "properties": {
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "metadata": true,
              "name": {
                "type": "string"
              },
              "request_schema": true,
              "response_schema": true
            },
            "required": [
              "name"
            ],
            "type": "object"
          },
          "ApiReferenceTrigger": {
            "properties": {
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "invocation_schema": true,
              "metadata": true,
              "name": {
                "type": "string"
              },
              "return_schema": true
            },
            "required": [
              "name"
            ],
            "type": "object"
          },
          "Dependency": {
            "description": "Worker dependency entry. Mirrors the `Dependency` schema in `openapi.yaml`.",
            "properties": {
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "name",
              "version"
            ],
            "type": "object"
          },
          "SkillsTree": {
            "properties": {
              "prompts": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/SkillsTreePrompt"
                },
                "type": "array"
              },
              "skills": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/SkillsTreeSkill"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "SkillsTreePrompt": {
            "properties": {
              "description": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "name"
            ],
            "type": "object"
          },
          "SkillsTreeSkill": {
            "properties": {
              "path": {
                "type": "string"
              }
            },
            "required": [
              "path"
            ],
            "type": "object"
          },
          "Worker": {
            "description": "Shared worker envelope used by both `directory::registry::workers::list` rows and the `worker` field of `directory::registry::workers::info`. Field names match the OpenAPI `WorkerListItem` schema. The shared core fields (`name`, `description`, `version`) line up with the engine's `engine::workers::list` row shape so callers learn one envelope across local + registry surfaces.",
            "properties": {
              "author": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/WorkerAuthor"
                  },
                  {
                    "type": "null"
                  }
                ],
                "default": null
              },
              "config": {
                "default": {},
                "description": "Free-form runtime configuration block from the publish payload."
              },
              "dependencies": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/Dependency"
                },
                "type": "array"
              },
              "description": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "image": {
                "description": "Container image tag, populated only for `type=image` workers.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "repo": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "supported_targets": {
                "default": [],
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "total_downloads": {
                "default": 0,
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "type": {
                "default": null,
                "description": "Worker kind — `binary`, `image`, or `engine`.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "version": {
                "default": null,
                "description": "Latest published version (worker-list) or the resolved version (worker-info, when called with `version` / `tag`).",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "name"
            ],
            "type": "object"
          },
          "WorkerAuthor": {
            "description": "Author block for a published worker. Field names match the `WorkerAuthor` schema in `openapi.yaml` (`pfp`, `verified`).",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "pfp": {
                "default": null,
                "description": "Profile picture URL. `null` when the author hasn't uploaded one.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "verified": {
                "default": false,
                "type": "boolean"
              }
            },
            "type": "object"
          }
        },
        "properties": {
          "api_reference": {
            "$ref": "#/definitions/ApiReference"
          },
          "readme": {
            "type": [
              "string",
              "null"
            ]
          },
          "skills_tree": {
            "$ref": "#/definitions/SkillsTree"
          },
          "worker": {
            "allOf": [
              {
                "$ref": "#/definitions/Worker"
              }
            ],
            "description": "Same shape as `directory::registry::workers::list` rows (and the engine's `engine::workers::list` rows for the shared core fields)."
          }
        },
        "required": [
          "api_reference",
          "skills_tree",
          "worker"
        ],
        "title": "WorkerInfoOutput",
        "type": "object"
      }
    },
    {
      "description": "List workers from the public registry (api.workers.iii.dev). Optional free-text `search` is matched fuzzily by the registry; omit it to browse by `total_downloads DESC`. Pagination is cursor-based with a server-authored page size — pass back `pagination.next_cursor` as `cursor` to fetch the next page. Shares the core `name` / `description` / `version` fields with the engine's `engine::workers::list`. Results are cached for `registry_cache_ttl_ms` (default 60s).",
      "metadata": {},
      "name": "directory::registry::workers::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "`directory::registry::workers::list` input. Mirrors the engine's `engine::workers::list` search input so callers can switch between local and registry surfaces without re-learning the API. Adds `cursor` for paging because the registry is paged (server-authored page size — the client cannot override it).",
        "properties": {
          "cursor": {
            "default": null,
            "description": "Opaque cursor returned by a previous call's `pagination.next_cursor`. Pass back verbatim to fetch the next page; omit (or pass `null`) to fetch the first page.",
            "type": [
              "string",
              "null"
            ]
          },
          "search": {
            "default": null,
            "description": "Optional free-text query. Forwarded to the registry as `?search=…`; the registry ranks results by `pg_trgm` similarity against `lower(name)` and `lower(description)`. When omitted, results are ordered by `total_downloads DESC`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "WorkerListInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Dependency": {
            "description": "Worker dependency entry. Mirrors the `Dependency` schema in `openapi.yaml`.",
            "properties": {
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "name",
              "version"
            ],
            "type": "object"
          },
          "Pagination": {
            "description": "Pagination envelope returned alongside a worker-list page. Mirrors the OpenAPI `Pagination` schema.",
            "properties": {
              "has_more": {
                "default": false,
                "type": "boolean"
              },
              "next_cursor": {
                "default": null,
                "description": "Opaque cursor for the next page. `null` on the last page.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "page_size": {
                "default": 0,
                "description": "Server-authored page size. The client cannot override this.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "Worker": {
            "description": "Shared worker envelope used by both `directory::registry::workers::list` rows and the `worker` field of `directory::registry::workers::info`. Field names match the OpenAPI `WorkerListItem` schema. The shared core fields (`name`, `description`, `version`) line up with the engine's `engine::workers::list` row shape so callers learn one envelope across local + registry surfaces.",
            "properties": {
              "author": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/WorkerAuthor"
                  },
                  {
                    "type": "null"
                  }
                ],
                "default": null
              },
              "config": {
                "default": {},
                "description": "Free-form runtime configuration block from the publish payload."
              },
              "dependencies": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/Dependency"
                },
                "type": "array"
              },
              "description": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "image": {
                "description": "Container image tag, populated only for `type=image` workers.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "repo": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "supported_targets": {
                "default": [],
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "total_downloads": {
                "default": 0,
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "type": {
                "default": null,
                "description": "Worker kind — `binary`, `image`, or `engine`.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "version": {
                "default": null,
                "description": "Latest published version (worker-list) or the resolved version (worker-info, when called with `version` / `tag`).",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "name"
            ],
            "type": "object"
          },
          "WorkerAuthor": {
            "description": "Author block for a published worker. Field names match the `WorkerAuthor` schema in `openapi.yaml` (`pfp`, `verified`).",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "pfp": {
                "default": null,
                "description": "Profile picture URL. `null` when the author hasn't uploaded one.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "verified": {
                "default": false,
                "type": "boolean"
              }
            },
            "type": "object"
          }
        },
        "properties": {
          "pagination": {
            "$ref": "#/definitions/Pagination"
          },
          "workers": {
            "items": {
              "$ref": "#/definitions/Worker"
            },
            "type": "array"
          }
        },
        "required": [
          "pagination",
          "workers"
        ],
        "title": "WorkerListOutput",
        "type": "object"
      }
    },
    {
      "description": "Search available functions for one to six external capabilities; returns compact function-id candidates grouped by worker.",
      "metadata": {},
      "name": "directory::search_functions",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "capabilities": {
            "description": "One to six non-empty capability searches derived from the goal and current execution state. For one search at each decision point, include all unmet external capabilities once. Exclude intrinsic reasoning, summarization, planning, or formatting, and do not repeat needs already represented or satisfied. Requests to summarize provided text or content are ignored. Write every entry in English, translating non-English user requests while preserving proper names, URLs, and function IDs.",
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "maxItems": 6,
            "minItems": 1,
            "type": "array",
            "uniqueItems": true
          }
        },
        "required": [
          "capabilities"
        ],
        "title": "SearchFunctionsRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "FunctionCandidate": {
            "properties": {
              "description": {
                "description": "First description sentence, capped at 160 bytes.",
                "type": "string"
              },
              "function_id": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "function_id"
            ],
            "type": "object"
          },
          "InstallCall": {
            "description": "A ready-made `worker::add` target and payload. Under agent-trigger exposure, the caller still supplies the wrapper's user-facing `description`.",
            "properties": {
              "function": {
                "type": "string"
              },
              "payload": true
            },
            "required": [
              "function",
              "payload"
            ],
            "type": "object"
          },
          "InstallableWorker": {
            "description": "A registry worker that is NOT installed but carries functions matching the requested capabilities. `name` is the registry slug `worker::add` installs.",
            "properties": {
              "description": {
                "type": "string"
              },
              "functions": {
                "description": "Compact candidates only. After installation, search again and fetch selected contracts through `engine::functions::info`.",
                "items": {
                  "$ref": "#/definitions/FunctionCandidate"
                },
                "type": "array"
              },
              "install": {
                "allOf": [
                  {
                    "$ref": "#/definitions/InstallCall"
                  }
                ],
                "description": "The `worker::add` target and payload; agent-trigger callers add `description`."
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "functions",
              "install",
              "name",
              "version"
            ],
            "type": "object"
          },
          "SearchWorker": {
            "properties": {
              "functions": {
                "items": {
                  "$ref": "#/definitions/FunctionCandidate"
                },
                "type": "array"
              },
              "namespace": {
                "type": "string"
              }
            },
            "required": [
              "functions",
              "namespace"
            ],
            "type": "object"
          }
        },
        "properties": {
          "guidance": {
            "type": "string"
          },
          "installable": {
            "description": "Matching registry workers from verified authors. Their functions are NOT callable until the worker is installed.",
            "items": {
              "$ref": "#/definitions/InstallableWorker"
            },
            "type": "array"
          },
          "latency_ms": {
            "format": "double",
            "type": "number"
          },
          "workers": {
            "items": {
              "$ref": "#/definitions/SearchWorker"
            },
            "type": "array"
          }
        },
        "required": [
          "guidance",
          "installable",
          "latency_ms",
          "workers"
        ],
        "title": "SearchFunctionsResponse",
        "type": "object"
      }
    },
    {
      "description": "Create a NEW filesystem-backed skill at <skills_folder>/<id>.md from full-file markdown content (frontmatter optional; only the size cap and a non-empty body are enforced, same as directory::skills::update). Rejects ids that already exist anywhere in the visible skill set (including system-installed agents skills), a target path that already exists on disk (even one the scanner would skip), ids in a namespace reserved by a system-installed agents skill, and — when filter_unregistered is on — ids the visibility filter would immediately hide. The write is atomic and fans out directory::skills::on-change with { op: \"create\" }. Use directory::skills::update to edit existing skills.",
      "metadata": {
        "tool": {
          "label": "Create skill"
        }
      },
      "name": "directory::skills::create",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL file content, frontmatter block included — the same form `directory::skills::update` takes. Frontmatter is optional for skills; only the size cap and a non-empty body are enforced.",
            "type": "string"
          },
          "id": {
            "description": "New skill id — becomes the file path (`<skills_folder>/<id>.md`), so it may contain `/`-separated segments (`[a-z0-9_-]` each). Must not collide with an existing visible skill (including system-installed agents skills) or an on-disk file at the target path.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "id"
        ],
        "title": "SkillCreateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "function_id": {
            "description": "Frontmatter `function_id:` of the content, `null` when absent.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "description": "The created skill's id (normalized form of the input id).",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "title": {
            "description": "Title resolved from the content (frontmatter `title:`, then `name:`, then body H1, then the id) — what `list` rows will show.",
            "type": "string"
          },
          "type": {
            "description": "Frontmatter `type:` of the content, `null` when absent.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bytes",
          "id",
          "modified_at",
          "title"
        ],
        "title": "SkillCreateOutput",
        "type": "object"
      }
    },
    {
      "description": "Permanently delete one EXISTING filesystem-backed skill by id. Accepts the same id forms as directory::skills::get, resolves against the same visible set as directory::skills::list, refuses read-only system-installed skills under agents_skills_folder, removes only that skill's markdown file (plus any parent directories the removal left empty), and fans out directory::skills::on-change with { op: \"delete\" }.",
      "metadata": {
        "tool": {
          "label": "Delete skill"
        }
      },
      "name": "directory::skills::delete",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "description": "Existing skill id — the same forms `directory::skills::get` accepts (bare id, `<id>.md`, `SKILL(S).md`, `iii://<id>`).",
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "title": "SkillDeleteInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "description": "Id of the skill whose file was removed — the RESOLVED on-disk id (e.g. `ns/index` for input `ns`), not the input form.",
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "title": "SkillDeleteOutput",
        "type": "object"
      }
    },
    {
      "description": "Download skills + prompts into skills_folder from EITHER source. Prefer the explicit directory::skills::download_from_registry / directory::skills::download_from_repo, whose schemas can't be mixed up. Pass {repo, skill, branch?} to clone one skill folder from a GitHub repo (branch defaults to \"main\"), or {worker, version?|tag?} to pull from the workers registry (tag defaults to \"latest\"). Specify exactly ONE source set. Files in the destination namespace are overwritten file-by-file.",
      "metadata": {
        "tool": {
          "label": "Download skills"
        }
      },
      "name": "directory::skills::download",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "branch": {
            "default": null,
            "description": "Source A: branch to clone. Defaults to `\"main\"`. Pass `\"master\"` (or any other branch name) for repos whose default branch is not `main`.",
            "type": [
              "string",
              "null"
            ]
          },
          "repo": {
            "default": null,
            "description": "Source A: GitHub repo URL. Pair with `skill`.",
            "type": [
              "string",
              "null"
            ]
          },
          "skill": {
            "default": null,
            "description": "Source A: subfolder under `skills/` inside the repo. Doubles as the destination namespace inside `skills_folder`.",
            "type": [
              "string",
              "null"
            ]
          },
          "tag": {
            "default": null,
            "description": "Source B: registry tag to pull (e.g. `latest`). Mutually exclusive with `version`. Defaults to `\"latest\"` when neither `version` nor `tag` is provided.",
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "default": null,
            "description": "Source B: explicit semver to pull. Mutually exclusive with `tag`.",
            "type": [
              "string",
              "null"
            ]
          },
          "worker": {
            "default": null,
            "description": "Source B: workers registry name. Pair with exactly one of `version` / `tag`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "DownloadInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "skills_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "source": true,
          "system_prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "namespace",
          "prompts_written",
          "skills_written",
          "source",
          "system_prompts_written"
        ],
        "title": "DownloadOutput",
        "type": "object"
      }
    },
    {
      "description": "Download one worker's skills + prompts from the workers registry into skills_folder. `worker` is required; pass either `version` (exact semver) OR `tag` (e.g. \"latest\", the default when both are omitted), not both. Files in the destination namespace are overwritten file-by-file. A missing worker returns a `D310 not_found` naming the next function to call. To pull from a GitHub repo instead, use directory::skills::download_from_repo.",
      "metadata": {
        "tool": {
          "label": "Download skills (registry)"
        }
      },
      "name": "directory::skills::download_from_registry",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input for `directory::skills::download_from_registry`. The required `worker` field is what makes this function's source unambiguous at the schema level.",
        "properties": {
          "tag": {
            "default": null,
            "description": "Registry tag to pull (e.g. `\"latest\"`). Mutually exclusive with `version`. Defaults to `\"latest\"` when neither is provided.",
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "default": null,
            "description": "Explicit semver to pull. Mutually exclusive with `tag`.",
            "type": [
              "string",
              "null"
            ]
          },
          "worker": {
            "description": "Worker name in the registry (e.g. `\"shell\"`).",
            "type": "string"
          }
        },
        "required": [
          "worker"
        ],
        "title": "RegistryDownloadInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "skills_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "source": true,
          "system_prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "namespace",
          "prompts_written",
          "skills_written",
          "source",
          "system_prompts_written"
        ],
        "title": "DownloadOutput",
        "type": "object"
      }
    },
    {
      "description": "Download one skill folder from a GitHub repo into skills_folder. `repo` (the repo URL) and `skill` (the subfolder under `skills/`, which also names the destination namespace) are required; `branch` defaults to \"main\". The repo URL is validated (https / ssh / git@ only). To pull a published worker instead, use directory::skills::download_from_registry.",
      "metadata": {
        "tool": {
          "label": "Download skills (repo)"
        }
      },
      "name": "directory::skills::download_from_repo",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input for `directory::skills::download_from_repo`. The required `repo` + `skill` fields make this function's source unambiguous at the schema level.",
        "properties": {
          "branch": {
            "default": null,
            "description": "Branch to clone. Defaults to `\"main\"`.",
            "type": [
              "string",
              "null"
            ]
          },
          "repo": {
            "description": "GitHub repo URL (validated: https / ssh / git@ only).",
            "type": "string"
          },
          "skill": {
            "description": "Subfolder under `skills/` inside the repo. Doubles as the destination namespace inside `skills_folder`.",
            "type": "string"
          }
        },
        "required": [
          "repo",
          "skill"
        ],
        "title": "RepoDownloadInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "skills_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "source": true,
          "system_prompts_written": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "namespace",
          "prompts_written",
          "skills_written",
          "source",
          "system_prompts_written"
        ],
        "title": "DownloadOutput",
        "type": "object"
      }
    },
    {
      "description": "Fetch one filesystem-backed skill by id and return its raw markdown body plus id, title, type, function_id, and modified_at. A worker overview is addressed by the bare worker name (e.g. \"iii-sandbox\") — that is the id `list`/`index` hand back. Input is forgiving: \"iii-sandbox/index\", \"iii-sandbox/SKILL.md\", a trailing \".md\", and an iii:// prefix all resolve to the same overview; and if the exact id misses, the worker name is matched case-insensitively as a substring (\"sandbox\" finds \"iii-sandbox\"). `title` prefers frontmatter `title:` over the body H1; `type` is the frontmatter `type:`. There is no `description` field here (the body already opens with that paragraph) — use directory::skills::list for the teaser-only view. On a miss you get a `D110 not_found` message naming the closest ids and the next function to call.",
      "metadata": {
        "tool": {
          "label": "Get skill"
        }
      },
      "name": "directory::skills::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "description": "Skill id (the same string returned by `directory::skills::list`, e.g. `\"directory/skills/list\"`). Two ergonomic variants are also accepted: the file-path form `<id>.md` (the trailing `.md` is stripped) and the legacy `iii://{id}` URI form. Other URI schemes are rejected. The filename `SKILLS.md` is aliased to `index.md` to match the filesystem scanner.",
            "type": "string"
          },
          "raw": {
            "default": null,
            "description": "When `true`, the response includes the FULL on-disk file content (frontmatter block included) as `raw`. For editors that need to round-trip the exact file (`directory::skills::update` takes the same full-file form); agent readers should leave this unset and use `body`.",
            "type": [
              "boolean",
              "null"
            ]
          }
        },
        "required": [
          "id"
        ],
        "title": "SkillGetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body": {
            "description": "Raw markdown body (post-frontmatter) from disk.\n\nNote: there is no `description` field. `description` is the body's first paragraph, which is already inside `body` — every caller asking for the body would otherwise pay for the prefix twice. Use `directory::skills::list` rows when you want the teaser without the full body.",
            "type": "string"
          },
          "function_id": {
            "description": "Frontmatter `function_id:` when present — the canonical bus function id this skill documents (e.g. `sandbox::create`). The response's `id` field is the SKILL path on disk; `function_id` is what the agent should pass to `agent_trigger`. `null` when the skill isn't 1:1 with a single function.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime as RFC 3339.",
            "type": "string"
          },
          "path": {
            "description": "Absolute on-disk path of the skill file. Its parent directory is the skill's base directory — where payload the body references by relative path (`scripts/`, `reference/`, agent-skills convention) lives. Only meaningful to callers sharing this worker's filesystem (shell/file tools on the same machine).",
            "type": "string"
          },
          "raw": {
            "description": "FULL on-disk file content (frontmatter block included). Present only when the request set `raw: true`. This is the exact string to hand back to `directory::skills::update`; fallback notes that `get` may prepend to `body` are never added here.",
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "description": "Frontmatter `title:` when present and non-empty, otherwise the first `# H1` line in the body, otherwise the bare `id`.",
            "type": "string"
          },
          "type": {
            "description": "Frontmatter `type:` (e.g. `index`, `how-to`, `reference`). `null` when the file has no frontmatter or omits the key.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "body",
          "id",
          "modified_at",
          "path",
          "title"
        ],
        "title": "SkillGetOutput",
        "type": "object"
      }
    },
    {
      "description": "Render a per-WORKER overview: one short markdown block per installed worker (each worker's root overview doc `<ns>/index`, whether or not it declares frontmatter `type: index`). Each block is a `## <worker title>` heading, the first paragraph of that worker's overview, and a `directory::skills::get` call to read the full reference. Token-light by design and intended for system-prompt injection; for individual per-SKILL rows call directory::skills::list.",
      "metadata": {},
      "name": "directory::skills::index",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "IndexSkillsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body": {
            "description": "Rendered markdown document — one short `## <title>` block per installed worker (each worker's root overview doc, whether or not it declares frontmatter `type: index`), carrying the worker's first-paragraph overview and a `directory::skills::get` call to read the full reference. Sorted lex by id.",
            "type": "string"
          },
          "workers_count": {
            "description": "Number of worker entries rendered (i.e. the count of worker overview rows that survived the filter). Cheap sanity check that doesn't require re-parsing the body.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "body",
          "workers_count"
        ],
        "title": "IndexSkillsOutput",
        "type": "object"
      }
    },
    {
      "description": "List skills as one row PER SKILL (id, title, type, function_id, description, bytes, modified_at) from skills_folder — use this when you need individual skill ids. A worker overview row's `id` is the bare worker name (e.g. `iii-sandbox`); pass it straight to directory::skills::get. For a per-WORKER overview instead, call directory::skills::index. Filters: `search` (case-insens. substring vs id+title+description), `prefix` (worker-namespace prefix; matches the overview row and its sub-skills), `type` (exact frontmatter type match). Pass `include_description: false` for token-light id+title+type rows (default: descriptions included). `title` prefers frontmatter `title:` over the body H1. Each row's `function_id` is the callable bus id (e.g. `sandbox::create`) — pass THAT to agent_trigger, not the row's `id` (which is a documentation address).",
      "metadata": {},
      "name": "directory::skills::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "include_description": {
            "default": null,
            "description": "When `false`, the response omits the first-paragraph `description` field on every row. Useful for token-light pickers that only need `id` + `title` + `type`. Default `true`.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "prefix": {
            "default": null,
            "description": "Exact prefix match against `id`. Combine with `search` to scope a fuzzy match to one worker namespace, e.g. `prefix: \"sandbox/\"`.",
            "type": [
              "string",
              "null"
            ]
          },
          "search": {
            "default": null,
            "description": "Case-insensitive substring match against `id`, `title`, and (when `include_description` is true) the first body paragraph. Omitted rows are filtered out cheaply on the FsSkill { id } pass before the per-file frontmatter read, so a narrowed list is dramatically cheaper for the caller than the unfiltered one.",
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "default": null,
            "description": "Exact match against the frontmatter `type:` field (`index`, `how-to`, `reference`, ...). `null` for entries with no frontmatter `type:`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "ListSkillsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "SkillEntry": {
            "properties": {
              "bytes": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "description": {
                "description": "First paragraph of the body, empty when the file has only headings. Also empty when the caller passed `list { include_description: false }` for a token-light row.",
                "type": "string"
              },
              "function_id": {
                "description": "Frontmatter `function_id:` when present — the canonical bus function id this skill documents (e.g. `sandbox::create`). The row's `id` field is the SKILL path on disk (e.g. `sandbox/skills/sandbox/create`); `function_id` is what an agent should pass to `agent_trigger`. `null` for skills that aren't 1:1 with a single function (index/reference).",
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "type": "string"
              },
              "modified_at": {
                "description": "File mtime as RFC 3339 (best effort; empty if unavailable).",
                "type": "string"
              },
              "title": {
                "description": "Frontmatter `title:` when present and non-empty, otherwise the first `# H1` line in the body, otherwise the bare `id`.",
                "type": "string"
              },
              "type": {
                "description": "Frontmatter `type:` (e.g. `index`, `how-to`, `reference`). `null` when the file has no frontmatter or omits the key.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "bytes",
              "description",
              "id",
              "modified_at",
              "title"
            ],
            "type": "object"
          }
        },
        "properties": {
          "skills": {
            "items": {
              "$ref": "#/definitions/SkillEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "skills"
        ],
        "title": "ListSkillsOutput",
        "type": "object"
      }
    },
    {
      "description": "Overwrite one EXISTING filesystem-backed skill with new full-file markdown content (frontmatter block included — pass back the `raw` field from directory::skills::get { raw: true }, edited). Accepts the same id forms as `get`. Never creates files (use directory::skills::create to author one, or directory::skills::download to materialize a bundle). Refuses read-only system-installed skills under agents_skills_folder. Content is validated against the read invariants (size cap, non-empty body after frontmatter); the write is atomic and fans out directory::skills::on-change with { op: \"update\" }.",
      "metadata": {
        "tool": {
          "label": "Update skill"
        }
      },
      "name": "directory::skills::update",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL new file content, frontmatter block included — the string `directory::skills::get { raw: true }` returns, edited.",
            "type": "string"
          },
          "id": {
            "description": "Skill id to overwrite — the same forms `directory::skills::get` accepts (bare id, `<id>.md`, `SKILL(S).md`, `iii://<id>`). The target file must already exist; update never creates skills.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "id"
        ],
        "title": "SkillUpdateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "function_id": {
            "description": "Frontmatter `function_id:` of the new content, `null` when absent.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "description": "The id that was updated (normalized form of the input id).",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "title": {
            "description": "Title resolved from the NEW content (frontmatter `title:`, then body H1, then the id) — what `list` rows will now show.",
            "type": "string"
          },
          "type": {
            "description": "Frontmatter `type:` of the new content, `null` when absent.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bytes",
          "id",
          "modified_at",
          "title"
        ],
        "title": "SkillUpdateOutput",
        "type": "object"
      }
    },
    {
      "description": "Create a NEW system prompt at <skills_folder>/system-prompts/<name>.md from full-file markdown content (frontmatter block included; a non-empty `description` is required, and a declared frontmatter `name` must match the requested name). Rejects names that already exist anywhere in the merged system-prompt scan, or a target path that already exists on disk (even one the scanner would skip). The write is atomic and fans out directory::system-prompts::on-change with { op: \"create\" }. Use directory::system-prompts::update to edit existing system prompts.",
      "metadata": {
        "tool": {
          "label": "Create system prompt"
        }
      },
      "name": "directory::system-prompts::create",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL file content, frontmatter block included — the same form `directory::prompts::update` takes. Frontmatter must carry a non-empty `description`; a declared `name` must match the requested name.",
            "type": "string"
          },
          "name": {
            "description": "New prompt name — becomes the file stem (`<skills_folder>/prompts/<name>.md` for command templates, `<skills_folder>/system-prompts/<name>.md` for system prompts). Same charset rules as list/get names; must not collide with an existing prompt of the SAME kind (a command prompt and a system prompt may share a name).",
            "type": "string"
          }
        },
        "required": [
          "content",
          "name"
        ],
        "title": "PromptCreateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "description": {
            "description": "Description parsed from the frontmatter.",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "name": {
            "description": "The created prompt's name (as requested).",
            "type": "string"
          }
        },
        "required": [
          "bytes",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptCreateOutput",
        "type": "object"
      }
    },
    {
      "description": "Permanently delete one EXISTING filesystem-backed system prompt by name. Resolves against the same merged scan as directory::system-prompts::list, removes only that prompt's markdown file, and fans out directory::system-prompts::on-change with { op: \"delete\" }.",
      "metadata": {
        "tool": {
          "label": "Delete system prompt"
        }
      },
      "name": "directory::system-prompts::delete",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "description": "Existing prompt name, as returned by the matching list function for this kind (`directory::prompts::list` / `directory::system-prompts::list`).",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptDeleteInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "description": "Name of the prompt whose file was removed.",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptDeleteOutput",
        "type": "object"
      }
    },
    {
      "description": "Fetch one filesystem-backed system prompt by name. Returns the raw markdown body plus name, description, and modified_at — no envelope, no templating.",
      "metadata": {},
      "name": "directory::system-prompts::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "name": {
            "type": "string"
          },
          "raw": {
            "default": null,
            "description": "When `true`, the response includes the FULL on-disk file content (frontmatter block included) as `raw`. For editors that need to round-trip the exact file (`directory::prompts::update` takes the same full-file form); agent readers should leave this unset and use `body`.",
            "type": [
              "boolean",
              "null"
            ]
          }
        },
        "required": [
          "name"
        ],
        "title": "PromptGetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body": {
            "description": "Raw markdown body (post-frontmatter) from disk.",
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime as RFC 3339.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "raw": {
            "description": "FULL on-disk file content (frontmatter included). Present only when the request set `raw: true` — the exact string to hand back to `directory::prompts::update`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "body",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptGetOutput",
        "type": "object"
      }
    },
    {
      "description": "List filesystem-backed system prompts (name, description, modified_at) from skills_folder (`system-prompts/` path segment).",
      "metadata": {},
      "name": "directory::system-prompts::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ListPromptsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PromptEntry": {
            "properties": {
              "description": {
                "type": "string"
              },
              "modified_at": {
                "description": "File mtime as RFC 3339.",
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "modified_at",
              "name"
            ],
            "type": "object"
          }
        },
        "properties": {
          "prompts": {
            "items": {
              "$ref": "#/definitions/PromptEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "prompts"
        ],
        "title": "ListPromptsOutput",
        "type": "object"
      }
    },
    {
      "description": "Overwrite one EXISTING filesystem-backed system prompt with new full-file markdown content. The frontmatter must keep a non-empty `description` (and a valid `name` when it declares one) — the same rules the scanner enforces, so an update can never produce a file the next directory::system-prompts::list would skip. The write is atomic and fans out directory::system-prompts::on-change with { op: \"update\" }. Returns the system prompt's effective name after the write (frontmatter `name:` wins over the file stem). System prompts only — command templates have their own directory::prompts::* family.",
      "metadata": {
        "tool": {
          "label": "Update system prompt"
        }
      },
      "name": "directory::system-prompts::update",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "FULL new file content, frontmatter block included. The frontmatter must keep a non-empty `description` — a prompt without one would be skipped by the next scan.",
            "type": "string"
          },
          "name": {
            "description": "Prompt name to overwrite, as returned by the matching list for this kind (`directory::prompts::list` for command templates, `directory::system-prompts::list` for system prompts). The target file must already exist.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "name"
        ],
        "title": "PromptUpdateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bytes": {
            "description": "Bytes written.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "description": {
            "description": "Description parsed from the new frontmatter.",
            "type": "string"
          },
          "modified_at": {
            "description": "File mtime after the write, RFC 3339.",
            "type": "string"
          },
          "name": {
            "description": "The prompt's EFFECTIVE name after the write: the new frontmatter `name:` when declared, otherwise the file stem. Differs from the input name when the update renames the prompt.",
            "type": "string"
          }
        },
        "required": [
          "bytes",
          "description",
          "modified_at",
          "name"
        ],
        "title": "PromptUpdateOutput",
        "type": "object"
      }
    },
    {
      "description": "Serve the iii-directory worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "iii-directory::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Fires after a directory::skills::download that wrote at least one command-template prompt, or a directory::prompts::update, create, or delete. Also fires with { op: \"external\" } when a watched prompts root changes on disk outside this worker.",
      "invocation_schema": {},
      "metadata": {},
      "name": "directory::prompts::on-change",
      "return_schema": {}
    },
    {
      "description": "Fires after a directory::skills::download that wrote at least one skill markdown file, or a directory::skills::update, create, or delete. Also fires with { op: \"external\" } when a watched skills root changes on disk outside this worker — including the read-only agents skills root, when that root exists at startup.",
      "invocation_schema": {},
      "metadata": {},
      "name": "directory::skills::on-change",
      "return_schema": {}
    },
    {
      "description": "Fires after a directory::skills::download that wrote at least one system prompt, or a directory::system-prompts::update, create, or delete. Also fires with { op: \"external\" } when a watched system-prompts root changes on disk outside this worker.",
      "invocation_schema": {},
      "metadata": {},
      "name": "directory::system-prompts::on-change",
      "return_schema": {}
    }
  ]
}
```
