iii-directory
v1.2.5-rc.2Engine introspection, workers registry proxy, filesystem-backed skills, system prompts, and agent profiles, plus lexical function search with a conditional pre-generate hint.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
exact versions are immutable; binary and bundle artifacts are digest-pinned.
readme
open as markdowniii-directory
Workers registry HTTP proxy and filesystem-backed skills, system prompts,
and agent profiles for the iii engine. 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, function_id, disable_model_invocation, description, bytes, modified_at } per row), a single-skill reader directory::skills::get { id } returning { id, title, type, function_id, disable_model_invocation, path, body, modified_at } (the full body instead of the list teaser), and directory::skills::index which renders a short per-worker overview document (one ## + 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). |
Orientation: "when and why to use my worker's tools" |
System prompts (directory::system-prompts::*) |
Identity prompts listed by list, read by get, authored by create, edited by update, and removed by delete. The list response keeps its prompts field name. Stored under any system-prompts/ path segment; create writes . |
What the chat's system-prompt picker offers as an identity prompt (enrich or replace) |
Agent Profiles (directory::agents::*) |
Reusable session identities whose file body is the system prompt, with display name, emoji logo, a skills filter, and optional model + reasoning_effort in required frontmatter. list rows carry the display/configuration metadata and get adds system_prompt and unknown_skills. Stored as direct files. See Agent profile storage. |
A named identity selected with harness::send { options: { agent } } |
Search (directory::search_functions) |
One to six external capabilities → compact function-id candidates (installed, plus registry workers under installable), with a conditional pre-generate hint pointing agents at it. |
"Which functions do I call for this task?" |
Registry (directory::registry::*) |
HTTP proxy over api.workers.iii.dev with workers::{list,info}. Rows share the core name / description / version fields with the engine's engine::workers::list and add publication metadata (type, config, supported_targets, total_downloads, dependencies, optional image). workers::list is cursor-paginated with a server-authored page size. |
"What's published in the public registry?" |
Engine introspection (functions / triggers / registered triggers /
workers) is served by the engine natively at
engine::functions::*, engine::triggers::*,
engine::registered-triggers::*, and engine::workers::*. Call the
engine ids directly. One wrapper survives for callers that can only
reach the directory:: namespace: directory::engine::functions::info
proxies a single function's schema (see its row below).
Skills and system prompts are sourced from skills_folder; agent profiles
use the dedicated agents_folder. Writes are the
directory::skills::download* functions, which pull markdown from either
the workers registry or a GitHub repo and route each
supported family to its configured root, plus the
per-kind single-file editors — directory::skills::{create,update,delete},
directory::system-prompts::{create,update,delete} and
directory::agents::{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).
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
- Install
- Configuration
- Quickstart: download some skills
- On-disk layout
- Skill ids
- Functions
- Function search & pre-generate hint
- Custom trigger types
- Local development & testing
- Migration from skills v0.2.x
Install
iii worker add iii-directoryiii 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:
npx skills add iii-hq/workers --skill iii-directoryBrowse or install every worker skill at once:
npx skills add iii-hq/workers --list
npx skills add iii-hq/workers --allConfiguration
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
# TOPOLOGY — changing any of these requires a worker restart.
skills_folder: skills # under III_COMPOSE_DIR, or the process cwd standalone
local_skills_folder: skills/iii # project-scoped overrides (whole-namespace local-wins)
agents_folder: agents # direct <id>.md agent profiles
agents_skills_folder: .agents/skills # READ-ONLY agent skills, with the same relative-path base
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: false # bind the directory::pre-generate search-hint hook (off: the harness identity prompt already teaches directory-first discovery)
hint_min_workers: 2 # minimum surface width before the hint fires (0 = always)
registry_search: true # include installable registry workers in every searchThe writable skills_folder and agents_folder roots are created when needed.
Zero-config default + seed
With no seed and no stored value the worker uses built-in defaults. Relative
folder paths use III_COMPOSE_DIR when set and the process current directory
otherwise (skills_folder: skills, agents_folder: agents,
registry_url: https://api.workers.iii.dev).
Pass --config 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). 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_folder / agents_skills_folder / auto_download) are refused with a "restart
required" log; the previous configuration is kept until the worker restarts.
The writable skills_folder, local_skills_folder, and agents_folder are
watch roots, and the watcher creates each one at boot if it is missing.
Direct edits under agents_folder fire
directory::agents::on-change with op: "external"; nested files are ignored.
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 skills/iii under
III_COMPOSE_DIR, or under the process current directory when the worker runs
standalone. An empty local root shadows nothing. Because the roots are
restart-required, the watch
roots are fixed for the process lifetime.
Quickstart: download some skills
# Pull a specific worker's directory bundle 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, system_prompts_written, agents_written, source }.
The three *_written fields list the files materialised in this run.
Registry entries shaped exactly as agents/ land in
; repo downloads do not install agent profiles.
After every successful download the worker fires the
directory::skills::on-change, directory::system-prompts::on-change,
and/or directory::agents::on-change trigger types so that
subscribers like the mcp worker can
forward MCP notifications/list_changed to their clients.
On-disk layout
The worker uses separate roots for directory content and agent profiles:
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
system-prompts/ # ← magic marker for system prompts
reviewer.md # ← identity prompt (needs YAML frontmatter)
system-prompts/ # ← where system-prompts::create writes
pirate.md
agents_folder/ # ← where agents::create writes
release-captain.md # ← agent profile (needs YAML frontmatter)
frontend-design.mdA second, READ-ONLY root — agents_skills_folder (default .agents/skills
under the same Compose or standalone base) — serves agent skills. It is
scanned shallowly: only becomes an entry
(id , displayed as ); 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_folderwith.mdstripped. Each segment must satisfy[a-z0-9_-]{1,64}. - Skill frontmatter is optional. When present, the reader recognises
title:(preferred title),name:(title fallback),type:(free-form classifier),function_id:(canonical bus function id surfaced bylistandget),description:(preferredlistteaser), anddisable-model-invocation:(boolean, defaultfalse, surfaced asdisable_model_invocationby both responses). The body H1 and first paragraph are the title and list-description fallbacks, respectively. Disabled skills remain visible to ordinary directory clients; model index consumers decide whether to filter them. Any other YAML keys are ignored. - System prompts live under any
*/system-prompts/*.mdpath, with YAML frontmatter (descriptionrequired,nameoptional). A path carrying both apromptsand asystem-promptssegment is a system prompt becausesystem-promptsis classified first. Other paths containing apromptssegment are ignored by scans and downloads. - 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 reachsystem-prompts/either by local authoring or viadirectory::skills::downloadfrom 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 ownsskills_folder. Worth knowing before you pointskills_folderat a directory other people can write to. - Agent profiles are direct
files (unrelated to the read-only/ .md agents_skills_folderabove, which holds external tools' skills). Nested profiles and the formerlayout are ignored. Frontmatter is required and must declare a non-empty/**/agents/*.md name(the display name — the id is always the file stem);descriptionmay be empty,logois emoji-only (≤16 bytes, no path characters),skills:filters the skill index (absent/empty = every skill), andmodel:names a model id for sessions using this profile (absent = the send decides), and optionalreasoning_effort:names its provider-native effort. Both are stored verbatim and resolved against the live model catalog where they are used. The body is the system prompt, verbatim. Unknownskillsids are warnings surfaced byget, never load failures. - Under a skills root, files outside
prompts/,system-prompts/, and reservedagents/segments are skills.
The download function namespaces by source:
| Source | Destination |
|---|---|
repo=URL skill=NAME branch?=main |
|
worker=NAME version=… |
Skills/prompts under ; exact agents/ entries under |
worker=NAME tag=… (default tag=latest) |
Same routing as the version form |
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. →
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
Functions sit 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 |
Download directory content. 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; paths under prompts/ and agents/ are ignored. |
directory::skills::download_from_registry |
Registry-only form: {worker, version?|tag?}. Installs a published worker's bundle from api.workers.iii.dev, routing exact agents/ entries to agents_folder. |
directory::skills::list |
Enriched listing of every fs-backed skill: { id, title, type, function_id, disable_model_invocation, 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), function_id identifies the documented bus function when present, and description is the frontmatter description or first body paragraph — 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, function_id, disable_model_invocation, path, body, modified_at }. It shares the list row's identity, classification, function, and invocation metadata, but returns the raw markdown body and absolute on-disk path instead of the teaser and byte count; there is no description field. The path's parent directory is the skill's base directory, where payload like scripts/ and reference/ lives, and is meaningful only to callers on this worker's machine. 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 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 → 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 ## heading + the worker's first overview paragraph + a Read iii:// pointer the agent can follow with directory::skills::get. Token-light by design; use directory::skills::list for per-skill rows. |
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 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 }. |
Agent Profiles — directory::agents::* (filesystem reader + editor)
| Function ID | Description |
|---|---|
directory::agents::list |
Metadata-only listing of every fs-backed agent profile: { id, name, description, logo, skill_count, model, reasoning_effort, icon, color, modified_at } per row (skill_count: null = every skill; model: null = the send decides). |
directory::agents::get |
Fetch one agent profile by { id }: system_prompt (the file body), skills + unknown_skills (filter entries matching no visible skill — warnings), model (null = the send decides), provider-native reasoning_effort, display icon/color, and modified_at. Pass raw: true to additionally get the FULL on-disk file as raw. |
directory::agents::update |
Overwrite one EXISTING agent profile file with new full-file content: { id, content }. Same rules the scanner enforces (required frontmatter with non-empty name, emoji-only logo, non-empty body); the id stays the file stem. Atomic write; fans out directory::agents::on-change with op: "update". |
directory::agents::create |
Create a NEW agent profile at from full-file content: { id, content }. Refuses an id that already exists in the configured agent-profile root, and a target path that already exists on disk even if the scanner would skip it. Atomic write; fans out directory::agents::on-change with op: "create". Returns { id, name, description, logo, bytes, modified_at }. |
directory::agents::delete |
Permanently remove one EXISTING agent profile file by { id }. Resolves against the same configured root as list/get, fans out directory::agents::on-change with op: "delete", and returns { id }. Sessions already using the profile are unaffected. |
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 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 — see
Migration below.
Function search & pre-generate hint
One-shot lexical function search over the live engine catalog, absorbed from
the former discovery worker. It returns only compact { function_id, description } candidates, grouped by worker in rank order. The model chooses
the candidates it needs, then fetches their contracts in one
engine::functions::info { function_ids: [...] } call instead of walking the
catalog with engine::functions::list.
| Function | Kind | What it does |
|---|---|---|
directory::search_functions |
public | { capabilities } → { guidance, workers[], installable[]?, latency_ms }: BM25 rank over the live engine catalog (at most 6 workers / 12 candidates) plus matching NOT-installed registry workers under installable. capabilities is a required list of one to six non-empty unmet external capability searches. Requests to summarize provided text/content are ignored. |
directory::pre-generate |
internal hook | Injects the conditional search hint into a harness generation (at most once per turn). |
directory::on-functions-change |
internal | Refreshes the search catalog on the engine's functions-available push. |
directory::hint-preview |
internal | The exact hint text per exposure mode, for the configuration UI. |
Ranking pipeline:
- Corpus: the live engine catalog (boot snapshot + push refresh),
slimmed to name + first description sentence + argument names.
engine::ids, functions published withmetadata.internal, and the search itself never participate — the worker's own publicdirectory::*functions are searchable like any other capability. - Scoring: Okapi BM25 (k1 1.2, b 0.75) with the function name indexed at
3× weight, camelCase segmentation (
presignUrl→ presign + url), a 22-word grammatical stoplist, conservative plural folding, JSON-key stripping from capability text, and a two-distinct-terms minimum match. - Capability queries: each
capabilitiesentry is ranked independently against its own leader and is authoritative. Include every currently unmet external capability once in the same call. Write every capability in English, translating non-English requests while preserving proper names, URLs, and function ids. Requests to summarize provided text or content are ignored. Results merge round-robin so every capability gets a candidate before any gets a rider. - 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.
- Registry search (
registry_search, default on): every call also consults the public workers registry in-process with the same capability queries plus informative-term retries (all concurrent; verified authors only). Candidates merge round-robin across search variants, returning up to 2 workers / 6 candidates that WOULD match if installed, withcompose::addguidance. - Session memory (keyed by caller-supplied OTel baggage, fail-open): repeat queries omit candidates already delivered.
The pre-generate hook appends one block pointing the
model at search_functions, telling it to derive capabilities from the goal
and current execution state and to batch selected ids through
engine::functions::info — at most once per turn, and only when every gate
clears: the function is callable in the surface, no search result is in the
current task window yet, the surface spans at least hint_min_workers distinct
workers, the current task (from the latest user message) has no real function
results yet, and it does not already name a callable function id. Measured on
26 pre-existing e2e scenarios: an unconditional hint induces redundant
discovery on guided tasks (up to +110% tokens). The hook ships OFF by default
(inject_hint: false): the harness identity prompt already teaches
directory::search_functions as the default discovery path, so the hint is
for deployments running a custom identity prompt without that doctrine. 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": "; update/create/delete: { "op": "; 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": "; update/create/delete: { "op": "; external: { "op": "external" } |
directory::agents::on-change |
After a directory::skills::download that wrote at least one agent profile, a directory::agents::update, create, delete, or external file change |
download: { "op": "download", "namespace": "; update/create/delete: { "op": "; 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
# --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.exampleTests
# 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 @download @download_repo @download_registry
cargo test --test bdd -- --tags @downloadThe BDD harness lives under tests/. Feature files mirror the
modules in src/functions/. Step definitions under
tests/steps/ drive each feature through the same
iii.trigger path the production binary uses.