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
75 changes: 49 additions & 26 deletions .github/scripts/build_skills_payload.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#!/usr/bin/env python3
"""Build the POST /w/<slug>/skills payload from a worker directory.

Walks ``<worker>/skill.md`` and ``<worker>/skills/**/*.md`` and produces the JSON
body expected by the workers-registry endpoint. Skill paths map to keys as:
Walks ``<worker>/skills/SKILL.md`` (and legacy top-of-tree paths) plus
``<worker>/skills/**/*.md`` and produces the JSON body expected by the
workers-registry endpoint. Skill paths map to keys as:

<worker>/skill.md -> "index.md"
<worker>/skills/index.md -> "index.md" (override; warn if both exist)
<worker>/skills/<rel>.md -> "skills/<rel>.md"
<worker>/skills/SKILL.md -> "index.md"
<worker>/skills/index.md -> "index.md" (legacy fallback)
<worker>/skill.md -> "index.md" (legacy fallback)
<worker>/skills/<rel>.md -> "skills/<rel>.md" (except SKILL.md / index.md)

If no non-empty markdown is found the script writes ``skip=true`` to
``$GITHUB_OUTPUT`` (so the calling workflow can gate the POST step off) and
Expand All @@ -25,38 +27,59 @@
KEY_RE = re.compile(r"^[a-z0-9][a-z0-9._/\-]*\.md$", re.IGNORECASE)


def _read_nonempty(path: pathlib.Path) -> str | None:
body = path.read_text(encoding="utf-8")
return body if body.strip() else None


def _resolve_top_skill(
worker_root: pathlib.Path,
) -> tuple[str | None, pathlib.Path | None]:
"""Return ``(index.md body, winning path)`` from the top-of-tree candidates.

Resolution order: ``skills/SKILL.md``, then legacy ``skills/index.md``, then
legacy ``skill.md``. When multiple candidates exist, a GitHub Actions
warning is emitted and the highest-priority file wins.
"""
leaves_dir = worker_root / "skills"
candidates: list[tuple[str, pathlib.Path]] = [
("skills/SKILL.md", leaves_dir / "SKILL.md"),
("skills/index.md", leaves_dir / "index.md"),
("skill.md", worker_root / "skill.md"),
]
present = [(label, path) for label, path in candidates if path.is_file()]
if not present:
return None, None

winner_label, winner_path = present[0]
for label, _ in present[1:]:
print(
f"::warning::{worker_root.name}: both {label} and "
f"{winner_label} present; using {winner_label} as the top-of-tree."
)
return _read_nonempty(winner_path), winner_path


def collect_skills(worker_root: pathlib.Path) -> dict[str, str]:
"""Return a ``{payload-key: markdown-body}`` map for one worker directory.

The top-of-tree resolution order is ``skills/index.md`` then ``skill.md``;
if both exist, a GitHub Actions warning is emitted and the nested one wins
(this matches ``iii-directory``'s on-disk convention). Empty bodies are
skipped silently so blank placeholder files don't end up in the registry.
The worker overview is always published as registry key ``index.md``,
sourced from ``skills/SKILL.md`` when present. 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"
skills_index = leaves_dir / "index.md"
intro = worker_root / "skill.md"

if skills_index.is_file():
body = skills_index.read_text(encoding="utf-8")
if body.strip():
skills["index.md"] = body
if intro.is_file():
print(
f"::warning::{worker_root.name}: both skill.md and "
"skills/index.md present; using skills/index.md as the "
"top-of-tree."
)
elif intro.is_file():
body = intro.read_text(encoding="utf-8")
if body.strip():
skills["index.md"] = body

top_body, _ = _resolve_top_skill(worker_root)
if top_body is not None:
skills["index.md"] = top_body

if leaves_dir.is_dir():
for path in sorted(leaves_dir.rglob("*.md")):
if path == skills_index:
if path in (skills_skill, skills_index):
continue
rel = path.relative_to(worker_root).as_posix()
if not KEY_RE.match(rel):
Expand Down
19 changes: 11 additions & 8 deletions .github/scripts/validate_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
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, skill.md exists, is non-empty, and is
within the 256 KiB cap — the harness bootstraps these onto disk via
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.

Expand Down Expand Up @@ -153,22 +153,25 @@ def soft(msg: str) -> None:
elif not any(tests_dir.iterdir()):
soft(f"{worker}/tests/ is empty")

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

Expand Down
2 changes: 1 addition & 1 deletion console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,4 @@ cd web && pnpm typecheck && pnpm lint

## License

Apache 2.0 — see [LICENSE](../LICENSE).
Apache 2.0 — see [LICENSE](https://github.com/iii-hq/workers/blob/main/LICENSE).
2 changes: 1 addition & 1 deletion database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,4 @@ A few operations are no-ops on certain drivers. They emit a `tracing::warn!` rat

## License

MIT.
Apache 2.0 — see [LICENSE](https://github.com/iii-hq/workers/blob/main/LICENSE).
2 changes: 1 addition & 1 deletion image-resize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ Benchmarks on a standard worker instance (2 vCPU, 512MB):

## License

Apache 2.0
Apache 2.0 — see [LICENSE](https://github.com/iii-hq/workers/blob/main/LICENSE).
2 changes: 1 addition & 1 deletion shell/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# shell — architecture and operator notes

The published `README.md` and `skill.md` for this worker are rendered from `docs/`. This file holds the operator/contributor material that does not belong in the published surfaces full configuration table, threat model, wire shapes for the streaming functions, troubleshooting, tests, and deferred work.
The published `README.md` and `skills/SKILL.md` for this worker are hand-maintained. This file holds the operator/contributor material that does not belong in the published surfaces: full configuration table, threat model, wire shapes for the streaming functions, troubleshooting, tests, and deferred work.

## Build and wire-up

Expand Down
Loading
Loading