# 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.3 |
| 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.3
```

## 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 natural-language query → the API reference of only the relevant functions (installed, plus installable 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. One natural-language query returns the API
reference of only the relevant functions, grouped by worker in rank order —
the model's next step is calling them directly instead of walking the engine
catalog with `engine::functions::list`/`info`.

| Function | Kind | What it does |
|---|---|---|
| `directory::search_functions` | public | `{ query }` → `{ guidance, workers[], installable[]?, latency_ms }`: BM25 rank over the live engine catalog (at most 3 workers / 12 contracts) plus matching NOT-installed registry workers under `installable`. |
| `directory::pre-generate` | internal hook | Injects the conditional search hint into a harness generation (at most once per session). |
| `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 the query, and a two-distinct-terms minimum match.
3. **Multi-intent queries**: clauses split on list punctuation and "and"
   (only when both sides keep two informative terms) are ranked on their own
   leaders with a fair share of the function cap.
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 (full query, then
   informative terms, all concurrent; verified authors only; candidates
   merged round-robin across query variants) and returns up to 2 workers / 6
   contracts that WOULD match if installed, with `worker::add` guidance.
6. **Session memory** (keyed by caller-supplied OTel baggage, fail-open):
   repeat queries omit contracts already delivered; two consecutive empty
   answers widen the next one to single-term matches.

The pre-generate hook appends one `<discovery_assist>` block pointing the
model at `search_functions` — at most once per session, and only when every
gate clears: the function is callable in the surface, no search result is in
the 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 every available function with one natural-language query describing everything the task needs; returns the full API reference for each relevant function, grouped by worker.",
      "metadata": {},
      "name": "directory::search_functions",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "query": {
            "description": "One natural-language query naming every capability the task needs.",
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "title": "SearchFunctionsRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "InstallCall": {
            "description": "A ready-made `worker::add` invocation: `{ function, payload }` matches the agent_trigger call envelope so the model can execute it verbatim.",
            "properties": {
              "function": {
                "type": "string"
              },
              "payload": true
            },
            "required": [
              "function",
              "payload"
            ],
            "type": "object"
          },
          "InstallableFunction": {
            "properties": {
              "description": {
                "type": "string"
              },
              "function_id": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "function_id"
            ],
            "type": "object"
          },
          "InstallableWorker": {
            "description": "A registry worker that is NOT installed but carries functions matching the query. `name` is the registry slug `worker::add` installs.",
            "properties": {
              "description": {
                "type": "string"
              },
              "functions": {
                "description": "Names + descriptions only — deliberately NO request schema: an uninstalled function must not look callable, so there is nothing here for a model to pattern-match into a direct call. Contracts arrive through a fresh search after the install registers them.",
                "items": {
                  "$ref": "#/definitions/InstallableFunction"
                },
                "type": "array"
              },
              "install": {
                "allOf": [
                  {
                    "$ref": "#/definitions/InstallCall"
                  }
                ],
                "description": "The exact call that installs this worker, ready to execute."
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "description",
              "functions",
              "install",
              "name",
              "version"
            ],
            "type": "object"
          },
          "SearchContract": {
            "properties": {
              "description": {
                "type": "string"
              },
              "function_id": {
                "type": "string"
              },
              "request_schema": true
            },
            "required": [
              "description",
              "function_id",
              "request_schema"
            ],
            "type": "object"
          },
          "SearchWorker": {
            "properties": {
              "functions": {
                "items": {
                  "$ref": "#/definitions/SearchContract"
                },
                "type": "array"
              },
              "namespace": {
                "type": "string"
              }
            },
            "required": [
              "functions",
              "namespace"
            ],
            "type": "object"
          }
        },
        "properties": {
          "guidance": {
            "type": "string"
          },
          "installable": {
            "description": "Registry workers (verified authors) with matching functions, present only when nothing installed matched. 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": {}
    }
  ]
}
```
