Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d4208a2
feat(iii-directory): ship iii and sandbox orientation skills
ytallo May 13, 2026
c617aca
refactor(workers): drop in-process skill registration from bundled wo…
ytallo May 13, 2026
ec07dbd
feat(harness): migrate to iii-directory with first-boot bootstrap
ytallo May 13, 2026
28b315a
chore: auto-render skill artifacts
github-actions[bot] May 13, 2026
b54bd93
docs: design spec for iii agent base prompt with default skills
ytallo May 13, 2026
9f43845
docs: implementation plan for iii base prompt with default skills
ytallo May 13, 2026
8862ccf
docs(iii-directory): absorb iii teaching content into iii.md
ytallo May 13, 2026
359ca21
docs(iii-directory): use agent-facing `function` in iii.md examples
ytallo May 13, 2026
ee91290
feat(turn-orchestrator): add system_default_skills config
ytallo May 13, 2026
881c2f5
test(turn-orchestrator): close impl-default cross-check gap
ytallo May 13, 2026
9a0b10b
feat(turn-orchestrator): thread config through state machine
ytallo May 13, 2026
13afa97
style(turn-orchestrator): clean Arc import + restore Stopped comment
ytallo May 13, 2026
2a62979
feat(turn-orchestrator): two-part system prompt with fetched defaults
ytallo May 13, 2026
3316d28
fix(turn-orchestrator): prompt newline + fetch error context
ytallo May 13, 2026
1727bbf
test(iii-directory): snapshot tests for agent-facing iii.md content
ytallo May 13, 2026
dfe84a3
test(iii-directory): tighten iii_skill_content snapshot needles
ytallo May 13, 2026
25eb26b
chore: address final review notes
ytallo May 13, 2026
7d9e3ea
docs(iii-directory): rename fetch-skill → get in iii.md + hygiene pins
ytallo May 13, 2026
6a3f062
refactor(turn-orchestrator): adopt directory::skills::get API
ytallo May 13, 2026
b8c7cad
docs: align spec + plan with directory::skills::get API rename
ytallo May 13, 2026
3fcf897
chore: auto-render skill artifacts
github-actions[bot] May 13, 2026
b03c587
refactor(harness): remove first-boot skills bootstrap
ytallo May 13, 2026
aa4da0d
chore: sync iii-directory Cargo.lock to v0.5.1
ytallo May 13, 2026
46fb6bf
docs: remove iii base prompt default skills plan and spec
ytallo May 13, 2026
d58cfcf
revert(iii-directory): drop sandbox orientation skill
ytallo May 13, 2026
54525fe
chore: update iii-directory dependency and improve worker bootstrapping
ytallo May 14, 2026
64b2aad
chore: auto-render skill artifacts
github-actions[bot] May 14, 2026
420327b
chore: update BUNDLE_EXCLUDE in release-harness-bundle.yml to include…
ytallo May 14, 2026
3fa0efb
chore: bump bundled iii.worker.yaml deps to ^0.2.0
ytallo May 14, 2026
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
44 changes: 43 additions & 1 deletion .github/scripts/validate_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
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
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.
downgraded to GitHub Actions notices instead of hard errors. Requirement
5 is always strict — it's a release-blocking guarantee, not a hygiene
check.
"""
from __future__ import annotations

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


# Workers the harness materialises into ./data/skills/<name>/index.md on
# boot via skills::download. Must match harness/src/skills.rs::BOOTSTRAP_NAMES.
BOOTSTRAP_WORKERS = frozenset({
"harness",
"iii-directory",
"auth-credentials",
"turn-orchestrator",
"approval-gate",
"shell",
"models-catalog",
"llm-budget",
"session",
})

SKILL_MD_SIZE_CAP = 256 * 1024 # 256 KiB


def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser()
p.add_argument("--worker", required=True, help="worker folder name")
Expand Down Expand Up @@ -137,6 +160,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.
if worker in BOOTSTRAP_WORKERS:
skill_md = root / "skill.md"
if not skill_md.exists():
hard(
f"{worker}/skill.md is missing — bundled workers must ship one "
f"(see AGENTS-NEW-WORKER.md §10)"
)
elif skill_md.stat().st_size == 0:
hard(
f"{worker}/skill.md is empty — must contain the H1 + summary "
f"(see AGENTS-NEW-WORKER.md §10.2)"
)
elif skill_md.stat().st_size > SKILL_MD_SIZE_CAP:
hard(
f"{worker}/skill.md exceeds 256 KiB cap "
f"({skill_md.stat().st_size} bytes; see AGENTS-NEW-WORKER.md §10.2)"
)

for e in errs:
print(f"::error::{e}")
return 1 if errs else 0
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,39 @@ jobs:
--base-ref "origin/$BASE_REF" \
--source-changed "$SOURCE_CHANGED"

# ─── Regression guard (C3a) ────────────────────────────────────
# `skills::register` as a *trigger* (the iii bus function-id) was
# removed when the harness migrated to iii-directory. The bootstrap
# now writes skills to disk; nothing should ever fire that trigger
# again. We match the function-id only when it appears as a quoted
# string literal (`"skills::register"`), so Rust function calls
# named `skills::register(...)` inside the iii-directory crate or
# is_hidden test fixtures are not false positives.
- name: Forbid "skills::register" trigger in Rust sources
if: matrix.worker == 'harness'
run: |
set -e
# We grep for TriggerRequest call sites only — the function_id is
# always passed as `function_id: "skills::register".into()`. The
# mcp hidden-prefix test fixture (`is_hidden_function_id(&c,
# "skills::register")`) is a pre-existing reference (the prefix
# is still hidden from MCP discovery even though no one fires it
# any more) and is intentionally not matched.
matches=$(grep -rn 'function_id:.*"skills::register"' \
--include='*.rs' . \
| grep -v '.worktrees/' \
| grep -v 'target/' \
| grep -v '.git/' \
|| true)
if [ -n "$matches" ]; then
echo "::error::\"skills::register\" TriggerRequest found in Rust sources — \
the iii-directory migration replaced it with on-disk skill.md (see \
docs/superpowers/specs/2026-05-12-iii-directory-migration-design.md). \
Matches:"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "$matches"
exit 1
fi

# ──────────────────────────────────────────────────────────────
# Rust per-worker lint + test
# ──────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-harness-bundle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
# Workers excluded from the bundle even when they're in harness deps.
# `iii-sandbox` is an external engine worker. `skills` is held by maintainers.
# `shell` releases on its own `shell/v*` tag via release.yml.
BUNDLE_EXCLUDE: iii-sandbox,skills,shell
BUNDLE_EXCLUDE: iii-sandbox,skills,shell,iii-directory
run: |
python3 - <<'PY'
import json, os, re, subprocess, sys, urllib.parse, urllib.request, yaml
Expand Down
95 changes: 49 additions & 46 deletions AGENTS-NEW-WORKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,25 @@ For `deploy: image`: `<worker>/Dockerfile` that respects `III_URL` and traps

## 10. Skill registration

Every worker should register a markdown skill on the [`skills` platform
worker](https://workers.iii.dev/workers/skills) at startup so MCP clients
(Claude Desktop, Cursor, MCP Inspector) can discover and orient to its
Every worker should ship a markdown skill so MCP clients (Claude Desktop,
Cursor, MCP Inspector) and the harness UI can discover and orient to its
functions. The skill body lives at `<worker>/skill.md` and is served at
`iii://<worker>`; the auto-rendered `iii://directory/skills` index
links every worker.

> The iii-directory worker version pinned by this convention is
> **v0.4.x** — the `directory::*` namespace (skills, prompts, engine
> **v0.5.x** — the `directory::*` namespace (skills, prompts, engine
> introspection, registry HTTP proxy) is the source of truth for every
> reader-side surface this guide refers to.

### 10.1 Skill ID validation rules (iii-directory v0.4.x)
> Registration is **file-based**, not RPC-based. There is no
> `skills::register` call at boot. Ship `<worker>/skill.md` at the worker
> root; the publish workflow (`.github/scripts/build_skills_payload.py`)
> uploads it as `index.md` to the registry; consumers (harness, Claude
> Desktop, etc.) pull it down on first run via
> `directory::skills::download`.

### 10.1 Skill ID validation rules (iii-directory v0.5.x)

- 1+ segments separated by `/`.
- Each segment: lowercase ASCII letters, digits, `-`, `_`; max 64 chars per segment.
Expand Down Expand Up @@ -290,45 +296,41 @@ group justifies its own router (e.g., a `harness` worker might expose
`harness/providers/anthropic` under a `harness/providers` group router).
Workers in this repo's current batch are single-depth.

### 10.4 Wire-up: lib.rs
### 10.4 No boot-time code

Expose the id, the router markdown, and the leaf bodies as `pub const`s in
`src/lib.rs` so both `main.rs` and the integration tests can reference them:
There is **no** `skills::register`, `skills::unregister`, or
`spawn_skill_register` to write. The worker's job is to ship
`<worker>/skill.md` (and any `<worker>/skills/<sub>.md` leaves) at the
worker root.

```rust
// In src/lib.rs (near the top):
pub const SKILL_ID: &str = "<worker>"; // must equal the folder name
pub const SKILL_MD: &str = include_str!("../skill.md");

// Per-function or per-group sub-skill bodies. Empty for workers with one function.
// Each id must be nested under SKILL_ID (i.e., start with "<worker>/").
pub const SUB_SKILLS: &[(&str, &str)] = &[
("<worker>/<sub_a>", include_str!("../skills/<sub_a>.md")),
("<worker>/<sub_b>", include_str!("../skills/<sub_b>.md")),
];
```
Publishing pipeline:

`include_str!("../skill.md")` resolves from `src/lib.rs`, so the file must
be at the worker root. Don't use `env!("CARGO_PKG_NAME")` — that returns
`iii-<worker>` (with the `iii-` prefix) for these workers, not the
folder name.
1. CI's `pr-checks` job verifies `<worker>/skill.md` exists, is non-empty,
and is ≤256 KiB.
2. `.github/scripts/build_skills_payload.py` runs at release time and
uploads the markdown files to the workers registry as `index.md` (and
any sibling leaves).
3. At consumer first boot (e.g. the harness), `iii-directory` calls
`directory::skills::download worker=<name>` to materialise the markdown
under `skills_folder/<name>/`. Subsequent boots are no-ops —
`directory::skills::list` reports everything is present.

### 10.5 Wire-up: main.rs

> **Migration note (iii-directory v0.4.x).** The state-backed
> **Migration note (iii-directory v0.5.x).** The state-backed
> `skills::register` / `skills::unregister` calls this section
> previously documented are gone. Skills now live on disk under the
> iii-directory worker's `skills_folder` and are populated by
> `directory::skills::download` (from the public registry or a GitHub
> repo). New workers should publish their bundled skills as part of
> their release pipeline rather than re-registering at boot. See the
> [iii-directory README](iii-directory/README.md) for the full flow.
>
> The `SUB_SKILLS` table from §10.4 still lives in `lib.rs` because
> integration tests reference the bundled markdown directly; what
> changes is just where the bus exposes those bodies (the
> iii-directory worker reads them off disk after a download instead of
> a worker pushing them up at boot).

If you need to reference the skill body or sub-skill names from your
worker's code (e.g. a self-test), use `include_str!("../skill.md")`
inside `#[cfg(test)]` only. Production code should not embed the body —
the markdown is the registry's job to distribute. `include_str!` resolves
from `src/lib.rs`, so the file must be at the worker root.

Catches BOTH SIGINT (Ctrl-C in dev) and SIGTERM (container kill) so the
worker shuts down cleanly in production container restarts:
Expand Down Expand Up @@ -357,19 +359,12 @@ async fn wait_for_shutdown() -> Result<()> {
}
```

Replace `<crate>` with the worker's library crate name (e.g.
`auth_credentials`, `auth_rbac`).

In `main()`, immediately after the worker's existing
`register_with_iii(...)` call, place three calls:
In `main()`, after `register_with_iii(...)` succeeds, simply await
shutdown — no skill-register boilerplate:

```rust
// After register_with_iii(...) succeeds:
spawn_skill_register(iii.clone());

wait_for_shutdown().await?;

unregister_skill(&iii).await;
Ok(())
```

Expand Down Expand Up @@ -457,16 +452,24 @@ fn sub_skills_well_formed() {
### 10.7 Lifecycle summary

```
boot:
worker boot:
register_worker() → configure_store/cfg → register_with_iii() → serve traffic

skills are populated separately (iii-directory v0.4.x):
skills are populated separately (iii-directory v0.5.x):
operator → directory::skills::download (registry or git repo)
→ markdown lands at <skills_folder>/<worker>/...
→ directory::skills::on-change fires for subscribers (mcp, etc.)

shutdown (SIGINT or SIGTERM):
wait_for_shutdown() → exit
(no per-worker skill unregister anymore — skills
live on disk under the iii-directory worker)
consumer first boot (e.g. harness):
directory::skills::list → for each missing <worker> in BOOTSTRAP_NAMES:
directory::skills::download {worker, version}
→ writes skills_folder/<worker>/index.md
→ fires directory::skills::on-change

consumer subsequent boots:
directory::skills::list → every <worker> present, no downloads needed.

worker shutdown (SIGINT or SIGTERM):
wait_for_shutdown() → exit. No skill cleanup; the markdown lives in
the registry, not in the worker process.
```
30 changes: 7 additions & 23 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
# Workers TODOs

## iii-sdk: register/unregister helpers for skill registration

**What:** Add `iii.register_skill_with_retry(id, markdown)` and `iii.unregister_skill(id)` to `iii-sdk`. Encapsulates the canonical 5s→60s→180s backoff + give-up loop and the 2s best-effort unregister currently inlined per worker.

**Why:** Each iii worker that registers a skill duplicates ~25 LOC of boot snippet + ~10 LOC of shutdown snippet. With 7 workers in this branch and more to come (harness, image-resize, mcp, iii-lsp, etc.), that's 175+ LOC of mechanical copy-paste. DRY violation; future tuning of retry policy needs N edits instead of 1.

**Fix:** ship the two helpers in `iii-sdk` (whatever crate that is upstream), version-bump, then replace each worker's inline snippet with the one-line call. Update `AGENTS-NEW-WORKER.md` §10 to use the helpers in its example.

**Effort:** ~30 LOC in iii-sdk + tests + version bump + replace 7 inlined sites. Coordination with the iii-hq/workers repo for the SDK release.

**Tracked here because:** publishing-map rule is "no cross-worker path dependencies" so the helpers can't be vendored; they have to live in iii-sdk. Out of scope for the skill-registration PR.

## CI: enforce `<worker>/skill.md` presence in pr-checks

**What:** Extend the per-worker `pr-checks` step in `.github/workflows/ci.yml` with three checks: `<worker>/skill.md` exists, is non-empty after trim, is ≤ 256 KiB. Three named failure messages (missing / empty / oversize), each pointing at `AGENTS-NEW-WORKER.md` §10.

**Why:** Skill registration is currently doc-driven only. New workers can ship without a skill and nothing in CI flags it; the runtime signal is absence from `iii://skills`, which is easy to miss. Add the gate when silent-skip becomes a recurring problem.

**Fix:** ~8 lines of shell inside the existing `pr-checks` loop. No new job. Same shape as the existing `README.md` check, plus the 256 KiB cap.

**Effort:** ~30 min including failure-message wording and a smoke test.

**Tracked here because:** the original skill-registration plan deliberately deferred the CI gate. Capturing the design here so it isn't re-derived when we decide to enforce.
> Closed by the iii-directory migration (2026-05-12):
> - `iii-sdk: register/unregister helpers` — the migration replaced the
> per-worker `skills::register` snippet with file-based publish via
> `iii-directory`. There is no boot-time RPC to wrap.
> - `CI: enforce <worker>/skill.md presence` — added to
> `.github/workflows/ci.yml`'s `pr-checks` job as part of the migration's
> C3 guard (per-bootstrap-worker presence + non-empty + ≤256 KiB).

## mcp: extract `unwrap_function_list` + `format_to_input_schema` (harness side gone)

Expand Down
2 changes: 1 addition & 1 deletion approval-gate/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Subscriber on `agent::before_function_call` that holds calls whose ids appear in `approval_required`, streams `approval_requested` events, and clears when the UI invokes `approval::resolve` or the timeout lapses.

- [`approval-gate`](iii://approval-gate)
- [`approval-gate`](iii://approval-gate/index)
- [`approval::resolve`](iii://approval-gate/resolve) — record `allow` or `deny` for a blocked function call ID in a session
- [`approval::list_pending`](iii://approval-gate/list_pending) — refreshable list of unresolved pending calls per session for the UI
- [`policy::approval_gate`](iii://approval-gate/policy_approval_gate) — hook handler bound to `durable:subscriber` on your configured topic; do not invoke directly unless probing registration
17 changes: 0 additions & 17 deletions approval-gate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,6 @@ use iii_sdk::{
};
use serde_json::{json, Value};

pub const SKILL_ID: &str = "approval-gate";
pub const SKILL_MD: &str = include_str!("../skill.md");
pub const SUB_SKILLS: &[(&str, &str)] = &[
(
"approval-gate/resolve",
include_str!("../skills/resolve.md"),
),
(
"approval-gate/list_pending",
include_str!("../skills/list_pending.md"),
),
(
"approval-gate/policy_approval_gate",
include_str!("../skills/policy_approval_gate.md"),
),
];

pub const FN_RESOLVE: &str = "approval::resolve";
pub const FN_LIST_PENDING: &str = "approval::list_pending";
/// Default `approval_state_scope` (matches [`WorkerConfig::default`]).
Expand Down
Loading
Loading