From 0a25ff5be5d0a910a90ac9c977c36ec9943e4bc2 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Tue, 19 May 2026 12:24:02 -0300 Subject: [PATCH 01/12] feat(directory): alias SKILLS.md to index.md in skill scanner --- iii-directory/src/fs_source.rs | 73 +++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/iii-directory/src/fs_source.rs b/iii-directory/src/fs_source.rs index c1ad3265..bb0292f0 100644 --- a/iii-directory/src/fs_source.rs +++ b/iii-directory/src/fs_source.rs @@ -7,6 +7,7 @@ //! / //! / # one folder per download namespace //! index.md # → iii:///index +//! SKILLS.md # → iii:///index (alias of index.md) //! anything.md # → iii:///anything //! deep/path.md # → iii:///deep/path //! prompts/ # ← magic marker for prompts @@ -164,11 +165,36 @@ fn walk_markdown(base_dir: &Path) -> Result, String> { Ok(out) } +/// Convert a ``-relative path to a skill id. +/// +/// `SKILLS.md` (the literal filename, any case-sensitive match) is +/// treated as an alias for `index.md`, so a file at `/SKILLS.md` +/// produces the id `/index`. The alias runs on the final path +/// component only — directories named `SKILLS` are *not* renamed. fn rel_to_id(rel: &Path) -> Result { let rel_str = rel .to_str() .ok_or_else(|| format!("non-UTF-8 path: {}", rel.display()))?; - let stripped = rel_str.strip_suffix(".md").unwrap_or(rel_str); + let aliased = if let Some(parent) = rel.parent() { + let last_is_skills_md = rel + .file_name() + .and_then(|s| s.to_str()) + .map(|n| n == "SKILLS.md") + .unwrap_or(false); + if last_is_skills_md { + let parent_str = parent.to_str().unwrap_or(""); + if parent_str.is_empty() { + "index.md".to_string() + } else { + format!("{}/index.md", parent_str.replace('\\', "/")) + } + } else { + rel_str.to_string() + } + } else { + rel_str.to_string() + }; + let stripped = aliased.strip_suffix(".md").unwrap_or(&aliased).to_string(); Ok(stripped.replace('\\', "/")) } @@ -520,6 +546,51 @@ mod tests { assert!(skipped.is_empty()); } + #[test] + fn scan_skills_treats_SKILLS_md_as_index_alias() { + let tmp = tempfile::tempdir().unwrap(); + let ns = tmp.path().join("resend"); + std::fs::create_dir_all(&ns).unwrap(); + std::fs::write(ns.join("SKILLS.md"), "# resend\n").unwrap(); + + let (skills, skipped) = scan_skills(tmp.path()); + assert!(skipped.is_empty(), "unexpected skips: {skipped:?}"); + assert_eq!(skills.len(), 1); + assert_eq!(skills[0].id, "resend/index"); + } + + #[test] + fn scan_skills_treats_nested_SKILLS_md_as_index_alias() { + let tmp = tempfile::tempdir().unwrap(); + let nested = tmp.path().join("resend/emails"); + std::fs::create_dir_all(&nested).unwrap(); + std::fs::write(nested.join("SKILLS.md"), "# emails\n").unwrap(); + + let (skills, skipped) = scan_skills(tmp.path()); + assert!(skipped.is_empty(), "unexpected skips: {skipped:?}"); + assert_eq!(skills.len(), 1); + assert_eq!(skills[0].id, "resend/emails/index"); + } + + #[test] + fn scan_skills_skips_one_when_both_index_and_SKILLS_present() { + let tmp = tempfile::tempdir().unwrap(); + let ns = tmp.path().join("resend"); + std::fs::create_dir_all(&ns).unwrap(); + std::fs::write(ns.join("index.md"), "# from index\n").unwrap(); + std::fs::write(ns.join("SKILLS.md"), "# from SKILLS\n").unwrap(); + + let (skills, skipped) = scan_skills(tmp.path()); + assert_eq!(skills.len(), 1, "should keep exactly one entry"); + assert_eq!(skills[0].id, "resend/index"); + assert_eq!(skipped.len(), 1, "second entry should be reported as duplicate"); + assert!( + skipped[0].reason.contains("duplicate id \"resend/index\""), + "expected duplicate-id skip, got: {}", + skipped[0].reason + ); + } + // ── scan_prompts ───────────────────────────────────────────────── #[test] From 7b982d98aef47b3084aad2fbb50caa37e57253e0 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Tue, 19 May 2026 12:27:40 -0300 Subject: [PATCH 02/12] test(directory): rename SKILLS.md alias tests to snake_case --- iii-directory/src/fs_source.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iii-directory/src/fs_source.rs b/iii-directory/src/fs_source.rs index bb0292f0..47675af4 100644 --- a/iii-directory/src/fs_source.rs +++ b/iii-directory/src/fs_source.rs @@ -547,7 +547,7 @@ mod tests { } #[test] - fn scan_skills_treats_SKILLS_md_as_index_alias() { + fn scan_skills_treats_skills_md_as_index_alias() { let tmp = tempfile::tempdir().unwrap(); let ns = tmp.path().join("resend"); std::fs::create_dir_all(&ns).unwrap(); @@ -560,7 +560,7 @@ mod tests { } #[test] - fn scan_skills_treats_nested_SKILLS_md_as_index_alias() { + fn scan_skills_treats_nested_skills_md_as_index_alias() { let tmp = tempfile::tempdir().unwrap(); let nested = tmp.path().join("resend/emails"); std::fs::create_dir_all(&nested).unwrap(); @@ -573,7 +573,7 @@ mod tests { } #[test] - fn scan_skills_skips_one_when_both_index_and_SKILLS_present() { + fn scan_skills_skips_one_when_both_index_and_skills_present() { let tmp = tempfile::tempdir().unwrap(); let ns = tmp.path().join("resend"); std::fs::create_dir_all(&ns).unwrap(); From 755f74c48b464839ac8f7a9f735112bbeef4b4ee Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Tue, 19 May 2026 12:29:32 -0300 Subject: [PATCH 03/12] feat(directory): accept .md file-path form in directory::skills::get --- iii-directory/src/functions/skills.rs | 90 ++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/iii-directory/src/functions/skills.rs b/iii-directory/src/functions/skills.rs index a9651e27..1be5c209 100644 --- a/iii-directory/src/functions/skills.rs +++ b/iii-directory/src/functions/skills.rs @@ -61,8 +61,9 @@ const GET_DESCRIPTION: &str = "Fetch one filesystem-backed skill by id. Returns the raw markdown body plus id, \ title, type, description, and modified_at — same flat shape as directory::prompts::get \ with `type` lifted from the YAML frontmatter and `title` preferring frontmatter \ - over the body H1. Accepts a bare id (e.g. \"directory/skills/list\") or the same \ - id prefixed with iii://."; + over the body H1. Accepts a bare id (e.g. \"directory/skills/list\"), the same id \ + suffixed with `.md` (e.g. \"directory/skills/list.md\"), or either form prefixed \ + with iii://."; #[derive(Debug, Default, Deserialize, JsonSchema)] struct ListSkillsInput {} @@ -108,9 +109,11 @@ struct IndexSkillsOutput { #[derive(Debug, Default, Deserialize, JsonSchema)] pub struct SkillGetInput { /// Skill id (the same string returned by `directory::skills::list`, - /// e.g. `"directory/skills/list"`). The legacy `iii://{id}` form is - /// also accepted for ergonomics; the prefix is stripped before - /// validation. Other URI schemes are rejected. + /// e.g. `"directory/skills/list"`). Two ergonomic variants are also + /// accepted: the file-path form `.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. pub id: String, } @@ -226,23 +229,39 @@ pub async fn get_skill(cfg: &SkillsConfig, req: SkillGetInput) -> Result Result { let trimmed = raw.trim(); if trimmed.is_empty() { return Err("id must be non-empty".into()); } - if let Some(rest) = trimmed.strip_prefix(URI_PREFIX) { - return Ok(rest.to_string()); - } - if trimmed.contains("://") { + let without_scheme = if let Some(rest) = trimmed.strip_prefix(URI_PREFIX) { + rest + } else if trimmed.contains("://") { return Err(format!( - "Invalid id (must be a bare skill path or an iii:// URI): {trimmed}" + "Invalid id (must be a bare skill path, a path ending in .md, or an iii:// URI): {trimmed}" )); - } - Ok(trimmed.to_string()) + } else { + trimmed + }; + let aliased = if let Some(stem) = without_scheme.strip_suffix("/SKILLS.md") { + format!("{stem}/index") + } else if without_scheme == "SKILLS.md" { + "index".to_string() + } else { + without_scheme + .strip_suffix(".md") + .unwrap_or(without_scheme) + .to_string() + }; + Ok(aliased) } // ---------- validation ---------- @@ -493,6 +512,47 @@ mod tests { assert!(normalize_get_id("ftp://nope").is_err()); } + #[test] + fn normalize_strips_md_suffix_on_bare_path() { + assert_eq!( + normalize_get_id("hello-worker/index.md").unwrap(), + "hello-worker/index" + ); + } + + #[test] + fn normalize_aliases_skills_md_to_index() { + assert_eq!( + normalize_get_id("hello-worker/SKILLS.md").unwrap(), + "hello-worker/index" + ); + } + + #[test] + fn normalize_aliases_nested_skills_md_to_index() { + assert_eq!( + normalize_get_id("resend/emails/SKILLS.md").unwrap(), + "resend/emails/index" + ); + } + + #[test] + fn normalize_strips_md_after_iii_prefix() { + assert_eq!( + normalize_get_id("iii://hello-worker/index.md").unwrap(), + "hello-worker/index" + ); + } + + #[test] + fn normalize_does_not_strip_md_in_middle_of_path() { + // ".md" inside a segment is a real id, not a file suffix. + assert_eq!( + normalize_get_id("hello-worker/index_md").unwrap(), + "hello-worker/index_md" + ); + } + // ── validate_id: happy paths ──────────────────────────────────────── #[test] From 7b85bf9f170933af95638ae8dee375049829edce Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Tue, 19 May 2026 12:37:22 -0300 Subject: [PATCH 04/12] feat(directory): emit file-path pointer from directory::skills::index --- iii-directory/src/functions/skills.rs | 35 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/iii-directory/src/functions/skills.rs b/iii-directory/src/functions/skills.rs index 1be5c209..5846ff14 100644 --- a/iii-directory/src/functions/skills.rs +++ b/iii-directory/src/functions/skills.rs @@ -98,7 +98,7 @@ struct IndexSkillsOutput { /// Rendered markdown document — one short `## ` block per /// installed worker (skills with frontmatter `type: index`), /// carrying the worker's first-paragraph overview and a read-more - /// link pointing at `iii://<ns>/index`. Sorted lex by id. + /// link pointing at the file path `<ns>/index.md`. Sorted lex by id. body: String, /// Number of worker entries rendered (i.e. the count of /// `type: index` skills that survived the filter). Cheap sanity @@ -199,7 +199,7 @@ fn register_index_skills(iii: &Arc<III>, cfg: &Arc<SkillsConfig>) { .description( "Render one short markdown entry per installed worker (skills with frontmatter \ `type: index`). Each entry is a `## <worker title>` heading, the first paragraph \ - of the worker's overview, and a `Read iii://<ns>/index` line the agent can \ + of the worker's overview, and a `Read <ns>/index.md` line the agent can \ follow via `directory::skills::get` for the full reference. Token-light by \ design; for per-skill rows use `directory::skills::list`.", ), @@ -385,7 +385,7 @@ pub fn extract_description(markdown: &str) -> Option<String> { /// /// <first paragraph from the worker's overview> /// -/// Read [`iii://<id>`](iii://<id>) for the full worker reference. +/// Read [`<id>.md`](<id>.md) for the full worker reference. /// ``` /// /// The description block is omitted (no extra blank line) when the @@ -411,7 +411,7 @@ fn render_index_markdown(entries: &[SkillEntry]) -> String { } out.push('\n'); out.push_str(&format!( - "Read [`iii://{id}`](iii://{id}) for the full worker reference.\n", + "Read [`{id}.md`]({id}.md) for the full worker reference.\n", id = worker.id )); } @@ -942,7 +942,7 @@ mod tests { ); // Filtered-out skills must not leak into the read-more pointers either. assert!( - !body.contains("iii://agent-memory/observe"), + !body.contains("agent-memory/observe.md"), "filtered-out how-to leaked a link; got: {body}" ); assert!(body.contains("1 worker(s).\n"), "wrong count; got: {body}"); @@ -1001,7 +1001,7 @@ mod tests { )]); assert!( body.contains( - "Read [`iii://agent-memory/index`](iii://agent-memory/index) for the full worker reference.\n" + "Read [`agent-memory/index.md`](agent-memory/index.md) for the full worker reference.\n" ), "missing dive-deeper pointer; got: {body}" ); @@ -1018,7 +1018,7 @@ mod tests { // Title comes immediately before the read-more line — no extra // blank paragraph in the middle. assert!( - body.contains("\n## bare\n\nRead [`iii://bare/index`](iii://bare/index)"), + body.contains("\n## bare\n\nRead [`bare/index.md`](bare/index.md)"), "blank-description block should compress; got: {body}" ); // And the rest of the document still has the header. @@ -1042,4 +1042,25 @@ mod tests { "headings out of order; got: {body}" ); } + + #[test] + fn render_index_emits_file_path_pointer() { + let entries = vec![SkillEntry { + id: "agent-memory/index".into(), + title: "agent-memory".into(), + kind: Some("index".into()), + description: "Memory worker overview.".into(), + bytes: 10, + modified_at: String::new(), + }]; + let body = render_index_markdown(&entries); + assert!( + body.contains("Read [`agent-memory/index.md`](agent-memory/index.md)"), + "expected file-path pointer, got:\n{body}" + ); + assert!( + !body.contains("iii://"), + "iii:// scheme should no longer be emitted, got:\n{body}" + ); + } } From 599236d7b9d371d0f874770cf947f3508d1201c9 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 12:42:44 -0300 Subject: [PATCH 05/12] test(directory): smoke-assert SKILLS.md round-trip via local registry --- iii-directory/examples/test_registry.rs | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 iii-directory/examples/test_registry.rs diff --git a/iii-directory/examples/test_registry.rs b/iii-directory/examples/test_registry.rs new file mode 100644 index 00000000..ca749fd5 --- /dev/null +++ b/iii-directory/examples/test_registry.rs @@ -0,0 +1,70 @@ +use iii_directory::sources::registry::{download, VersionSpec}; +use iii_directory::fs_source::{scan_skills, scan_prompts}; + +#[tokio::main] +async fn main() -> Result<(), String> { + let registry = std::env::var("REGISTRY") + .unwrap_or_else(|_| "http://localhost:3111".to_string()); + let worker = std::env::var("WORKER").unwrap_or_else(|_| "hello-worker".to_string()); + let tag = std::env::var("TAG").unwrap_or_else(|_| "latest".to_string()); + + let tmp = tempfile::tempdir().unwrap(); + let skills_folder = tmp.path(); + + println!("→ Downloading {worker} (tag={tag}) from {registry}"); + println!(" skills_folder = {}", skills_folder.display()); + + let spec = VersionSpec::Tag(tag); + let result = download(®istry, &worker, &spec, skills_folder, 30_000).await?; + + println!("\n[download result]"); + println!(" namespace = {}", result.namespace); + println!(" skills_written = {:?}", result.skills_written); + println!(" prompts_written = {:?}", result.prompts_written); + + let (skills, skill_skipped) = scan_skills(skills_folder); + let (prompts, prompt_skipped) = scan_prompts(skills_folder); + + println!("\n[scan_skills]"); + for s in &skills { + println!(" id={:<40} path={}", s.id, s.abs_path.display()); + } + if !skill_skipped.is_empty() { + println!("\n[skill skips]"); + for s in &skill_skipped { + println!(" {} → {}", s.path.display(), s.reason); + } + } + + println!("\n[scan_prompts]"); + for p in &prompts { + println!(" name={:<30} path={}", p.name, p.abs_path.display()); + } + if !prompt_skipped.is_empty() { + println!("\n[prompt skips]"); + for s in &prompt_skipped { + println!(" {} → {}", s.path.display(), s.reason); + } + } + + println!( + "\nDONE — skills scanned={}, prompts scanned={}", + skills.len(), + prompts.len() + ); + + // Soft assertions so the example doubles as a smoke test. + let any_index = skills.iter().any(|s| s.id.ends_with("/index")); + if !any_index { + eprintln!("FAIL: no skill with id ending in /index was scanned"); + std::process::exit(1); + } + for s in &skills { + if s.id.chars().any(|c| c.is_ascii_uppercase()) { + eprintln!("FAIL: skill id leaked uppercase: {}", s.id); + std::process::exit(1); + } + } + println!("smoke: at least one /index id present, no uppercase leaks"); + Ok(()) +} From 0d4efaf35abddc0f291aba8ef852ecb0a2581eb0 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 16:48:41 -0300 Subject: [PATCH 06/12] style(iii-directory): apply cargo fmt + sync lockfile version --- iii-directory/Cargo.lock | 2 +- iii-directory/examples/test_registry.rs | 6 +++--- iii-directory/src/fs_source.rs | 6 +++++- iii-directory/src/functions/registry.rs | 15 ++++++++------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/iii-directory/Cargo.lock b/iii-directory/Cargo.lock index 006b8673..3ef56bfd 100644 --- a/iii-directory/Cargo.lock +++ b/iii-directory/Cargo.lock @@ -1044,7 +1044,7 @@ dependencies = [ [[package]] name = "iii-directory" -version = "0.5.1" +version = "0.5.2" dependencies = [ "anyhow", "async-trait", diff --git a/iii-directory/examples/test_registry.rs b/iii-directory/examples/test_registry.rs index ca749fd5..be79ba2a 100644 --- a/iii-directory/examples/test_registry.rs +++ b/iii-directory/examples/test_registry.rs @@ -1,10 +1,10 @@ +use iii_directory::fs_source::{scan_prompts, scan_skills}; use iii_directory::sources::registry::{download, VersionSpec}; -use iii_directory::fs_source::{scan_skills, scan_prompts}; #[tokio::main] async fn main() -> Result<(), String> { - let registry = std::env::var("REGISTRY") - .unwrap_or_else(|_| "http://localhost:3111".to_string()); + let registry = + std::env::var("REGISTRY").unwrap_or_else(|_| "http://localhost:3111".to_string()); let worker = std::env::var("WORKER").unwrap_or_else(|_| "hello-worker".to_string()); let tag = std::env::var("TAG").unwrap_or_else(|_| "latest".to_string()); diff --git a/iii-directory/src/fs_source.rs b/iii-directory/src/fs_source.rs index 47675af4..1aeb6044 100644 --- a/iii-directory/src/fs_source.rs +++ b/iii-directory/src/fs_source.rs @@ -583,7 +583,11 @@ mod tests { let (skills, skipped) = scan_skills(tmp.path()); assert_eq!(skills.len(), 1, "should keep exactly one entry"); assert_eq!(skills[0].id, "resend/index"); - assert_eq!(skipped.len(), 1, "second entry should be reported as duplicate"); + assert_eq!( + skipped.len(), + 1, + "second entry should be reported as duplicate" + ); assert!( skipped[0].reason.contains("duplicate id \"resend/index\""), "expected duplicate-id skip, got: {}", diff --git a/iii-directory/src/functions/registry.rs b/iii-directory/src/functions/registry.rs index 357a886f..4363cfd6 100644 --- a/iii-directory/src/functions/registry.rs +++ b/iii-directory/src/functions/registry.rs @@ -462,11 +462,7 @@ pub async fn worker_info( input: WorkerInfoInput, ) -> Result<WorkerInfoOutput, String> { let (name, spec) = classify_worker_info_input(input)?; - let cache_key = format!( - "worker-info:{name}:{}={}", - spec.label(), - spec.query_value() - ); + let cache_key = format!("worker-info:{name}:{}={}", spec.label(), spec.query_value()); if let Some(cached) = cache.get::<WorkerInfoOutput>(&cache_key).await { return Ok(cached); } @@ -618,7 +614,9 @@ fn parse_skills_tree(value: &Value) -> SkillsTree { .filter_map(|s| { s.get("path") .and_then(|p| p.as_str()) - .map(|p| SkillsTreeSkill { path: p.to_string() }) + .map(|p| SkillsTreeSkill { + path: p.to_string(), + }) }) .collect() }) @@ -748,7 +746,10 @@ mod tests { assert_eq!(out.workers[0].kind.as_deref(), Some("binary")); assert_eq!(out.workers[0].version.as_deref(), Some("1.2.3")); assert_eq!(out.workers[0].total_downloads, 42); - assert_eq!(out.workers[0].supported_targets, vec!["x86_64-unknown-linux-gnu".to_string()]); + assert_eq!( + out.workers[0].supported_targets, + vec!["x86_64-unknown-linux-gnu".to_string()] + ); let author = out.workers[0].author.as_ref().unwrap(); assert_eq!(author.name.as_deref(), Some("iii")); assert!(author.verified); From 789f4ac7e63ed19493ddc6f4fcc7d51b325122d9 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 16:48:48 -0300 Subject: [PATCH 07/12] docs(iii-directory): add skill.md required by bundled worker validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR-checks workflow requires iii-directory/skill.md per .github/scripts/validate_worker.py (BOOTSTRAP_WORKERS). skill.md is rendered from docs/{intro,quickstart,companions}.md via iii-skill-render, which becomes the source-of-truth going forward — README and skills/index.md remain unchanged for now to preserve existing content. --- iii-directory/docs/companions.md | 5 +++++ iii-directory/docs/intro.md | 5 +++++ iii-directory/docs/quickstart.md | 37 ++++++++++++++++++++++++++++++++ iii-directory/skill.md | 13 +++++++++++ 4 files changed, 60 insertions(+) create mode 100644 iii-directory/docs/companions.md create mode 100644 iii-directory/docs/intro.md create mode 100644 iii-directory/docs/quickstart.md create mode 100644 iii-directory/skill.md diff --git a/iii-directory/docs/companions.md b/iii-directory/docs/companions.md new file mode 100644 index 00000000..95a937ed --- /dev/null +++ b/iii-directory/docs/companions.md @@ -0,0 +1,5 @@ +`directory::skills::download` pulls bundles from the public workers registry (`api.workers.iii.dev` by default). For self-hosted setups, repoint `registry_url` in `config.yaml` at your own registry. The `directory::registry::*` proxies share their envelope with `directory::engine::workers::*`, so a single parser handles both local and remote worker discovery. + +```bash +iii worker add iii-directory +``` diff --git a/iii-directory/docs/intro.md b/iii-directory/docs/intro.md new file mode 100644 index 00000000..f7b9b4b2 --- /dev/null +++ b/iii-directory/docs/intro.md @@ -0,0 +1,5 @@ +Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces — `skills`, `prompts`, `engine`, and `registry` — so callers learn one envelope across local files, the running engine, and the public workers registry. + +<!-- llm-only:start --> +Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter; `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). +<!-- llm-only:end --> diff --git a/iii-directory/docs/quickstart.md b/iii-directory/docs/quickstart.md new file mode 100644 index 00000000..475fc588 --- /dev/null +++ b/iii-directory/docs/quickstart.md @@ -0,0 +1,37 @@ +```rust +use iii_sdk::{register_worker, InitOptions, TriggerRequest}; +use serde_json::json; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let worker = register_worker("ws://localhost:49134", InitOptions::default()); + + let result = worker + .trigger(TriggerRequest { + function_id: "directory::skills::download".into(), + payload: json!({ + "worker": "hello-worker", + "tag": "latest", + }), + action: None, + timeout_ms: Some(30_000), + }) + .await?; + + println!("{result:#?}"); + Ok(()) +} +``` + +```typescript +import { registerWorker } from 'iii-sdk' + +const worker = registerWorker('ws://localhost:49134') + +const result = await worker.trigger({ + functionId: 'directory::skills::list', + payload: {}, +}) + +console.log(result) +``` diff --git a/iii-directory/skill.md b/iii-directory/skill.md new file mode 100644 index 00000000..e3100d1e --- /dev/null +++ b/iii-directory/skill.md @@ -0,0 +1,13 @@ +<!-- generated by iii-skill-render. DO NOT EDIT (changes here are overwritten on the next render). Edit docs/intro.md or docs/companions.md. --> + +# iii-directory + +Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces — `skills`, `prompts`, `engine`, and `registry` — so callers learn one envelope across local files, the running engine, and the public workers registry. + +Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter; `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). + +`directory::skills::download` pulls bundles from the public workers registry (`api.workers.iii.dev` by default). For self-hosted setups, repoint `registry_url` in `config.yaml` at your own registry. The `directory::registry::*` proxies share their envelope with `directory::engine::workers::*`, so a single parser handles both local and remote worker discovery. + +```bash +iii worker add iii-directory +``` From 3e2329028c41daa21d5afb3e5456e01100be7db7 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 16:52:36 -0300 Subject: [PATCH 08/12] test(iii-directory): cover SKILLS.md alias + file-path pointer in BDD read.feature additions: - SKILLS.md at namespace root aliased to <ns>/index - nested SKILLS.md aliased to <ns>/<sub>/index - directory::skills::get accepts <id>.md file-path form - directory::skills::get accepts SKILLS.md filename - directory::skills::get accepts iii:// + .md combined form Updated the existing index scenario to assert the new file-path pointer (Read [<ns>/index.md](<ns>/index.md)) and explicitly check that the rendered body no longer contains the iii:// scheme. docs/intro.md: replaced em dashes with parens/periods to pass the skill-check Vale rule (Terminology.EmDash). skill.md re-rendered. --- iii-directory/docs/intro.md | 4 +- iii-directory/skill.md | 4 +- iii-directory/tests/features/read.feature | 63 ++++++++++++++++++++++- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/iii-directory/docs/intro.md b/iii-directory/docs/intro.md index f7b9b4b2..e0bfbe58 100644 --- a/iii-directory/docs/intro.md +++ b/iii-directory/docs/intro.md @@ -1,5 +1,5 @@ -Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces — `skills`, `prompts`, `engine`, and `registry` — so callers learn one envelope across local files, the running engine, and the public workers registry. +Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces (`skills`, `prompts`, `engine`, and `registry`), so callers learn one envelope across local files, the running engine, and the public workers registry. <!-- llm-only:start --> -Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter; `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). +Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter. `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). <!-- llm-only:end --> diff --git a/iii-directory/skill.md b/iii-directory/skill.md index e3100d1e..1b09f5de 100644 --- a/iii-directory/skill.md +++ b/iii-directory/skill.md @@ -2,9 +2,9 @@ # iii-directory -Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces — `skills`, `prompts`, `engine`, and `registry` — so callers learn one envelope across local files, the running engine, and the public workers registry. +Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces (`skills`, `prompts`, `engine`, and `registry`), so callers learn one envelope across local files, the running engine, and the public workers registry. -Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter; `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). +Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter. `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). `directory::skills::download` pulls bundles from the public workers registry (`api.workers.iii.dev` by default). For self-hosted setups, repoint `registry_url` in `config.yaml` at your own registry. The `directory::registry::*` proxies share their envelope with `directory::engine::workers::*`, so a single parser handles both local and remote worker discovery. diff --git a/iii-directory/tests/features/read.feature b/iii-directory/tests/features/read.feature index e4106de2..28e5b75b 100644 --- a/iii-directory/tests/features/read.feature +++ b/iii-directory/tests/features/read.feature @@ -203,7 +203,66 @@ Feature: filesystem-backed reads (directory::skills::list / directory::skills::g And the index response body contains "## team-b" And the index response body contains "Alpha team's worker. Owns alpha-only tooling." And the index response body contains "Bravo team's worker. Handles all bravo workflows." - And the index response body contains "Read [`iii://team-a/index`](iii://team-a/index) for the full worker reference." - And the index response body contains "Read [`iii://team-b/index`](iii://team-b/index) for the full worker reference." + And the index response body contains "Read [`team-a/index.md`](team-a/index.md) for the full worker reference." + And the index response body contains "Read [`team-b/index.md`](team-b/index.md) for the full worker reference." + And the index response body does not contain "iii://" And the index response body does not contain "team-a/foo" And the index response body does not contain "## Foo" + + # ── SKILLS.md alias (case-sensitive entry-point) ──────────────────── + + Scenario: SKILLS.md at namespace root is aliased to <ns>/index + Given a skill file at "ns-skills/SKILLS.md" with body: + """ + # ns-skills entry-point + + Body served via the SKILLS.md alias. + """ + When I list skills + Then the listing has an entry with id "ns-skills/index" + And no listing entry has id "ns-skills/SKILLS" + + Scenario: nested SKILLS.md is aliased to <ns>/<sub>/index + Given a skill file at "ns-nested/section/SKILLS.md" with body: + """ + # nested entry-point + + Body for nested SKILLS.md. + """ + When I list skills + Then the listing has an entry with id "ns-nested/section/index" + + # ── directory::skills::get accepts the file-path forms ────────────── + + Scenario: directory::skills::get accepts the <id>.md file-path form + Given a skill file at "ns-path/lookup.md" with body: + """ + # Lookup via path + + Body for the path-form lookup. + """ + When I get skill "ns-path/lookup.md" + Then the get response has id "ns-path/lookup" + And the get response body contains "Body for the path-form lookup." + + Scenario: directory::skills::get accepts the SKILLS.md filename + Given a skill file at "ns-skills-get/SKILLS.md" with body: + """ + # ns-skills-get entry-point + + Body served via SKILLS.md lookup. + """ + When I get skill "ns-skills-get/SKILLS.md" + Then the get response has id "ns-skills-get/index" + And the get response body contains "Body served via SKILLS.md lookup." + + Scenario: directory::skills::get accepts the iii:// + .md combined form + Given a skill file at "ns-combo/lookup.md" with body: + """ + # Combo + + Body for combined-form lookup. + """ + When I get skill "iii://ns-combo/lookup.md" + Then the get response has id "ns-combo/lookup" + And the get response body contains "Body for combined-form lookup." From faf4e10e95bf9d8807adbf4c74e80e6103e02195 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 17:07:07 -0300 Subject: [PATCH 09/12] feat(directory): dual-emit file-path + legacy iii:// pointer in skills::index Output now reads: Read [`<id>.md`](<id>.md) (legacy `iii://<id>`) for the full worker reference. Both forms travel together so harnesses that grep for the old URI scheme keep working alongside new consumers that prefer the markdown link target. The parser already accepted all three input forms (bare id, .md path, iii:// URI), so dual-emit closes the loop for back-compat with existing prompts and tooling. Updated unit test (render_index_emits_both_file_path_and_iii_pointer) and BDD scenario (read.feature) to assert both forms appear in the rendered body. --- iii-directory/src/functions/skills.rs | 18 +++++++++++------- iii-directory/tests/features/read.feature | 5 ++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/iii-directory/src/functions/skills.rs b/iii-directory/src/functions/skills.rs index 5846ff14..defd7ccb 100644 --- a/iii-directory/src/functions/skills.rs +++ b/iii-directory/src/functions/skills.rs @@ -385,9 +385,13 @@ pub fn extract_description(markdown: &str) -> Option<String> { /// /// <first paragraph from the worker's overview> /// -/// Read [`<id>.md`](<id>.md) for the full worker reference. +/// Read [`<id>.md`](<id>.md) (legacy `iii://<id>`) for the full worker reference. /// ``` /// +/// The legacy `iii://<id>` form is emitted alongside the file-path +/// pointer so harnesses that grep for the old URI scheme keep working +/// while new consumers prefer the markdown link target. +/// /// The description block is omitted (no extra blank line) when the /// overview body has no paragraph. Entries must already be sorted lex /// by `id` (the order `fs_source::scan_skills` returns); this function @@ -411,7 +415,7 @@ fn render_index_markdown(entries: &[SkillEntry]) -> String { } out.push('\n'); out.push_str(&format!( - "Read [`{id}.md`]({id}.md) for the full worker reference.\n", + "Read [`{id}.md`]({id}.md) (legacy `iii://{id}`) for the full worker reference.\n", id = worker.id )); } @@ -1001,7 +1005,7 @@ mod tests { )]); assert!( body.contains( - "Read [`agent-memory/index.md`](agent-memory/index.md) for the full worker reference.\n" + "Read [`agent-memory/index.md`](agent-memory/index.md) (legacy `iii://agent-memory/index`) for the full worker reference.\n" ), "missing dive-deeper pointer; got: {body}" ); @@ -1044,7 +1048,7 @@ mod tests { } #[test] - fn render_index_emits_file_path_pointer() { + fn render_index_emits_both_file_path_and_iii_pointer() { let entries = vec![SkillEntry { id: "agent-memory/index".into(), title: "agent-memory".into(), @@ -1055,12 +1059,12 @@ mod tests { }]; let body = render_index_markdown(&entries); assert!( - body.contains("Read [`agent-memory/index.md`](agent-memory/index.md)"), + body.contains("[`agent-memory/index.md`](agent-memory/index.md)"), "expected file-path pointer, got:\n{body}" ); assert!( - !body.contains("iii://"), - "iii:// scheme should no longer be emitted, got:\n{body}" + body.contains("legacy `iii://agent-memory/index`"), + "expected legacy iii:// pointer for back-compat, got:\n{body}" ); } } diff --git a/iii-directory/tests/features/read.feature b/iii-directory/tests/features/read.feature index 28e5b75b..c5653817 100644 --- a/iii-directory/tests/features/read.feature +++ b/iii-directory/tests/features/read.feature @@ -203,9 +203,8 @@ Feature: filesystem-backed reads (directory::skills::list / directory::skills::g And the index response body contains "## team-b" And the index response body contains "Alpha team's worker. Owns alpha-only tooling." And the index response body contains "Bravo team's worker. Handles all bravo workflows." - And the index response body contains "Read [`team-a/index.md`](team-a/index.md) for the full worker reference." - And the index response body contains "Read [`team-b/index.md`](team-b/index.md) for the full worker reference." - And the index response body does not contain "iii://" + And the index response body contains "Read [`team-a/index.md`](team-a/index.md) (legacy `iii://team-a/index`) for the full worker reference." + And the index response body contains "Read [`team-b/index.md`](team-b/index.md) (legacy `iii://team-b/index`) for the full worker reference." And the index response body does not contain "team-a/foo" And the index response body does not contain "## Foo" From e2627eabec9be51284d8a69cc063647d9fc64b5b Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 17:38:27 -0300 Subject: [PATCH 10/12] =?UTF-8?q?test(directory):=20pin=20iii://=20back-co?= =?UTF-8?q?mpat=20=E2=80=94=20alias=20composition=20+=20output=E2=86=94inp?= =?UTF-8?q?ut=20round-trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds explicit coverage for the legacy iii:// URI form on the input side and proves the dual-emit output round-trips: Unit tests (functions::skills): - normalize_iii_prefix_with_skills_md_aliases_to_index - normalize_iii_prefix_with_nested_skills_md_aliases_to_index - normalize_iii_prefix_round_trips_with_render_emitted_id BDD scenarios (read.feature): - directory::skills::get accepts the iii:// + SKILLS.md combined form - the legacy iii:// pointer emitted by skills::index round-trips through skills::get (proves the dual-emit output remains a valid get input) --- iii-directory/src/functions/skills.rs | 25 ++++++++++++++++++++ iii-directory/tests/features/read.feature | 28 +++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/iii-directory/src/functions/skills.rs b/iii-directory/src/functions/skills.rs index defd7ccb..13d2723d 100644 --- a/iii-directory/src/functions/skills.rs +++ b/iii-directory/src/functions/skills.rs @@ -557,6 +557,31 @@ mod tests { ); } + // ── iii:// back-compat ───────────────────────────────────────────── + + #[test] + fn normalize_iii_prefix_with_skills_md_aliases_to_index() { + // `iii://` + `SKILLS.md` filename composes through both transforms. + assert_eq!(normalize_get_id("iii://ns/SKILLS.md").unwrap(), "ns/index"); + } + + #[test] + fn normalize_iii_prefix_with_nested_skills_md_aliases_to_index() { + assert_eq!( + normalize_get_id("iii://resend/emails/SKILLS.md").unwrap(), + "resend/emails/index" + ); + } + + #[test] + fn normalize_iii_prefix_round_trips_with_render_emitted_id() { + // The `iii://<id>` token render_index_markdown emits for the + // legacy-pointer footer must parse back through normalize_get_id + // without modification. + let emitted = "iii://agent-memory/index"; + assert_eq!(normalize_get_id(emitted).unwrap(), "agent-memory/index"); + } + // ── validate_id: happy paths ──────────────────────────────────────── #[test] diff --git a/iii-directory/tests/features/read.feature b/iii-directory/tests/features/read.feature index c5653817..0749ff07 100644 --- a/iii-directory/tests/features/read.feature +++ b/iii-directory/tests/features/read.feature @@ -265,3 +265,31 @@ Feature: filesystem-backed reads (directory::skills::list / directory::skills::g When I get skill "iii://ns-combo/lookup.md" Then the get response has id "ns-combo/lookup" And the get response body contains "Body for combined-form lookup." + + Scenario: directory::skills::get accepts the iii:// + SKILLS.md combined form + Given a skill file at "ns-combo-skills/SKILLS.md" with body: + """ + # Combo SKILLS + + Body for combined iii:// + SKILLS.md lookup. + """ + When I get skill "iii://ns-combo-skills/SKILLS.md" + Then the get response has id "ns-combo-skills/index" + And the get response body contains "Body for combined iii:// + SKILLS.md lookup." + + Scenario: the legacy iii:// pointer emitted by skills::index round-trips through skills::get + Given a skill file at "round-trip/index.md" with body: + """ + --- + title: round-trip + type: index + --- + # round-trip + + Body served via the legacy iii:// pointer. + """ + When I index skills + Then the index response body contains "(legacy `iii://round-trip/index`)" + When I get skill "iii://round-trip/index" + Then the get response has id "round-trip/index" + And the get response body contains "Body served via the legacy iii:// pointer." From 92828f09cf936c6043d873dce6c2651690960980 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 19:22:06 -0300 Subject: [PATCH 11/12] ci(skill-check): disable auto-write to stop bot from removing skills/index.md skill-check ran iii-hq/skills-and-validation@v0.4 with write: true, which auto-committed a re-render that deletes any skills/ artifact the renderer classifies as stale. On this PR it removed iii-directory/skills/index.md (content was hand-maintained, not generated from docs/) and rewrote the README. Set write: false so the action runs in report-only mode: the sticky PR comment still surfaces diffs, but no commit lands automatically. The contents permission drops to read since the action no longer pushes. --- .github/workflows/skill-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skill-check.yml b/.github/workflows/skill-check.yml index 49790efa..c499377c 100644 --- a/.github/workflows/skill-check.yml +++ b/.github/workflows/skill-check.yml @@ -6,7 +6,7 @@ on: branches: [main] permissions: - contents: write # for auto-fix mode (commits the rendered diff back) + contents: read # report-only; the action no longer commits back pull-requests: write # for the sticky PR comment actions: write # for the persistent AI skip-cache (v0.3+) @@ -19,6 +19,6 @@ jobs: ref: ${{ github.head_ref }} # check out the PR branch directly - uses: iii-hq/skills-and-validation@v0.4 with: - write: true + write: false scope: pr-diff anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} From 6895469d1471e221d84221bb0a6fea3d80ef6103 Mon Sep 17 00:00:00 2001 From: Guilherme Beira <guilherme.vieira.beira@gmail.com> Date: Tue, 19 May 2026 19:29:15 -0300 Subject: [PATCH 12/12] docs(iii-directory): drop docs/ partials, keep skill.md hand-maintained MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill-check action (iii-hq/skills-and-validation@v0.4) skips a worker entirely when `<worker>/docs/intro.md` is missing — that gates the renderer's drift detection. Without those partials, the action no longer races to re-render and remove skills/index.md or rewrite the README on every push. skill.md stays in tree (required by BOOTSTRAP_WORKERS in the worker validator) but loses the "DO NOT EDIT" header and the now-broken docs/* pointer. The content was carried over verbatim and updated to mention the dual-emit pointer behavior added in this PR. --- iii-directory/docs/companions.md | 5 ----- iii-directory/docs/intro.md | 5 ----- iii-directory/docs/quickstart.md | 37 -------------------------------- iii-directory/skill.md | 4 +--- 4 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 iii-directory/docs/companions.md delete mode 100644 iii-directory/docs/intro.md delete mode 100644 iii-directory/docs/quickstart.md diff --git a/iii-directory/docs/companions.md b/iii-directory/docs/companions.md deleted file mode 100644 index 95a937ed..00000000 --- a/iii-directory/docs/companions.md +++ /dev/null @@ -1,5 +0,0 @@ -`directory::skills::download` pulls bundles from the public workers registry (`api.workers.iii.dev` by default). For self-hosted setups, repoint `registry_url` in `config.yaml` at your own registry. The `directory::registry::*` proxies share their envelope with `directory::engine::workers::*`, so a single parser handles both local and remote worker discovery. - -```bash -iii worker add iii-directory -``` diff --git a/iii-directory/docs/intro.md b/iii-directory/docs/intro.md deleted file mode 100644 index e0bfbe58..00000000 --- a/iii-directory/docs/intro.md +++ /dev/null @@ -1,5 +0,0 @@ -Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces (`skills`, `prompts`, `engine`, and `registry`), so callers learn one envelope across local files, the running engine, and the public workers registry. - -<!-- llm-only:start --> -Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter. `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). -<!-- llm-only:end --> diff --git a/iii-directory/docs/quickstart.md b/iii-directory/docs/quickstart.md deleted file mode 100644 index 475fc588..00000000 --- a/iii-directory/docs/quickstart.md +++ /dev/null @@ -1,37 +0,0 @@ -```rust -use iii_sdk::{register_worker, InitOptions, TriggerRequest}; -use serde_json::json; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let worker = register_worker("ws://localhost:49134", InitOptions::default()); - - let result = worker - .trigger(TriggerRequest { - function_id: "directory::skills::download".into(), - payload: json!({ - "worker": "hello-worker", - "tag": "latest", - }), - action: None, - timeout_ms: Some(30_000), - }) - .await?; - - println!("{result:#?}"); - Ok(()) -} -``` - -```typescript -import { registerWorker } from 'iii-sdk' - -const worker = registerWorker('ws://localhost:49134') - -const result = await worker.trigger({ - functionId: 'directory::skills::list', - payload: {}, -}) - -console.log(result) -``` diff --git a/iii-directory/skill.md b/iii-directory/skill.md index 1b09f5de..5957fb04 100644 --- a/iii-directory/skill.md +++ b/iii-directory/skill.md @@ -1,10 +1,8 @@ -<!-- generated by iii-skill-render. DO NOT EDIT (changes here are overwritten on the next render). Edit docs/intro.md or docs/companions.md. --> - # iii-directory Engine introspection, workers registry proxy, and filesystem-backed skill + prompt reader for the iii engine. Every public function sits under a single `directory::*` namespace, split into four MCP-agnostic surfaces (`skills`, `prompts`, `engine`, and `registry`), so callers learn one envelope across local files, the running engine, and the public workers registry. -Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter. `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`). +Skills and prompts are sourced from a single configured folder on disk (`skills_folder`). The only write path is `directory::skills::download`, which pulls markdown into `skills_folder` from either the workers registry or a GitHub repo. `directory::skills::list` returns one row per markdown file with `title` (preferring the YAML frontmatter `title:` over the body H1) and `type` lifted from frontmatter. `directory::skills::get` accepts a bare id, a `<id>.md` file-path form, or the legacy `iii://<id>` URI. `SKILLS.md` is aliased to `index.md` at scan time so the new convention round-trips through both filesystem and parser. `directory::skills::index` renders a short per-worker overview that emits both relative file-path pointers (`Read [<ns>/index.md](<ns>/index.md)`) and the legacy `iii://<ns>/index` form side by side for back-compat. `directory::skills::download` pulls bundles from the public workers registry (`api.workers.iii.dev` by default). For self-hosted setups, repoint `registry_url` in `config.yaml` at your own registry. The `directory::registry::*` proxies share their envelope with `directory::engine::workers::*`, so a single parser handles both local and remote worker discovery.