Skip to content

CLI reference

One binary, two command groups.

A cast, end to end How one vekna cast streams its rite tree to its terminal and its events to the daemon, which journals them and shows them on every window; how a cast is resumed, who answers its prompts, and how it follows the casts it queued. A cast, end to end One vekna cast is one process. It draws its rite tree on its own terminal and, when a daemon listens, sends the same events over the socket, where they are journaled and shown to every window. THE STREAM the cast process vekna cast <ritual> --flag v runs in the directory it was typed in, step after step every step and every medium call opens a rite it never starts a daemon; it uses one if it is there to its tty the wire its terminal ▶ gates ↳ shell mise run lint:py ✓ shell mise run lint:py ✓ gates result: {"green":true} ✓ ok · ✗ error · ⊘ cancelled; the last line is the result as JSON, or null the daemon's socket if one listens: every event goes to it none listening? the cast runs on its own, leaves no record, and joins the next daemon that starts the journal ~/.local/state/vekna/runs/<cast_id>/ run.json what the cast was, how it ended, its result events.jsonl the wire, verbatim — what a resume replays newest 200 kept at startup; queued and running ones spared vekna log lists them, newest first ◌ gap lost its tail · damaged has no readable run.json every window: one row per cast sorted by what needs you first waiting on child running recovering queued done failed aborted socket closed without a goodbye; its row prints the resume command then the ones that ended · drill in for the rite tree, live output, the error THE MECHANISMS Carrying a cast on vekna cast --continue <id> journal cut replay off the journal live from the first mismatch • a fresh process, with an id of its own • it re-walks the ritual's steps • finished agent calls, commands and prompts come back, not run twice • an agent rite cut mid-flight reruns on the session it had opened • a ritual taking a new path is not made to pretend otherwise an 8-char prefix works · ambiguous or damaged: refused Who answers a prompt a decide, a tool gate, or the agent asking the cast's terminal answers by default one window owns it after taking it t Esc, or the window closes only one answer ever counts A cast the daemon spawned has no terminal, so a window answers it: drill in, type a <answer> the prompt line says what it takes: a yes|no · a red|green · a <anything> an answer it does not offer is refused before it leaves the window nobody answers? the cast waits; a decide never defaults Following what it queued its own steps end, its terminal stays on ✓ fan_out following the casts this one queued [ci_watch 3f9a2b11] ▶ watch [pr_fix 91bb0c4d] ▶ repair_ci [pr_fix 91bb0c4d] ✓ done [ci_watch 3f9a2b11] ✓ done • children, and theirs, stream under their own name • their prompts are asked here, one at a time • exits 1 if any of them failed • Ctrl-C stops following, leaves them running --no-follow ends with its own steps WHAT IT TELLS YOU Notifications OSC 777 to a tty only — Ghostty, kitty, wezterm, foot show it decide a question waiting on you done the cast finished failed with the error in the body Exit codes 0 the cast finished 1 the cast failed 2 bad arguments or ritual, or a medium with no focus

vekna cast

Run a ritual.

vekna cast <ritual> [--<component> value ...]

Each of the ritual's component fields is an option. --flag value and --flag=value are both accepted; a flag with no value is an error naming the flag rather than swallowing the next one. A flag the ritual does not take is refused, with the list of those it does.

vekna cast fix_tests --bound 5
vekna cast review --base=main --only src/

Options before the ritual name are vekna's; everything after it belongs to the ritual, ---prefixed or not. So a ritual is free to take a --continue of its own, and vekna's never has to guess which was meant:

vekna cast --continue 6f1c2a9e     # vekna carries a cast on
vekna cast release --continue      # the release ritual's own flag

Output streams live as a tree of rites — one node per step, one nested under it per medium call, with the call's own output indented beneath:

▶ gates
  ↳ shell  mise run lint:py
  ↳ shell  echo hi; sleep 1; echo bye
    All checks passed!
  ✓ shell  mise run lint:py
    hi
    bye
  ✓ shell  echo hi; sleep 1; echo bye
✓ gates
result: {"green":true}

A medium's line quotes its first argument when that is a string, whitespace collapsed and cut to 60 characters, on the line that opens the rite and the one that closes it.

At a terminal those are one line when nothing printed in between: the ↳ turns into ✓, ✗ or ⊘ where it stands. A rite that printed, or that something else printed after, closes on its own line as above. Output sent to a file or a pipe always keeps both lines and carries no cursor codes.

A shell command that exits non-zero ends in ✗ with its code, as in ✗ shell make check — exit 2. The mark reports the call; the ritual still reads the exit code and decides, so the cast carries on, and a resumed cast takes the result off the journal rather than running the command again.

The last line is the result as JSON, or null for a ritual that finishes without one.

Exit codes: 0 cast finished, 1 cast failed, 2 the arguments or the ritual source were wrong, or a medium the ritual calls has no focus registered.

Following what it queued

A cast typed with a daemon running stays at its terminal once its own steps end, for as long as the casts it queued — and the ones they queued — are still going. Each one's rites stream under its name:

✓ fan_out
result: {"queued":[…]}
following the casts this one queued — Ctrl-C leaves them running
[ci_watch 3f9a2b11] ▶ watch
[ci_watch 3f9a2b11] ✓ watch
[pr_fix 91bb0c4d] ▶ repair_ci
[pr_fix 91bb0c4d] ✓ done
[ci_watch 3f9a2b11] ✓ done

Their prompts are asked here, one at a time, under the same name. A prompt taken over in vekna stops being asked here, and is asked again if that window lets it go or closes. The cast exits 1 if any of them failed. Ctrl-C stops following and leaves them running; --no-follow ends the cast with its own steps:

vekna cast --no-follow sweep

vekna cast --prompt

vekna cast --prompt "explain what this module does"
vekna cast -p "explain what this module does"

A one-shot cast on the coding medium, with no rituals.py needed. The shortest way to check the agent is reachable at all.

vekna cast --unattended

vekna cast --unattended nightly_triage

For a cast nobody watches. Every decide fails where it is asked with UnattendedPromptError, naming the question, instead of waiting on a terminal nobody reads. The error raises inside the step, so a ritual can route it to a recovery step (see Rituals). Without the flag nothing changes.

A step can also ask first: attended() from vekna.lexicon is false under the flag, so a ritual can skip the work a question would have approved and say so. A cast carried on with --continue stays unattended whether or not the flag is typed again, and the flag on --continue makes an attended cast unattended. A cast it queues is unattended too.

vekna cast --help

Lists the rituals it can find from here, each with its options. If the source cannot be loaded, it says why rather than reporting an empty library.

vekna rituals

Inspect the library without casting anything.

vekna rituals list          # every ritual and the options it takes
vekna rituals show <name>   # one ritual's components and its step graph

show draws the graph from each step's return annotation — the exits mypy checked:

countdown
max steps: 100

components:
  --start <int>
  --label <str>  (optional)

steps:
  (start) → tick
  tick → tick, (done)

An exit no step takes is refused as the library loads, so list, show and cast all stop with <step> may return <Class>, which no step takes — usually a submodule that was never swept because it is missing an __init__.py.

Notifications

A cast that stops for an answer, or that ends, raises a desktop notification — OSC 777, which Ghostty, kitty, wezterm and foot turn into an OSD. A terminal that does not know the sequence drops it, and a redirected cast never sees one: notifications go to a tty and nowhere else.

Three kinds:

  • decide — a question waiting on you: the decide medium's own, coding's tool gate, or the agent asking mid-rite.
  • done — the cast finished.
  • failed — the cast failed, with the error in the body.

vekna

With no subcommand, the dashboard of the project it was typed in. The first vekna starts the daemon, detached: it binds $XDG_RUNTIME_DIR/vekna.sock (mode 0600, falling back to /tmp/vekna-<uid>/vekna.sock, in a directory of the user's own), writes what it has to say to ~/.local/state/vekna/daemon.log, and keeps running when the window closes. Every vekna, the first one too, is a surface on it, and q closes only the window. vekna stop ends the daemon.

Both ends have to compute the same path, and XDG_RUNTIME_DIR is what decides it. A sandbox that cannot write to the session's runtime directory exports a private one of its own instead — fence does, and it deletes it again on the way out — so a cast started under one looks for a socket in a directory nothing outside can name, and vekna in the shell never sees that cast. Nothing is blocked: unix sockets cross a sandbox fine, the two ends were simply dialing different paths. VEKNA_SOCKET is read before XDG_RUNTIME_DIR and settles it, exported on both sides. This is a development concern rather than an operating one — a cast belongs in the shell, where it is the one running the agent — but a suite or a cast run from inside a sandbox will otherwise look like it vanished.

vekna
vekna --debug
vekna --no-tui

--debug starts the daemon writing a line per event to ~/.local/state/vekna/debug.log — the daemon is the one place every message passes, and the log says what it did with each one, including the ones it dropped. A daemon already running is not restarted for it; the window says so.

In a terminal the window is drawn with Textual, as two pages of a book. The left page is the contents: a tree of project, ritual, the ritual's casts newest first (@now while live, @3h ago once ended), and their rites. By default it shows the running casts, each as … and the rite it is on now, or the rite that asks, and the failed ones you have not opened yet. The right page is the line under the cursor in full: a project's branch and directory, a ritual's casts with their flags, a cast's whole rite tree, or a rite's whole error. Below it is the output, which follows the newest line until you scroll up. The branch is read off git every few seconds, because a cast can switch it.

A fold hides everything under its line, and the line says what still happens there: ▸ ci_watch (1 asks, 1 fail, 2 running). A project line always says it. A failure counts until you have opened its cast and moved on.

Key Does
space fold the line under the cursor, or open it again
e every cast and every rite, or the running ones only again
w the next line that asks, a fold included
j / k / h down, up, to the line above in the tree
c start a ritual here: c <ritual> [--flag value …]
t take over the prompt under the cursor, and answer it in a dialog
a answer it in one line: a <answer>
x cancel the cast under the cursor; twice, to be sure
: the command line, empty
g / p every project / this one
q close the window; the daemon keeps running

Esc in the dialog lets the prompt go, back to whoever was answering it. --no-tui, or no terminal on stdin and stdout, gives the line view described below, which repaints whole and reads a line at a time.

A project is a repository, not a directory: every worktree of one shares its git common dir, and its casts group under that. A directory in no repository is its own project. g shows every project's casts, read only; p comes back to this one.

One row per cast, and no output in any of them — the row is for deciding which cast to go and look at:

vekna — 1 running · 1 waiting · 1 done · 1 aborted  (all projects)

  #  cast      ritual           project     status      elapsed  steps  now
  1  7c01ffab  triage           ludamus     waiting       1m03s      1  merge #74 now, or wait?
  2  3f9a2b11  merge_ready      vekna       running       4m12s      3  land · coding  1m02s
  3  dd44ee55  ping             deep        done             7s      1
  4  91bb0c4d  fix_demo         vekna       aborted      10m09s      7  vekna cast --continue 91bb0c4d

elapsed is how long the cast has been going, steps how many it has finished, and now what it is doing this second — the running step, the medium inside it, and how long that step has been running. A step that has not moved in ten minutes is the thing this view exists to show. Casts waiting on an answer sort to the top, then the ones still running, then the ones that ended.

The status word is running, waiting, recovering, queued, done, failed or aborted. Queued is a cast the daemon has not started yet, because its project already runs as many casts as it allows. Recovering is a running cast whose last step raised and went to a recovery step; drilling in shows the error beside the step. Aborted is a cast whose socket closed without a goodbye, which is the one worth carrying on with, so its row prints the command that does it.

A number drills into a cast, b comes back, g and p switch between every project and this one, q closes the window. Drilling in is where the rite tree, the live output and the error a failed cast ended on are.

c <ritual> [--<component> value …] starts a ritual in the directory the window was opened in, with the flags vekna cast takes. The daemon spawns it as vekna cast with no terminal, logging its output to cast.log beside its record. A ritual that is not there, or a component that will not validate, shows as that cast failing, with the reason in its drilled view.

A cast the daemon started has no terminal, so its prompts are answered here: drill into it and type a <answer>. The prompt line says what it takes — a yes|no, the options offered, or a <anything> for free text. An answer it does not offer is refused before it leaves the window. A cast nobody answers waits. A cast typed as vekna cast asks its own prompts on its own terminal, and the ones of the casts it queued; answering one from a window takes it over, and that terminal stops taking answers for it. Only one answer ever counts: a prompt taken over answers only to the window that took it, until that window lets it go or closes.

vekna cast still runs in the directory it was typed in, attached or not. It does not start a daemon: with none listening it runs on its own and joins the next one that starts.

The circle

How much runs at once is set under [circle], in the project's .vekna.toml or in ~/.config/vekna/config.toml:

[circle]
processes = 4   # casts running at once in this project; the project's own wins
coding = 2      # agent sessions at once, across every project; user config only

processes counts every running cast of the project, whether c started it or somebody typed vekna cast; past it, c queues. It defaults to 4. coding limits how many coding calls talk to an agent at once, in every cast attached to the daemon: past it, a call waits for a slot before its session starts, and gives it back when the session ends. It defaults to no limit. With no daemon running there is nobody to share with, and nothing waits. Both are read again for every cast, so an edit counts from the next one. A [circle] that does not validate is noted in daemon.log, and the defaults stand.

A cast that a ritual's await_cast blocks shows on child and the id it waits on, and holds no slot until that cast ends. A cast queued by another names it in the drilled view: queued by <id>. What is queued survives vekna stop: the next daemon runs it.

Worktrees

A ritual declared @ritual("fix", worktree=True) runs in a pooled worktree rather than in the directory c was typed in. The daemon leases a tree before it spawns the cast and releases it once the process exits. The pool takes two more keys:

[circle]
worktrees = 2                          # pooled trees per project
prepare = "mise install && poetry install"   # run once in each new tree

Trees live under ~/.local/state/vekna/worktrees/<repo>-<hash>/<n>, each parked on its own branch, vekna/wt-<n>, while no cast holds it. The pool is whatever git worktree list shows there, so a restarted daemon finds the same trees. A tree is made when every existing one is held and the pool is under worktrees (default 2), from the HEAD of the tree the cast was asked from; prepare runs once in it, logging to <n>.prepare.log beside it. A prepare that fails fails the cast and runs again at the tree's next lease. Nothing removes a tree.

After the cast, the daemon parks a clean tree. A tree with uncommitted or untracked changes, or with a merge, rebase, cherry-pick or revert in progress, is left as it is and marked blocked: the cast's drilled view and daemon.log say which tree and why. It joins the pool again once it is clean, by hand or through vekna purify. Gitignored files are not changes, so whatever prepare builds must be gitignored, or every tree reads dirty. Past the pool, a cast waits queued for a tree while another cast holds one; once every tree is blocked and none is held, nothing would free one, so the cast fails and names the blocked trees. A cast also fails when every tree that is not blocked belongs to a cast awaiting one that waits for a tree: neither would ever end.

A tree holds only what git tracks. A .vekna.toml or rituals.py kept out of git is not there, and a cast in the tree will not find its rituals. A worktree=True ritual typed as vekna cast runs where it was typed.

vekna purify

Cleans every pooled worktree of the project it is typed in and parks it: an open merge, rebase, cherry-pick or revert is aborted, uncommitted and untracked changes are thrown away, and the tree goes back to its parking branch. Gitignored files stay, so a prepared venv survives. A tree a running cast holds is left alone. Neither the checkout you work in nor the pool's size is touched.

vekna purify          # throws away what the trees had not committed
vekna purify --save   # commits it to a branch of its own first
/home/you/.local/state/vekna/worktrees/vekna-1a2b3c4d/1: purified — its uncommitted work is on vekna/rescue/fix-flaky-1f2e3d4c
/home/you/.local/state/vekna/worktrees/vekna-1a2b3c4d/2: held by a running cast, left alone

--save names each branch vekna/rescue/<branch the tree was on>-<hash>, and keeps it local. The daemon does the work, since only it knows which trees are held; with none listening, vekna purify starts one. A cast whose tree is parked again, purified or found clean by a lease, loses its blocked mark in every window. Exits 1 when any tree would not come clean, saying why.

vekna stop

Ends the daemon, and every window on it with it. Casts still running carry on, and rejoin the next daemon with everything they have done so far.

vekna stop

Prints stopped the daemon once the socket stops answering, so vekna stop && vekna starts a fresh one. With nothing listening, no daemon running; both exit 0.

vekna log

The casts the daemon has recorded, newest first — running, finished and gone.

vekna log
91bb0c4d  ✗  fix_demo          2026-08-21 14:02  /home/you/vekna
7c01ffab  ▶  triage            2026-08-21 13:58  /home/you/ludamus
3f9a2b11  ✓  merge_ready       2026-08-21 13:40  /home/you/vekna  ↳ 91bb0c4d
6f1c2a9e  ✗  cover_diff        2026-08-21 13:12  /home/you/vekna  ◌ gap
a1b2c3d4  ?  damaged           2026-08-21 12:50

The id is cut to eight characters, the timestamp is in your own zone, and a trailing ↳ names the cast this one was carried on from. A cast that ran with no daemon listening leaves no record: the journal is the daemon's, and there was none.

◌ gap is a run whose log the daemon could not finish writing. It is still resumable; what it lost is the tail.

damaged is a run whose run.json is missing or cannot be read — a daemon killed inside a write, a disk that lost the file. The id is the directory's name and the time is the directory's; nothing else is known, and vekna cast --continue refuses it — saying the run is damaged, not that the journal never saw it.

The journal itself is ~/.local/state/vekna/runs/<cast_id>/ — run.json for what the cast was and how it ended, events.jsonl for the wire verbatim. The root is trimmed to the newest 200 casts at startup; whatever is still running is spared, and a damaged run is collected like a finished one.

vekna cast --continue

Runs a cast on from where it was interrupted, in the directory it ran in.

vekna cast --continue 6f1c2a9e

A fresh process, always. It re-runs the ritual's steps — cheap, and the same walk it took before — while every agent call, shell command and prompt it had already finished comes back off the journal instead of happening twice. An agent rite that was interrupted mid-flight runs again, on the session the cast had already opened, so it remembers what it was told.

Replay stops at the first rite that does not match what was recorded, and the cast runs live from there. A ritual that takes a different path this time is not made to pretend otherwise.

The id may be the eight characters vekna log and the aborted row print — a prefix is resolved against the journal, and one naming two casts is refused rather than guessed. What comes back is a cast of its own, with an id of its own; vekna log and the drilled-in header both say which cast it carries on from.

A run the daemon could not write in full is resumed like any other. The log never grows past a write it lost, so what a gap costs is the tail — the same thing an interrupted cast loses — and the replay stops there and runs live from there.