# wcode
A minimal, pi-like coding agent for the terminal. It streams an LLM conversation
and gives it content-addressed tools — read (numbered lines), grep/find, bash,
edit/edits, write, and ast-grep search/rewrite — and stays out of the way.
Built on a small kernel
([`wcode-harness`](crates/wcode-harness)) that speaks to any OpenAI-compatible
chat-completions endpoint.
## Install
Install with [Homebrew](https://brew.sh) — a tap by URL, no extra repo:
```
brew tap WeiNyn/wcode https://github.com/WeiNyn/wcode
brew install wcode
```
From crates.io (a Rust toolchain is required):
```
cargo install wcode-cli
```
Or grab a prebuilt binary — Linux (x86_64, aarch64) and macOS (Intel, Apple
Silicon) tarballs are attached to each
[release](https://github.com/WeiNyn/wcode/releases). Build the latest from source
with `cargo install --path crates/wcode-cli`.
More at **[weinyn.github.io/wcode](https://weinyn.github.io/wcode/)**.
## Configure
`~/.config/wcode/config.toml`:
```toml
model = "gpt-4.1" # required
base_url = "https://api.openai.com/v1" # optional; any OpenAI-compatible endpoint
api_key = "sk-..." # optional
# Per-model providers: when a model id is selected (mid-session `/model`, a
# worker's `spec.model`, or `--model`), this entry's endpoint/base_url/api_key
# re-point the provider. Absent fields inherit the globals above. Quote ids that
# contain dots or slashes.
[models."gpt-5.1"]
endpoint = "responses"
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
[hooks]
# (default auto = on only if the rtk binary is on PATH)
[tools]
parallel = true # optional; true|false — run independent tool calls from one batch
# concurrently (default true; --sequential forces it off)
[compaction]
# Compact the conversation as it grows. An absent table = harness defaults.
budget = 100000 # optional; working token ceiling to compact near
# (default: the model's context window)
window = 200000 # optional; context-window override for unknown models
min_remaining = 16384 # compact once fewer than this many tokens remain
keep_recent_tokens = 20000 # recent verbatim context kept after compacting, in tokens
keep_recent_turns = 2 # complete turns always kept, whatever their size
[instructions]
# Instruction ("reference") files folded into the system prompt. An absent table
# = discover the candidate names from the working dir up to the repo root, plus
# a global file in the config dir, merged global-first. `file` overrides
# discovery with a single name/path; "off" disables.
file = "AGENTS.md" # optional; load exactly this instead of discovery
names = ["AGENTS.md"] # optional; candidate names per directory
# (default: AGENTS.override.md, AGENTS.md, CLAUDE.md)
global = true # optional; also load one from ~/.config/wcode
[skills]
# Discover `SKILL.md` packages and fold their name + description into the system
# prompt; the body loads on demand when the agent `read`s the path. An absent
# table = discover from the standard roots.
enabled = true # optional; discover skills at all. Default true
dirs = ["./team-skills"] # optional; extra roots, scanned first
disabled = ["noisy-skill"] # optional; names to skip
[retry]
# Retry the connect/handshake (and the first streamed item) on transient
# failures — 429/5xx statuses and transport errors. An absent table = defaults.
max = 3 # optional; retries after the first attempt (0 disables)
base_ms = 500 # optional; base backoff for the first retry
cap_ms = 8000 # optional; cap on a single backoff wait
ttft_ms = 60000 # optional; time-to-first-token timeout (0 disables)
idle_ms = 120000 # optional; inter-item idle timeout (0 disables)
[workspace]
# Whole-file compare-and-swap on mutations: wcode auto-attaches the digest from
# its last `read` to a mutation and refuses the write if the file changed since
# (a concurrent edit, a formatter), so a stale edit cannot silently clobber.
digest_cas = true # optional; true|false — default ON
[theme]
# TUI theme, presentation-only. `name` selects a built-in preset —
# default | dark | light | gruvbox-dark | nord | solarized-dark | solarized-light,
# or `omarchy` for the live desktop theme (see below).
# `default` is named-16 (safe on any terminal); the others are truecolor, so they
# are a no-op on a non-truecolor terminal (a `light` preset also needs a light
# terminal). A role value is a named ANSI color (`cyan`, `light-yellow`,
# `dark-gray`, ...) or hex `#rrggbb`; absent roles inherit the palette. Roles:
# accent, dim, muted, border, user, body, error, success, warn, code, heading,
# heading_sub, link, tool_name, thinking, diff_add, diff_del. Hex is honored only
# under truecolor; NO_COLOR wins over everything. Select a preset live with the
# TUI's `/theme [name]` (bare `/theme` opens a picker) or start with `--theme <name>`;
# `--list-themes` prints the catalog. Explicit role keys survive a preset switch.
name = "gruvbox-dark" # optional; a catalog preset (default: the `default` named-16 palette)
# `name = "omarchy"` loads the current Omarchy desktop theme — the palette at
# ~/.local/state/omarchy/current/theme/colors.toml — and re-reads it on
# `/theme omarchy`, so an `omarchy theme set` shows up at the next switch. It is
# listed by `--list-themes` only where that file exists.
link = "light-cyan" # optional; role = color (overrides the preset)
muted = "#5f5f5f" # optional; hex is truecolor (opt-in)
[tui]
# TUI presentation (nothing here changes the agent's behavior). `width` is the
# transcript's initial measure (content width) in columns — the reading column
# centers when the band is wider than it; default 68, range 40..=200. An
# out-of-range value fails the load loudly. Adjust it live with `/width <cols>`
# or `Alt-[` / `Alt-]` (the runtime value is not persisted).
width = 96 # optional; 40..=200 (default: 68)
# A preset team the orchestrator starts with (a non-empty [team] auto-enables
# agents — no --agents needed). One [[team]]
# entry per member; names are unique (a duplicate fails the load loudly). Keys:
# name (required) the member's address, unique
# role extra system-prompt text (`# Role`) appended to the worker blurb
# model model id override (default: the orchestrator's)
# tools tool allow-list (`message` is always kept; unknown names rejected)
# read_only enforce read-only: refuse the mutating tools and mutating bash/bg
# effort reasoning-effort override (`-`/`none`/`off` clears; default: inherit)
# base_url provider base URL override (default: the orchestrator's)
# api_key provider API key override (default: the orchestrator's)
[[team]]
name = "explorer"
role = "recon only; never edit; cite file:line"
tools = ["read", "grep", "find"]
read_only = true
[[team]]
name = "reviewer"
role = "verify diffs; PASS / NITS / FAIL"
effort = "high"
```
### `[workflow]` — a plan template (requires `--agents`)
A single `[workflow]` table seeds the task DAG at boot. Each `[[workflow.node]]`
becomes a node, instantiated in **topological order** (so a `depends_on` may name
a later-authored sibling):
```toml
[workflow]
max_attempts = 3 # optional; the rework cap (default 3)
[[workflow.node]]
id = "explore" # required; a unique label
[[workflow.node]]
id = "implement"
member = "builder"
depends_on = ["explore"] # sibling ids; resolved to Task ids at boot
[[workflow.node]]
id = "verify"
depends_on = ["implement"]
gate = true
script = "cargo test --workspace" # a physical gate (exit code = verdict)
```
A node is a `member` (a `[team]` name) OR a `script` (`sh -c`, run under the bash
automation policy); `gate = true` makes its reject re-open its deps, and a gate
needs at least one `depends_on`. The graph is validated acyclic at load. The plan
shows in the TUI's `/tasks` listing as
`#id [state] title (owner) ← #deps ×attempts`.
A node may carry an optional `title` (its label, dispatched to the worker as
`#<id> <title>`; absent → the node `id`). The one placeholder recognized in a
title is `{{task}}` — substituted with the run's task; any other brace form is left
literal, and `script`s are not templated.
Passing `--task <text>` (or the `WCODE_TASK` env) turns a `[workflow]` run
**headless** (no TUI): wcode seeds the root with the task, drives the plan to a
terminal state, and exits — `0` once every node is `Done`, `1` if any node is
`Failed`, if the `--timeout <secs>` **wall-clock** cap elapses (`0`/absent = no
cap), or if the root's first turn errors (the cap starts after that turn). `--task` requires `[workflow]` and
`--agents`, and cannot be combined with `-p`; a `{{task}}` template without a task
is a boot error. `-p` is unaffected (it stays a single root turn), and a pure
`[team]` (no `[workflow]`) remains the interactive, per-task path.
```toml
[orchestrator]
# Root-only workflow guidance (requires --agents), folded into the system prompt
# as a `# Orchestrator workflow` section after the team roster.
guidelines = """
Step 1: message a member by name; Step 2: verify their result.
"""
[peers]
# Persistent phonebook entries for `message`'s `to` (requires --agents). A value
# containing `:` is an address alias (`agent:<id>`); otherwise it is a socket
# path, connected lazily (Unix only).
explorer = "/tmp/wcode/explorer.sock"
helper = "agent:w7"
```
Environment variables beat the toml: `WCODE_BASE_URL`, `WCODE_API_KEY`, and
`OPENAI_API_KEY` as a key fallback, plus `WCODE_ENDPOINT`, `WCODE_EFFORT`,
`WCODE_RTK` and `WCODE_GREP`/`WCODE_FIND` (which override
`tools.grep`/`tools.find`), and `WCODE_COMPACT_BUDGET`, `WCODE_COMPACT_WINDOW`,
`WCODE_COMPACT_MIN_REMAINING`, `WCODE_COMPACT_KEEP_RECENT_TOKENS`,
`WCODE_COMPACT_KEEP_RECENT_TURNS`, and `WCODE_INSTRUCTIONS` (a file name/path, or
`off`), `WCODE_SKILLS` (extra skill roots, or `off`), `WCODE_TASK` (a `[workflow]`
run's task when `--task` is absent), and `WCODE_RETRY_MAX`,
`WCODE_RETRY_BASE_MS`, `WCODE_RETRY_CAP_MS`, `WCODE_RETRY_TTFT_MS`,
`WCODE_RETRY_IDLE_MS` (the `[retry]` table).
Provider precedence is `[models.<id>]` > flags/env/config: the toml globals,
`WCODE_*`, and `--base-url`/`--endpoint` together set the **default provider**;
a `[models.<id>]` entry for the selected model overrides the
`endpoint`/`base_url`/`api_key` fields it names (an unmapped model — or a field
the entry omits — uses the default).
`--config <path>` (or `WCODE_CONFIG`; flag beats env, relative to the cwd)
points at an **overlay** file deep-merged over the global config: tables merge
per key, so an overlay's `[tools] grep = true` adds that flag without clobbering
the rest of the global `[tools]`, while a scalar or array is replaced wholesale.
This keeps team/model overlays (per repo, per project) out of the provider
config. The explicit overlay layers **on top of** the auto-discovered project
overlays (see below); a missing or unparseable overlay is an error.
`--model` / `--base-url` / `--endpoint` / `--effort` / `--config` override a
successfully loaded config (and `--model` rescues a missing `model`), but
cannot rescue an unreadable or invalid `config.toml` — that still exits with an
error. `--effort -` (or `none`/`off`) clears back to send-nothing. When `--model`
selects an id, its `[models.<id>]` entry overrides the default provider for the
fields it names, and switching back to an unmapped model restores the default
provider (a model switch is reversible).
`--no-instructions` skips the instruction files; `--dump-system-prompt` prints
the composed system prompt (instructions included) and exits — no model needed.
With `--agents` and a `[team]`, wcode spawns each member at startup and the
root's system prompt gains a `# Your team` block listing them by name (and role);
members are addressed by name with `message`. The root also gets `member`:
`op = "status"` peeks a member's last completed work (no model call), `op = "ask"`
puts a tool-free question to the member's own model, and `op = "cancel"` stops its
in-flight run — all three work while the member is mid-run (a read comes from its
last turn boundary, a cancel takes effect without waiting for the run to end). A
non-empty `[team]` auto-enables agents mode — no `--agents` needed — and a served
`--owner` worker never sees the block. An
`[orchestrator] guidelines` string is appended as a `# Orchestrator workflow`
section after the team roster, so the workflow can reference the team.
Project config is auto-discovered from the working dir: `./.wcode/config.toml`
then `./.wcode/team.toml` (team wins where they overlap), folded over the global
config, with an explicit `--config`/`WCODE_CONFIG` overlay still layered on top.
Agent definitions are authored as markdown — YAML frontmatter (`name`, `model`,
`effort`, `tools`, `read_only`, `base_url`, `api_key`) plus a body that becomes
the member's `role`. They are discovered from two roots:
1. **project** — `./.wcode/agents/**/*.md`, folded **under** the TOML `[team]`
(a TOML member of the same name wins);
2. **global** — `~/.config/wcode/agents/**/*.md`, the lowest precedence, so a
team travels with you across repos.
A `[[team]]` entry may also point at a file outside either root, which is how a
preset references a role that lives elsewhere:
```toml
[[team]]
file = "roles/judge.md" # cwd-relative (absolute allowed)
model = "claude-haiku-4-5" # a sibling key overrides the file's
```
The file supplies the member; a sibling key overrides it. A `name` given both
inline and in the file must agree, and a missing or unparseable file is a hard
error (unlike a scanned `.md`, which is skipped with a warning).
`--no-project-config` (or `WCODE_PROJECT_CONFIG=off`) disables **project**
auto-discovery only — the `./.wcode/{config,team}.toml` overlays and the
`./.wcode/agents/` scan. The global `config.toml` and the global
`~/.config/wcode/agents/` scan are not project config and still load.
Named project teams are one file per team under `./.wcode/teams/<name>.toml`,
selected with `--team <name>` (mutually exclusive with `--config`); its
`[[team]]` replaces the project `team.toml`'s wholesale and suppresses the
project `./.wcode/agents/` scan ("use this team instead"). The directory is the
name list the TUI/VS Code `/team` picker walks.
The repo ships `.wcode/team.toml` as exactly this — the `[orchestrator]` guidelines
and `[tools] grep/find = true`, with the members in `.wcode/agents/*.md` and the
provider/model left in the global config — so here `wcode` (or `wcode --agents`)
starts the full team with no flags. It also ships the named teams
`.wcode/teams/{recon,pair,full-stack}.toml` and `.wcode/workflow.toml`, a
`[workflow]` plan template for `--config .wcode/workflow.toml --task "…"`.
Keyless local OpenAI-compatible endpoints (Ollama, llama.cpp, vLLM, ... over
`localhost` / `127.0.0.1` / `[::1]`) work without a key — wcode sends a
placeholder bearer token so `--base-url http://localhost:11434/v1` just works.
## Use
```
wcode # interactive TUI (REPL when piped)
wcode -p "explain this repo" # one-shot: run, print reply, exit
wcode --agents --task "fix bug 123" --timeout 600 # headless [workflow] run
wcode --resume # continue the latest session
wcode --no-session --model m --base-url http://localhost:11434/v1
wcode --list-models # print GET {base_url}/models ids, exit
wcode --list-themes # print the theme names (built-ins + `omarchy`), exit
wcode --theme nord # select a preset or `omarchy` (or `[theme] name` in config)
wcode --no-instructions # run without loading instruction files
wcode --dump-system-prompt # print the composed system prompt, exit
wcode --detect-endpoint # when base_url is unset, probe common local endpoints
wcode --dump-config # print the resolved endpoint/base_url/model and key SOURCES, exit
wcode --no-skills # run without discovering skills
wcode --sequential # run each tool call one at a time
wcode --tui # force the full-screen TUI (default on a TTY)
wcode --no-tui # force the line REPL (pipes/CI)
wcode serve --stdio # serve the frame protocol over stdin/stdout (any platform; implies serve)
wcode --peer explorer=/tmp/wcode/explorer.sock # register a remote peer for `message` (repeatable)
wcode serve --name w7 --owner agent:orchestrator # serve this session as <owner>'s worker
```
The interactive path: with no `-p`, `wcode` starts a full-screen TUI when stdin
and stdout are a terminal, and the line REPL otherwise. `--tui`/`--no-tui` force
either; `-p`, `serve`, and piped input always take the non-TUI path. The TUI
renders the same `AgentEvent` stream, over a local session or a `--socket` one.
The [`editors/vscode/`](editors/vscode) extension is a second **client** of the
same kernel: it spawns `wcode serve --stdio` and speaks the same frame protocol
as the TUI.
TUI keys: `Enter` submit · `Shift-Enter`/`Ctrl-J` newline · `Up`/`Down` recall
prompts · `PgUp`/`PgDn` (or the wheel) scroll · `Esc`/`Ctrl-C` cancel a run or
quit · `Ctrl-Y` copy the last reply (OSC-52) · `Ctrl-T` expand/collapse every
tool's output · `Ctrl-N`/`Shift-Tab` (or `Alt-1..9`) switch surface · `Ctrl-B`
toggle the sidebar · `Ctrl-A`/`E`/`W`/`U`/`K` readline-style input editing.
`Ctrl-G` enters **browse mode**, a `▌` cursor over the transcript (`j`/`k` next/
prev, `g`/`G` first/last, `Home`/`End` jump to the top/bottom block in view,
`{`/`}` previous/next block, `/` search the transcript with `n`/`N` repeating,
`Enter`/`Space` expand/collapse the selected block, `y` copy it, `?` help,
`Esc`/`q`/`Ctrl-G` leave). The mouse is captured: click a block to select it in
browse, click a sidebar row to focus that surface, drag across the transcript to
select and copy its text (left button only; a modal owns the mouse). `F1` (or
`/help`) opens the full keymap. The rounded input box carries the chrome in its
four corners (project · ⎇ branch · model · effort · the context gauge · the
mode/state), and a **working-team band** sits above the box — one dim row per
member while it runs. The input
box wraps and grows (capped at 8 rows, or half the screen, then scrolls to the
cursor); a paste over 100 chars or more than 3 lines collapses to a `❰ pasted … ❱`
chip; the full text is sent (outer whitespace trimmed). TUI `/`-commands: `/exit`, `/model <id>`, `/effort
[level]`, `/theme [name]` (bare `/theme` opens a picker), `/compact [text]`, `/usage`, `/copy`, `/team`,
`/surface`, `/btw <question>`, `/plan [on|off]`, `/verify`, `/tasks`, `/help` — the
subset that maps to a `Request` under the current session — plus `/changes` (list
the files this run changed, re-showing a diff), `/resume` (pick a session; the
CLI re-execs into it), `/new` (alias `/clear`; start a fresh session — a new
group for a team — by re-exec'ing with no `--resume`; refused over a socket), and
`/reload [--no-session]` (rebuild + re-exec into the current session, mirroring the REPL;
refused over a socket). The remaining session-lifecycle commands below stay in
the REPL.
`--agents` and a team, each member runs as its own **surface** (the root, plus one
per member): input and `/`-commands go to the **focused** surface — switch with
`/surface` (a picker), `Ctrl-N`/`Shift-Tab` (cycle), or `Alt-1..9` — and a
**left-docked** sidebar (30 columns wide, shown only when the terminal is ≥ 80
columns; `Ctrl-B` toggles it) with **Team / Todos / Changes** sections, the
focused row marked by a `*`. Bare `/team` opens a picker to switch team;
`/team roster` prints the listing and `/team <name>` switches directly.
REPL commands (unknown `/...` lines go to the LLM as prompt text):
| `/exit` | quit |
| `/new` | fresh conversation + session file |
| `/model <id>` | switch model mid-conversation (logged as a `ModelChange`); bare `/model` lists models |
| `/models [filter]` | list `GET {base_url}/models` ids, `*` marks current; optional case-insensitive substring filter |
| `/effort [level]` | show/set reasoning effort (free-style, logged as `EffortChange`); `/effort -` clears to send-nothing |
| `/resume [path]` | reopen a session and replay its history as context (default: latest); restores model + effort |
| `/sessions` | list sessions, `*` marks the current one |
| `/usage` | print aggregate token usage for the current conversation (input/output, plus cache read/write when reported) |
| `/btw <question>` | a tool-free side question answered from the current context (never committed); a dim `btw: …` prints while it is in flight — the TUI's status corner shows `⠹ btw…` until the reply |
| `/plan [on|off]` | toggle (bare) or set plan mode: the editing tools are disabled and mutating shell commands refused (the `PlanModeHooks` denylist), and a `# Plan mode` section is appended to the system prompt — explore and propose, then record the plan as a todo list and `/plan off` to execute |
| `/verify` | render the plan's progress: the last cached todo checklist, its done count, and whether anything is unfinished |
| `/compact [prompt]` | summarize older messages now, keeping the most recent; an optional `prompt` focuses the summary |
| `/skills` | list the discovered `SKILL.md` packages (name, description, file) |
| `/skill <name> [args]` | force-load a skill's body into a turn — for when the model doesn't pick it up from the prompt section on its own; extra `args` become the task |
| `/reload [--no-session]` | `cargo build --bin wcode` then re-exec into the same session (`--no-session` starts fresh); a failed build keeps the current binary running |
Effort fans out per endpoint: Chat sends `reasoning_effort`, Responses
sends `reasoning: { effort }`, unset sends nothing. Model listing only
returns id metadata — no capability flags — so effort support stays
user-managed.
Plan mode (`/plan [on|off]` in the REPL or the TUI) is a kernel `Hooks` policy
(`PlanModeHooks`) over a shared on/off handle: while on, the editing tools are
refused and mutating shell commands are blocked, and the `# Plan mode` section
(`PLAN_SECTION`) is appended to the system prompt. It is never persisted — a
`/new` or `/resume` starts with plan mode off.
Ctrl-C aborts the run in flight and stays in the REPL; when idle it exits.
Output: text streams to stdout, thinking and tool output are dimmed
(`⚙ name ✓ first-line-of-output`), errors to stderr.
## Tools
| `webfetch` | GET a URL over http(s); HTML reduced to readable text. `format` = `text`/`markdown`/`html` (default `markdown`); `timeout` seconds (default 30, max 120). Body capped at 5 MiB, output at 40k chars. Read-only, always registered |
| `read` | file contents as `<line number> <content>`. The number is for **locating**, never an address. A line longer than 300 chars is truncated with `…(+N)` — such a line is not usable as an `old_string` (use `grep`). `offset`/`limit` page; without `limit` the page is capped |
| `grep` | regex search; results are `path:lineno:content`, so a hit feeds straight into `edit` as an `old_string` and into `read` as an `offset` (match lines marked ` <--`). **Respects `.gitignore`** (plus `.git`/`target`/`node_modules` as a built-in floor) and skips binaries; `no_ignore:true` searches ignored files too. Glob include filters, context, case-insensitive. **Registered only when `[tools] grep = true` (off by default — `bash` can search)** |
| `find` | glob-based file/dir listing, one path per line. **Respects `.gitignore`**; `no_ignore:true` lists ignored entries too. **Registered only when `[tools] find = true` (off by default — `bash` can list files)** |
| `bash` | `sh -c` in the working dir; stdout, labeled `[stderr]`, exit code; 30s default timeout, Ctrl-C kills. An always-on catastrophic-risk gate (`BashRiskHooks`) refuses device writes, `mkfs`/`wipefs`/`shred` on a device, recursive `rm`/`chmod`/`chown` of `/`/`~`/`$HOME` (incl. the `/*` fold), and the fork bomb — returned as a `blocked:` tool error, no config knob |
| `bg` | manage tasks started with `bash { background: true }`. `list` (all tasks), `status <id>`, `output <id> [tail_lines]` (tail + the spill file path), `wait <id> [timeout_secs]` (block until it finishes; default 60s, max 300s), `kill <id>`. A finished task **pushes** `[message from bg]` back to the session — a turn if idle, next-turn context if running. Always registered |
| `ast_search` | AST-structural search via `ast-grep` (`$UPPERCASE` wildcards); **registered only when an `ast-grep`/`sg` binary is on PATH** |
| `ast_edit` | AST-structural rewrite of **one file** via `ast-grep` (`pattern`/`rewrite` with `$UPPERCASE` wildcards); `commit:false` dry-runs (diff only), default commits atomically and echoes the diff. **Registered only when an `ast-grep`/`sg` binary is on PATH** |
| `edit` | replace an exact `old_string` with `new_string`. Matching is **byte-exact** (whitespace counts) and must be unique unless `replace_all` — a miss is `E_NO_MATCH`, an ambiguous match `E_AMBIGUOUS_MATCH`, nothing written either way. Because the match *is* the check there is no separate staleness guard. `new_string` is verbatim; empty deletes. Whole-file rewrite = `write` |
| `edits` | apply a batch of the same literal-text ops (`{path, edits: [{old_string, new_string, replace_all?}]}`) to **one file** in a single call. Ops run **in order** against the accumulating content; the batch is atomic — if any op fails, nothing is written |
| `write` | create/overwrite; parents created automatically. Content is written byte-for-byte — indentation preserved, never reformatted |
| `todo` | maintain a session-local todo list — call with `todos` to replace the whole list (each item `content` + `status` pending/in_progress/completed), or with no args to read it back. Always registered, every session (workers included) |
| `session_search` | search prior session transcripts: `scope:all` (default) scans every session in the store, grouped by session, excluding the current one unless `include_current`; `scope:current` reads this session (including turns a compaction replaced with a summary) and also takes `turns{start,end}` and `stats`. Terms are ANDed, case-insensitive; thinking/tool text is skipped unless `include_tools`. Read-only, always registered |
| `spawn` | spawn a worker agent for a task — it runs its own session and reports its result back. Returns the worker's address (use it with `message`). Optional `model`/`role`/`tools`/`base_url`/`api_key`/`read_only`/`effort`; `to` defines the worker on a served peer (`--peer`/`[peers]` name or `agent:<id>`) instead of in-process. Root-only |
| `message` | send a message to another agent session: `to` is the recipient's address (omit → your orchestrator); `mode` is `wake` (default), `notify`, `interrupt`, or `ask` |
| `peers` | list the peers you can message, as `name -> address [state]` |
| `member` | inspect or stop a teammate without waiting for its turn: `op` is `status` (its last completed work, no model call), `ask` (a tool-free question to the member's own model; needs `text`), or `cancel` (stop its in-flight run). `to` is the member's name or address. Root-only |
| `task` | plan and track the team's tasks: `op` is `create` (optional `deps`/`gate`), `assign`, `depends`, `complete`, `reject`, `reset`, or `list`. Root-only |
`edit`/`edits`/`write` share a mutation lock and write via a temp file +
rename, so concurrent file mutation can't interleave or truncate.
When the model issues several tool calls in one message, the independent ones run
**concurrently** — `read`/`grep`/`find`/`ast_search`/`webfetch` are read-only and opt in;
mutating tools and `bash` are barriers, so no read can race a write inside a
batch. Results are still appended in call order. `--sequential` (or
`[tools] parallel = false`) restores strictly one-at-a-time execution.
## Design: editing by literal text
Line numbers are positional — insert one line and every number below silently
means something else, the classic way agents corrupt files. So the editing tools
do not address lines by position at all: `edit` replaces an exact `old_string`
with `new_string`, and `edits` batches those ops. This is still
**content-addressed** — the address is the literal text rather than a hash of it
— so the drift-proof property is unchanged (D007).
- `read` prints `<line number>\t<content>`. The number is for *locating* — it
feeds `offset` and a `file:line` citation — never an address.
- `edit` never re-types the target: it sends the old text and the new text, and
the match is **byte-exact**, so whitespace counts and indentation is
meaningful. A miss is `E_NO_MATCH`; a non-unique match is `E_AMBIGUOUS_MATCH`
(add surrounding lines, or set `replace_all`). Nothing is written either way.
- **The match *is* the staleness check.** If the text changed since the read, it
simply no longer matches — which is why there is no separate whole-file digest
guard (the hash-anchor scheme needed one to cover the range *between* two
anchors; text matching covers the whole span by construction).
- `edits` applies its ops **in order** against the accumulating content, so a
later op can match what an earlier one wrote. The batch is atomic: any failing
op refuses the whole call and nothing is written.
- Fully stateless: no anchor store, no session memory, so external edits (bash,
formatters, other tools) are always picked up.
- `edit`/`edits`/`write` share a mutation lock and write via a temp file +
rename, so concurrent mutation cannot interleave or truncate.
Structural search is a separate axis: `ast_search`/`ast_edit` shell out to an
`ast-grep` binary (the legacy `sg` alias is the fallback; the same auto-detect
pattern as the rtk hook).
## Skills
A skill is a directory with a `SKILL.md` — YAML frontmatter (`name`,
`description`) plus a body:
```markdown
---
name: pdf-tools
description: Extract text and tables from PDFs. Use for PDF documents.
---
# PDF tools
Run `scripts/extract.sh <file>`.
```
Only `name` and `description` enter the system prompt (as an `# Available
skills` section); the body loads on demand — the agent `read`s the SKILL.md
when a task matches. That is progressive disclosure: many skills cost many
one-line entries, not many bodies. A skill may ship `scripts/`, `references/`,
`assets/`; the body refers to them by relative path.
Roots, highest priority first (first `name` wins):
1. `[skills] dirs` / `WCODE_SKILLS` explicit roots,
2. the working dir and its ancestors up to the repo root — `.wcode/skills/`,
then the shared dir `.agents/skills/` (falling back to `.claude/skills/`
where `.agents` is absent),
3. global — `~/.local/share/wcode/skills/`, then the same shared pair.
Malformed skills (bad name, missing description, invalid YAML) warn on stderr
and are skipped. `--no-skills` disables discovery.
## Sessions
JSONL files in `~/.local/share/wcode/sessions`, named `<millis>_<id>.jsonl`
(`~/.local/share` is used even on macOS, matching the config convention).
One entry per message plus `model_change` / `effort_change` markers; torn
final lines are tolerated. `/resume` replays the history as the conversation
context and restores the last model + effort (`--model` / `--effort` flags
win when passed alongside `--resume`).
On a clean interactive exit (`/exit`, or EOF; the TUI's `/exit`) wcode prints the
exact command to bring the session back, e.g.
`resume: wcode --resume <sessions>/<id>/root.jsonl --model <id> --agents` for a
team — copy-paste it to relaunch. A remote (`--socket`) session prints nothing.
With a team, each root session lives in its own **group directory**
`<millis>_<id>/` holding `root.jsonl`, a `manifest.json` (the team topology),
and one `members/<name>.jsonl` per worker — so a team survives a crash and
`--resume` rebuilds the whole team (the root's history plus each member's).
Legacy flat `<millis>_<id>.jsonl` files still list and resume as bare roots.
## Philosophy
pi-like, deliberately small. One kernel — event loop, tool dispatch, hooks,
JSONL sessions — and a thin CLI on top. The kernel crate (`wcode-harness`) is
about 11k lines and the whole workspace about 58k source lines (≈64k including
tests), a fraction of a
batteries-included agent. Absent by
design: MCP, permission prompts, approval flows, config files for
behavior. That logic belongs in code, via `Hooks`: block or rewrite tool calls
(`before_tool_call`), transform context per turn, force stops. Build the
variant you want instead of configuring one.
### Extending
- **Tools** — implement `TypedTool` (typed args + schema via schemars), wrap
with `erased()`, add to `default_tools()`.
- **Policies** — implement `Hooks` (`before_tool_call` returning a reason
blocks execution and feeds the reason back to the LLM as an error).
- **Providers** — implement `StreamFn`: context + system prompt + tool defs
in, stream of `LlmStreamEvent` out. `rig_stream_fn()` is the built-in
OpenAI-compatible adapter.
## License
[MIT](LICENSE) © 2026 Wei