The complete SPAVN reference
One Rust binary, no async runtime, three direct dependencies. Everything below is taken from the shipped source: 11 builtin tools, 37 slash commands, every config key, and the six gates that stand between a model and your filesystem. Press / or ⌘K to search.
Overview#
SPAVN is a terminal coding agent: you describe a change, it reads the repo, edits files, runs commands and reports back. It runs as a single native binary with a synchronous TUI — there is no Node, no Electron, no plugin tree, and no daemon.
The design bets
| Bet | What it means in the code |
|---|---|
| Gates live in code, never in the prompt | Access, phase, path, egress and MCP checks are if statements in guard.rs / tools.rs. A model cannot talk its way past them. |
| No hidden network peers | The binary talks to the endpoint you configure and to processes you name in mcpServers. Tested by a source scan, not a promise. |
| Config over hardcode | Search backend, providers, hosts, tools and the update endpoint all come from ~/.spavn/config.json. No vendor host ships in the binary. |
| One binary, boring deps | serde, serde_json, ureq. No async runtime; threads only where a child process would otherwise block. |
Autonomous harness intelligence (v0.2.0)
| Layer | Module | How it works |
|---|---|---|
| Zero-Stop Verification Gate | src/turn/mod.rs | Automatically runs cargo check --quiet, tsc --noEmit, python3 -m py_compile, or go vet when a turn modifies source files, feeding compiler diagnostics back into the turn loop before stopping. |
| Signature & Arity-Aware Symbol Map | src/tools/search.rs | Extracts exact function, method, struct, and type signatures with 1-based line numbers across modified and neighboring files ([SPAVN LIVE PROJECT SIGNATURES]) so models never hallucinate parameter counts. |
Fuzzy Self-Heal edit Engine | src/tools/edit.rs | Recovers from intra-line whitespace/indentation drift (fuzzy_find_unique_line_block), auto-expands off-by-N line ranges (±25 lines), and supports multi-hunk edits[] plus cross-file atomic files[] transactions. |
Stale-File Guard (FS_STALE_VERSION) | src/tools/edit.rs | Tracks per-file SystemTime snapshots on read and refuses edit / write calls when a file was modified externally since it was last read. |
| Canonical Repeat-Tool Breaker | src/turn/mod.rs | Recursively canonicalizes JSON tool arguments (sorted object keys) and injects [SPAVN REPEAT-TOOL BREAKER] after 2 identical read-only calls to break infinite loops. |
| Turn-Aware Schema & Budget Compaction | src/tools/schema.rs, src/session/compactor.rs | Trims tool schema verbosity after Turn 0 and scales context pruning to the active model's context_window. |
Transactional /undo Stack | src/tools/edit.rs | Snapshots files before every write / edit for instant single or multi-step rollback via /undo [N]. |
At a glance
| Version | 0.2.0 · MIT |
|---|---|
| Platforms | macOS (arm64, x86_64) · Linux (x86_64, arm64) · Windows (x86_64) |
| Rust | 1.80+ (uses LazyLock) |
| State | ~/.spavn · mode 0700, secrets 0600 |
| External tools | ripgrep for the search tool · ssh for remote hosts |
| Bridge | spavn --rpc headless NDJSON JSON-RPC 2.0 stdio server for spavn-desktop & IDEs |
| TUI | Synchronous, alternate scroll region, no flicker, no cursor ghost |
Install#
Five channels. Pick one; they all install the same binary.
| Channel | Command | Update with |
|---|---|---|
| Install script | curl -fsSL https://spavn.dev/install.sh | sh | re-run it |
| Homebrew | brew install tugayoktayokay/tap/spavn | brew upgrade spavn |
| Cargo (git) | cargo install --git https://github.com/tugayoktayokay/spavn-cli | re-run it |
| Prebuilt archive | GitHub Releases + .sha256 sidecar | download again |
| From source | git clone … && cargo build --release | git pull |
Install script
curl -fsSL https://spavn.dev/install.sh | sh
# custom target directory
SPAVN_INSTALL_DIR=/usr/local/bin sh install.sh
# build from a local binary instead of downloading
SPAVN_LOCAL_BIN=./target/release/spavn sh install.sh
Lands in $HOME/.local/bin/spavn plus the spvn alias, then executes the
binary once to prove execve works. Staging uses a fresh inode and an ad-hoc signature
so macOS amfid does not kill the result.
Homebrew
brew install tugayoktayokay/tap/spavn
brew upgrade spavn # later
# Homebrew 6+ trusts third-party taps explicitly:
brew trust tugayoktayokay/tap
The formula lives in tugayoktayokay/homebrew-tap and is generated, never hand-edited:
packaging/homebrew/sync-tap.sh <version> <tap-checkout> pulls each
.sha256 sidecar and rewrites the URLs.
Cargo
cargo install --git https://github.com/tugayoktayokay/spavn-cli # tracks main
cargo install spavn # once publishedcargo install produces only spavn. The spvn alias comes from
the install script and the Homebrew formula.
Verify a download
gh release download v0.2.0 --pattern '*aarch64-apple-darwin*'
shasum -a 256 -c ./*.sha256Quickstart#
spavn # interactive TUI in the current directory
spavn "explain the spill module" # one-shot prompt, still interactive
spavn -p "fix the failing test" -q # headless: print and exit
spavn --resume # session picker
spavn --help # every flag, path and env varFlags
| Flag | Effect |
|---|---|
-m, --model | Model key, e.g. sonnet (claude-sonnet-5-5), opus (claude-opus-5-5), ollama:qwen2.5-coder |
-a, --access | safe · auto · sovereign |
-P, --phase | plan (read-only) · code |
-H, --host | Target host from hosts[] |
-s, --session | Named session file |
-c, --continue | Resume the most recent session |
-r, --resume | Interactive session picker |
-i, --interactive | Force the TUI even with a positional prompt |
-p, --prompt | Explicit prompt (same as positional) |
-t, --think | Show the reasoning stream |
-q, --quiet | Headless: no HUD |
-j, --json | Machine-readable output |
--rpc | Headless JSON-RPC 2.0 stdio bridge (prompt, abort, set_phase, set_model, get_state) |
-V, --version | Print version, OS and arch |
-h, --help | Full reference |
First five minutes
spavn— the TUI opens with a dock: model, access mode, phase, host, context use./byok— paste a provider key (stored in~/.spavn/secrets.env,0600) or start a local model instead./model— pick from local → BYOK → catalog → free tier./access auto— read-only calls run freely, mutations ask./planthen describe the change;/codewhen you are ready to write.
config.json holds endpoints and key names; the material lives in
secrets.env, which the path policy treats as secret material in every mode.
Models & routing#
One picker, four sources, resolved in this order: local → BYOK credits → provider catalog → free tier. Zero vendor lock-in: bring your own local GPU server or API key.
Local models
# Ollama ships as a provider group at http://127.0.0.1:11434/v1 — zero config
spavn --model ollama:qwen2.5-coder "refactor the spill module"
# Other OpenAI-compatible servers: point discovery at the /v1 root once
# LM Studio http://localhost:1234/v1
# llama.cpp http://localhost:8080/v1
export SPAVN_LOCAL_MODELS_URL=http://localhost:8080/v1
export SPAVN_LOCAL_BASE_URL=http://localhost:8080/v1Your own gateway
"providers": [
{
"id": "my-gw",
"title": "My gateway",
"base_url": "https://gw.example.test/v1",
"api_key_env": "MY_GW_KEY",
"models": [{ "key": "fast", "model_id": "gw-fast-1", "desc": "cheap", "context_window": 32768 }]
}
]
Set byokCatalog: false to drop the built-in catalog entirely and drive the picker
from providers alone.
/bench and /fleet
/bench runs one prompt across every candidate and reports TTFT, tokens/sec and
whether the tool-call format actually parsed — JSON, NO TOOL CALL, or
a malformed call. /fleet probes every hosts[] entry over
ssh -o ConnectTimeout=3 and prints round-trip latency plus the host's uptime line.
Slash commands#
37 entries, all with aliases. /help prints the same set in the TUI; /
opens the menu.
| Command | Aliases | Effect |
|---|---|---|
/model | /m /models /local | Model picker: local → BYOK → catalog → free |
/byok | /b /keys /login /provider /providers /connect /auth | Key setup, balance, provider login |
/bench | — | Tournament across candidate models |
/fleet | /f | Host panel with latency probes |
/quota | /q | Quota badge / pool from configured scripts |
/host | /h | Target host selection |
/access | /a | safe 🔒 · auto ⚡ · sovereign 🔥 |
/plan | — | Lock to the read-only design phase |
/code | /act | Unlock writes |
/role | — | auto · coder · reviewer · security · test · docs · refactor |
/think | — | Toggle the reasoning stream |
/recap | — | Idle recap; on / off / toggle |
/todo | /tasks | Live task list; interactive arrow-key /todo done picker, clear |
/queue | /qlist /qedit | Inspect / edit queued prompts |
/queue clear | /qclear /qcancel | Drop the queue and any steers |
/queue pop | /qpop | Pop the last queued prompt |
/new | /n | Fresh session |
/resume | /s /sessions | Session picker |
/fork | — | Branch the current session |
/compact | /c | Compact context |
/clear | — | Clear the transcript |
/reload | /r /rb | Rebuild and re-exec, session preserved |
/update | /upgrade /upd | Check for a newer release; prints the exact upgrade command |
/exit | /quit /x :q | Leave |
/stop | /cancel | Interrupt a running turn |
/diff | /d | Working-tree diff |
/commit | /cm | Commit helper |
/undo | /u | Revert the last N edits via transactional UndoStack (/undo [N]) |
/janitor | /j | Dead code / stray artifact sweep |
/audit | — | HMAC-chained action log, filterable by file |
/blame | — | Attribution for a path |
/pin | /p /pins | Context pins: add / list / remove |
/goal | /g /goals | Session goals |
/wiki | /w | Project wiki view |
/skill | /s /skills | Skill discovery and listing |
/lang | /language /dil /en /tr | UI language |
/help | — | This list |
Keyboard
| Keys | Action |
|---|---|
| Enter | Send |
| Shift+Enter | Newline |
| ↑ ↓ | History / menu navigation |
| Tab | Complete a slash command or @file |
| Ctrl+C | Interrupt the running turn |
| Ctrl+D | Exit |
| Esc | Close the menu / cancel the picker |
Piping into a busy session
busy_input_mode decides what happens when you type during a turn:
queue (default) defers to the next turn, steer interrupts and redirects
the model mid-flight.
Tools#
Eleven builtins, each a fixed contract in tool_definitions(). Anything else comes
from an mcpServers entry.
| Tool | Purpose | Gate |
|---|---|---|
read | File, directory, SQLite, PDF or image; disjoint ranges (src/x.rs:97-112,1066-1076), records mtime snapshot | path policy |
edit | Fuzzy self-heal old_text → new_text, auto line-window expansion, multi-hunk edits[], cross-file atomic files[], stale-file guard | write gate |
write | Create or overwrite with stale-file guard (FS_STALE_VERSION); <host>:/path for remote | write gate |
bash | Local or remote; cwd, timeout_sec, background (PID + log path) | command classifier |
search | Regex content search via ripgrep | path policy |
glob | File listing; skips node_modules, target, .git unless hidden | path policy |
memory | Recall or record decisions; local store or an external binary | read-only tier |
skill | Load domain instructions from ~/.spavn/skills, ~/.claude/skills | read-only tier |
web | Search or fetch a URL as Markdown; redirects re-validated per hop | egress classifier |
subagent | Delegate a read-only investigation with its own context window | cannot mutate |
lsp | Definitions, references, symbols; also the generic MCP call | read-only tier |
Points worth knowing
- Oversized output spills, it is not truncated away.
bashkeeps 8 MiB per stream in~/.spavn/tool-output/(newest 40 kept,0600) and tells the model the path instead of silently dropping bytes. readbeatsbash cat. Range syntax avoids re-reading a whole file and never spawns a shell.subagentcannot write. Investigate in parallel, then apply the change yourself.webhas no built-in endpoint. Search stays off until you pointwebSearchUrlorwebSearchServerat a backend.
MCP servers#
The extension path. There is no builtin desktop-control tool and no hardcoded path to one — the catalog is read from your config, so nothing assumes a vendor or an install location.
{
"mcpServers": {
"memory": { "command": "noospire-memory", "args": ["--stdio"], "readOnly": true },
"desktop": { "command": "open-computer-use", "args": ["mcp"], "readOnly": false },
"remote": { "url": "https://bridge.example.test/mcp", "readOnly": true }
}
}// the model reaches any of them through the generic MCP call
{ "op": "mcp", "server": "desktop", "tool": "click", "arguments": { "x": 640, "y": 320 } }Rules
- Every configured server name is listed in the
lsptool description, so the model finds them without a second prompt. - Servers not in
mcpServersare unreachable. readOnly: truedeclares no side effects; without it, calls pass the mutation gate.- In PLAN phase a mutating MCP call is refused outright — no prompt, no override.
- Stdio servers get a scrubbed environment: only what
envdeclares survives. urlselects a streamable-HTTP server instead of a subprocess.
PATH, unless you list it in
env. That is deliberate: a tool cannot read your shell secrets by accident.
Config#
~/.spavn/config.json. Every key is optional; an absent file is a working default.
Both camelCase and snake_case spellings parse.
{
"default_model": "sonnet",
"busy_input_mode": "queue",
"language": "en",
"byokCatalog": true,
"updateCheck": true,
"updateCheckIntervalHours": 24,
"providers": [],
"mcpServers": {},
"hosts": [{ "key": "buildbox", "ssh_target": "deploy@10.0.0.5", "badge": "BB", "sensitive": true }]
}General
| Key | Meaning |
|---|---|
default_model | Model key used at startup |
fallback_cloud_model | Used when the primary is unreachable |
busy_input_mode | steer interrupts mid-turn, queue defers |
language | tr · en |
byokCatalog | true merges the built-in catalog; false runs the list from providers only |
updateCheck | One background release check per interval |
updateCheckIntervalHours | Hours between checks, minimum 1, default 24 |
updateCheckUrl | Endpoint answering with tag_name; unset derives from the packaged repo, empty disables |
Web / search backends
| Key | Meaning |
|---|---|
webSearchServer webSearchTool | Run web search through that mcpServers entry |
webFetchTool | Tool used for page fetch on that server; unset keeps the built-in fetch path |
webSearchUrl | HTTP MCP endpoint for web. Empty or unset means no HTTP backend |
webSearchHttpTool webFetchHttpTool | Tool names your endpoint serves, if they differ |
webSearchApiKeyHeader webSearchApiKeyEnv | Header and env var carrying the endpoint's key |
webSearchUrl or webSearchServer somewhere.
The refusal message names both routes, so an unconfigured web teaches instead of
failing dumbly.
Local discovery and paths
| Key | Meaning |
|---|---|
local_discovery_url | OpenAI-compatible /v1 URL probed for local models |
local_base_url | Pins the base URL for local / local:<id> keys |
protected_paths allowed_paths | Path policy inputs, ~ expanded |
quota_quick_script quota_full_script | Commands whose output becomes the quota badge / picker pool |
memory_recall_bin memory_remember_bin | External recall bridge instead of the local store |
Remote hosts
| Key | Meaning |
|---|---|
hosts[].key | Alias used as host in tool calls |
hosts[].ssh_target | ssh destination; local means "never remote" |
hosts[].sensitive | Require approval in auto; remote hosts are sensitive even without the flag |
read, write, bash, glob and search
all accept <host>:/path, so one investigation can span machines.
MCP servers
| Key | Meaning |
|---|---|
mcpServers[].command args | Stdio server; ~ expanded, environment scrubbed |
mcpServers[].env | The only variables the child inherits |
mcpServers[].url | Streamable-HTTP server instead of a subprocess |
mcpServers[].readOnly | Declares no side effects; without it, calls pass the mutation gate |
Environment variables#
| Variable | Effect |
|---|---|
SPAVN_HOME | Config and state root instead of ~/.spavn |
SPAVN_MODEL | Default model key |
SPAVN_LANG | Initial UI language (tr / en) |
SPAVN_SHELL | Shell used for bash tool invocations |
SPAVN_BASE_URL SPAVN_API_KEY | Fallback endpoint when no provider resolves |
SPAVN_FORCE_ANTHROPIC | Force the Anthropic wire format |
SPAVN_LOCAL_MODELS_URL SPAVN_LOCAL_BASE_URL | Override local discovery / local base URL |
SPAVN_WEB_SEARCH_URL | HTTP MCP endpoint for web when webSearchUrl is unset |
SPAVN_UPDATE_CHECK | 0 disables the release check |
SPAVN_INSTALL_DIR SPAVN_LOCAL_BIN | Install script target directory / local binary to install |
OPENAI_API_KEY ANTHROPIC_API_KEY GEMINI_API_KEY GROQ_API_KEY DEEPSEEK_API_KEY XAI_API_KEY OPENROUTER_API_KEY | Provider credentials |
Security gates#
Access modes
| Mode | Behaviour |
|---|---|
🔒 safe | Every tool call asks for confirmation |
⚡ auto | Read-only calls run freely; mutations and non-trivial shell commands ask |
🔥 sovereign | Nothing asks — except the categories that always prompt |
The six gates, in order
write/edit/bash and mutating MCP calls refused in PLANtools.rsprotected_paths, secret material, outside allowed_pathsguard.rsreadOnly: truetools.rsstdin at EOF is not
consent.
Phases
/plan is a read-only planning mode enforced in code: the model can read, search and
think, but write, edit, bash and mutating MCP calls are
refused. /code unlocks them. Prompt text cannot talk its way out of the phase.
What is on disk
~/.spavn/ 0700
├── config.json providers, paths, MCP servers
├── secrets.env 0600 · flagged as secret material by the path policy
├── PROFILE.md user profile injected into the prompt
├── sessions/ transcripts
├── skills/ on-demand domain instructions
├── tool-output/ spilled tool output, newest 40 kept
└── audit.key HMAC key for the audit chainContext & memory#
Four layers, increasing specificity. Everything is a file you can read and edit.
| Layer | Location | Loaded |
|---|---|---|
| Repo rules | AGENTS.md in the working directory | always |
| Profile | ~/.spavn/PROFILE.md | always; written back by memory |
| Skills | ~/.spavn/skills, ~/.claude/skills | on demand via skill |
| Pins | per project slug | always, until removed with /pin |
| Wiki | ~/.spavn | on /wiki |
External recall
memory can delegate to your own binaries via memory_recall_bin and
memory_remember_bin instead of the local store. The local store is the default,
not the only path.
Compaction
/compact rewrites the transcript into a shorter form and keeps going;
/recap controls the idle summary line. Context-window usage is shown in the dock as
a bar, sourced from the model's declared window.
Architecture#
Modules
src/
├── main.rs CLI parsing, TUI & --rpc entry point
├── lib.rs module surface
├── rpc.rs headless JSON-RPC 2.0 stdio bridge
├── router/ model resolution, catalog, OAuth, SSE stream watchdog
├── turn/ agentic turn loop, Zero-Stop compiler gate, repeat-tool breaker
├── tools/ schema, fuzzy self-heal edit, stale-file guard, live signature map, shell, MCP
├── guard.rs access, path, shell AST risk and egress enforcement
├── hud/ dock, input, keys, menus, pickers, panels, tool cards
├── slash_commands.rs command dispatch and /help
├── session/ JSONL persistence, budget-relative compactor, fork
├── spavn_engine.rs audit chain, secret scrubbing, janitor, goals, pins
├── memory.rs recall / remember bridge & repo map builder
├── skills.rs skill discovery and injection
├── update.rs release check and upgrade advice
├── spill.rs oversized-output spill files
├── markdown.rs terminal Markdown rendering
├── state.rs shared signals between TUI and agent
├── http.rs the only socket code
├── ansi.rs escape sequences & OSC-52 clipboard
└── config.rs config loading, defaults, provider catalog (Claude 5.5, etc.)Invariants
- Gates are code, never prompt text. Only
guard.rscan permit. - No implicit peers. The binary talks to the endpoint you configured and to processes you named in
mcpServers. - One socket layer. All network I/O goes through
http.rs, which classifies every host before dialling.
Threads
No async runtime. The render loop is synchronous; threads exist only where a child process would
otherwise block: the pipe drainer that caps large output, the background-job waiter, bounded
subagent fan-out, and the one-shot update check. A long tool call still blocks the
render loop; interruption is by signal.
The request lifecycle
- Prompt is assembled from
AGENTS.md, profile, skills and pins. - Router resolves model key → endpoint.
- Egress is classified and secrets are scrubbed before the body leaves.
- Tokens stream into the transcript; tool calls arrive as structured JSON.
- Each tool call walks the six gates; a mutation may prompt.
- Result returns to the model; the dock and tool cards update.
Updates#
There is no auto-updater and no daemon. The binary compares its own version against the newest published tag and tells you the command to run.
- Background check — once per
updateCheckIntervalHours(default 24), a single GET. Nothing is downloaded and nothing is executed. - Banner notice — when a newer release is known, the welcome output carries a box with both versions and the upgrade command.
- Mid-session notice — if the check finishes later, the notice is parked and printed once the prompt is idle. It is never shown twice.
/update— check on demand and print the exact command.
/update
# the command it chooses depends on how this binary was installed
# brew → brew upgrade spavn
# cargo → cargo install spavn --locked --force
# curl → curl -fsSL https://spavn.dev/install.sh | shTurning it off
// ~/.spavn/config.json
{ "updateCheck": false }
# or per invocation
SPAVN_UPDATE_CHECK=0 spavn
/update says the check is disabled rather than falsely reporting "up to date".
Release channels
One tag drives everything: the GitHub archives with SHA-256 sidecars, the Homebrew formula
(generated by packaging/homebrew/sync-tap.sh) and the crates.io publish. Nothing is
rebuilt per channel.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| ⚠️ NO TOOL CALL | The model cannot produce the tool format | /bench it, then pick another model |
search errors out | ripgrep missing | brew install ripgrep / apt install ripgrep |
web refuses to search | No backend configured | Set webSearchUrl or webSearchServer |
| Remote host shows 🔴 | ssh target unreachable | /host, then check ssh_target |
| Approval never appears | No TTY — the answer is deny by design | Run from a real terminal |
| Writes refused | Still in PLAN phase | /code |
Everything asks in auto | Command classified as mutating, or the host is sensitive | Expected; use /access sovereign for speed |
brew refuses the tap | Homebrew 6+ requires explicit trust | brew trust tugayoktayokay/tap |
brew install says Xcode is too outdated | Homebrew aborts any install on an old Xcode | Update Xcode, or point DEVELOPER_DIR at the Command Line Tools |
| Binary killed on macOS after replacing it | Stale code signature in the kernel cache | Use the install script: it stages a fresh inode and re-signs |
Health checks
spavn --version # version, OS, arch
spavn --help # flags, paths, env vars
/audit # what actually ran, HMAC-chained
/fleet # host reachability and latency
/local # local model discoveryFAQ#
Does it phone home?
No telemetry and no analytics. Two outbound calls exist: the provider endpoint you configured,
and — if you leave it enabled — one release check per interval against
updateCheckUrl. Both are auditable in http.rs.
Why is web search off by default?
Because shipping a default host would send your queries to a third party nobody chose. Point
webSearchUrl at your own bridge, or name an MCP server with
webSearchServer. Fetching a URL you gave it works without any config.
Can I use it fully offline?
Yes, with a local model: spavn --model ollama:qwen2.5-coder, or any
OpenAI-compatible server via SPAVN_LOCAL_BASE_URL. Set
updateCheck: false and nothing leaves the machine.
How is this different from a chat wrapper?
The gates. Access mode, phase, path policy, egress classification, MCP read-only checks and an
HMAC-chained audit log are enforced in Rust, not requested in a prompt. A model that decides to
write to ~/.ssh in PLAN phase is refused.
Does it work over SSH on a remote box?
Add the host to hosts[] and pass <host>:/path to
read, write, bash, glob or
search. Remote hosts are treated as sensitive automatically.
Where are my API keys stored?
~/.spavn/secrets.env, mode 0600. config.json stores key
names only. The path policy classifies secrets.env as secret material, so
no tool can read it back out.
What happens if I pipe input into it?
Without a TTY there is nobody to approve anything, so the answer is deny. Headless use
(-p … -q) is designed for read-only or pre-approved work.
How do I add desktop control?
Through MCP, not a builtin. Register the server in mcpServers and call it via the
lsp tool's mcp operation. Nothing assumes a vendor or a path.
Why is there no async runtime?
A synchronous render loop is simpler to reason about and starts in under a millisecond. Threads are used only where a child process would block: pipe draining, background jobs, subagents, and the update check.
Can I ship it inside a company?
MIT licensed. Pin a version, mirror the archives, and set updateCheckUrl to your
own release feed — or turn the check off entirely. Provider endpoints and key env names are
config, so internal gateways work without patching.
Contributing#
Build and gate
cargo build --release
cargo fmt --all --check
cargo clippy --all-targets
cargo test
# the ignored test needs a live MCP server from your own config
cargo test -- --ignoredRules that keep the project intact
- No new runtime dependency without a strong reason, and no async runtime.
- Every gate is enforced in code, never only in the prompt.
- New behaviour needs a test that fails without it.
- User-facing changes update
README.mdandCHANGELOG.md.
Release order
cargo fmt --all --check && cargo clippy --all-targets && cargo test
cargo publish --dry-run
git commit -am "chore(release): v0.1.1" && git tag v0.1.1 && git push --tags
# CI builds the matrix and attaches SHA-256 sidecars
cargo publish # needs a token
sh packaging/homebrew/sync-tap.sh 0.1.1 /path/to/homebrew-tap # or let CI do itcargo publish runs last so the crate's repository field and the README
images point at a release that already exists.