A portable skill runner: load a SKILL.md-format skill and run it against
a local Ollama model, Claude Code, Codex, OpenCode, or the Anthropic API —
with every run logged as a durable evidence record.
Four of the five backends need no API key of their own: they reuse CLI tools you're already logged into. Sylvae spans four agent families without any single vendor's credentials being required for it to work.
Phase 1 goal, architecture, and rationale: see
docs/superpowers/specs/2026-08-21-sylvae-phase1-skill-runner-design.md.
uv sync --locked --extra dev
uv run --locked --extra dev pytest
Use uv 0.12.19, as CI does. The committed uv.lock includes runtime
dependencies and the optional dev and mcp extras. Add --extra mcp to
sync when using the MCP server. uv lock --check verifies manifest/lock
consistency; update deliberately with uv lock --upgrade and review and commit
the resulting diff. A plain pip install does not consume this lockfile.
sylvae run skills/summarize-diff --backend anthropic --input path/to/diff.txt
Override the backend's default model with --model. For Ollama, a bare
model name is auto-prefixed with litellm's ollama/ convention if you
leave it off:
sylvae run skills/summarize-diff --backend ollama --model mistral:latest --input path/to/diff.txt
Or let the skill decide: --backend auto reads the tier a skill declares
in its SKILL.md frontmatter (tier: cheap or tier: frontier) and routes
accordingly. A skill with no declared tier defaults to frontier — the safe
choice, not the cheap one:
| tier | default backend | why |
|---|---|---|
cheap |
ollama |
local, free, unlimited |
frontier |
opencode |
capable, and on its own account — automatic routing shouldn't quietly spend your interactive Claude budget |
agent |
shellout (Codex) |
a real agent harness with tools, for work that needs more than one step |
agent is a distinct tier rather than "expensive frontier" because Codex
and OpenCode aren't more-expensive Ollama — their bootstrap overhead is
fixed regardless of task size (~6s/11.5k tokens for a trivial Codex
prompt). That makes them a poor fit for small work and a good one for
anything genuinely needing tool use.
Override per tier with SYLVAE_BACKEND_CHEAP / SYLVAE_BACKEND_FRONTIER /
SYLVAE_BACKEND_AGENT. The right target genuinely differs per operator,
and hardcoding it is what previously left auto broken for every non-cheap
skill.
The tier value is validated at load time. tier: cheep is a clear error,
not a silent fallthrough — a typo that quietly routes to the most expensive
backend is the kind of mistake you'd only notice on the bill. An empty
tier: is an error too; omit the key entirely if you mean "unset".
sylvae run skills/disk-report --backend auto --input path/to/disk-usage.txt
The shellout (Codex) and opencode backends both require their own CLI
on PATH and their own auth already configured — both are real agent
invocations, not plain completion calls, so expect real latency (several
seconds minimum, agent bootstrap overhead) and real cost even for trivial
prompts. Neither participates in --backend auto routing for this reason —
they aren't a "cheap" tier the way Ollama is, just a different kind of
resource:
sylvae run skills/disk-report --backend shellout --input path/to/disk-usage.txt
sylvae run skills/disk-report --backend opencode --model opencode/big-pickle --input path/to/disk-usage.txt
opencode's own model catalog (opencode models) is large — gpt-5.x,
kimi, glm, deepseek, big-pickle, and many more — all reachable through
--model opencode/<name>.
Runs the local claude CLI headlessly, authenticating with your existing
Claude subscription rather than an ANTHROPIC_API_KEY — useful when you
have the former and not the latter:
sylvae run skills/disk-report --backend claudecode --input path/to/disk-usage.txt
It invokes claude -p --output-format json --setting-sources "" --strict-mcp-config.
Those last two flags are there for cost, not tidiness. Measured on the
same trivial prompt:
| invocation | tokens | cost |
|---|---|---|
| default | 28,424 | $0.171 |
| with both flags | 3,146 | $0.027 |
The default loads every installed plugin, skill, and agent before
answering — bootstrap Sylvae has no use for, since a skill run is plain
text in, text out. Dropping the setting sources sheds all of it; auth
lives outside settings and survives. (--bare sheds more but also drops
auth; replacing the system prompt with --system-prompt made it worse,
back up to 16,574 tokens.)
One caveat worth knowing before routing to it. Codex and OpenCode
draw on separate accounts. This one shares your interactive Claude
budget — every run spends the same five-hour allowance you use to work.
Hitting that limit is reported as unavailable, not failed: the run
never happened, it didn't happen badly.
A coordinator can preallocate Sylvae's run id and use the same namespaced identity as WeftMark's evidence producer. This keeps the join explicit and lets WeftMark execute and classify the real Sylvae command rather than importing or guessing a result:
RUN_ID=$(python -c 'import uuid; print(uuid.uuid4().hex)')
weftmark --repo /path/to/worktree \
--producer-id "sylvae:run/$RUN_ID" --producer-kind worker \
evidence run CHANGE_SET_ID --kind test --command \
sylvae run /path/to/skill --backend auto --input /path/to/input \
--run-id "$RUN_ID"
--run-id accepts only Sylvae's canonical 32-character lowercase UUID hex
form. Omitting it preserves the normal behavior: Sylvae allocates the id at
run time. The local JSONL record remains the run record; WeftMark records the
commit-bound command evidence and its producer identity.
sylvae system [--snapshot FILE] [--json] shows the optional Minotaur runtime
snapshot (default RAGBAZ_RUNTIME_STATUS) and evidence/signing workflow hints.
The local review page shows the same data and serves authenticated /api/system.
/healthz returns explicit review-server liveness without loading the run log;
it obeys the server's existing token/loopback Host protection.
Native WeftMark evidence and human review do not require Ephor. Governed holds in Rebekah require enabled Ephor and its authenticated Dash HITL path. Runtime observations cannot approve work. Web reads are not logged updates or delivery receipts; the pages read current files on request.
With Nostoi installed, deliberately verify/sign the audit chain from a terminal:
sylvae audit --runs-dir runs verify
sylvae audit --runs-dir runs attest --principal you@host --key ~/.ssh/id_ed25519
sylvae audit --runs-dir runs verify-attestation --principal you@host \
--allowed-signers signers --fingerprint SHA256:PINThe chain defaults to runs/nostoi.jsonl; SYLVAE_NOSTOI_LEDGER overrides it.
Daily run JSONL and the audit chain are distinct records. The browser excludes
the audit chain from run lists. Sidecar existence is not verified signing,
signatures do not prove tests passed, and the human signing step is never run by
a page refresh or a background agent.
Browse the evidence log in a local, loopback-only web page — filter by skill/backend/status, expand a run to see its input/output/error — and trigger new runs straight from the page (pick a skill, backend, optional model, paste input, submit):
sylvae review
Opens at http://127.0.0.1:8971/ by default. --runs-dir, --skills-dir,
--host, and --port override the defaults. Without a token the server
answers only to loopback Host names (127.0.0.1, localhost, [::1]), so a
DNS-rebinding page cannot use it. To share it (e.g. --host 0.0.0.0), set
SYLVAE_REVIEW_TOKEN in the environment: every request must then send
Authorization: Bearer <token>, and binding beyond loopback without one is
refused.
Triggered runs use real backends — a run through shellout/opencode can
take a while and costs the same as running it from the CLI; there's no
"cheap preview" mode.
For a terminal view of the evidence log — no web server — list recorded runs, most recent first:
sylvae runs
sylvae runs --skill summarize-diff --status ok --limit 10
sylvae runs --json
The table shows each run's timestamp, status, skill, backend, and
runtime_ref — the run's sylvae:run/<id> cross-system identity, the same
string a coordinator uses to record it as evidence elsewhere. --json emits a
machine-readable array (including run_id, runtime_ref, model, and
duration_ms) for scripting or correlation. --runs-dir overrides the default
runs/; --skill, --backend, and --status filter, and --limit caps the
count (0 for all).
To inspect one run in full — its input, output, and any error — pass its id (or
an unambiguous prefix) to show:
sylvae show 9f2c1a
sylvae show 9f2c1a3b4c5d6e7f8091a2b3c4d5e6f7 --json
The default view prints the run's metadata (including its runtime_ref) followed
by the recorded input, output, and error sections; --json emits the stored
record verbatim plus its runtime_ref. An unknown id, or an ambiguous prefix,
is refused rather than guessed.
Output is colored (run status, table header, show sections) only when writing
to a terminal. It is plain when piped or redirected, when NO_COLOR is set, or
under TERM=dumb, so --json and scraped output stay byte-clean; FORCE_COLOR
forces it on.
sylvae completion <shell> prints a completion script for the top-level
subcommands. Set it up once:
# bash — add to ~/.bashrc:
eval "$(sylvae completion bash)"
# …or install system-wide:
sylvae completion bash | sudo tee /etc/bash_completion.d/sylvae
# fish:
sylvae completion fish > ~/.config/fish/completions/sylvae.fish
The script is derived from the live command list, so it stays in sync as subcommands are added.
Sylvae can expose itself as an MCP server, so an agent can delegate work to a cheaper model mid-task instead of doing it inline:
pip install -e ".[mcp]"
claude mcp add sylvae -- /path/to/.venv/bin/sylvae mcp \
--skills-dir /path/to/skills --runs-dir /path/to/runs
Two tools: sylvae_list_skills and sylvae_run_skill. Runs still land in
the evidence log exactly as CLI runs do. The run result includes run_id and
its namespaced runtime_ref. A coordinator may also supply the optional
canonical run_id before execution, allowing an MCP-triggered run and external
WeftMark evidence to share the same sylvae:run/<id> identity.
Two guards, on by default. Runs default to the cheap local backend — an
agent calling Sylvae and landing on something as expensive as itself has
achieved nothing while still looking like it worked. And the claudecode
backend is refused over MCP: it spawns an agent harness that can call
Sylvae again, and it spends the same interactive quota the caller is
already using. --allow-recursive-backends lifts that, but it's a flag
you set when starting the server — deliberately not something a calling
model can grant itself.
--backend auto is refused here too, since tier routing could select a
recursion-risk backend and reopen the hole.
On transport integrity, and what actually guards it. The MCP SDK owns
this, at the file-descriptor level: while serving, it dups the real
descriptors aside and points fd 0 at the null device and fd 1 at stderr,
restoring both on exit. A tool doing print(), sys.stdout.write(), or
even raw os.write(1, ...) cannot corrupt the wire, and a subprocess
cannot swallow protocol bytes from stdin. That's verified empirically in
tests/test_mcp_transport_integrity.py rather than taken on faith.
Sylvae adds nothing there — nothing at Python level could improve on it.
What Sylvae does add is a consequence of that design: since fd 1 is
redirected to stderr, every stray byte lands on stderr, and MCP clients
routinely capture stderr to a log file. LiteLLM logs completion payloads
at INFO — for Sylvae that's the skill's input and output, arbitrary
caller-supplied text. So dependency loggers default to warning.
--dependency-log-level debug lifts that for troubleshooting and will
write caller content to disk; the flag's help says so.
pytest