Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .github/scripts/build_skills_payload.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Build the POST /w/<slug>/skills payload from a worker directory.

Walks the canonical ``<worker>/skills/SKILL.md`` entrypoint plus
``<worker>/skills/**/*.md`` and produces the JSON body expected by the
workers-registry endpoint. Skill paths map to keys as:
Walks the optional ``<worker>/skills/SKILL.md`` entrypoint plus every other
``<worker>/skills/**/*.md`` document and produces the JSON body expected by
the workers-registry endpoint. Skill paths map to keys as:

<worker>/skills/SKILL.md -> "SKILL.md"
<worker>/skills/<rel>.md -> "skills/<rel>.md" (except SKILL.md)
Expand Down Expand Up @@ -35,17 +35,16 @@ def _read_nonempty(path: pathlib.Path) -> str | None:
def collect_skills(worker_root: pathlib.Path) -> dict[str, str]:
"""Return a ``{payload-key: markdown-body}`` map for one worker directory.

The worker overview is always published as registry key ``SKILL.md``,
sourced from ``skills/SKILL.md`` when present. Empty bodies are skipped
silently so blank placeholder files don't end up in the registry.
The optional worker overview is published as registry key ``SKILL.md``,
sourced from ``skills/SKILL.md`` when present. Other markdown documents do
not require that overview. Empty bodies are skipped silently so blank
placeholder files don't end up in the registry.
"""
skills: dict[str, str] = {}

leaves_dir = worker_root / "skills"
skills_skill = leaves_dir / "SKILL.md"
markdown = sorted(leaves_dir.rglob("*.md")) if leaves_dir.is_dir() else []
if markdown and not skills_skill.is_file():
raise ValueError(f"{worker_root.name}/skills/SKILL.md is required when skill documents are present")

top_body = _read_nonempty(skills_skill) if skills_skill.is_file() else None
if top_body is not None:
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/test_build_skills_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def test_single_skill_md_publishes_bundle_root_skill_md(self) -> None:
skills = collect_skills(root)
self.assertEqual(skills, {TOP_SKILL_KEY: "# My Worker\n"})

def test_nested_documents_require_the_canonical_entrypoint(self) -> None:
def test_nested_documents_do_not_require_the_canonical_entrypoint(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp) / "incomplete-worker"
(root / "skills").mkdir(parents=True)
(root / "skills" / "topic.md").write_text("# Topic\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "skills/SKILL.md is required"):
collect_skills(root)
skills = collect_skills(root)
self.assertEqual(skills, {"skills/topic.md": "# Topic\n"})

def test_skill_md_plus_nested_extra(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
Expand Down
6 changes: 6 additions & 0 deletions .github/scripts/tests/test_validate_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ def test_happy_path_passes(self, tmp_path):
r = run_script(repo, "smoke", source_changed=["smoke"])
assert r.returncode == 0, r.stderr

def test_worker_without_skill_md_passes(self, tmp_path):
repo = make_worker(tmp_path, "shell")
init_git(repo)
r = run_script(repo, "shell", source_changed=["shell"])
assert r.returncode == 0, r.stdout + r.stderr

def test_missing_readme_fails_in_strict_mode(self, tmp_path):
repo = make_worker(tmp_path, "smoke")
(repo / "smoke" / "README.md").unlink()
Expand Down
37 changes: 1 addition & 36 deletions .github/scripts/validate_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,9 @@
2. iii.worker.yaml parses and has required fields + valid enum values.
3. The manifest version on this ref is greater than or equal to on --base-ref.
4. tests/ exists and is non-empty.
5. For workers in BOOTSTRAP_WORKERS, skills/SKILL.md exists, is non-empty,
and is within the 256 KiB cap — the harness bootstraps these onto disk via
iii-directory on first boot; a missing or oversized file breaks the
chat surface's orientation.

If `--worker` is not in `--source-changed`, requirements 1, 3, and 4 are
downgraded to GitHub Actions notices instead of hard errors. Requirement
5 is always strict — it's a release-blocking guarantee, not a hygiene
check.
downgraded to GitHub Actions notices instead of hard errors.
"""
from __future__ import annotations

Expand All @@ -29,16 +23,6 @@
import _lib # noqa: E402


# Workers whose skills the harness stack requires at boot, making
# skills/SKILL.md a hard PR gate. Keep in sync with what the harness
# actually bootstraps.
BOOTSTRAP_WORKERS = frozenset({
"iii-directory",
"shell",
})

SKILL_MD_SIZE_CAP = 256 * 1024 # 256 KiB

# `iii worker add <worker>` downloads the release archive and looks for a
# binary named after the WORKER (see iii crates/iii-worker binary_download.rs:
# extract_binary_from_targz(worker_name, ...)). The registry payload carries
Expand Down Expand Up @@ -222,25 +206,6 @@ def soft(msg: str) -> None:
elif not any(tests_dir.iterdir()):
soft(f"{worker}/tests/ is empty")

# 5. Bundled workers must ship skills/SKILL.md within the size cap.
if worker in BOOTSTRAP_WORKERS:
skill_md = root / "skills" / "SKILL.md"
if not skill_md.exists():
hard(
f"{worker}/skills/SKILL.md is missing — bundled workers must ship one "
f"(see docs/sops/binary-worker.md)"
)
elif skill_md.stat().st_size == 0:
hard(
f"{worker}/{skill_md.relative_to(root).as_posix()} is empty — "
f"must contain the H1 + summary (see docs/sops/binary-worker.md)"
)
elif skill_md.stat().st_size > SKILL_MD_SIZE_CAP:
hard(
f"{worker}/{skill_md.relative_to(root).as_posix()} exceeds 256 KiB cap "
f"({skill_md.stat().st_size} bytes; see docs/sops/binary-worker.md)"
)

for e in errs:
print(f"::error::{e}")
return 1 if errs else 0
Expand Down
10 changes: 5 additions & 5 deletions DOCUMENTATION_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ This guide describes how to author a worker's skill doc so agents know **when**
and **why** to use a worker without duplicating what the engine already exposes
as structured API reference.

Each worker ships one file:
Workers may optionally ship a canonical overview file:

```text
engine/src/workers/<worker>/skills/SKILL.md
<worker>/skills/SKILL.md
```

The file is lean on purpose. Inputs, outputs, JSON schemas, and worked examples
Expand All @@ -16,10 +16,10 @@ live in the API reference — agents fetch them with `iii list functions` and
intent, lists what's available, and covers trigger binding when the worker emits
events.

## SKILL.md structure
## Recommended SKILL.md structure

Every worker `SKILL.md` has YAML frontmatter and four body sections (the last
one only when the worker exposes a trigger type).
When present, a worker `SKILL.md` has YAML frontmatter and four body sections
(the last one only when the worker exposes a trigger type).

### Frontmatter

Expand Down
20 changes: 7 additions & 13 deletions docs/architecture/skills-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,27 @@

How agent-facing skill docs and default permissions are managed.

## skills/SKILL.md lifecycle
## Skill documentation lifecycle

### Authoring

Each worker may ship `skills/SKILL.md` — a lean intent doc for agents (when to
use, boundaries, function catalogue). **Not** JSON schemas or worked examples;
those live in `iii get function info`.
use, boundaries, function catalogue). It is optional, including when other
markdown documents exist under `skills/`. **Not** JSON schemas or worked
examples; those live in `iii get function info`.

Author per [`DOCUMENTATION_GUIDELINES.md`](../../DOCUMENTATION_GUIDELINES.md).

### PR validation

| Case | Rule |
|---|---|
| Bootstrap workers (`shell`, `iii-directory`) | `skills/SKILL.md` **required**, non-empty, ≤ 256 KiB |
| Other workers | Optional; validated only if present |

Bootstrap list: `BOOTSTRAP_WORKERS` in
[`validate_worker.py`](../../.github/scripts/validate_worker.py) — the workers
whose skills the harness stack requires at boot. Keep it in sync with what the
harness actually bootstraps when that set changes.
Skill documents are optional for every worker. PR validation does not require a
canonical `skills/SKILL.md` entrypoint.

### Publish

On every successful release (when `interface_smoke != false`):

1. `build_skills_payload.py` collects `skills/SKILL.md` and `skills/<rel>.md`
1. `build_skills_payload.py` collects any non-empty markdown under `skills/`
2. `POST /w/<worker>/skills` — skipped cleanly when no markdown found

## iii-permissions.yaml
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/testing-and-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ version/tests/README gates downgrade to GitHub notices.
2. `iii.worker.yaml` parses with required fields
3. Manifest version ≥ version on base branch
4. `tests/` exists and is non-empty
5. Bootstrap workers (`shell`, `iii-directory`): `skills/SKILL.md` present,
non-empty, ≤ 256 KiB

Skill documentation is optional and is not part of this validation gate.

## Language jobs

Expand Down
8 changes: 2 additions & 6 deletions docs/sops/new-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,6 @@ slug against this catalog, so there is no second tag-pattern list to maintain.
| # | Location | Action |
|---|---|---|
| 1 | Release Control typed release policy | Add the worker capability, manifest exception if any, channel policy, and required validation |
| 2 | [`.github/scripts/validate_worker.py`](../../.github/scripts/validate_worker.py) | Add to `BOOTSTRAP_WORKERS` **only if** the harness stack requires this worker's skill at boot — makes `skills/SKILL.md` a hard PR gate (currently `shell`, `iii-directory`) |

**Worked example:** `session-manager` is a standard catalog entry and has no
`BOOTSTRAP_WORKERS` entry because Harness does not require its skill at boot.

## 7. Agent permissions

Expand All @@ -150,8 +146,8 @@ Ship `skills/SKILL.md` when agents should discover **when** to use the worker
(intent, boundaries, function catalogue — not JSON schemas). Author per
[`DOCUMENTATION_GUIDELINES.md`](../../DOCUMENTATION_GUIDELINES.md).

- **Bootstrap workers** (`shell`, `iii-directory`): `skills/SKILL.md` is
**required** (≤ 256 KiB) — the harness stack expects these skills at boot.
- **Optional:** workers may publish other markdown under `skills/` without a
canonical `skills/SKILL.md` entrypoint.
- **On release:** skills are auto-uploaded via `POST /w/<worker>/skills` when
markdown is present; skipped cleanly when absent.

Expand Down
Loading