Headless-first AI coding agent in Rust: one wavecode binary serves single-turn
execution, an interactive REPL, an inline console UI (full-viewport frames on
the main screen, native scrollback preserved), and legacy session resume over
a shared agent core (multi-turn ReAct loop, tools, skills, memory, MCP).
🚧 Under active development; no stable release yet.
Prebuilt binaries (sha256-verified) ship with each v* release:
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/daftpunkwav/wave-code/main/scripts/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/daftpunkwav/wave-code/main/scripts/install.ps1 | iexOr build from source:
cargo build --bin wavecode
wavecode exec "fix the failing test" # single turn, exit code follows outcome
wavecode exec --json "summarize" # JSONL events on stdout
wavecode repl # interactive multi-turn session
wavecode resume # list previous sessions / resume one
wavecode # inline console on a TTY, REPL otherwise
wavecode --plan # start in plan mode (-y for auto mode)
wavecode --debug # debug-level file logging (~/.wavecode/logs)
wavecode metrics # per-model, per-tool quality report
wavecode grants list # what "always allow" has been approving since
wavecode eval tasks # task-level suite: real turns, judged workspaces
wavecode doctor # validate local setup, no provider contact
wavecode update # check for a newer published release
wavecode update --install # download + verify + swap in the newer releaseOn first run without configuration, WaveCode prints a creation guide with a
config template. Create ~/.wavecode/config.toml:
model = "your-model"
model_provider = "your-provider"
[model_providers.your-provider]
type = "anthropic"
base_url = "https://api.example.com/anthropic"
env_key = "YOUR_API_KEY_ENV" # key read from this env var (wins over inline api_key)
# rpm_limit = 30 # optional local throttle (requests/minute)
# fallback_providers = ["backup-provider"] # ordered failover: each provider
# retries transient errors (429/5xx, honoring Retry-After) before the next
# one takes over; auth and quota errors fail fast
# prompt_cache_ttl = "1h" # Anthropic cache entries live 1h instead of
# 5m: writes cost 2x base input instead of 1.25x, but quiet stretches
# (long builds, pauses) stop expiring the whole prompt prefix
# Wire dialect per provider, set with `type`:
# "anthropic" -> Anthropic Messages (POST {base_url}/v1/messages)
# "openai-compatible" -> OpenAI Chat Completions (POST {base_url}/chat/completions)
# "openai-responses" -> OpenAI Responses (POST {base_url}/responses)
# Chat Completions is what most third-party gateways speak; Responses is the
# endpoint that serves models with no chat route (o1-pro, gpt-5-codex) and the
# recommended one for the newer reasoning families. All three stream, carry
# tools and images, and pair tool calls with their results across rounds.
# Optional: entries for the `/model` picker (alias -> provider + wire model).
[models.fast]
provider = "your-provider"
model = "your-model-fast"
# reasoning_effort = "low" # OpenAI-compatible providers only
# The same picker also reads `~/.wavecode/models.json`, a standalone model
# catalog holding full provider specs (endpoint, API kind, context/output
# limits, thinking variants, modalities). Edit it in the console with
# `/provider list` / `add` / `set` / `remove`, or by hand — bare
# `/provider add` walks a step-at-a-time guided wizard over every spec
# field (alias, API dialect, provider, base URL, key env var, limits,
# thinking variants, modalities); `/model` itself only switches. Its models merge into
# `[models]` at startup, and a config.toml provider with the same id
# wins over a catalog one.
# Optional: cheap model for routine side sessions. `/btw` answers sample
# through this [models] entry instead of the primary model; an explicit
# /model choice in the session still wins, and a dangling alias degrades
# to the primary with a warning (doctor reports it).
# secondary_model = "fast"
# Optional: tool-round ceiling per turn. Unset uses the runner default
# (256 — wavecode targets super-long-horizon work); an open session goal
# re-arms the ceiling up to 7 more times (8 ceilings per turn). `0`
# stops every turn before its first tool round.
# max_tool_rounds = 256
# Optional: permission rules, `Scope(pattern)` syntax. Allow entries skip
# approval; deny entries refuse in every mode (deny always wins). `*` matches
# any run of characters, `?` one. Read from this home file only — never from a
# repo-local config, because the working directory is the agent's to write in.
# [permissions]
# allow = ["Bash(cargo test *)", "File(docs/**)"]
# deny = ["Bash(curl *)"]exec: one prompt, one turn. Streams the answer to stdout; tool activity, approvals, and usage go to stderr.--jsonswaps to JSONL events on stdout with human rendering on stderr — a leading{"meta":"session",…}line carries the session id and itswavecode --sessionresume command, and the finished turn is journaled so the session can be resumed. Ctrl-C interrupts the turn (exit 130).exec --image <path>: attaches an image (PNG/JPEG/WebP/GIF, ≤5 MB) to the prompt for vision-capable models; repeatable. Providers without vision reject it with a visible error. Each request carries only the two newest images; older ones travel as[image … omitted]text placeholders so a screenshot-heavy session keeps its budget (the stored session keeps every image, and the placeholder names what was left out).exec --approvals: opts in to answering parked approvals from stdin. Off by default, so unattended runs keep the fail-closed deny. The JSON dialect answers a specific request with one stdin line:<call_id> allow|always|deny[:reason](the call id comes from theapproval_requestedevent). The text dialect prompts on stderr and takesy/a/n. stdin closing (EOF) denies everything still parked immediately — no run can hang waiting for an answer.repl: multi-turn session over one conversation. Slash commands:/compact(compress context now),/memory(show memory index),/mcp(list servers),/permissions(approval mode),/quit(end session),/help./skill-name argsinvokes a user-invocable skill by name.- Console: same session behind a full-viewport inline interface (native
scrollback preserved) — approval prompts render inline
(file-write approvals show the affected lines as a colored diff),
/mcpstatus rows,/btwside questions (read-only, answers stream into a panel without touching the conversation), and dialogs for/model(provider tabs, search, session-only Alt+S, a thinking-level row on OpenAI-compatible providers) plus/provider(a step-at-a-time guided wizard over the full~/.wavecode/models.jsonentry: API dialect, endpoint, key env var, limits, thinking variants, modalities; bare/providerlists saved providers and preseeds the wizard from one;list/set/removeedit saved entries),/permissions,/theme(built-ins plus custom themes),/settings(ten rendering and behavior tunables), and/help(scrollable keybinding + command reference),/doctor(config, credential, catalog, and settings health check),/hooks(the configured hook table),/release-notes(newest changelog sections),/agents(the background-job table). Session commands:/sessions(alias/resume) resumes a recorded session in place,/forksnapshots a resumable copy,/titlerenames,/newstarts a fresh session,/initasks the agent to write AGENTS.md,/statussummarizes the session,/undo [n](or double-Esc) rewinds the conversation by whole turns,/compact [instruction]compresses context with optional steering,/export [path]and/copytake the dialogue out,/usageshows token split,/editor <cmd>sets the Ctrl+G external editor. Completed turns journal under~/.wavecode/sessions/;wavecode --session <id>andwavecode --continueresume from the CLI. Resume is block-level — tool calls, tool results and images come back intact — because each session keeps a write-ahead history journal beside its turn snapshot; a tool whose result was lost to a crash is reported as unresolved rather than re-run. Sessions written before that journal existed resume from their text snapshot. When compaction replaces earlier turns, the summary ends with a## Context Recoverynote naming that journal (and the live task list), so the agent can look up exact earlier output instead of guessing; eachtasksubagent logs its own turns undersessions/children/<parent>/. resume:wavecode resumelists recent legacy sessions newest-first;wavecode resume <thread-id>imports its history as text and continues interactively. Tool calls import as[tool:name]/[error:...]markers (text import is the documented scope; replay never re-executes).serve: local HTTP app server (REST + SSE) over live sessions. Binds127.0.0.1only, requires a per-run bearer token (printed on startup,--tokenoverrides).POST /sessionsassembles a parking-enabled session;GET /sessions/{id}/eventsstreams wire events as SSE;POST /sessions/{id}/promptsubmits a turn; parked approvals and questions are answered viaPOST .../approvals/{call_id}and.../questions/{call_id};POST /shutdownstops the server.metrics: aggregates the local metrics ledger (~/.wavecode/metrics/) into a per-model, per-tool table — executed calls with their success rate, refusals and denials kept in separate columns, prompt-cache read share, turns, approvals. Offline: it reads local files and never contacts a provider.--session <id>narrows to one session;--jsonemits the merged totals for scripts.grants: lists the "always allow" decisions previous sessions persisted (~/.wavecode/grants.jsonl) with the indexwavecode grants remove <i>takes;grants clearrevokes all of them. Every action here only tightens authority — a revoked grant goes back to asking. Exit 1 when a revoke could not be carried out or the index is not in the table.doctor: validates local setup without contacting any provider — config parse, provider api key resolution (never printed),[models]entries, permission rules (invalid entries, and allows a deny rule shadows), the OS confinement backend, console settings, custom themes, and session records. Exit 1 when any check fails.eval tasks: runs the task-level suite underbenchmarks/tasks/(run it from the repo root). Each task copies its fixture into a throwaway work root, drives one realexecturn there, then judges the workspace with assertions — a check command run by argv, or a file that must match, contain, stop containing, exist, or stay byte-for-byte as it was. A task passes only if the turn ended cleanly and every assertion holds, so a crashed step cannot pass on files something else left behind. Costs real tokens: this is a nightly measurement, not a per-PR gate.--filter,--tag,--json,--out <path>and--agent-bin <path>(compare two builds) narrow a run;--permission-mode waveis what lets an unattended suite act at all. Exit 1 unless every selected task passes.
Diagnostics: every surface logs to a daily rolling file under
~/.wavecode/logs/ (14 days retained), never to stdout/stderr, so the
exec --json stream stays clean. Levels: --debug wins over the
WAVECODE_LOG env var, which wins over the warn default (WAVECODE_LOG
takes full env-filter syntax — wavecode=debug for this crate's debug
logs; a bare word filters by target and would silence everything). A panic in a
live UI restores terminal modes before the report prints.
update: compares the running version against the newest GitHub release (10s probe). Prints the release page when newer, "up to date" otherwise, "no published release yet" before the first tag. A failed probe exits 1 so scripts never mistake it for "no update". The TUI probes once at startup and showsupdate available: <tag>in the footer when a newer release exists.update --install: downloads the newest release binary together with its published sha256, verifies the checksum before touching the running binary, then swaps atomically (Windows keeps the replaced binary as<name>.bakfor manual rollback). Refuses source builds (target/; usecargo build --releaseinstead) and platforms without a prebuilt release asset; any failure exits 1.
Three modes: plan (read-only exploration; the model is nudged to propose a
plan and may also simply answer), auto (asks only for command execution and
destructive tools), wave (fully automatic; deny rules still apply). Sources
in precedence order:
--permission-mode <mode>CLI flag (global, wins over config),permission_modein config,- built-in
auto.
Legacy names still parse (guarded/default/acceptEdits → auto,
bypassPermissions/yolo → wave) with a startup warning. Unknown values
warn and fall back to auto. Shift+Tab (or /permissions in the REPL)
cycles the mode for the running session.
Rules sit under the modes: deny entries refuse in every mode, allow
entries skip the prompt (with [permissions] in the home config, see the
template above). Two bounds are worth knowing when you write one: a wildcard
allow never exempts a compound Bash command — git status && curl … still
asks, because * spans command separators — and one entry with a bad syntax
costs only itself, reported as a startup warning rather than dropping the
table it sat in.
Answering always allow stores that exact command or path in
~/.wavecode/grants.jsonl, so the next session starts exempt to the same
call instead of asking you again. Stored grants are literals on purpose: an
entry carrying * or ? would re-parse as a wildcard and approve more than
the human did, so those stay in-memory for the session that approved them,
with a warning in that session's log. Revoke with wavecode grants list /
grants remove <i> / grants clear.
Approval is intent, not containment. OS confinement of shell spawns is
opt-in via WAVECODE_SANDBOX_OS=1; when enabled, the first available backend
wins (bwrap → Landlock → seatbelt → Windows job object) and a platform with
none fails closed instead of running unconfined. Note what that means per
platform: Linux and macOS backends bound the filesystem (cwd + temp writable,
no network for shell spawns), while the Windows job object bounds only process
lifetime and count — no filesystem or network boundary, so an approved command
there runs with your own account's privileges. wavecode doctor prints which
backend is live and how far it reaches.
The default dark look is the synthwave identity (neon cyan primary, amber
user input, deep purple-dark ground); the previous teal deepwave identity
stays selectable. One selection colors both the interface chrome and code
blocks: synthwave pairs with the bundled SynthWave '84 syntax theme, deepwave
with base16-ocean.dark, light with base16-ocean.light. Color depth is
probed once at startup — truecolor by default, nearest-256 or the 16 classic
ANSI colors on plainer terminals.
/theme light|dark|deepwave|auto switches live (auto re-probes the
terminal background); /theme <name> loads a custom theme from
~/.wavecode/themes/<name>.json:
{
"base": "dark",
"syntax_theme": "ocean-dark",
"colors": { "primary": "#ff0000" }
}base is dark, light or deepwave; colors overrides any subset of the
20 semantic tokens with #rrggbb values; syntax_theme is an optional alias
(synthwave-84, ocean-dark, ocean-light). Unknown token names, malformed
colors and unknown aliases are rejected at load — a typo never silently
renders as the base.
- Goals keep the turn going. An objective recorded through the
goaltool (/goalshows its status; per home, CAS-versioned) steers the loop twice over: when the model stops making tool calls while the goal is stillActive, the turn continues — up to 8 times — and when the turn hits its tool-round ceiling with the goal still open, the ceiling re-arms — 8 ceilings per turn (2048 rounds at the default), matching the cap the goal driver sets on itself. Every one of those nudges states what is left of the context, round, re-arm and continuation budgets, so the model can wrap up instead of opening work it cannot finish. Marking the goalcompleted,blockedorpausedstops all steering at once; those are statements that work should stop, and the loop never writes goal state itself. - History is durable per mutation. Every conversation change appends a
synced record to
~/.wavecode/sessions/<id>.history.jsonl, so a crash costs at most the step that was in flight, a resumed session keeps its tool calls and results rather than only prose, and a tool whose result never landed is reported as unresolved instead of silently re-run.
- Skills: Markdown files with frontmatter discovered from home and project
directories. Inline skills expand into the turn; fork skills run as
background child tasks honoring their
allowed-toolssurface, with completion re-injected as notifications.task_output/task_stoplet the model poll and stop them. - Memory: per-turn transcript distillation appends
[category]entries to a home-scoped store; the next session reads them back as its index. - MCP: stdio servers configured under
[mcp_servers.<name>](commandplus optionalargs/env) and streamable-HTTP servers (url, optional OAuth client credentials) connect at startup withinitialize+tools/listand bridge each tool asmcp__<server>__<tool>(honoringannotations.readOnlyHint; capability-gatedread_resource/get_promptdiscovery tools bridge too). Unreachable servers degrade to warnings, never failed startups. Prompts-to-skills conversion is future work.
@wavecode/sdk drives the agent from JavaScript:
execSession({ prompt }) spawns wavecode exec --json, exposes the typed
JSONL event stream as an async iterator, and — with approvals: true — lets
your code answer approval requests (session.answerApproval(callId, "allow")).
Zero runtime dependencies; the wire types in src/types.ts mirror the Rust
EventMsg one-to-one. Tests run against a real binary (see the SDK README).
| Document | Role |
|---|---|
docs/architecture.md |
The crate DAG, dependency direction, and composition root |
docs/development.md |
Build, test, and CI commands; layout conventions |
docs/themes.md |
Authoring guide for theme JSON files |
docs/subsystems/ |
Per-subsystem contracts (core loop, tools, UI, safety, memory, sessions, evals, extensibility) |
docs/cookbook/ |
Task recipes: adding a tool, a subagent, recording and replaying |
Every directory in crates/ carries its own README.md (file map and
per-crate contracts); a Chinese mirror (README.zh.md / *.zh.md)
sits next to each English document.
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo fmt --checkLayout: the workspace is a flat crate DAG under crates/<group>/<crate>,
dependencies pointing downward only — infrastructure (leaf primitives),
foundation (wire types, config, llm, protocol vocabulary, auth) and
capabilities (tools, skills, memory, mcp, sandbox, hooks, context) form
the capability stack, state (store, persistence, checkpoint, goal, plan)
and safety (gate, guardrail, audit, secrets) hold durable data and
policy, action (tasks, workflow, jobs, browser, retrieval) and runtime
(run loop, child turns, scheduler, prompt assembly, plugin seam) execute,
operations (actor, bootstrap composition root, gateway, eval) assembles,
transport carries the MCP byte framing, and frontends hosts the
wavecode binary and the console UI. The TypeScript SDK lives in
sdk/typescript. See docs/architecture.md.
Not every crate in that DAG is reachable from the shipped binary: the
architecture doc's Wiring status table
lists the unwired ones, so a crate existing on disk is not a feature claim.
Documentation: architecture overview, development guide, contributing guide, and AGENTS.md for coding agents.
Commits follow Conventional Commits (feat:/fix:/docs:/refactor:/...,
imperative subject, one change per commit).
MIT, see LICENSE.