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) that speaks to any OpenAI-compatible
chat-completions endpoint.
Install
Install with Homebrew — 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. Build the latest from source
with cargo install --path crates/wcode-cli.
More at weinyn.github.io/wcode.
Configure
~/.config/wcode/config.toml:
= "gpt-4.1" # required
= "https://api.openai.com/v1" # optional; any OpenAI-compatible endpoint
= "sk-..." # optional
= "chat" # optional; chat|responses, default chat
= "high" # optional; free-style reasoning effort, omitted = not sent
# 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.
[]
= "responses"
= "https://api.openai.com/v1"
= "sk-..."
[]
= "auto" # optional; auto|true|false — route bash output through the
# rtk proxy (https://github.com/rtk-ai/rtk) to cut tokens
# (default auto = on only if the rtk binary is on PATH)
[]
= true # optional; true|false — register the grep tool (off by default)
= true # optional; true|false — register the find tool (off by default)
= true # optional; true|false — run independent tool calls from one batch
# concurrently (default true; --sequential forces it off)
[]
# Compact the conversation as it grows. An absent table = harness defaults.
= 100000 # optional; working token ceiling to compact near
# (default: the model's context window)
= 200000 # optional; context-window override for unknown models
= 16384 # compact once fewer than this many tokens remain
= 20000 # recent verbatim context kept after compacting, in tokens
= 2 # complete turns always kept, whatever their size
[]
# 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.
= "AGENTS.md" # optional; load exactly this instead of discovery
= ["AGENTS.md"] # optional; candidate names per directory
# (default: AGENTS.override.md, AGENTS.md, CLAUDE.md)
= true # optional; also load one from ~/.config/wcode
[]
# 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.
= true # optional; discover skills at all. Default true
= ["./team-skills"] # optional; extra roots, scanned first
= ["noisy-skill"] # optional; names to skip
[]
# Retry the connect/handshake (and the first streamed item) on transient
# failures — 429/5xx statuses and transport errors. An absent table = defaults.
= 3 # optional; retries after the first attempt (0 disables)
= 500 # optional; base backoff for the first retry
= 8000 # optional; cap on a single backoff wait
= 60000 # optional; time-to-first-token timeout (0 disables)
= 120000 # optional; inter-item idle timeout (0 disables)
[]
# 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.
= true # optional; true|false — default ON
[]
# 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.
= "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.
= "light-cyan" # optional; role = color (overrides the preset)
= "#5f5f5f" # optional; hex is truecolor (opt-in)
[]
# 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).
= 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)
[[]]
= "explorer"
= "recon only; never edit; cite file:line"
= ["read", "grep", "find"]
= true
[[]]
= "reviewer"
= "verify diffs; PASS / NITS / FAIL"
= "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):
[]
= 3 # optional; the rework cap (default 3)
[[]]
= "explore" # required; a unique label
= "explorer" # EXACTLY ONE of member | script
= "Explore: {{task}}" # optional label; {{task}} is the run's task
[[]]
= "implement"
= "builder"
= ["explore"] # sibling ids; resolved to Task ids at boot
[[]]
= "verify"
= ["implement"]
= true
= "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 scripts 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.
[]
# Root-only workflow guidance (requires --agents), folded into the system prompt
# as a `# Orchestrator workflow` section after the team roster.
= """
Step 1: message a member by name; Step 2: verify their result.
"""
[]
# 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).
= "/tmp/wcode/explorer.sock"
= "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:
- project —
./.wcode/agents/**/*.md, folded under the TOML[team](a TOML member of the same name wins); - 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:
[[]]
= "roles/judge.md" # cwd-relative (absolute allowed)
= "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/ 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):
| command | effect |
|---|---|
/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]` |
/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
| tool | behavior |
|---|---|
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).
readprints<line number>\t<content>. The number is for locating — it feedsoffsetand afile:linecitation — never an address.editnever 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 isE_NO_MATCH; a non-unique match isE_AMBIGUOUS_MATCH(add surrounding lines, or setreplace_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).
editsapplies 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/writeshare 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:
name: pdf-tools
description: Extract text and tables from PDFs. Use for PDF documents.
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 reads 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):
[skills] dirs/WCODE_SKILLSexplicit roots,- 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.agentsis absent), - 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 witherased(), add todefault_tools(). - Policies — implement
Hooks(before_tool_callreturning 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 ofLlmStreamEventout.rig_stream_fn()is the built-in OpenAI-compatible adapter.
License
MIT © 2026 Wei