The mark alone — for a favicon, an avatar, anywhere the wordmark does not
fit — ships as the same light/dark pair: assets/mark.svg for dark
backgrounds, assets/mark-light.svg for light ones.
A coding agent with spec-driven development built in.
Status: v0.7. Early, tested, and honest about what it does not do yet.
Vesna is built for the moment when work with a coding agent stops being a chat and becomes a process: a request is classified, designed, written up, planned, and approved — then built by subagents in parallel git worktrees, each task reviewed by a fresh reviewer that can read and cannot write, each merged only when its review passes. The person approves twice and reads the result; the runtime does the rest, and shows every step as it happens.
It is for two kinds of people. Those who want that process done for them, fast, with the evidence on screen. And those who want to learn how spec-driven development works by watching one run — the column on the right is the whole method, live.
A task is finished when a check the system ran says so, not when the agent
says so. task_verify takes a command that fails when the work is not done;
Vesna runs it and reads the exit status. A review counts only if the
reviewer answered through review_verdict, not in prose. A plan builds only
if a person approved it — y to the question, or /approve plan; no tool
can. The state of the work is a
log of typed events, so "I finished T2" in a chat message is never mistaken
for the fact of it.
$ vesna do "add a function sub to math.ts that subtracts two numbers; verify with: grep -q 'export function sub' math.ts"
· read 0ms
· edit 2ms
· plan 5ms
· task_start 1ms
· task_verify 17ms
Added to `math.ts`:
```ts
export function sub(a: number, b: number) { return a - b; }
```
Verification passed:
```sh
grep -q 'export function sub' math.ts
```
5 steps · 18.5s · $0.0000npm install -g @seasons-ai/vesna # needs Bun: the CLI runs TypeScript directlyThen run vesna. With nothing configured it asks which service to use,
finds any key already in your environment, and proves the choice with one
real call before writing anything:
$ vesna
Which service should Vesna talk to?
anthropic — Anthropic
openai — OpenAI
codex — ChatGPT subscription, borrowed from the Codex CLI
openrouter — OpenRouter
groq — Groq — key found in $GROQ_API_KEY
ollama — Ollama (local)
lmstudio — LM Studio (local)
vllm — vLLM (local)
custom — Anything else that speaks the OpenAI API
service: ollama
model [llama3.2]: qwen3
verified — Ollama (local) answered as qwen3That writes ~/.vesna/settings.yaml — a machine-wide default that any folder
uses until a project pins its own with vesna init:
# Written by Vesna. Safe to edit, safe to delete.
# A .vesna/config.yaml in a project overrides everything here.
provider: ollama
model: qwen3Switch from inside a conversation with /provider and /model; both list
what is available and say whether a credential was found. custom needs a
baseUrl; subscription needs an oauth block in a project's config.
Five phases, in order: design → spec → plan → build → done. Ask for
anything and the agent calls classify first — a spike ends in an
answer, a bounded change is designed in the conversation and built, an
architectural one goes through every phase. Overrule it with
/classify <shape>; a person's word is final.
/spec what there is
/spec new <name> start one by hand
/spec open <slug> switch to it
/classify <shape> overrule the agent: spike, bounded or architectural
/approve spec closes design — the plan can be written
/approve plan closes plan — /build may run it
/build run the approved plan: build, review, merge
/build cancel stop it after the task in flight is interrupted
/build resume a build a killed process left: continue that task
/build retry <task> redo one task from scratch
/build abort abandon the interrupted build
ctrl-g show or hide the column
A task in plan.md can name its own check on the line under its heading:
### Task 1: Add the first line
verify: test -f NOTES.md && grep -q one NOTES.md
Append a line containing the word `one` to NOTES.md.Vesna runs that command itself, twice: in the task's worktree once the
review passes, and on the base branch once the task is merged. A failure
before the merge is a fix round for the worker, on the same counter as
review rounds; a failure after the merge keeps the merge as it is, writes
verify.failed, and stops the build for a person. Both runs log to
.vesna/specs/<slug>/verify/. A done task in the garden says who produced
its evidence — ✓ worker ✓ reviewer ✓ vesna — with a dash for a task that
declared no check and a cross for a check that failed after the merge.
After a turn that leaves a plan waiting, the chat lists the tasks with their
checks and asks; only a typed y writes the approval:
T1 Add the first line verify: test -f NOTES.md && grep -q one NOTES.md
T2 Add the second line no verify — worker and reviewer only
approve the plan? [y] yes [n] not yet
approved: plan — /build will run it
The approval names the text that was read. Edit plan.md afterwards and the
build refuses until someone approves it again:
$ vesna build notes
vesna: plan.md changed after it was approved — approve it again
$ echo $?
2/build runs the approved plan one task at a time, in dependency order: a
brief cut from plan.md, a worker in its own worktree, a reviewer that must
answer through review_verdict, up to five fix rounds, then a merge. After
the last task a review of the whole branch stops the build on "not met" or a
critical finding. Every step is an event; the garden shows it live. The same
loop from the shell:
$ vesna build demo-arithmetic
· building
· T1 building
· T1 review: met, 0 findings
· T1 merged e0c6c92
· T2 building
· T2 review: met, 0 findings
· T2 merged 2790604
· branch review: met, 0 findings
· done
$ echo $?
0An unattended build cannot ask you anything. In the default ask mode the
first write is a question, and a question nobody can answer is a refusal:
$ vesna build demo-arithmetic
· building
· T1 building
· stopped: T1: the worker was not allowed to: edit math.ts
vesna: T1: the worker was not allowed to: edit math.ts
$ echo $?
1Set permissions.mode: auto in the project's .vesna/config.yaml before an
unattended build, and know what that means: under auto the approval gate
binds you, not the model's shell. Exit codes: 0 done, 1 stopped for a
person, 2 never started —
$ vesna build nope
vesna: no spec called "nope"
$ echo $?
2ctrl-c and /build cancel end a build through the log: the task in flight
marked failed, the build stopped. A process killed outright writes nothing,
so the log still says "building" — and the next /build refuses:
$ vesna build demo-arithmetic
vesna: a build of "demo-arithmetic" was interrupted — /build resume, /build retry <task>, or /build abort
from the shell: vesna build demo-arithmetic --resume | --retry <task> | --abort
$ echo $?
2resume continues the interrupted task in the checkout it was left in,
retry <task> throws that checkout away and builds the task again, abort
abandons the build. A merged task's worktree and branch are removed; a
stopped task's are kept, so there is something to look at.
A build the branch review stopped, or whose last check failed on the base, is
finished by a plain /build once the base is fixed: the red check runs
again, no task runs, and the branch is reviewed again — from the commit the
build first started at, whether or not it was resumed along the way. A build
that already finished refuses instead:
$ vesna build demo-notes
vesna: nothing to build — every task is merged
$ echo $?
2Three modes in .vesna/config.yaml — plan looks and changes nothing, ask
asks before each new kind of action and remembers your answer, auto allows
everything but the always-ask list: secrets, the spec's event log, sudo,
rm -r, force pushes, publishing. Reading never asks. A reviewer's shell is
held to a read-only allowlist. The chat's bottom line names the mode in force
and the key that cycles it — /help · shift-tab: ask → auto · ctrl-c twice to leave — so auto is never a surprise.
This is policy, not a sandbox. shell and script run with your own
privileges; the classifier that decides what is read-only has needed a fix in
every review it has had. Containment is on the roadmap, not claimed.
One core, four clients. vesna is the full-screen chat; vesna --plain is
the same conversation one line at a time, for a dumb terminal; vesna serve is the core with no screen at all — vesna --help lists it beside
the others — and a VS Code extension (vscode/, on the
Marketplace
as seasons-ai.vesna) — the chat, the garden and the findings beside the
code:
vesna serve serve the agent over JSON-RPC on stdio (for editors)
It speaks JSON-RPC 2.0 on stdio, each message framed by a Content-Length
header the way a language server's are. Requests: initialize, send,
command (with the line as typed, so it is quoted back like the chat's),
answer, interrupt, shutdown, exit. Notifications from the
server: transcript (one line of the conversation at a time), state (the
whole status again on every change), ask (a question and its choices) and
ask.resolved. The first exchange, verbatim, from a folder whose config
names ollama — every header line ends in CRLF:
Content-Length: 109
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientName":"vscode-vesna","clientVersion":"0.1.0"}}
Content-Length: 384
{"jsonrpc":"2.0","id":1,"result":{"serverVersion":"0.7.0","capabilities":{"transcript":1,"state":1,"ask":1},"state":{"mode":"ask","busy":false,"building":false,"buildState":"idle","model":"llama3.2","service":"ollama","usage":{"inputTokens":0,"outputTokens":0,"costUsd":0},"spec":null,"specSlug":null,"chats":null,"chatId":"20260912-182938-iaqe","root":"/private/tmp/demo.6O84/cwd"}}}
Both chats hold the core in-process; tests/core/border.test.ts is what
keeps them clients — neither reaches the agent past it. The editor
extension is the fourth client, built on exactly this protocol.
Now — a stable SDD agent, and the automation around it. Verification in
the plan has shipped: a task declares its check with verify:, Vesna runs it
after the review and again after the merge, approval is tied to the text it
approves, and the garden says who produced each task's evidence. A build the
branch review stopped, or whose last check failed on the base, is finished by
a plain /build once the base is fixed. The agent is now a core with three
clients — the full-screen chat, --plain, and vesna serve, JSON-RPC on
stdio — and a test keeps the border. Next is the editor extension.
Next — the editor extension. VS Code, on vesna serve: a side-panel
chat with streaming markdown, tool-call cards from transcript.step, the
questions as buttons, the garden as a tree view, spec.md and plan.md as
documents, review findings as diagnostics, the mode in the status bar —
published to the Marketplace. Done when a plan can be approved and a build
watched from the editor without a terminal.
Later — parallel independent tasks, containment for workers and reviewers, and an evaluation suite that measures whether the reviewer catches what it should.
Detail, and what counts as done for each, in docs/roadmap.md.
An issue first, then a pull request — CONTRIBUTING.md has the three rules and why. Vulnerabilities go to SECURITY.md, not a public issue.
— started it, and keeps the reviewer honest.
Merged a contribution? Add yourself here — avatar, link, and one sentence if you like. See CONTRIBUTING.md for the three limits on the sentence.
MIT.