Skip to content
tabeniusPublic

About

agent invariant memory

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Sylvae

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.

Setup

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.

Run

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>.

The claudecode backend

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.

Correlate a run with WeftMark

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.

Review

Runtime observations, audit and human signing

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:PIN

The 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.

List runs

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.

Shell completion

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.

Use from an agent (MCP)

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.

Test

pytest

About

agent invariant memory

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages