
The evoke manual
Software, by reflex. evoke turns a sentence into a call of a small program. That program is a reflex: a
recipe anyone can write, share and improve. A classifier chooses it. A confidence gate decides whether it runs.
A sentence can choose a program. It can never invent a value. The classifier is Jev,
TypeSafe AI's System One model, reached through either of two built-in adapters. The design is bound to none.
$ evoke "kill the lights in the den"
lights room="den" state="off" 0.92
den lights off
One tool does three jobs. A CLI where you type what you want. A package manager that installs reflexes from git. A TypeScript SDK that puts the same decisions inside your app. You invoke a function; you evoke a reflex.
Where to start
| You want to… | Read |
|---|---|
| Type what you want and see it happen | Install, then The first ten minutes |
| Learn the words this manual uses | Concepts |
| Write a reflex of your own | Your first reflex |
| Decide and run reflexes inside your application | The SDK |
| Look something up | Reference |
The manual
Start
| Page | Read it when |
|---|---|
| Install | Putting evoke and its SDK on a machine |
| The first ten minutes | Your first key, your first reflexes, your first lesson |
| Concepts | The words, and the three steps of a decision |
Use: the CLI
| Page | Read it when |
|---|---|
| Typing a request | Bare input, the REPL, a pipe, --json, --tag, exit codes |
| Languages | English, German, French and Spanish: how evoke tells, what stays as typed, what why says |
| Outcomes | Run, confirm, ask and abstain; try, why, run |
| Weaving | Several things in one sentence: the plan, what a step takes from another |
| Installing reflexes | add, remove, update, sync, trust; refs, the lock, the store |
| Tuning | teach, overlays, vocabularies, config, show, test |
| Calibrating | calibrate: what the confidence meant on your records, and over repeats |
| Projects | Your home project, an app's project, evoke.toml, trust |
Author: writing reflexes
| Page | Read it when |
|---|---|
| Your first reflex | new, check, a try, a run: a reflex in ten minutes |
| The manifest | reflex.toml, key by key |
| Arguments | Options, vocabularies, picks and flags; ranges; renames |
| The body | File bodies and argv bodies; context, results, the deadline |
| Playbooks | A reflex whose body is a plan: steps with slots, one yes over the whole |
| Examples and tests | Records, assertions, evoke test, lint |
| The rules | How a reflex, a playbook and a set are written, and the checklists |
| Publishing | Tags, versions, the contract diff, collections, the Hub |
SDK: @evoke-build/evoke
| Page | Read it when |
|---|---|
| Getting started | The first hour: one file, one reflex, one decision |
| Projects in code | load(), roots, reflexes as code, with(), generated types |
| Decisions | The Decision union, fill, run, handle; steps and weave |
| Adapters | jev(), openjev() and clef(), thresholds, and writing an adapter of your own |
| Testing | replay(): offline, deterministic tests over a recording |
| Errors | The three errors, and what each asks of you |
Reference
| Page | Holds |
|---|---|
| Commands | Every command, flag and exit code |
| Files | evoke.toml, evoke.lock, overlays, vocabularies, the types |
| The JSON line | What --json prints, field by field |
| Environment | Variables evoke reads and the paths it writes |
| Diagnostics | Every line that ends in a fix, and what the fix does |
| Glossary | The words, in one place |
Around the tool
| Page | Holds |
|---|---|
| The collection | The first-party reflexes: what a Mac does at a word |
| Security | What runs as you, what is guaranteed, and what is not |
Conventions
- A line beginning
$is a command you type. The lines under it are what the terminal shows. - What you asked to see goes to stdout: a reflex's result, or the answer of
show,try,whyandtest. Whatevokesays about a run goes to stderr, indented two spaces. - Every line that needs something from you ends with the command that does it:
→ evoke trust. - macOS and Linux; Windows through WSL. The collection's programs are for macOS.
evoke is Apache-2.0; the collection is MIT. The name
and the logo are trademarks: except for displaying the
licence details and identifying us as the origin of the software, you have no right under the licence to use them.
Install
evoke is one small native binary. Reflexes that run a JavaScript file need Node next to it. Nothing else is
installed for you.
What you need
| For | Needed |
|---|---|
| The binary | macOS or Linux; Windows through WSL |
Installing reflexes (add, update, sync) | git on your PATH; your own git configuration and credentials apply |
Reflexes that run a .mts or .mjs file | Node 24 or newer on your PATH when you run evoke add or evoke sync |
| Deciding | A key for the classifier: TYPESAFE_API_KEY for the jev adapter, OPENJEV_API_KEY for openjev |
Some reflexes run a program with arguments instead of a JavaScript file. Those are argv reflexes. They need no runtime at all.
The binary
On macOS or Linux, use the script. It fetches the release built for your machine, checks it against the
release's SHA256SUMS, and puts evoke in ~/.local/bin. Set EVOKE_INSTALL to choose another directory. Set
EVOKE_VERSION to pick a version.
$ curl -fsSL https://evoke.build/install.sh | sh
With mise:
$ mise use --global github:evoke-build/evoke
From source, with a current Rust toolchain:
$ cargo install --locked --git https://github.com/evoke-build/evoke evoke
Every way ends the same:
$ evoke --version
evoke 0.21.0
The archives are on GitHub releases. Each is named
evoke-<target>.tar.gz, for aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-musl or
x86_64-unknown-linux-musl. The Linux binaries are static. Each archive holds evoke, LICENSE and NOTICE.
SHA256SUMS sits beside them, and so does a build attestation, which
gh attestation verify <archive> --repo evoke-build/evoke checks. To update, run the script again, or mise upgrade.
The SDK
$ npm install @evoke-build/evoke
The package needs Node 24.5 or newer, and it is ES modules only. It carries the core as WebAssembly, so it has no build step and no dependencies. The SDK has its own manual section: Getting started.
The key
Jev is TypeSafe AI's classifier. evoke reaches it through one of two adapters. Each reads
its key from your shell's environment, only at the moment it decides, and never writes it to a file.
The jev adapter is the default. It posts to TypeSafe AI's own API, under a key from typesafe.ai:
$ export TYPESAFE_API_KEY=<value>
The openjev adapter posts to OpenJEV, an independent service that forwards the request
to Jev and returns its answers. OpenJEV is not TypeSafe AI's. evoke is affiliated with neither, and vouches for
neither. A key from OpenJEV is on OpenJEV's terms, and whether it may offer Jev is a matter between OpenJEV and
TypeSafe AI. As of this release it publishes no terms and no privacy policy, so weigh that before you choose it.
Name it in evoke.toml and export its key:
adapter = "openjev"
$ export OPENJEV_API_KEY=<value>
Without the key, the first decision prints a line naming the one it needs, and exits 3. Both adapters ask the same questions and ship the same bars, so a project moves from one to the other with that one line.
Next: The first ten minutes.
The first ten minutes
Ten minutes, from an empty machine to a reflex that learned a phrase of yours. Every output line below is what the terminal shows.
1. Say something
$ evoke "lock the screen"
jev needs TYPESAFE_API_KEY, a key from typesafe.ai → export TYPESAFE_API_KEY=<value>
[3]
Two things happened. evoke wrote your home project: the file ~/.config/evoke/evoke.toml, with one line,
adapter = "jev". And it told you the one thing it needs. [3] is the exit code. It means needs a human. Export
the key and try again:
$ evoke "lock the screen"
no reflexes are installed → evoke add evoke-build/reflexes
[3]
2. Install the collection
evoke-build/reflexes is the first-party collection: fifteen reflexes for what a Mac does at a word. Installing
a repository installs every reflex in it.
$ evoke add evoke-build/reflexes
+ awake evoke-build/reflexes/awake 0.1.0 write runs awake.mts
needs runs caffeinate
+ download evoke-build/reflexes/download 0.1.0 write runs download.mts
needs writes {to} ~/Downloads · hosts *
+ lock evoke-build/reflexes/lock 0.1.0 write runs lock.mts
needs runs open
+ mail evoke-build/reflexes/mail 0.1.0 write runs mail.mts
needs runs open
+ note evoke-build/reflexes/note 0.1.0 write runs note.mts
needs writes {file}
+ open evoke-build/reflexes/open 0.1.0 read runs open
needs reads {place}
+ power evoke-build/reflexes/power 0.1.0 destructive runs power.mts
needs runs osascript
+ screenshot evoke-build/reflexes/screenshot 0.1.0 write runs screenshot.mts
needs writes ~/Desktop · runs screencapture
+ sleep evoke-build/reflexes/sleep 0.1.0 write runs sleep.mts
needs runs pmset
+ sound evoke-build/reflexes/sound 0.1.0 write runs sound.mts
needs runs osascript
+ timer evoke-build/reflexes/timer 0.1.0 write runs timer.mts
needs runs osascript
+ trash evoke-build/reflexes/trash 0.1.0 destructive runs osascript
+ visit evoke-build/reflexes/visit 0.1.0 read runs visit.mts
needs reads ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist · runs open plutil
+ volume evoke-build/reflexes/volume 0.1.0 write runs volume.mts
needs runs osascript
+ wifi evoke-build/reflexes/wifi 0.1.0 write runs wifi.mts
needs runs networksetup
inactive note: config "file" is not set → evoke config note file <value>
inactive open: vocabulary "places" is empty → evoke vocab places add <word> "<meaning>"
inactive visit: vocabulary "sites" is empty → evoke vocab sites add <word> "<meaning>"
One row per reflex: its local name, where it came from, its tag, its effect, and what it runs. Under it, what
it touches: the paths, the network as hosts *, the programs; a row without that line declares nothing, and its
body touches nothing but its own folder. The kernel holds each body to that line: Security.
Then the inactive lines. A reflex that needs your words or a setting stays out of every decision until it has them. Each
line ends with the command that gives it what it lacks. Nothing is guessed for you.
3. Use it
$ evoke "set the volume to 40 percent"
volume level="40 percent" 0.96
volume 40%
The indented line is evoke's own, on stderr. It shows the call it chose and its confidence. The last
line is the reflex's result, on stdout. The confidence was over the bar for a write reflex, so it ran without asking.
A less certain decision confirms first. [t]each records what you meant, then runs:
$ evoke "kill the wifi"
wifi state="off" · write · weakest: state 0.86
a write runs at 0.90 or more
Turn Wi-Fi off? [y]es [n]o [t]each > t
+ overlays/wifi.toml [examples] "kill the wifi" = { state = "off" }
wi-fi off
That one line in overlays/wifi.toml is yours. It is an example the classifier now sees, in a file no update
will touch.
A destructive one always confirms, however sure:
$ evoke "restart the computer"
power action="restart" · destructive · weakest: route 0.98
it cannot be undone, so it always waits for a yes
Make the laptop restart now? [y]es [n]o [t]each > n
[2]
[2] means declined: you said no, or nothing fit. When what you said fits nothing, evoke abstains and shows
you the ranking, and reminds you of what is not yet in play:
$ evoke "what time is it"
none of them 0.85 · timer 0.10 · awake 0.03 · lock 0.02
note, open and visit are inactive → evoke show
[2]
4. Give it your words
Some reflexes need words only you can supply: your folders, your sites. A vocabulary is that list. It is one file you own, and every reflex that names it reads it.
$ evoke vocab places add desktop "The desktop." --value "~/Desktop"
+ vocab/places.toml desktop = { what = "The desktop.", value = "~/Desktop" }
$ evoke "open my desktop folder"
open place="desktop" 0.95
The meaning is what the classifier reads. The value is what the reflex receives: open opened that folder. A
setting works the same way:
$ evoke config note file "~/notes.txt"
+ evoke.toml [config.note] file = "~/notes.txt"
The notes file must exist, since the reflex declares it: touch ~/notes.txt first. A secret is only ever named,
never stored: evoke config <reflex> <key> --env <VAR> writes the variable's name, and the value is read from
your environment when the reflex runs.
5. Teach it
Teaching works from the command line too, and you can teach what something is not:
$ evoke teach "go dark" wifi state=off
+ overlays/wifi.toml [examples] "go dark" = { state = "off" }
$ evoke teach "what time is it" not timer
+ overlays/timer.toml [examples] "what time is it" = false
6. Look inside
try decides without running and shows every judgment. why explains the last sentence. show prints what is
installed, or one reflex as it is used, with + next to every line that is yours.
$ evoke try "kill the wifi"
"kill the wifi"
which reflex wifi 0.95 (none of them 0.03 · power 0.01)
Turn Wi-Fi on or off.
state off 0.86 (on 0.10 · not said 0.04)
asked a second way: off 0.86; the less sure of the two counts
is it off? yes 0.95
→ waits for a yes: wifi state="off" · write · weakest: state 0.86
a write runs at 0.90 or more
replay answered 25 questions in 2 rounds
Which reflex, then each value with what it stands on, then what would happen and why. The weakest answer is the confidence. It sits under the write bar, so this one would wait for a yes.
Where things are
| What | Where |
|---|---|
Your project: evoke.toml, evoke.lock, overlays/, vocab/ | ~/.config/evoke/ |
| Installed code, keyed by content hash | ~/.cache/evoke/store/ |
| The decision log, the REPL's history | ~/.local/state/evoke/ |
Your project holds only what you wrote, the lock, and generated types. Put it in your dotfiles. Everything else
is a cache, and evoke sync rebuilds it on a new machine.
Next: Concepts, or straight to Typing a request.
Concepts
Seven words, three steps, and one idea that makes the rest follow.
The idea
The classifier evoke uses answers closed questions: which of these? is this stated? yes or no? It answers
with calibrated probabilities. Everything it knows about a
reflex comes from the reflex's own description, sent in the request. So the wording is the tuning, and one
line is documentation, tuning, test and customization at once:
"kill the lights" = { state = "off" }
A reflex is a package of such wording plus the program it describes. evoke fetches, versions and refines that
wording, and runs the program. The rules say how to write it so that it is read right.
The words
| Concept | Meaning |
|---|---|
| Reflex | A directory: reflex.toml and the file or program it names. Its identity is its location, owner/repo[/dir] at a git tag. |
| Manifest | reflex.toml: what the classifier reads and what the program receives. Its wording is yours to override. Its contract is not: argument names, option keys, sources, ranges, what it runs. |
| Project | A directory of files you own: evoke.toml, evoke.lock, overlays/, vocab/. Your home project lives in ~/.config/evoke/. An application has its own at its root. |
| Overlay | Your wording for one reflex, in one file, laid over the shipped manifest. It adds and replaces. It never deletes. |
| Vocabulary | Your word = "meaning" list, shared by every argument that names it. A package never ships or writes one. |
| Adapter | The classifier behind a decision, by name. jev and openjev both reach Jev, through TypeSafe AI's API or through OpenJEV. The core names no engine. |
| Runtime | What runs a program: Node for a .mts or .mjs file, nothing for an argv. Chosen from the manifest. Authors never see it. |
Two more words come up everywhere. A call is a reflex with its arguments filled in, on one line:
lights room="den" state="off". An effect is what running a reflex does to the world: read, write or
destructive. The effect decides how sure evoke must be before it runs without asking.
A tag is a word a manifest carries, so --tag can narrow a decision. A collection is a repository of
reflex directories. The Hub is where reflexes are found. For now, that is the first-party collection, evoke-build/reflexes.
The three steps
evoke "mute the office lights"
- Decide. Every active reflex's manifest becomes questions. Which reflex does the input ask for? For each argument, which value, or was it left unstated, or stated in words that are not among the choices? One request carries all the questions. The adapter answers each with a probability. Where an answer leaves a doubt, the reading checks itself with another question: it asks about a value a second way, whether a word of the input means the value it seems to, what the words no value holds do, and, before a call runs, whether the call holds all the input says. What it reads, it reads from your words; nothing is invented.
- Gate. Confidence is the weakest of those answers. Each effect has a bar. Against that bar, the outcome is settled: run, confirm, ask for a missing argument, or abstain. A destructive reflex always confirms. Code stays in control: the classifier proposes, the gate disposes.
- Run. The reflex's program runs with its arguments, a clean environment and a deadline. Its one line of text comes back.
What the classifier can and cannot do
It selects. An argument's value is one of five things: one of the author's options, one of your vocabulary words, a piece of what you typed, taken word for word, or, in a sentence of several steps, a field of an earlier step's result or that step's whole result, which the plan shows; or, when the words leave it out, a value an earlier result of this session holds, which you choose at the prompt. That piece can be a number, a duration, an email address, a URL, a quoted phrase, a date, a time, an amount or a code, and it is checked against its range. So a sentence can choose a call, but it can never invent a value. And what it chooses still passes the gate. That is the shape of every guarantee in Security.
Next: Typing a request.
Typing a request
evoke takes one sentence and does three things with it: decide, gate, run. This page is about how the sentence
gets in and how the answer comes out.
Three ways in
evoke "kill the lights in the den" one input, as the argument
evoke the REPL, on a terminal
echo "kill the lights" | evoke a filter: one input per line of stdin
As the argument. Everything after evoke is the input, unless the first word is exactly a command name. Bare
words are joined by one space, so evoke kill the lights and evoke "kill the lights" are the same call. Quote a
sentence that holds a character the shell would take, such as ? or *.
The REPL. Run evoke alone on a terminal. It prompts > and decides each line as if you had typed it as the
argument. It skips blank lines. It keeps one warm connection to the adapter. You can edit the line, and the arrow
keys recall earlier ones from ~/.local/state/evoke/history. Ctrl-C cancels the line. Ctrl-D on an empty line
ends the session with exit 0. Under TERM=dumb the line is read plain: no editing, no history, and Ctrl-C
ends the session.
$ evoke
> kill the lights in the den
lights room="den" state="off" 0.92
den lights off
> what time is it
none of them 0.85 · timer 0.10 · lights 0.03 · volume 0.02
>
The session remembers what its reflexes returned. A pick whose author named a recent field offers those values
back when a sentence leaves the argument out, so after a listing, «roll back the last deploy» asks with the
releases numbered; words that point at one, «buy it» after a product shown, «pay my last order», take the newest
without asking (Arguments):
> list the checkout deploys
checkout: 2 deploys today, the last 4.12.0 at 13:58
> roll back the last deploy
From which release? [1] 4.12.0 [2] 4.11.3 > 1
Roll back 4.12.0? [y]es [n]o [t]each > y
A day in a sentence, tomorrow, next friday, the 14th, is read as you said it and becomes a date when the
program runs, against your machine's clock (Arguments).
Words the reading takes as they are
A value is always one of your own words. A text an argument wants between quotes, like a timer's label, is read
from the words you typed without them: in set a timer for 10 minutes called tea the label is tea, and the call
waits for your yes, since the words could have been cut otherwise. A word that stands for a value from a list but
is not on it — the snug for a room called den, garage for no room at all — is read where the list's meanings
say what it is, and the call waits for a yes; where they do not, evoke asks, and names the word:
"garage" is not on the list. evoke why shows each of these under the value it gave (Outcomes).
Languages
evoke reads a request typed in English, German, French or Spanish, and there is nothing to set: the words of the
request say which language it is in, and a request that mixes two is read in both. A value stays as you typed it and
is never translated: zieh die Umsätze für August reads the month, timer fünfundvierzig Minuten für die Wäsche
reads the length and keeps Wäsche as the label, treinta y uno is 31. The questions evoke asks you and what it
says back stay English in this release. evoke why names the language that read a request when it was not English,
with the words that showed it (Languages).
A value typed the way you say it
You can type a value the way you would say it aloud. evoke reads an address, a URL, a code, a number, a day
or a length of time this way: dana dot weiss at example dot org, k d oh three one five, seven oh, an hour and twenty minutes. The call shows the value as it would be typed.
A code takes the shape of the reflex's own examples. Where every example reads like HS-0409, your letters
become capitals and the dash is added. The call then waits for your yes, with your words on the line:
$ evoke "earmark k d oh three one five"
reserve sku="KD-0315" · write · weakest: route 0.95
sku was read from "k d oh three one five"
Reserve every unit of KD-0315? [y]es [n]o [t]each > n
Some words can be read in more than one way. ops twenty at example dot com may be ops20@example.com or
20@example.com, and thirty one fifteen may be the start of a longer code. evoke does not choose for you.
It asks, and offers what your words read as (Outcomes):
$ evoke "email the update to ops twenty at example dot com"
To which email address? you wrote "ops twenty at example dot com" [1] ops20@example.com [2] 20@example.com [0] none of these > 1
email to="ops20@example.com" · destructive · weakest: route 0.95
it cannot be undone, so it always waits for a yes
Email the shipment update to ops20@example.com? [y]es [n]o [t]each > n
A name typed another way
A word of your vocabulary may be typed in a form that is not on the list. evoke reads it only where your
words leave no doubt about which listed word they name.
- A word cut short or typed with a slip is read where your own words name the listed word:
septforseptember,eu-wwestforeu-west.evoke whyshows the check under the value, asasked of "sept" alone. - A longer name, a pet name or a name one letter away may be another person:
Samuel,SammyorSalbeside a colleague listed assam.evokenever reads such a name as the listed word. It asks, and puts the nearest word ready. Aytakes it:
$ evoke "where is Samuel's laptop"
Which colleague's laptop? you wrote "Samuel's": sam? [y]es, or [1] sam [2] ana [3] jo [+] add one [0] none of these > y
device person="sam" 0.95
sam: serial C02XK1ABJG5M, last seen 07:41
A filter. Pipe lines into evoke, or into evoke try, and every line is one input, answered in order. A line that needs a
prompt, a confirm or an ask, cannot be answered without a terminal. That line exits 3 and names the command to run
yourself. The filter still answers every other line, and exits with the first non-zero code.
$ printf 'kill the lights in the den\nstart a timer\n' | evoke
lights room="den" state="off" 0.92
den lights off
an ask needs a terminal → evoke "start a timer"
[3]
What comes out
What you asked to see goes to stdout: a reflex's result, and the answer of show, vocab, why, try,
test and check. What evoke says about a run goes to stderr, indented two spaces: the call and its
confidence, a prompt, the line of a write, the ranking after an abstain, and every diagnostic. So
evoke "…" > out.txt captures the result alone, and evoke show | grep timer finds the row. A script reads the
rest from the exit code.
$ evoke "kill the lights in the den"
lights room="den" state="off" 0.92 ← stderr: the call, and the confidence it ran on
den lights off ← stdout: what the reflex returned
Flags
| Flag | Does |
|---|---|
--json | One JSON line per input instead of the lines above. It holds the input, the decision, the trace and the result. This is the filter format: see The JSON line. |
--tag <tag> | Offers only the reflexes carrying the tag. Repeat it to widen: --tag home --tag sound. |
-- | The rest is input, even when it begins with a command word: evoke -- test the alarm. |
--help, -h or help, and --version or -V, only count in the first place. They print to stdout.
evoke add --help prints the help too, as does -h right after any command word. To say the word help as a
sentence, put -- before it.
A command word written after a flag, like evoke --json try "lock it", is refused rather than decided. The line
names the command and how to write it. Nothing runs by accident.
Command words
The first argument selects a command only when it is exactly one of these words:
help try why run add remove update sync trust show teach vocab config test calibrate new check
Four more words are reserved for later: edit, search, publish, adapter. Reserving them now
means adding one later never changes what a sentence means. Lines from stdin are always input, whatever they begin with.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Ran. Also --help, --version, and every command that did what it said |
| 1 | The program, or the machine, failed. Also evoke test with a failing case, and evoke calibrate with a call wrong at or over its bar |
| 2 | Abstained, or you declined |
| 3 | Needs a human: a missing key, an untrusted project, a prompt with no terminal, a line to fix |
| 4 | The adapter failed |
Ctrl-C while a body runs ends the body and everything it started, a JavaScript body's signal aborting first;
then evoke ends as an interrupted program does, and the shell reports exit 130. In a weave, every step that
did not finish reads skipped · cancelled (Weaving). Anywhere else, Ctrl-C ends
evoke at once.
Limits and time
An input over 2 000 characters is refused. One decision has 30 seconds. The classifier's answer and the program's
run share them. A prompt waiting for you never counts. When the deadline passes, evoke stops and says so. It
never guesses.
Colour
On a terminal, evoke colours what matters. The reflex's name in a call. The top answer of every judgment try
shows. The effect: green for read, yellow for write, red for destructive. The weakest judgment, dimmed. The
→ of a fix. The + and - of a write. A spinner turns while a request or a fetch is in flight, and counts the
cases of a test. Piped, logged, under NO_COLOR, or with TERM=dumb, the same words print plain. On a
terminal narrower than the help, --help puts each description under its command.
Next: Outcomes.
Languages
evoke reads a request in English, German, French or Spanish. Each language is a pack of words built into evoke:
the words that join two steps, the ones that ask for nothing, the pronouns that point back, the numbers, the days
and months, the clock, the units and the currencies, the signs said aloud. There is nothing to install and nothing
to set.
How it tells
The words of the request show the language. A request holding more of one pack's own words than of any other's is
read by that pack, and by English always; one that mixes two languages, Zeig mir die Sessions von Ana, por favor,
is read by both where they show alike. A request so short that no pack shows, wipe C02TM6PNHV29, is read by every
pack, each holding back its two-letter words and the words English knows. A code, an address or a link shows no language.
What stays as typed
A value is always one of your own words, in your own language: a day, a number, a length of time, an amount, a time
of day said in German is read as the value the program takes, and a text — a label, a note, a message — is kept as
typed, never translated. The questions evoke asks you, its prompts and why's lines stay English in this release.
At a confirm prompt a yes or a no in any of the four languages does: y, yes, ja, oui, sí run; n, no,
nein, non decline (Outcomes).
What why says
evoke why and evoke try name the language that read the request when it was not English, with the words that
showed it:
read by German, shown by «für», «den»
An English request says nothing of the kind. evoke calibrate and evoke test print a line per language other
than English that read a record, with how its records fared (Calibrating, Tuning).
What a change to a pack does
A plan file carries the digest of every pack it was read with. A release that changes a pack refuses a plan file
made under the old one as stale, as it does when a question's wording changes. evoke run then names the
evoke try --save that makes the plan again (Weaving).
Outcomes
Every input ends in one of four outcomes. This page shows each one and what decides it. It also covers the three
commands that look at a decision without changing anything: try, why and run.
Run
Confidence cleared the bar for the winner's effect. Every required argument was stated. Nothing else needed attention. The call prints with its confidence, the program runs, and the result prints.
$ evoke "kill the lights in the den"
lights room="den" state="off" 0.92
den lights off
Confirm
The call is complete, but something holds it at a question. evoke's own line names the call, the effect and
the weakest judgment; the line under it says why the call waits. Then comes the reflex's one-line prompt, and
three answers.
$ evoke "dim the office"
lights room="office" state="dim" · write · weakest: state 0.85
a write runs at 0.90 or more
Set the office lights dim? [y]es [n]o [t]each > y
group-7 lights dim
| Answer | Does |
|---|---|
y, yes | Runs; so does a yes in any language evoke reads, ja, oui, sí (Languages) |
n, no | Declines, exit 2; so does nein, non |
t, teach | Records what you said as an example of this call in your overlay, then runs |
These are the reasons a decision stops at confirm, each with the line it prints; several print on one line,
separated by ;.
- destructive: a destructive reflex always confirms, however sure.
it cannot be undone, so it always waits for a yes. - no gate: the adapter shipped no thresholds, so nothing runs on its own.
the adapter ships no bars, so nothing runs on its own. - under the bar: the weakest judgment is under the bar for this effect.
a write runs at 0.90 or more. - read from your words: a value from a list was taken from a word of the input, where the answers about it
did not agree; or a value was read from words that do not spell it as it is typed, like a day misspelt, a
code typed with a space, or a code said aloud that took its capitals or its dash from the reflex's examples.
room was read from "snug",sku was read from "hs 0409". - a text without quotes: a text was read from words you typed without quotes.
label was typed without quotes, which always waits for a yes. - more words: the input holds words that ask for another thing, which the call does not hold.
"lock the door too" asks for another thing. Words that say the call is wanted once more, liketooorthe same for, count as these when the plan holds the call once. - part of a value: words right after a value answer the same question, so the value may be cut short of
them.
"30" may be part of the duration. - a text in quotes: the input holds a text in quotes that no value took, and the reflex takes one.
'tea' is in quotes and the call does not hold it. - a text left over: the reflex takes a text, the call holds none, and the input has words left that say what
is wanted from the result. They may be that text.
"saying tea is ready" may be the label. - less than you typed: the call, read back against the input, leaves out part of what you typed.
it holds all you said at 0.20, and a call runs at 0.30 or more; it leaves out "in the morning". The words named are ones no value holds: words a value was read from are not among them. Words that say what you want from the result are named apart:"newest first" says what is wanted from the result. - a detail beside it: a part of the sentence that is not in the plan may add a detail to this step; or the
step was cut from a longer part read with a value its call lacks; or it was cut from the part beside it at a
sign typed for
and, and the cut is not sure."the ones since noon" may add a detail this call does not hold(Weaving). - read with the whole sentence: the step's words match nothing alone, and the step comes from reading them
with the rest of the sentence.
"payments too" matches nothing alone, and the whole request reads it as errors at 0.85, which always waits for a yes(Weaving). - one of several: the value was read from words that point at several things named earlier, like
both, which make one step for each of them.person was read from "both"(Weaving).
[t]each records only what the input stated. An argument you filled in at a prompt is not recorded. The answer
is read from the terminal, never from stdin. With no terminal, a confirm exits 3 with the command to run yourself.
Ask
The winner is clear, but an argument is missing. The input never stated a required one, or the value fell
outside its range, or the input names a value that is not among the argument's choices, or it names one that
could be read two ways. evoke asks the argument's own question, offers what it may be, and gates again with
your answer.
$ evoke "kill the lights"
Which room? [1] den [2] office [+] add one [0] none of these > 1
lights room="den" state="off" · write · weakest: state 0.74
a write runs at 0.90 or more
Set the den lights off? [y]es [n]o [t]each > y
den lights off
$ evoke "set the volume to 150 percent"
How loud, in percent? 150 percent is outside 0–100 > 40
volume level="40" 0.96
volume set to 40%
$ evoke "kill the lights in the garage"
Which room? "garage" is not on the list [1] den [2] office [+] add one [0] none of these > 2
lights room="office" state="off" 0.95
group-7 lights off
The prompt names the words of the input that answer the question, where evoke found them.
- A choice takes its number or its own text. A pick takes what you type, read the same way as the input. A pick is a number, a duration, an address, a URL, a quoted phrase, a date, a time, an amount or a code. A quoted argument takes the whole line when nothing is quoted; a code takes a quoted one.
- A pick whose manifest names a
recentfield lists what the bodies of this session returned under it,From which release? [1] 4.12.0 [2] 4.11.3 >, and a number picks one; words that point at one,it,that order,my last order, take the newest with no prompt (Arguments). - Where one listed word is the likely answer, the prompt puts it ready before the choices:
Which colleague's laptop? you wrote "Samuel's": sam? [y]es, or [1] sam [2] ana [3] jo [+] add one [0] none of these >.ytakes it. The choices keep their order, and a number or a word still picks any of them (Saying things). - Where your words say a pick's value aloud and can be read in more than one way, the prompt quotes them and
offers what they read as:
Which product code? you wrote "k d thirty one fifteen" [1] KD-3115 [0] none of these >. A number picks one, and you can still type the value yourself. For a number argument the readings are named without a number,you wrote "three eleven" 311 >, since a number you type is the answer itself (Saying things). - An answer that does not fit is asked again, with the reason on the line. An empty line asks again.
+at a vocabulary's prompt asksWord?andMeaning?, thenPath?when a reflex's declaration takes the word's value as a path, writes the word to your vocabulary, and goes on.0, none of these, declines a listed question, as the end of input,Ctrl-D, declines any: nothing runs, the line says so, and the exit code is 2.
Abstain
none won the route, or the winner is under the route bar. The ranking prints, nothing runs, and the exit code
is 2.
$ evoke "make it cosy"
lights 0.45 · none of them 0.40 · timer 0.10 · volume 0.05 · a reflex is picked at 0.50 or more
[2]
An inactive reflex is never in the ranking. When one is left out, an abstain names it on the next line, and
evoke show says why:
$ evoke "what time is it"
none of them 0.85 · timer 0.10 · lights 0.03 · volume 0.02
open is inactive → evoke show
[2]
What confidence is
For each decision, the adapter answers a question about the route and questions about each argument. Confidence is the lowest probability among the route and every value the call holds. Each value counts by what gave it: where two questions about it agree, the less sure of the two counts. A flag, and an argument the input says nothing of, add nothing to it. An argument you typed at a prompt is settled, and its judgment leaves the gate.
A call that clears the bar of its effect is then read back against the input, in the reflex's own words: its
whole description, then each argument with its value. One more question is asked: does this reading hold
everything the input says? The call runs when the answer clears a bar of its own, whole. The words no value
took are read beside it, each with what the reflex does: they say what to do, answer an argument, say what is
wanted from the result, ask for nothing, or ask for another thing.
The bars come from the adapter. Both built-in adapters reach Jev and ship the same numbers. They are calibrated,
so each number means the probability this is right, as evoke calibrate measures it on your own records (Calibrating):
| Floor | Default | Gates |
|---|---|---|
route | 0.5 | Under it, abstain |
read | 0.8 | A read reflex runs at or above it |
write | 0.9 | A write reflex runs at or above it |
whole | 0.3 | A call that would run does so when it holds all you typed at or above it |
fits | 0.3 | At add, a new reflex that fits another's example at or above it is named |
| destructive | — | Always confirms |
A write changes something, so it asks for more than a read. whole is a second look at a call that already
cleared its bar, so it asks for less: it holds a call that leaves part of the input out, and lets through one
the answer only hesitates on.
You may raise or lower them for your own machine, under your adapter's table in evoke.toml, [adapters.jev] or
[adapters.openjev]. read may never exceed
write. No number makes a destructive reflex skip its question. There is no --yes. To run unattended, set a bar
in a file you own, not a flag on a pipeline.
[adapters.jev]
gate = { write = 0.95 }
try: decide, and show the work
evoke try "<input>" decides without running, and prints the sentence as it was read. First the sentence,
then, where it matters, how the sentence was read as a whole: whether it asks one thing or several and where it
was cut, each part set aside or kept out of the plan, and what was left out because you said not to do it. A
part set aside by its own words says what it is: "before I forget": about what you do, not a request, "if you would": a courtesy. Then
each step, under its number when there are several, with a row a judgment:
which reflex: the answers about the reflex, the most probable first, then what the reflex does.- One row per argument the call holds, asks, or the input says something of: the answer, or the value where an answer did not give it, and under it what the value stands on — what a second question about it answered, the words of the input that hold it, the yes that took it, the step it was taken from, the part of the sentence that gave it, or your own answer at the prompt.
holds all you said: how far the call holds all you typed, where the call was read back against the input; under the bar, the words it leaves out, and the words that say what is wanted from the result.- What a rule did:
the same call, a part that adds to this call;read with, how a part that matched nothing alone was settled;read apart, how a step was read on its own, out of a longer part;another value, what the classifier answered about the value a step was written for (Weaving);the playbook, the playbook that wrote the step;takes, the results it takes whole;runs if, what picks it. →: what would become of the call —runs,waits for a yes,asks,refused— with the call as the plan prints it, and under it why.
A last line says how many questions the adapter answered and in how many rounds, or that the cache did. An
answer's key prints in words: not said for a question the input says nothing to, said, and not on the list
for a value that is not among the choices, none of them for no reflex. try shares the cache with a real
decision. It is never logged.
$ evoke try "kill the lights"
"kill the lights"
which reflex lights 0.95 (none of them 0.03 · timer 0.01)
Turn the lights in one room on, off, or dim them.
room not said 0.87 (den 0.10 · office 0.03)
state off 0.74 (on 0.19 · dim 0.06)
asked a second way: off 0.74; the less sure of the two counts
is it off? yes 0.95
→ asks: lights state="off" room=?
it needs room
replay answered 11 questions in 2 rounds
try --json prints the decision as one JSON line: The JSON line.
why: the last sentence, explained
Every real decision is logged. evoke why prints the last sentence as try would have, from the log alone,
and says what became of each step: ran, ran at your yes, declined, asked, refused, failed,
cancelled, skipped. After a sentence of several steps, it shows each step under its number (Weaving).
$ evoke why
"kill the lights in the den"
which reflex lights 0.95 (none of them 0.03 · timer 0.01)
Turn the lights in one room on, off, or dim them.
room den 0.92 (not said 0.05 · office 0.03)
asked a second way: den 0.92; the less sure of the two counts
state off 0.94 (on 0.03 · dim 0.02)
asked a second way: off 0.94; the less sure of the two counts
is it off? yes 0.95
holds all you said 0.90
→ ran: lights room="den" state="off" 0.92
a write runs at 0.90 or more, and its least sure judgment is room, 0.92; it holds all you said at 0.90, and a call runs at 0.30 or more
replay answered 13 questions in 3 rounds
The last line counts the questions the adapter answered, and the rounds they were asked in: the parts of a
sentence are asked about side by side, and the answers to one round may open the next. When every answer came
from the cache, it reads answered from the cache; a plan run from a file names the file.
run: by name, no classifier
evoke run <call> runs a call you write yourself. Nothing is decided, so nothing prints but the call. The effect
policy still holds: a destructive call confirms. Every value is checked, just as a decision's would be.
$ evoke run lights room=den state=off
lights room="den" state="off"
den lights off
$ evoke run power action=restart
power action="restart" · destructive
it cannot be undone, so it always waits for a yes
Make the laptop restart now? [y]es [n]o > y
restart in 5 seconds
$ evoke run lights state=off
lights: lights needs room → evoke show lights
[3]
The call grammar is name arg=value…. A value with spaces is quoted: duration="10 minutes". A flag is its bare
name. A word the vocabulary lacks, an option not offered, a number out of range, or an argument the reflex does
not have: each is refused, with the command that shows what is allowed. A call by name is not logged.
evoke run --json <call> prints the call and its result as one line, for a script: The JSON line.
The cache
The adapter's answers are cached by the installed set, the sentence and the questions asked, not by the
decision. Repeat an input and it costs nothing. A decision narrowed with --tag asks fewer questions, so it has
an entry of its own. Change a threshold, and the same answers gate differently. try and why explain a
cached decision exactly like a fresh one. evoke test never uses the cache.
Next: Weaving.
Weaving
evoke reads every sentence for its steps. One step is decided as it always was, unless its reflex takes an
earlier step's result: then the request stops before anything runs. More than one is a weave: each part
decided on its own, in the order the words give, a result of one step threaded into a later one, and the plan
shown before anything runs.
$ evoke "kill the lights in the den and start a 10 minute timer"
1 lights room="den" state="off" 0.92
2 timer duration="10 minute" 0.95
den lights off
10 minute timer started
How a sentence is read
- A connective can separate two things:
and,then,but, a comma,after,before. At each one the classifier is asked whether it does. "kill the lights in the den and start a 10 minute timer" is two things. "set a timer for 10 minutes and 30 seconds" is one. A sentence with more than two dozen such points, a pasted list, is decided as one input. - A sign or a letter typed for
andis such a point too:+,&,n,nd,adn, alone between two words. "check the errors for checkout n pull its logs" is asked about atn. It is no such point beside a number or a single letter,2 + 2,R & D, between two capitalised words,Hartwell & Sons, after a comma, or inside quotes. Where the classifier cuts at such a sign without being sure, the two parts may be one request: each confirms before it runs, naming the other,"start a 10 minute timer" may add a detail this call does not hold. - With the connectives, the classifier is asked how many things the sentence asks for. Where it says one, the
sentence is one step, and no connective cuts it: "check the errors for checkout, and only in eu-west". Two
cases still give several steps. One action asked for several values is a step for each value
(One action for several values). And a cut at a sign typed for
andthat the classifier was not sure of stands when each part, read alone, asks for a different reflex. Each part then waits for a yes that names the other. - Each part is decided as one input is: routed, gated, its arguments read. A part that matches nothing on its own
is tried as another item of its neighbour's task first: "check stock for widgets and gadgets" is two stock
checks. So is a part that is only a determiner and one word, "the office" in "kill the lights in the den and
the office", whatever it would mean on its own: it stays a step of its own only when it fits no item of its
neighbour's task. A part read as an item must be the value it stands in for, "gadgets" for "widgets", never a
longer phrase that holds one, and it never stands in for a text in quotes: in
start a 10 minute timer called "pasta", and ring me after, no second timer is made. When the classifier was sure the two parts were separate things and one still matches nothing, the whole request is refused rather than half done. When it was not sure, the part is read with its neighbour as one request, andevoke whysays what its words do. - A part that holds only values, with no word of its own for what to do, continues the step before it: in
buy 3 of OUT-503 and 2 of BOK-603, the second part is a purchase too. Read alone, such a part is a toss between the reflexes that take such values, here buying and filling the cart. So it is never left to that reading: the part is put in the words of the step before it,buy 2 of BOK-603, and decided as that step's reflex, whatever it looked like alone, so that the same sentence reads the same way every time.evoke whyshows the words it was read in, andread with the words of the part before it, as one request. A word likemore,anotherortoobeside the values changes nothing:and 2 more of BOK-603is a purchase as well. A part that names its own action is read as it always was:add "aurora kettle" to my cart and check outis two different steps. - Two verbs before one thing are two steps: in
buy and pay 3 of OUT-503, the product is bought, then the order is paid, the second step taking the order the first one yields, asbuy 3 of OUT-503 and pay for itdoes.evoke whyshows the second step asread apart,a second thing to do before the object of the part before it, which it takes from. The same holds where the classifier cuts the sentence at theand,buy and pay 2 of HOM-403, and with two things,buy and pay 3 of OUT-503 and 2 of BOK-603: each purchase is paid. A second verb that names the same reflex, or none, changes nothing; andpay order 1024afterbuy andkeeps its own order, since the words name it. - A part that begins with
not,don't,neverorwithoutis left out, however the apostrophe is typed. What you ask evoke not to do is no step. One step beside such a part is decided as one input. A sentence that is only such parts is nothing to do, and one line says so. A part that also names a value the step beside it lacks is read with that step: in "pull the payments logs, not in us-east, in eu-west" the logs are pulled in eu-west. Where what follows the part is a request of its own, it is kept as one: in "not the office, kill the lights in the den", the office is left out and the den's lights go off. And where the classifier says the sentence asks for one thing, nothing is left out: the sentence is read whole, what it rules out with what it asks. then,after thatandnextorder the steps.after you X, YandY after you Xread asX, then Y.before you X, YandY before you Xread asY, then X. When a write is among the steps, every step runs alone, in the order you wrote them. A plan of reads may run them side by side.- A word the sentence says once for several steps reaches each of them. "check checkout's errors, deploys and
logs in eu-west" names the service and the region once: every lookup takes both. A step that lacks a required
word takes the one word of its kind the sentence names, as an answer would give it. A step that could take an
optional word takes the one said once in its part of the sentence, before any
then, written into its words and decided again under the same gate. A step whose own words name a word of that kind keeps its own, and a sentence that names two, "checkout" and "payments", carries neither. Only a word from a vocabulary is carried: never a number, a date, an address, a quoted value or an option. And only a word no one reflex owns: a vocabulary one reflex alone asks for, the folderopenopens, is that reflex's own and never reaches another step. The plan shows the word on each step it reached, andevoke whysays which words were shared. - A value one part states can be another part's where the classifier says so. It is asked only for a part whose
words point at something, "it", "there", "the same", or for a word from a vocabulary that several reflexes
ask for: in "check the errors for checkout in eu-west, then pull its logs there", the logs are pulled in
eu-west. An option or a flag of a call reaches a part that asks for the same again, "the same", "likewise",
"as before": in "take a screenshot of the window to the clipboard, then the same for the whole screen", both
go to the clipboard.
evoke whysays where the value came from. - Two parts that read as the same call, no value of one differing from the other's, are one step: "check the
errors for checkout, and the errors in eu-west" is one check, of checkout in eu-west. Where a part could repeat
either of two calls, its own words pick one, "the second one", "the last", or it stays a step of its own. The
plan names the part:
folded "the errors in eu-west" into 1. Where the part's own words say what to do and the whole sentence reads them as another reflex, the part is a step of its own, and it waits for a yes. - What you say you will do yourself is no step. A clause that opens with
beforeorafterand hasIorweas its subject is set aside where it stands: in "pull the payments logs before I forget", the logs are pulled, and the plan saysset aside "before I forget".before you Xandafter you Xstill order two steps. - A courtesy that opens with
ifis no condition:if you would,if you don't mind,if possible,if so,if it's not too much trouble. At the head or the end of a part it is set aside the same way. - For a part that asks for something and still matches no reflex, the classifier is asked once more which reflex
its words ask for, this time reading the whole sentence. Where it picks one, the part is decided as that reflex,
its values read from its own words, and becomes a step. Like any step it takes a word or a value the sentence
says once, and it always confirms before it runs: in "check the errors for checkout and post in #incident,
payments too", the errors for payments are a third step,
"payments too" matches nothing alone, and the whole request reads it as errors at 0.85, which always waits for a yes. A part that names a listed word its step cannot take stays unread, and the whole request is refused: read aspost, which takes no service, the part shows"payments too" · no reflex. - A part that matches no reflex and asks for nothing is set aside, and the plan runs without it:
set aside "thanks a lot for this". Where the classifier says the part may add a detail to the step beside it, that step confirms before it runs, its line ending inwithout "the ones since noon", and the plan names the part:not in the plan "the ones since noon". A part that asks for something and matches nothing, alone or with the whole sentence, still refuses the whole request. - Two items the classifier read as one thing, "invoices, card expenses", are two steps when each is a reflex of
its own: the joint is split, and a word the sentence says once reaches both. A joint it read firmly as one
task stays one. A step made this way confirms where the whole was read as its reflex with a value the step's
own call lacks, and names the value's words:
"friday" may add a detail this call does not hold. - A sentence reads most surely when it asks for a few steps. The plan is settled before anything runs, so the longer a sentence grows, the more often one of its steps stops to ask.
One action for several values
A request can name several values for one action: three people to look up, or two regions to check. evoke
plans a step for each value the request asks for. It reads the request for one value first. Then it asks the
classifier whether the request asks for each other value as well. Each step is judged alone: one that is sure
runs, and one that is less sure waits for a yes.
$ evoke "look up the hires ana, maria and sam"
1 hire person="ana" 0.91
2 hire person="maria" 0.87 · with 1
3 hire person="sam" · read · weakest: route 0.79 · with 1, 2
a read runs at 0.80 or more
ana: product designer, starts monday 19 october
maria: backend engineer, starts monday 5 october
3 hire person="sam" · read · weakest: route 0.79
a read runs at 0.80 or more
Look up sam's hire? [y]es [n]o [t]each > y
sam: account executive, starts monday 12 october
A part that asks for the same with another value repeats the step before it, with that value in place. After two
different actions, such a part is read on its own, since the same could mean both.
$ evoke "error rate for payments in eu-west, and the same for us-east"
1 errors service="payments" region="eu-west" 1.00
2 errors service="payments" region="us-east" 1.00 · with 1
payments in eu-west: 8.4% errors since 14:02
payments in us-east: 8.4% errors since 14:02
Words that point at several things named earlier, like both, become one step for each of them. both names
none of them itself, so each of those steps waits for a yes. Its line names the words its value was read from: person was read from "both".
$ evoke "look up ana & maria, order both a pro"
1 hire person="ana" · read · weakest: route 0.80
person was read from "ana"
2 hire person="maria" · read · weakest: person 0.70
a read runs at 0.80 or more
3 order person="ana" model="pro" · destructive · weakest: person 0.77
it cannot be undone, so it always waits for a yes; person was read from "both"
4 order person="maria" model="pro" · destructive · weakest: person 0.91
it cannot be undone, so it always waits for a yes; person was read from "both"
…
3 order person="ana" model="pro" · destructive · weakest: person 0.77
it cannot be undone, so it always waits for a yes; person was read from "both"
Order a laptop for ana? [y]es [n]o [t]each > y
pro ordered for ana, PO-20463, at the desk by monday 19 october
4 order person="maria" model="pro" · destructive · weakest: person 0.91
it cannot be undone, so it always waits for a yes; person was read from "both"
Order a laptop for maria? [y]es [n]o [t]each > y
pro ordered for maria, PO-20461, at the desk by monday 5 october
evoke why shows how each step was read. The first step's read apart row says it was read for its own value
alone. Each other step was decided on your words, its own value in place of the values you listed:
look up the hires sam. Its another value row shows what the classifier answered: whether the request asks for
that value as well, means something else by it, rules it out, or wants it instead of the value read. The step's
which reflex is the share for asked for as well. For sam it was 0.79, under the 0.80 a read needs, so that
step waited for a yes.
$ evoke why
"look up the hires ana, maria and sam"
1 · look up the hires ana, maria and sam
…
read apart for the one value it holds: each other value it names is a step of its own after it
…
3 · look up the hires sam
which reflex hire 0.79
…
another value asked for as well 0.79 · says something else 0.14 · ruled out 0.06 · in place of the one read 0.01
→ ran at your yes: hire person="sam" · read · weakest: route 0.79
a read runs at 0.80 or more
What a step takes from another
A body may declare what its result holds, under [yields] in its manifest (The manifest).
A later step may refer to it with it, them, that report or the address. It takes a field of the right kind
into the argument it lacks:
$ evoke "look up dana's address and email them"
1 contact name="dana" 0.95
2 mail · takes email from 1
dana <dana@example.com>
2 mail to="dana@example.com" 0.94
drafted to dana@example.com
mail needed an address the words did not give. contact yields one, and them names it. A required argument
is filled with the value. An optional one is decided again with the value written into the words, so the
classifier assigns it, under the same gate as any words. A date a step yields reaches the next as a day,
2026-05-06, never as tomorrow: a relative day is nothing to take. A day of the month in a step that takes
from a step naming a month and a day is read beside that day, not beside today: in show my calendar for october 12th, then move those meetings to the 20th, the meetings move to October 20th, and the 3rd would be
November's. The plan shows the day as read, and evoke why says read beside the day of step 1. Nothing is guessed: a reference that
several fields could satisfy, or one record of a list, stops with a line naming them. Within one sentence a step refers to another; across sentences, in one
session, a pick with recent recalls what earlier bodies returned, at its ask
(Arguments). A step that refers to another whose result it takes
nothing from asks before anything runs:
$ evoke "look up dana's address and start a 10 minute timer for them"
1 contact name="dana" 0.95
2 timer duration="10 minute" 0.94 · after 1
step 2 refers to step 1, but takes nothing from it
Run the plan as it stands? [y]es [n]o > y
dana <dana@example.com>
10 minute timer started
A step that takes whole results
A step may take earlier steps' results whole. Its manifest names what it takes with takes; each earlier reflex
names what it returns with returns (The manifest).
The plan binds them by that name before anything runs, whatever the words refer to, and the step waits for the
steps it takes from:
$ evoke "check the errors for checkout, list the checkout deploys and pull the checkout logs, then find the suspect"
1 errors service="checkout" 0.95
2 deploys service="checkout" 0.95 · with 1
3 logs service="checkout" 0.95 · with 1, 2
4 suspect 0.95 · takes errors from 1, deploys from 2, logs from 3
checkout: 8.4% errors since 14:02
checkout: 1 deploy today, 4.12.0 at 13:58
checkout: 412 timeouts calling payments
4.12.0 at 13:58, four minutes before 8.4% errors and 412 timeouts
Steps 1 to 3 run side by side; step 4 receives their three results as its arguments, beside its decision and never in its words, an empty list as a list. A result over 1 MiB is not handed: the step is skipped, and the weave fails as it does when a source yields nothing. When no step before it returns what a step takes, or two steps do, the request stops before anything runs, and says which; so does a field taken from a step that runs once per record:
$ evoke "check the errors for checkout, then find the suspect"
1 errors service="checkout" 0.95
2 suspect 0.95 · takes errors from 1
step 2 takes deploys, which no step before it returns → evoke show deploys
step 2 takes logs, which no step before it returns → evoke show logs
[2]
A plan a playbook wrote
A short sentence can reach a playbook, a reflex whose body is a plan of sentences (Playbooks). Its steps stand in the plan as if you had typed them, each decided over what you have installed, and every line ends with the playbook and the step that wrote it. Under the plan comes the sentence's own line, then one question over the whole. Your yes covers every step, and a step's own confirm is asked again at its turn.
$ evoke "checkout is failing in eu-west"
1 errors service="checkout" region="eu-west" 0.95 · outage 1
2 deploys service="checkout" region="eu-west" 0.95 · outage 2
3 logs service="checkout" region="eu-west" 0.95 · outage 3
4 suspect 0.95 · takes errors from 1, deploys from 2, logs from 3 · outage 4
5 rollback service="checkout" region="eu-west" · takes release from 4 · outage 5
6 post channel="#incident" 0.95 · outage 6
7 status component="checkout" 0.95 · outage 7 · service as component
outage service="checkout" region="eu-west" · destructive · weakest: route 0.95
it cannot be undone, so it always waits for a yes
Run the outage plan for checkout? [y]es [n]o [t]each > y
A slot the sentence does not state is asked before the plan prints, unless a part of the sentence states it for
a step the playbook writes: in "we have an outage, check the errors for payments", the service is payments. A
part of your sentence that repeats a step of the plan folds into that step, and the plan says so: folded "show me the error rate" into 1. A part that
says what not to do beside such a plan is refused, since the step it may have meant is already there. A sentence
that opens with a condition, if, unless or in case, is refused whatever follows: evoke judges no
condition, so ask for the check first, then type what to do. A step of the plan that matches nothing here, or
that reaches a plan inside a plan inside a plan, refuses the whole plan, and the line names the step. When the
sentence holds more than the playbook, the question is Run the plan as it stands?.
A playbook may list steps that run only under a value an earlier step yields, a branch. The plan prints every
one before anything runs: the line of the step whose result picks ends then 4 on "yes", 5 on "no", and each
alternative's if 3 yields landing "yes". At that step's turn the value picks. The step it picks runs, the
others are skipped clean and say so at the end, skipped · not chosen: step 3 yielded landing "yes", and a
value no step lists picks nothing, which no step lists, the plan going on (Playbooks).
Before anything runs
The plan is settled first. A step whose required argument no other step provides is asked for it up front, as one input would be at its turn. Then the plan is shown, numbered, one line per step:
$ evoke "kill the lights and start a 10 minute timer"
1 lights state="off" · asks room
Which room? [1] den [2] office [+] add one [0] none of these > 1
1 lights room="den" state="off" · write · weakest: state 0.74
a write runs at 0.90 or more
2 timer duration="10 minute" 0.95
1 lights room="den" state="off" · write · weakest: state 0.74
a write runs at 0.90 or more
Set the den lights off? [y]es [n]o [t]each > y
den lights off
10 minute timer started
An answer out of range is asked again with the reason, as one input's would be. Each line of the plan is what
evoke would say of that step alone: the call and its confidence, or the confirm's own line with its weakest
judgment. After it: asks <arg> for what the step still needs, takes <name> from <n> where a result threads
in, the name a field's or a whole result's, after <n> where the words ordered it, with <n> where the step
runs beside earlier ones, <playbook> <n> where a playbook wrote the step, then <n> on "<value>" where the
step's result picks among later steps, if <n> yields <field> "<value>" where the step runs only under that
value, no reflex where nothing matched.
Under the plan, folded "…" into <n> names a part that repeats another step, set aside "…" a part that asks
for nothing, not in the plan "…" a part that may add a detail to a step, and left out "…" a part that said
what not to do.
At each step's turn
Each step then goes through the same gate as one input: a run runs, a confirm prompts, an ask asks. A step that differs from its plan line, a bound value now in place, prints again. In the SDK a step's confirm can wait for someone else: the decision travels as plain data to whoever answers it, and the weave goes on when they do (Decisions). A failure, a decline or a refusal ends the weave after its stage. The steps after it are skipped, and say so:
$ evoke "start a 25 minute timer and kill the lights in the den"
1 timer duration="25 minute" · write · weakest: duration 0.85
a write runs at 0.90 or more
2 lights room="den" state="off" 0.92
1 timer duration="25 minute" · write · weakest: duration 0.85
a write runs at 0.90 or more
Start a 25 minute timer? [y]es [n]o [t]each > n
2 lights room="den" state="off" 0.92 · skipped
[2]
A step decided again with a bound value in its words may match no reflex then. It is refused, and says so:
2 "copy sam@example.com on a note to dana@example.com" · no reflex
A part that matches nothing refuses the whole request before anything runs:
$ evoke "kill the lights in the den and feed the cat"
1 lights room="den" state="off" 0.92
2 "feed the cat" · no reflex
[2]
The exit code is the worst step's. 0 every step ran. 1 a body failed, or a step yielded nothing the next
could take, or more than it can be handed. 2 a step was declined, a part matched nothing, or a step takes a
result no step before it returns, or two do. 3 a step needed a terminal, or a question only you can answer.
Stopping it
Ctrl-C while a step runs stops the weave. The body running at that moment is told to stop and ended with
everything it started: a JavaScript body sees its signal abort and has a second to finish. That step and
every step after it read skipped · cancelled, on the terminal and in the log, so why and teach still see
the whole plan:
$ evoke "wait a while and start a 10 minute timer"
1 wait 0.95
2 timer duration="10 minute" 0.95
waiting
^C
1 wait 0.95 · skipped · cancelled
2 timer duration="10 minute" 0.95 · skipped · cancelled
Then evoke ends as an interrupted program does, and the shell reports exit 130. Under --json every step's
line prints first, its why { "type": "cancelled" }.
try and --json
evoke try shows the plan, then the sentence as it was read: where it was cut, what was folded, set aside or
left out, and every step under its number, a playbook's own block first when one wrote the plan
(Outcomes). evoke try --json prints the plan whole, on one line, with every adapter
call it took. evoke --json prints one line per step as it runs: the line of one decision, with step and
steps first, bound where a value came from another step, from where a playbook wrote it, and status at
the end, with why when the step stopped; the sentence's own line prints first, as step 0, with no status
(The JSON line). A plan stopped before any
step ran, refused, its question or its prompt declined, or with no terminal to ask, prints every step's line at
once, with what stopped it.
Saving a plan, and running it for someone else
evoke try --save <file> "<sentence>" writes the plan as a file, and runs nothing. The file holds the sentence as
you typed it, the plan as try --json prints it, and every answer the classifier gave, in clear; and the pins the
plan was decided under: the evoke version, the adapter and its gate, the installed set's digest, each reflex and
vocabulary by its own hash, a remote reflex by the lock's h1. It carries no time and no name. A plan that asks
cannot be saved: a file holds no one's answers, so the sentence must say the value, kill the lights in the den
rather than kill the lights. The name <name>.plan.json is recommended, not required.
$ evoke try --save ~/month.plan.json "pull september's bank transactions, invoices, card expenses and payroll, reconcile them, post the closing entries to the ledger, then send the report to cfo@example.com"
1 bank month="september" 0.95
2 invoices month="september" 0.95
3 cards month="september" 0.95
4 payroll month="september" 0.95
5 reconcile 0.95 · takes transactions from 1, invoices from 2, expenses from 3, payroll from 4
6 ledger month="september" · write · weakest: route 0.89
a write runs at 0.90 or more
7 send to="cfo@example.com" 0.95 · after 1, 2, 3, 4, 5, 6
+ ~/month.plan.json
evoke run <file> runs it, on your machine or another with the same project: the same reflexes at the same
content, the same words, the same adapter and gate. run tells a file from a call by its first word, which is no
reflex name. The plan is made again from the file's answers alone, so the classifier is asked for nothing, and a
plan that does not read the same as the file is refused. The plan prints, one yes over the whole plan is asked at
your terminal, whatever its verdict, then each step goes through the gate at its turn as any sentence's does: a
confirm asks you, a value bound at run time is decided again by your adapter under the same gate. Every step's line
in your log names the file, and evoke why shows it as from ~/month.plan.json.
$ evoke run ~/month.plan.json
1 bank month="september" 0.95
…
7 send to="cfo@example.com" 0.95 · after 1, 2, 3, 4, 5, 6
the plan of ~/month.plan.json
Run the plan as it stands? [y]es [n]o > y
september: 214 transactions, 18 204.55 at the close
…
6 ledger month="september" · write · weakest: route 0.89
a write runs at 0.90 or more
Post september to the ledger? [y]es [n]o [t]each > y
september posted to the ledger
sent to cfo@example.com
A pin that moved refuses the file with one line naming it, exit 3, and the command that mends it: a plan written
by another evoke, decided by another engine or under another gate, a reflex not installed here or inactive here,
a remote reflex whose body moved, a reflex or a vocabulary whose wording moved, a reflex installed here the plan
never saw, or the project's files differing at all. A file edited by hand does not read the same as its answers,
and is refused too. The fix is to make the plan again here and read it again:
$ evoke run ~/month.plan.json
months differs here from the plan's → evoke try --save ~/month.plan.json "pull september's …"
[3]
In the SDK, steps returns the same object and weave takes it (Decisions).
Afterwards
Every step is logged under its number. evoke why shows the last sentence as it was read, each step with what
became of it — ran, failed, declined, refused, skipped or unanswered, and why, cancelled among
the reasons — a playbook's own block first when one wrote the plan, and under a value the step it was taken from
or the part of the sentence that gave it. evoke teach <call> with no utterance takes the step the lesson's
reflex decided; when none or several did, it names the steps and asks you to say which (Tuning).
Next: Installing reflexes.
Installing reflexes
Git is the registry. A reflex lives in a repository, at a version tag. evoke add fetches it, pins it in a lock,
and keeps its code in a store keyed by content. It runs the code only while it still hashes to the lock. Nothing is
installed that a person did not name. No install-time code ever runs.
Refs
| Form | Means |
|---|---|
owner/repo | Every reflex directory in the repository, on GitHub |
owner/repo/dir | One reflex, the directory dir of the repository; dir may nest |
owner/repo/dir@1.2.0 | Pinned: update keeps it there. Tags are X.Y.Z or vX.Y.Z. The newest is the highest |
https://host/repo.git#dir@1.2.0 | Any git host over https or ssh. #dir and @tag are optional |
ssh://git@github.com/owner/repo | The same over ssh, the user before the host |
./dir, ../dir | A local directory, relative to evoke.toml. Never fetched, never locked; one outside the project is shown where it is, ~/hello |
An unpinned ref means its newest tag at add, and update moves it forward. A repository with no version tag
cannot be installed. Its author publishes one with git push --tags. A password or a token in a URL is refused:
git's credential helper holds those, never a project file.
add
$ evoke add radhi/home/lights radhi/timer
+ lights radhi/home/lights 1.2.0 write runs lights.mts
+ timer radhi/timer 1.0.1 write runs timer.mts
inactive lights: vocabulary "rooms" is empty → evoke vocab rooms add <word> "<meaning>"
inactive lights: config "bridge" is not set → evoke config lights bridge <value>
inactive lights: config "token" is not set → evoke config lights token --env <VAR>
A reflex whose manifest declares what its body touches shows it on a second line under its row, needs writes {to} ~/Downloads · hosts *, as the first ten minutes show; a
row without one declares nothing.
evoke add <ref>… [--as <name>] does, in order:
- Fetches each ref at its pin or its newest tag. This is a bare, shallow fetch, read without a checkout,
once per repository and tag. Symlinks and submodules are refused. The tree is kept in the store. A local ref,
./dir, is read where it is and written toevoke.tomlas a path relative to that file. It is never fetched and never locked. - Reads and lints every manifest. A manifest that does not parse refuses the whole add. Nothing is written
until every newcomer is in hand. Lint only reports; it never refuses. It flags a summary over 100 characters, a
description over 1 000, more than eight
not_forentries, more than 24 options, more than 40 records per table, an utterance over 200 characters, or text that addresses a model instead of describing an action. - Tests for theft. The examples already installed are routed over the new set, a few at a time, each
newcomer's fit asked too. A phrase a newcomer wins prints as
<thief>: steals "<phrase>" from <owner> → evoke teach "<phrase>" not <thief>; one it fits over the floor, so that two reflexes claim the phrase, as<thief>: also fits "<phrase>" of <owner> (0.82), with the same fix. The add still proceeds. The fix is one line in your overlay. When the classifier cannot answer, or has no key yet, the line says the test did not finish, andevoke testruns it again. - Writes the
[reflexes]lines toevoke.toml, the lock, andevoke.d.ts. It records the JavaScript runtime when a newcomer runs a file. Then it prints one row per newcomer behind+, then the lint lines, the theft lines, and each newcomer's inactive lines.
The local name is the ref's last segment. --as <name> picks another, for a single ref. The local name is
what the classifier reads, and your overlay file is named after it. A name already taken is refused. For one ref,
the fix is the --as line; for a collection, the line that adds the rest, or evoke update when every reflex
of it is installed already. So is the same reflex under another name, the one the lock already holds by
content: clock: is timer under another name → evoke show timer.
remove
evoke remove <name> drops the [reflexes] line and the lock entry, and prints the row behind -. Your overlay,
your vocabularies and your settings stay, because they are yours. The store's copy stays too; it is a cache.
update
$ evoke update
lights 1.2.0 → 2.0.0 major · code changed
description rewritten, taken (you have no override)
args.state renamed power; your 1 example follows
evoke update [<name>] moves every unpinned remote reflex, or just one, to its newest tag. A pinned reflex moves
to its pin. It never prompts, never blocks, and never rewrites your files. When nothing moves, it prints
up to date. A reflex whose new manifest does not read is reported at its new tree in the store and skipped; the
rest move, and the exit is 3. For each move, it prints the level of the change and one line per detail:
| Line | Means |
|---|---|
same, minor, major | Wording only, or a declaration narrowed; additions, a declaration widened among them; something your files or calls may not survive |
· code changed | A file other than the manifest changed |
description rewritten, taken | You have no override, so upstream's new text is used. Otherwise: yours kept |
args.state renamed power; your 1 example follows | The author declared the rename; your records follow it at merge time |
stale | You override something upstream changed: yours wins, both are shown once |
orphaned | A line of yours addresses nothing any more: skipped, the rest applies |
effect tightened to write | Upstream tightened the effect; it applies |
effect write upstream; destructive kept until you accept | Upstream loosened it; you keep what you consented to |
needs narrowed: hosts * dropped | Upstream narrowed what the body touches; it applies |
needs widened: writes ~/notes upstream; none kept until you accept | Upstream widened it; you keep what you consented to |
The lock holds the effect and the declaration you consented to. Upstream may tighten either at any time. They
loosen only through evoke update --accept <name>, which takes the looser effect and the wider declaration at
the current tag and says so: effect write accepted; was destructive, needs writes ~/notes accepted; was none. A new reflex in a repository you already use is reported once, with its add line. update never adds
one for you.
sync
On a new device, or in CI, you have the lock and the store is empty. evoke sync fetches each repository once,
at every locked tag it needs, checks that each tree hashes to the lock, and records the runtime. It prints a +
row per reflex placed, or up to date. It never changes the lock. A tag that moved or vanished is refused, with
evoke update <name>. Before sync, evoke show lists what the lock names, each missing tree as an inactive
line. A git call that has not finished within a minute is a failure, never a hang.
$ evoke sync
+ lights radhi/home/lights 1.2.0 write runs lights.mts
trust
Your home project is trusted by construction, and evoke trust there says so. Any other project, an
application's or a clone's, must be trusted before evoke decides in it. A project's files decide what runs:
$ cd app && evoke "kill the lights in the den"
~/app is not trusted → evoke trust
[3]
$ cd app && evoke trust
+ trusted ~/app
Trust binds to the content of the four owned paths: evoke.toml, evoke.lock, overlays/, vocab/. evoke's
own writes renew it. Any other edit, yours or a git pull's, makes the next run say changed since you trusted it → evoke trust. Trust lives in ~/.local/state/evoke/trust.toml, on this machine only.
The lock and the store
evoke.lock records, for each remote reflex, the ref, the tag, the commit, the content hash h1 of the reflex
directory, and the effect and the declaration you consented to. It also records the adapter the project decides
with, by name and id.
Commit it with your project.
Fetched code lives under ~/.cache/evoke/store/<h1>/. It is re-hashed against the lock every time evoke starts.
A store entry that does not match is a miss, and evoke sync places it again. Your project directory never holds
code you did not write.
The runtime
A reflex that runs a .mts or .mjs file needs Node 24 or newer. add and sync find node on your PATH
and record its path in ~/.local/state/evoke/runtime. Nothing is searched at run time, so a cron job's PATH
cannot break a decision. If none is found and none is recorded, the command ends with
node is not on PATH; a .mts reflex needs Node 24 or newer → evoke sync and exit 3, after its rows. Install
Node, and evoke sync records it. An argv reflex needs no runtime.
Next: Tuning.
Tuning
A reflex ships its wording. You own the last word. Every change lands in a file under your project. evoke
writes it with your comments and order kept, and every later decision reads it. No command here needs the
classifier's key but test, which asks it.
show: what is installed, and one reflex as used
$ evoke show
lights radhi/home/lights 1.2.0 write runs lights.mts
timer radhi/timer 1.0.1 write runs timer.mts
inactive lights: HUE_TOKEN is not set → export HUE_TOKEN=<value>
evoke show <name> prints the reflex's effective manifest: the shipped one with your overlay merged in. It
prints TOML, one key per line, with + next to every line that is yours: A line of your overlay that addresses nothing the reflex has, an argument it
dropped, is listed after the manifest as skipped, as update reported it.
$ evoke show lights
description = "Turn the lights in one room on, off, or dim them.\nCeiling and lamp lights only."
…
[args.state]
+ ask = "On, off or dim?"
options.on = "Switch on."
…
[examples]
"turn on the kitchen lights" = { state = "on" }
+ "kill the lights" = { state = "off" }
teach: one line in your overlay
$ evoke teach "kill the lights" lights state=off
+ overlays/lights.toml [examples] "kill the lights" = { state = "off" }
$ evoke teach "what time is it" not timer
+ overlays/timer.toml [examples] "what time is it" = false
$ evoke teach lights state=dim
+ overlays/lights.toml [examples] "dim the office" = { state = "dim" }
evoke teach "<utterance>" <call>: the utterance means this call. The call uses the same grammarevokeprints,name arg=value…. Only what you assert is recorded.lights state=offteaches the route andstate, and leavesroomto the input.evoke teach "<utterance>" not <name>: the utterance is never this reflex.evoke teach --forget "<utterance>" <name>: the line your overlay holds for the utterance, an example or a test, is removed; the shipped wording stands again. One not there is refused.- A phrase the reflex's examples already hold with the same values is said to be one, and nothing is written.
- Leave the utterance out, and it means the last input you gave. Say something, see it decided, then
evoke teach lights state=dimcorrects it. After a sentence of several steps, it means the step the lesson's reflex decided; when none or several did,teachnames the steps and asks you to say which. - A one-word utterance needs no quotes:
evoke teach dim lights state=dim. The first word is the utterance when a call follows it, unless the word names an installed reflex. Then the call begins with it. [t]eachat a confirm prompt does the same for the input just decided.
A value is checked at the door. A word the vocabulary lacks, an option not offered, or an argument the reflex does not have is refused, with the command that shows what is allowed.
Overlays
overlays/<name>.toml is your wording for one reflex, in the manifest's own shape. It is merged over the shipped
one by one rule: tables merge by key, and every value replaces whole. An overlay adds and replaces. It never deletes.
# overlays/lights.toml
description = "Turn the lights in one room on, off, or dim them." # replaces the shipped text whole
[args.state]
ask = "On, off or dim?" # new wording for an argument
options.dim = "Lower the brightness; the lights stay on." # new wording for an existing option
[examples]
"kill the lights" = { state = "off" } # sent to the classifier
[tests]
"turn on the kitchen lights" = { state = "on" } # a shipped example, moved here: held out now
What you may change is wording: description, not_for, tags, confirm, each argument's ask, the
meaning of its existing options, examples and tests. What you may not change is the contract: what the reflex
runs, its argument names, option keys, sources and ranges. A contract key in an overlay makes the reflex inactive,
and names the line to remove.
- The highest layer that names an utterance decides its table and its value. Move a shipped example under
[tests], and it is no longer sent.= falserejects it. effectin your overlay may tighten:writeoverread, ordestructiveover either. It may never loosen. A loosening line makes the reflex inactive.- Lists, like
not_forandtags, replace whole. - Your records may assert vocabulary arguments, like
{ room = "den" }. Shipped manifests may not. - Wording in another language is just an overlay.
At update, an overlay follows a declared rename. evoke reports what went stale: you override something
upstream changed, and yours wins. It reports what is orphaned: a line of yours addresses nothing now, and is
skipped. An overlay that fails to parse makes its reflex inactive, never silently looser.
Vocabularies
vocab/<name>.toml is a closed list of your words. Every argument that names the vocabulary shares it. A package
never ships or writes one. A reflex whose required argument reads an empty vocabulary stays inactive until you
fill it; an optional one is simply never stated.
# vocab/rooms.toml
den = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }
The meaning is what the classifier reads, so other names for the thing help. The value, when set, is what the program receives instead of the word. The classifier never sees it. It can be a path, a URL, or a device id.
$ evoke vocab rooms
den = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }
$ evoke vocab rooms add attic "The loft upstairs." --value group-9
+ vocab/rooms.toml attic = { what = "The loft upstairs.", value = "group-9" }
$ evoke vocab rooms remove attic
- vocab/rooms.toml attic
add of a word already there replaces its meaning. A word may contain spaces. none and unstated are
reserved. At an ask over a vocabulary, + adds a word without leaving the prompt, its path too when a reflex
declares the word's value as one it reads or writes.
config: settings and secrets
A manifest declares under [config] what it needs from you. Each setting lands under [config.<name>] in
evoke.toml. A reflex with a setting unset stays inactive.
$ evoke config lights bridge 10.0.0.2
+ evoke.toml [config.lights] bridge = "10.0.0.2"
$ evoke config lights token hue-example
lights: config "token" is a secret; it is set from a variable → evoke config lights token --env <VAR>
[3]
$ evoke config lights token --env HUE_TOKEN
+ evoke.toml [config.lights] token = { env = "HUE_TOKEN" }
A setting a declaration reads or writes as a path, writes = ["{file}"], is looked for: one that is not there
yet is said, and the body waits for it. A secret is only ever set with --env <VAR>. Your file names the variable. The value reaches the program
from the environment at run time, and nowhere else. --env works for any key. An undeclared key is refused, with
evoke show <name>, which lists what the reflex declares.
test: every record, judged
$ evoke test
lights 3 passed · 1 failed
"make it darker in here" state: expected "dim", read "off"
timer 8 passed
"ping me in 2 hours" passes, but would wait for a yes: the call holds all of it at 0.20, and a call runs at 0.30 or more
1 of 12 cases failed → evoke test
[1]
evoke test [<name>] decides every example and every test of every active reflex, or of one. It decides against
the whole installed set, a few cases at a time, never through the cache. It judges each case on its route and its asserted arguments. It
exits 1 when a case failed, so a script can act on it. It never blocks an install. A case that passes while its
call would wait for a yes, holding less than the case's own words, is named under its reflex and fails nothing (Records).
A case typed in a language other than English puts a line before the reflexes, one per such language, with how
many of its cases passed and failed (Languages). A case that passed last time and fails now is
decided twice more. Failing two of three marks it · regression.
The last verdicts are kept per installed set, on this machine. What the confidence beside each call meant on the
same records is evoke calibrate's answer: Calibrating.
Thresholds
Everything engine-specific lives under the adapter's own table, so one dotfiles repository serves machines on different engines:
[adapters.jev]
gate = { route = 0.5, read = 0.85, write = 0.95, whole = 0.4 }
The openjev adapter takes the same table under [adapters.openjev]. Both reach Jev, so the numbers mean the
same. Each number means the probability this is right. read may not exceed write. whole is the third
bar: a call that cleared its effect's bar is read back against your sentence, and runs when it holds all you
typed at whole or over; raise it and more calls wait for a yes with the words they leave out named, lower it
and fewer do. Destructive reflexes always confirm. Outcomes explains what the
numbers gate, and Calibrating measures each bar on your own records.
The clef and clef_flash adapters reach Clef, Cloudflare's model. They ship no bars, so every call through
them waits for a yes. To let calls run, give all three of route, read and write in the gate of
[adapters.clef] or [adapters.clef_flash]. whole and fits are yours to add. Choose the numbers from your
own records, which evoke calibrate measures. Environment lists the two variables
these adapters read.
Next: Calibrating.
Calibrating
Every call prints a number: lights room="den" state="off" 0.85. It claims that a call like this is right
about 85 times in 100. evoke calibrate checks the claim against the records you own, and prints what it found.
It needs the classifier's key, or a recording to answer from (below).
What the number claims
The number is the lowest of the probabilities the classifier gave the route and each value the call holds
(Outcomes). So the one event it can be measured on is the whole call is right:
the route, and every argument the record names. A record is an example or a test of a reflex
(Examples and tests). calibrate decides every record of every active reflex once
more, never through the cache, against the whole installed set, the whole reading of each; a sentence that is
several records is decided once. It judges each record as evoke test judges it. Then it counts whole calls by
the confidence they claimed.
The report
Over the manual's own lights and timer records, on a recording, after three decisions were made and logged:
$ evoke calibrate
replay · 12 records over 2 reflexes · 12 inputs, decided once
7 run · 3 ask · 2 abstain
whole call right, by confidence
0.80–0.90 1 call right 0 · 0% (0–79) · claimed 0.80 · over-confident · thin
0.90–1.00 9 calls right 9 · 100% (70–100) · claimed 0.95 · thin
abstained 2 inputs right 2 · 100% (34–100)
wrong at or over write 0.90 0 of 6 · 0 per thousand, at most 393 · thin
near write 0.90 at 0.80 6 run, 0 wrong · at 0.85 6, 0 · at 0.90 6, 0 · at 0.95 5, 0 · at 1.00 0, 0
wrong at or over whole 0.30 0 of 6 run · 0 per thousand, at most 393 · 1 of 7 held for a yes, 1 right · thin
each judgment on its own
route 12 judgments right 100% (76–100) · claimed 0.95
options 3 judgments right 67% (21–94) · claimed 0.90
pick 9 judgments right 100% (70–100) · claimed 0.95
Brier 0.066 · reliability 0.066 · resolution 0.090
misses
"make it darker in here" ask lights 0.80 · state: expected "dim", read "off"
the log · 3 decisions under replay · 1 ran · 1 confirmed and stopped · 1 abstained
stopped at confirm, by confidence 0.80–0.90 1
3 were records · right 2 · 67% (21–94)
thin: under 100 calls in every bin, nothing proven at any bar
A record typed in a language other than English adds a block, by language pack, with a line per language that read
one: how many inputs it read, and how many of their whole calls were right and wrong (Languages).
Line by line:
- The head counts the records, the reflexes they belong to and the distinct inputs, and says how many times each input was decided. Then how many inputs ran, confirmed, asked or abstained; an outcome none reached is left out.
- Whole call right, by confidence. One line per bin: its range, the calls in it, how many were right, that
share in percent with its 95 % interval, and the confidence the calls claimed on average, as a probability.
The bins' edges sit at the bars, 0.50, 0.80 and 0.90, so no bin straddles one; a range with a few hundred
calls splits into bins of about a hundred; a bin with no call is left out.
thinmarks a bin under a hundred calls: it is shown, and it proves nothing.over-confidentmarks a bin whose claim is above its interval. A record offalse, which says never this reflex, routed to a reflex no record names isunknown, counted apart. The abstains have a line of their own: an abstain is right on afalserecord, wrong on any other. - Wrong at or over a bar. For each effect with a call, the complete calls at or over its bar — an ask never runs, so it is not among them — how many were wrong, the rate per thousand, and the most it could be at 95 %. This is the number a script gates on. Under it, the neighbourhood: at the bar and two steps of 0.05 either side, how many calls would run and how many of those are wrong.
- Wrong at or over
whole. The calls over the bar of their effect are read back against the input (Outcomes). This line counts those the third bar lets run, how many of them were wrong, per thousand with its bound; then what the bar costs: how many it held for a yes, and how many of those were right. - Each judgment on its own: the route, then the arguments by the source of their values, options, a vocabulary, a pick or a flag, each judged by its own probability where a record names it.
- Brier, the mean squared distance between each claim and the truth, with its reliability, how far the
claims sit from the shares, and its resolution, how far the bins' shares sit from the whole;
--jsoncarries the uncertainty too. - The misses: each record a decision missed, what was decided, and where it missed, as
evoke testsays it. - The log: every line of
~/.local/state/evoke/log.jsonlunder this adapter, counted by what became of it: ran, confirmed and ran, confirmed and stopped, asked and stopped, abstained, failed. The confidence of what stopped at a confirm, by range. The lines whose input is a record, judged by it. - The last line says
thinwhile every bin is under a hundred calls. With a hundred calls in some bin, it says whether any such bin is over-confident at 95 %.
Nothing is said by colour alone: thin, unknown and over-confident are words.
What a record proves, and what the log does not
A record proves one call. An asserting record says whether the route and every argument it names are right; an
argument it does not name is not compared. A false record proves only not this reflex: routed to its own
reflex the call is wrong, abstained it is right, routed to a third reflex it proves nothing, unless another
record of the same sentence names that reflex. Examples are sent to the classifier, so a right example shows
less than a right test, which the classifier never saw. The report counts both.
The log's lines are weaker. A run that a person watched says nothing about the call. A confirm answered yes
says a person read the call and agreed. A confirm without a result says no, or no answer, or no terminal: the
line cannot tell. So the log's block counts what became of each decision, and never mixes into the bins above.
A line whose input is a record is judged by the record, which settles it.
--repeat
evoke calibrate --repeat 3 decides each input three times, a few at once, never through the cache. Every
number above still counts each input once, by its first decision: the repeats measure stability, not accuracy.
They add one line, and under it the inputs that moved most:
repeats 3 · winner flips 0 of 12 · verdict flips 0 · spread median 0.00, 90th 0.00, max 0.00 · straddling a bar 0 · wrong at or over a bar 0
A winner flip is an input whose route came out differently across repeats; a verdict flip, one whose call was right in some repeats and wrong in others. The spread is the range of the confidence over the repeats, per input. An input straddles a bar when some repeats sat under the bar of its effect and others at or over it. Wrong at or over a bar counts the calls, in any repeat, that were wrong there. Under the line, each input that moved: its confidence and route ranges, its winners and outcomes with their counts, and how many repeats were wrong. A miss says how many of the repeats missed the same way. On a recording the spread is nothing, as above; against a live engine, the report says how far the same sentence moves from one decision to the next.
Ten repeats over a thousand records is ten thousand requests: minutes, and the key's cost.
A recording, in CI
The report needs no key over a recording. Set adapter = "replay" in evoke.toml and the recording's path in
EVOKE_ANSWERS, as the SDK's Testing page shows, and the same
report runs offline, the same every time. That is how this manual's block above was made.
--json
evoke calibrate --json prints the report as one object: adapter, records, reflexes, inputs, repeats,
outcomes, bins, unknown, abstained, bars with read, write and whole, questions, brier,
misses, variance under --repeat, and log: The JSON line.
The exit
calibrate exits 1 when a call was wrong at or over its bar, in any repeat, or when a bin of a hundred calls is
over-confident at 95 %, with the reason on its last line:
1 call wrong at or over its bar → evoke calibrate
[1]
Otherwise it exits 0, however thin the bins. Nothing to calibrate, no active reflex with examples or tests, says so in one line. Nothing runs, and nothing is logged.
Next: Projects.
Projects
A project is a directory of files you own. evoke reads the nearest one and writes only inside it.
~/.config/evoke/ or <app root>/
├── evoke.toml yours: the adapter, the reflexes, their settings
├── evoke.lock written by add, update and remove: what exactly is installed
├── evoke.d.ts written with the lock: types for an app's decisions
├── overlays/lights.toml yours: your wording for one reflex, named by its local name
└── vocab/rooms.toml yours: your words
Which project
The CLI looks for the nearest evoke.toml, upward from the working directory. If there is none, it uses your
home project in ~/.config/evoke/ (or $XDG_CONFIG_HOME/evoke/). First use writes the home project with its
one line, adapter = "jev". Any project but home must be trusted before evoke decides in it.
The SDK takes an explicit root and never searches: Projects in code.
evoke.toml
adapter = "jev" # who decides; a name, never a path; always stated
[reflexes] # local name = where it comes from
lights = "radhi/home/lights" # unpinned: update moves it forward
timer = "radhi/timer@1.0.1" # pinned
hello = "./hello" # local, relative to this file: never locked
[config.lights] # what the reflex's [config] declares
bridge = "10.0.0.2" # a value, stored plain
token = { env = "HUE_TOKEN" } # a secret: the variable, never the value
[adapters.jev] # everything engine-specific, under the adapter's name
gate = { write = 0.95 }
- A local name matches
[a-z][a-z0-9_]*.none,unstated,fitsandweaveare reserved. The local name is what the classifier reads, and the overlay file is named after it. - The built-in adapters are
jev, TypeSafe AI's own API,openjev, the same model through OpenJEV, an independent service, andreplay, a recording for tests. The first two each read their own key and take the samegatetable under their own name: Install. - Two more adapters,
clefandclef_flash, reach Clef, Cloudflare's model, on its Workers AI service. They read a token and an account id, and ship no bars of their own: Thresholds. - An adapter name resolves only against
evoke's built-ins, never from the project directory. An adapter sees every input, so that matters. A table for an adapter you did not select is inert. So one file serves devices on different engines. - Edit the file by hand if you like.
evokevalidates it whole and names the line to fix. Its own writes keep your comments and order.
Safe for dotfiles
A project holds only what you wrote, the lock and generated types. No fetched code, no secrets, no cache. Commit
it. On a new machine, evoke sync rebuilds the store from the lock and records the runtime. Decisions run the same.
Machine-local state lives outside the project, under XDG. The store and the answer cache are in ~/.cache/evoke/.
The log, the trust file, the runtime path and the REPL's history are in ~/.local/state/evoke/. The full list: Environment.
An application's project
An app keeps its project at its root and reads it with the SDK. evoke add next to the app writes the same three
files, and evoke.d.ts types the app's decisions by reflex. Overlays and vocabularies under that root tune the
app's wording, just as your home project tunes yours. The SDK can also compile the same project over a tenant's
own vocabularies, with with({ vocab }). So a multi-tenant server keeps each tenant's words in their own plan.
Local reflexes
hello = "./hello" names a reflex directory next to the file. It can be an author's own, or one you keep out of
git. It is never locked. Its content is hashed at run, and its effect is what its manifest says. The store is not involved.
Trust, briefly
Trust binds a project's four owned paths to their content. evoke's own writes renew it. Any other change asks
for evoke trust again. Home is trusted by construction. Why this matters: Security.
Next: the Author section, or the Reference.
Your first reflex
A reflex is a directory: a manifest and the file it runs. evoke new writes a working one. evoke check keeps it
honest. A project next to it lets you try it before anyone installs it.
1. Make it
$ evoke new hello
+ hello/reflex.toml
+ hello/hello.mts
+ hello/reflex.d.ts
Three files, from a template that already works:
# hello/reflex.toml
reflex = 1
description = """
Say hello to someone.
The name is the text in quotes, "like this"; the greeting is printed and nothing changes."""
not_for = ["saying goodbye"]
effect = "read"
confirm = "Say hello to {who}?"
run = "hello.mts"
[args.who]
ask = "Who should be greeted?"
pick = "quoted"
[examples]
'say hello to "Ada"' = { who = "Ada" }
'greet "Grace"' = { who = "Grace" }
'a hello for "Edsger"' = { who = "Edsger" }
[tests]
'wave at "Linus"' = { who = "Linus" }
"say hello" = { who = false }
"bid everyone goodbye" = false
// hello/hello.mts
import type { Reflex } from "./reflex.d.ts"
export default (async ({ who }) => `Hello, ${who}!`) satisfies Reflex
reflex.d.ts is generated from the manifest. It holds Args, with who: string, and the Reflex type that
checks the body's arguments and its return. The body imports that one file and nothing else.
For a playbook, a reflex whose body is a plan of steps, evoke new --playbook <name> writes
reflex.toml alone:
$ evoke new --playbook drill
+ drill/reflex.toml
2. Check it
$ cd hello && evoke check
hello read runs hello.mts
evoke check reads the manifest and names every line to fix. It confirms that the file run names exists,
loads, and exports a function by default. It reports lint. When the arguments changed, it rewrites reflex.d.ts
and prints + reflex.d.ts. Inside a git repository with a version tag, it also diffs the contract against that
tag: Publishing.
$ evoke check
broken: run: "nothing.ts" must end in .mts or .mjs → ~/broken/reflex.toml:4:1
[3]
3. Try it
A reflex is decided on inside a project. Keep one next to your reflexes while you work: a file naming the
adapter, and evoke add ./hello writes the rest.
~/dev/
├── evoke.toml
└── hello/
# ~/dev/evoke.toml
adapter = "jev"
A project outside home must be trusted once. Edits to the reflex itself never need it again. Trust binds to the project's own files, not to the reflexes.
$ cd ~/dev && evoke trust
+ trusted ~/dev
$ cd ~/dev && evoke add ./hello
+ hello ./hello read runs hello.mts
$ cd ~/dev && evoke try 'say hi to "Ada"'
"say hi to \"Ada\""
which reflex hello 0.97 (none of them 0.03)
Say hello to someone.
who "Ada" 0.98 (not said 0.02)
holds all you said 0.90
→ runs: hello who="Ada" 0.97
a read runs at 0.80 or more, and its least sure judgment is which reflex, 0.97; it holds all you said at 0.90, and a call runs at 0.30 or more
replay answered 5 questions in 2 rounds
$ cd ~/dev && evoke 'wave at "Grace"'
hello who="Grace" 0.96
Hello, Grace!
evoke test hello runs the manifest's own examples and tests against the classifier, and reports every miss.
evoke run hello who=Ada calls the body without the classifier at all.
4. Make it yours
From here, three pages carry the rest: the manifest key by key, the four kinds of argument, and what a body receives and returns. Then the rules, which say how to write a manifest so that what people type reaches your reflex. And publishing, which is a git tag.
This page took you through the first steps of working on a reflex. Rule 45 lists every step, in the order that finds each fault first. Read your reflex against the checklist before its first tag.
Next: The manifest.
The manifest
reflex.toml is what the classifier reads and what the body receives. It has no name and no version. A
reflex's name is whatever each user installs it as. Its version is the git tag it was fetched at.
This page says what each key is and what evoke does with it. Rules for reflexes, playbooks and
collections says how to write each one so that what people type reaches the right reflex.
The whole shape
reflex = 1 # the format's version; frozen
description = """
Turn the lights in one room on, off, or dim them.
Ceiling and lamp lights, through the Hue bridge; their colours and schedules stay as they are."""
not_for = ["colour scenes and schedules", "asking whether a light is on"]
tags = ["home", "lighting"]
effect = "write" # always written; absent means destructive
confirm = "Set the {room} lights {state}?"
run = "lights.mts" # or argv: ["hue", "set", "{room}", "{state}"]
[needs] # what the body touches; absent means its own directory and TMPDIR
hosts = ["*"] # the bridge is on the network
[config]
bridge = "Hue bridge address"
token = { about = "Hue API key", secret = true }
[args.room]
ask = "Which room of the house?"
vocab = "rooms" # options come from the user
[args.state]
ask = "What should the lights do?"
options.on = "Switch on." # options come from the author
options.off = "Switch off."
options.dim = "Lower the brightness without switching off."
[args.brightness]
ask = "How bright, in percent?"
pick = "number" # options come from the input
range = [1, 100]
optional = true
[yields] # what the returned data holds, for a later step
level = "number"
[examples] # sent to the classifier
"turn on the kitchen lights" = { state = "on" }
"dim the office to 30 percent" = { state = "dim", brightness = "30 percent" }
"bedroom lights off" = { state = "off" }
[tests] # held out, never sent
"make it darker in here" = { state = "dim", brightness = false }
"flip the hall lights" = { state = false }
"is the porch light still on" = false
"set the lounge to a sunset scene" = false
"light a candle" = false
After reflex = 1, the keys are written in the order evoke show prints them, which is the order of the table
below. Within an argument, ask comes first and its source next, then range, recent, optional and was.
The arguments with takes come after the others. In TOML a plain key written below a table header belongs to
that table, so the plain keys come first: rule 36.
Key by key
| Key | Required | Meaning |
|---|---|---|
reflex | yes | 1. The format is frozen. Keys are never removed or given new meanings, so every manifest ever tagged stays readable |
description | yes | What the reflex does. The first line is the summary, never empty. The classifier reads all of it, not the summary alone. The summary says the action and what it acts on, and the next line draws the boundary: rule 11 |
not_for | no | What it does not do, one line per entry. It is read as the no side of "does this reflex do what was asked?". It names the near neighbours: rule 13 |
tags | no | Words for --tag to narrow a decision by. No other meaning. [a-z][a-z0-9_]* |
effect | always | read, write or destructive. It sets how sure evoke must be before a call runs unasked, and a destructive call asks for a yes every time. Left out, it still means destructive, and lint reports effect is absent, which means destructive; write it. It is the author's claim, and not trusted: a user may tighten it, never loosen it. A reflex has one effect. Claim the one the action has: rule 33 |
confirm | yes | The one-line question a person answers. A {placeholder} names a required argument. A pick shows its span. evoke check refuses a placeholder for an optional argument, a flag or a taken result. The confirm reads the call back: rule 23 |
run | one of | The body: a path ending in .mts or .mjs inside the directory, or an argv. The body |
steps | one of | A plan in place of a body: one sentence per step, {slot} for an argument, [ words with a {slot}] for an optional one, { say = "…", when = { field = "value" } } for a step that runs only under what the step before it yields; at most 24. None of a body's keys with it. Playbooks |
platforms | no | Where the body runs, when not anywhere: ["macos"], ["linux"], or both. On another machine the reflex is inactive, and add and show say so: runs on macOS only |
returns | no | The name of what the body's data is, whole, for a later step to take: returns = "deploys". Not with an argv run. A result another step takes whole |
[needs] | no | What the body touches, held by the kernel: reads, writes, hosts, runs. Left out, the tightest declaration: its own directory and TMPDIR. What the body touches |
[config] | no | Settings the user provides with evoke config: key = "about" or key = { about, secret = true }. A secret is only ever set from an environment variable |
[args.<name>] | no | The arguments: an ask and exactly one source, or takes alone. Arguments |
[yields] | no | What the body's data holds, for a later step to take (Weaving) or for a pick's recent to recall (Arguments): per field, the recognizer that reads it, number, duration, email, url, quoted, date, time, amount or code; or { each = { … } } for a list of records. Declare each field a later step takes, by the noun a person would type for it: rules 30 and 31 |
[examples] | no | Utterances with what they assert, sent to the classifier. Examples and tests |
[tests] | no | The same shape, held out: never sent, run by evoke test |
A result another step takes whole
A body may name what it returns: returns = "deploys". The name says what the body's data is, as a whole. A
later step whose argument has takes = "deploys" receives that data as the body returned it
(Arguments). Two reflexes meet when their names are equal, as two arguments
share a vocabulary by naming it. Give each whole result one noun and one shape across the collection:
rule 32. A reflex never returns a name it takes: a step that refines a result
returns a new name, so a later step has one source. An argv body returns no data, so a reflex whose run is an
argv names none, and declares no [yields] either.
What the body touches
[needs]
reads = ["{place}"] # the value of an argument or a config key
writes = ["~/Downloads", "{to}"] # under the home; or absolute, like "/tmp/out"
hosts = ["*"] # the network, all of it; absent means none
runs = ["open", "/usr/bin/plutil"] # by name on PATH, or by absolute path
Each key is a list, and leaving the table out is the tightest declaration: the body's own directory and its
private TMPDIR, nothing else. reads and writes name paths: ~/… under the home, absolute, or {name} for
the value of an argument or a config key, so a folder the user names reaches the body and nothing beside it. A
path names a file, or a folder with everything under it, whichever is on the disk when the body runs. A path in
writes may be read too. A declared path must exist when the body runs. A body that makes a file declares
its folder, and a path the machine lacks stops the run before the body starts: [needs] reads names ~/nowhere, which is not there; a folder in writes ends in the mkdir -p that makes it. hosts is all or nothing:
["*"] reaches every host, absent reaches none, and a name is refused at check. runs names programs, by name
on PATH or by absolute path; each of them runs held to the same declaration, and what it asks the system to do
happens outside it. The kernel holds the body to all of this: Security.
A {name} names an argument that carries a value, a word's or a typed one, or a config key; evoke check
refuses one that names neither, or a flag. An entry over an optional argument left unstated is dropped. A value
that is not a path, a word whose value is a name rather than a folder, refuses the run and points at the value's
source: [needs] writes names {to}, whose value "desk" is not a path → evoke vocab places add desk "<meaning>" --value <path>.
A reach past the declaration ends the run with what was reached and the key, ~/secret.txt is not in [needs] reads, and a fix by where the declaration is written: this manifest's line for a local reflex; for a fetched
one, evoke update --accept when upstream's declaration already allows it, else evoke remove. [needs] is
contract: an overlay cannot touch it; upstream may narrow it at any tag, which is same; widening waits for the
user's evoke update --accept, and is minor: Publishing.
Text rules
- Every string is clean text: no control characters, except the line feed in
description, and no bidi controls.evokerefuses the rest, so a manifest can never repaint a terminal. - Names of arguments, tags, config keys and vocabularies match
[a-z][a-z0-9_]*. An argument name is also never a JavaScript reserved word, so a body can destructure it. - Option keys are one clean line each.
noneandunstatedare reserved. - Unknown keys are reported by
evoke check, never fatal:unknown lights: warmth is not a key this evoke reads. The schema athttps://evoke.build/schemas/reflex.jsonis strict, so an editor flags a typo beforeevoke checkdoes.
What is contract, what is wording
| Contract: a user cannot override it, and changing it is a version bump | Wording: a user may override it, and you may improve it at any tag |
|---|---|
run or steps; platforms; [needs]; argument names and their sources; option keys; range; recent; config keys; yields; returns; what an argument takes | description, not_for, tags, confirm; every ask; the meaning of each option; examples and tests |
An argument may be renamed by declaring its former names, like was = ["state"]. Every user's overlay and call
then follows. Publishing says how evoke check diffs one tag against the next.
Lint
At evoke check and evoke add, lint reports and never refuses. It flags a text or a list longer than its cap,
a phrase that addresses a model, a step a plan would misread, and what a reader of the file would miss, like
effect left out or fewer than three examples. Each finding names the key, and the text is yours to weigh.
Diagnostics lists every line.
Next: Arguments.
Arguments
An argument is a value the body receives. Most are also a question the classifier answers about the input: each
of those has an ask, and exactly one source for its values: the author, the user, or the input. An
argument with takes has a fifth source, an earlier step's result, and no ask: nothing is asked.
[args.state]
ask = "What should the lights do?" # the question a person would be asked; sent verbatim
options.on = "Switch on." # the author's closed set
options.off = "Switch off."
[args.room]
ask = "Which room of the house?"
vocab = "rooms" # the user's closed set: vocab/rooms.toml
[args.brightness]
ask = "How bright, in percent?"
pick = "number" # a span of the input, read by a built-in recognizer
range = [1, 100]
optional = true
[args.clipboard]
ask = "To the clipboard instead of a file?"
flag = true # a yes/no switch
The five sources
| Source | Values come from | The body receives | Asked when missing |
|---|---|---|---|
options | The author: key = "meaning" | The key | A numbered choice |
vocab | The user: vocab/<name>.toml | The word's value if set, else the word | A numbered choice, plus [+] add one |
pick | The input, as typed | number → the number; duration → whole seconds; email, url, quoted, code → the text; date → the day, 2026-05-05; time → 17:30; amount → { amount, currency } | Typed freely, read by the recognizer; a recent field's values numbered before it |
flag | The input | true, or nothing | Never |
takes | An earlier step of the same request, whose reflex returns that name | That step's data, as it returned it | Never: the request stops before anything runs |
A value from an open set is a pick, a switch is a flag, options are for a closed set, and a value only the user knows is a vocabulary: rule 4 and rules 19 and 20.
ask is the question a person would be asked. The same line serves the classifier and the prompt. Each ask is
one question, as a person would ask it: rule 17.
optional = true means an unstated argument is omitted and the body's own default applies. A required
argument left unstated makes evoke ask. A flag is optional by nature and takes no optional.
Options
The author's closed set. The key is what the body receives and what a call shows, as in state="off". The
meaning is what the classifier reads. Keys are one clean line each, never none or unstated. Examples that
assert an option teach it: "kill the lights" = { state = "off" }. A key is a word a person knows at a glance,
and its meaning answers the ask in one short line: rule 18.
Vocabularies
The user's closed set, by name. A reflex that names a vocabulary, vocab = "<name>", reads the words the user put
in vocab/<name>.toml. A package never ships or writes one. An empty vocabulary makes the reflex inactive until a
word is there, when the argument is required. An optional argument over an empty vocabulary is never asked and
never stated, and the reflex stays active. A word's value is for the body only: a path, a URL, a device id.
Reflexes share a list only by naming it alike, so each kind of thing has one published name:
Vocabulary names.
A shipped manifest may not assert a vocabulary argument in its records, not even as unstated. The words are not yours to know. A user's own overlay may.
Picks
A pick reads a piece of the input, word for word. Nine recognizers exist:
pick | Recognizes | Value | range |
|---|---|---|---|
number | A number in digits or in words up to the hundreds, unit words kept in the span: 30 percent, fifty percent, twenty-five, two hundred and forty; a leading minus is the number's: -5 | The number | yes |
duration | One number and one unit, the number in digits or in words: 10 minutes, 2 hours, twenty five minutes; an hour, half an hour, a quarter of an hour, an hour and a half, two and a half hours. A length across units, 1 hour 30 minutes, is read from a sentence as a value said aloud, and at a prompt or in a call by name as it is typed | Whole seconds | yes |
email | An address | The text | no |
url | A URL | The text | no |
quoted | "…", “…” or ‘…’; a straight single quote is an apostrophe | The text between the quotes | no |
date | A day: today, tomorrow, tonight, yesterday, the day after tomorrow; a weekday, alone or after next, this or last; a day of the month: on the 14th, friday the 14th; a month and a day, with a year or not: may fifth, March 6th 2017, the 22nd of march; in two weeks, three days from now; 2026-05-05; 27/03/2017 when one order alone is possible. Never a period, a month or a year alone, every monday or now | The day, 2026-05-05, on your machine's calendar when the program runs | no |
time | A clock time with its half of the day: 5pm, 7 a.m., 5:30 pm, seven thirty am, six in the morning, 4 o'clock in the afternoon, noon, midnight; or a two-digit hour on the 24-hour clock: 13:00, 09:30. An hour word alone is a number: at three. 5:30 and 4 o'clock with no half read as nothing at all. Type 3pm, 15:00 or 05:30. in ten minutes is a duration | The time, 17:30 | no |
amount | A figure or number words with a currency: $30, €1,200, £19.99, 50 dollars, twelve hundred euros, 1000 USD. $ is the US dollar. A currency alone is nothing; pound after a count is a weight, pounds is money | The number and the currency's code, { amount, currency } | no |
code | An identifier as typed, in capitals or small letters: a version 4.12.0, a ticket INC-311, a flight tp1043, a serial C02XK1ABJG5M; quoted or not. order 4821 is a number, 10mins a duration | The text | no |
- The classifier chooses among the candidates found. It never invents one. A pick with no candidate reads as
unstated. If it is required,
evokeasks for it. - A text typed without quotes is read for a
quotedargument where the classifier says the input states one: in set a timer for 10 minutes called tea, the label istea. The text is a run of the input's words, copied, and the call confirms. - A value spelled out is read as it is typed, where the classifier says it is meant: an address, a URL, a code,
a number, a day or a length of time said aloud,
dana dot weiss at example dot org,five dot oh dot two,an hour and twenty minutes; a weekday misspelt,wendesday; a code typed with a space,hs 0409. The call shows the typed value. After a misspelt day or a spaced code it confirms, with your words on the line (Outcomes). - A pick's examples set its shape. The values your examples state for a
codeor anumberargument tellevokehow a value said aloud is written: where every example reads likeHS-0409,k d oh three one fivebecomesKD-0315, and the call confirms. Where the examples differ in case, the call confirms too. Where they leave the length open, or the words read two ways,evokeasks and offers what the words read as. A code argument with no example value is read only as it is typed. So give each pick argument examples that show its real forms (Saying things). - Text people type freely is never a
quotedargument, and the body reads it frominput: rule 21. - Quotes hide what they enclose. A typed span hides the bare numbers inside it. In timer for 3 minutes called
"eggs", the duration is
3 minutes, the quoted text iseggs, and3alone is never a number. A date, a time, an amount or a code hides its figures the same way: in pay €1,200 on may fifth at 5pm no bare number is a candidate. - A span that a recognizer reads and no argument takes is left to the words around it: where they ask for
another thing, or the call holds less than was typed, the call stops at confirm
(Outcomes). A vocabulary word that the person typed is never such a span,
even where a recognizer reads it too:
tomorrowin a vocabulary of days is the word, not a date. - A date is resolved when the program runs, against your machine's clock in its own time zone, or against
EVOKE_TODAYwhen it is set (Environment). Before that, everywhereevokeshows the value, it shows your words; in a plan file,tomorrowmeans the tomorrow of the run. range = [min, max]compares the value, in seconds for a duration. It is allowed onnumberanddurationonly. Out of range,evokeasks, with the reason:150 percent is outside 0–100.- In a
confirmtemplate, the placeholder shows the span. In an argv, it shows the value's text; an amount cannot stand in an argv.
A value recalled
[args.release]
ask = "From which release?"
pick = "code"
recent = "release" # what earlier results of this session hold under that field
A pick may name a field of [yields] with recent. When the words leave the argument unstated, evoke lists
the values the bodies of this session returned under that field, newest first and five at most, each one the
recognizer reads whole, and asks:
> list the checkout deploys
deploys service="checkout" 0.95
checkout: 2 deploys today, the last 4.12.0 at 13:58
> roll back the last deploy
From which release? [1] 4.12.0 [2] 4.11.3 > 1
rollback release="4.12.0" · destructive · weakest: route 0.95
it cannot be undone, so it always waits for a yes
Roll back 4.12.0? [y]es [n]o [t]each > y
A number picks one; anything else is read as typed. A number pick names the values as hints instead, since a
number typed is its own answer. Words that point at one of the values take it without a question: it, that order, this one, the last order, my latest order, the most recent order, with the argument's name or
the field's as the noun. After a product is shown, buy it buys it; after two orders, pay that order pays the
newest, and pay the last 2 orders, those two orders or both orders pay the two newest, one step each.
evoke why names the words and the value's place: the newest of this session's results, named by "it". A
number before the plural of the argument's name, 2 orders, is a count and never the value; where the call
still has to ask, the prompt names those words. the order, them and a bare count point at nothing, and the
ask lists the values as above. The session is one process: the REPL and the stdin filter remember every result,
a one-shot call remembers none, and in the SDK an application hands the results it keeps. A sentence of several
steps takes nothing from memory: its second step asks as it always did. The classifier is never asked about a
recalled value; the words or you choose, and evoke why names what was offered. recent is contract, beside a
pick only.
Flags
flag = true is a yes/no switch. evoke reads the classifier's answer as yes at or above one half. The body
receives true, or nothing. A flag never appears in a confirm template, and it cannot be an argv element. A body
that needs one is a file. In records, a flag is asserted as { clipboard = true }.
A whole result taken
[args.deploys]
takes = "deploys" # an earlier step's whole result, by the name its manifest returns
A takes argument is required, and stands alone: no ask, no optional, no range, no was. Its reflex runs
only after a step that returns the name, in one sentence (Weaving):
alone, when no step before it returns the name, or when two do, the request stops before anything runs and says
which. The argument's own name is the author's; the plan and evoke why print the result's. The body receives the
data as its source returned it, and checks its shape itself: reflex.d.ts types it unknown. A confirm cannot
show a whole result, so a reflex that runs destructive takes none: it takes what it acts on as a field of
[yields], which its confirm names. evoke run cannot give a takes argument, and no sentence states one.
Names and renames
An argument name matches [a-z][a-z0-9_]*. It is never a JavaScript reserved word, like for, class or
default, so a body can destructure it. When one kind of value plays two roles, each role is an argument of its
own, named by the role: rule 3. To rename an argument, declare the old name
and keep it declared forever:
[args.power]
ask = "What should the lights do?"
was = ["state"]
Every user's overlay and every call written with the old name follows the rename at merge time. was is flat
and cumulative. A retired name never returns as a live argument, and it never leaves the list. evoke check
enforces both against the previous tag. update tells each user that their records followed.
In the confirm line and in an argv
confirm = "Set the {room} lights {state}?" names required arguments only, never one with takes. The confirm
reads the call back: rule 23. In an argv run, a placeholder is a whole
element, as in ["hue", "set", "{room}", "{state}"]. It names an options, vocab or pick argument; a date
stands as 2026-05-05, a time as 17:30. An element whose optional argument is unstated is dropped. Literal
braces, flags, an amount and a taken result need a file body.
Next: The body.
The body
The body is what run names. It is either a JavaScript file with a function as its default export, or a program
with its arguments. evoke calls it once per decision, with the arguments filled in, a context, and a deadline.
A file body
run = "lights.mts"
import type { Reflex } from "./reflex.d.ts" // Args, Config, Context, Result, Reflex — written by evoke check
export default (async ({ room, state, brightness = 30 }, { config, signal }) => {
await hue(config, room, { on: state !== "off", bri: state === "dim" ? brightness : 100 }, signal)
return `${room} lights ${state}`
}) satisfies Reflex
- The file ends in
.mtsor.mjsand lives inside the reflex directory. It is a module, whatever a nearbypackage.jsonsays. It runs on Node 24 or newer, which strips types on its own. There is no build step. - It is self-contained. Web APIs and
node:builtins are there, andevokeinstalls no package for it. The body is one file, with anything else bundled into it: rule 44. satisfies Reflextypes the arguments from the manifest and checks the return.evoke checkwritesreflex.d.tsnext to the body whenever the arguments change. Commit it.
What the body receives
(args: Args, context: { input: string; config: Config; signal: AbortSignal })
| Value | Is |
|---|---|
args | Per argument: an option's key; a vocabulary word's value, or the word; a pick's value, meaning the number, the seconds, or the text; true for a flag; for an argument with takes, the whole data of the earlier step that returned it, as it was returned. An optional argument left unstated is absent |
input | The person's own words, for a typed sentence. When a plan runs the reflex as a step, the step's words. Empty when evoke run calls it by name. Free text, a reminder's message, is read from here: rule 21 |
config | Each [config] key as a string, secrets resolved from the environment for this run only |
signal | Aborted at the deadline, on Ctrl-C or SIGTERM, when an app cancels the run, and when evoke ends. The body then stops: rule 44 |
What it returns
A string: the text a person reads. Or { text, data? }, where data is anything an app might use: a pid, a
time, a path. A field of data that a later step of one request may take is declared under [yields] in the
manifest, and the whole of data, under a name, with returns:
The manifest.
Throwing is failure: the message prints on one line, and the exit code is 1. Anything else returned is a failure
too. A thrown error's frames are kept for its author: after a typed sentence, evoke why shows them under its
decision, and --json carries them as frames.
The text says what happened, with the value that lets a person check it: rule 35. A failure says what went wrong, in the body's own words: rule 44.
The run
- Environment. Scrubbed:
PATH,HOME,TMPDIR,LANGandTERM, nothing else.TMPDIRis a private folder, made for the run and removed after it. Secrets reach the body throughconfig, never the environment. - Reach. The body touches what
[needs]declares, and nothing else: The manifest. Its own directory andTMPDIRare always there. A read past the declaration throws in the body, and the run ends with what was reached and the key,~/secret.txt is not in [needs] reads, and the fix the manifest page gives by where the declaration is written. A word's value or a plain setting that begins with~/reaches the body as a path under the home, inconfig, in an argv and inEVOKE_CONFIG_<KEY>, so the value, the argv and the declaration name one path. - Deadline. 30 seconds per decision, shared with the classifier's answer. At the deadline
signalaborts. A body still running a second later fails. What must outlive the run, like a timer orcaffeinate, detaches: rule 44. - Output. The result is what the function returns. The body's own
stdoutandconsolego to stderr, so a strayconsole.lognever corrupts a result. - Process. The runtime starts once the decision is made, in the body's directory, held by the kernel. Its
life is bounded by
evoke's. OnCtrl-C, and for a body that will not end, the process group is ended:SIGTERM, a short grace, thenSIGKILL. - Platform. On a system
platformsleaves out, the reflex is inactive, andaddandshowsay so:runs on macOS only. When the body runs on one system alone, name it inplatforms: rule 43.
An argv body
run = ["networksetup", "-setairportpower", "en0", "{state}"]
A program and its arguments, run directly, never through a shell. The first element is a literal: the program.
It is a name found on PATH, or an absolute path. It is never a path relative to wherever evoke runs. A
placeholder is a whole element. It names an options, vocab or pick argument, and is replaced by the option
key, the word's value or the word, or the pick's value: the number, the seconds, or the text. A date stands as
2026-05-05, a time as 17:30. An element whose optional argument is unstated is dropped. A value that would
start with - is refused. Config arrives as EVOKE_CONFIG_<KEY>, and the input as EVOKE_INPUT. Stdout is the
result. A non-zero exit is failure. No runtime is needed. The program is the body, so it runs without being named
in runs, held to what [needs] declares.
Flags, an amount, a taken result and literal braces cannot be expressed in an argv. Write a file instead.
The generated types
// reflex.d.ts — generated by evoke check; do not edit.
export interface Args {
/** How long? */
duration: number
/** What is the timer for? */
label?: string
}
export interface Config {}
export interface Context { input: string; config: Config; signal: AbortSignal }
export type Result = string | { text: string; data?: unknown }
export type Reflex = (args: Args, context: Context) => Result | Promise<Result>
Option keys become a union: "on" | "off" | "dim". A word, a quoted text, an address, a URL and a code are
string. A number and a duration are number. A date is a string, 2026-05-05, resolved against the day the
body runs; a time a string, 17:30; an amount { amount: number; currency: string }. A flag is true. A taken
result is unknown, since evoke checks no shape.
An optional argument is marked ?. Each member carries its ask or about, so an editor's hover shows the
question. The file imports nothing.
Testing a body
A body is a function. Import it and call it. The collection's own tests do exactly that, under node --test,
with a context built by hand: { input: "", config: { file }, signal: new AbortController().signal }. Nothing of
evoke is needed to unit-test a reflex.
Next: Playbooks.
Playbooks
A playbook is a reflex whose body is a plan: steps, one sentence per step, and no run. A person reaches it
with a short sentence that states the situation they face: «checkout is failing in eu-west». That sentence is
routed by your description, not_for and examples, like any request. Each step is then decided over what that
person has installed, as a typed sentence would be, and the whole plan prints before anything runs. The
playbook's summary and examples state the situation, and its not_for names the steps typed alone:
rules 5 to 7.
# outage/reflex.toml
reflex = 1
description = """
Handle a service that is down or failing.
Checks its errors, deploys and logs, finds the release behind the failure, rolls it back, tells the incident channel and updates the status page; the rollback asks again before it runs."""
not_for = [
"a service's error rate on its own",
"listing a service's deploys on its own",
"pulling a service's logs on its own",
"rolling back one release on its own",
"posting to a channel or updating the status page on its own",
"writing up an incident's postmortem",
"asking whether a service is healthy",
"a slow service that still answers",
]
tags = ["outage"]
effect = "destructive"
confirm = "Run the outage plan for {service}?"
steps = [
"check {service}'s errors[ in {region}]",
"list {service}'s deploys[ in {region}]",
"search {service}'s logs[ in {region}]",
"find the release behind it",
"roll {service} back from that release[ in {region}]",
"post to the incident channel",
"update the status page for {service}",
]
[args.service]
ask = "Which service is down?"
vocab = "services"
[args.region]
ask = "In which region?"
vocab = "regions"
optional = true
[examples]
"payments is down in us-east" = {}
"search is broken" = {}
"we have an outage on checkout" = {}
"checkout is on fire in eu-west" = {}
"payments stopped working" = {}
[tests]
"checkout is failing in eu-west" = {}
"we lost search in us-east" = {}
"how bad are the errors on payments right now" = false
"downgrade search from 6.0.1" = false
"ping #ops that checkout recovered" = false
"flip the status page to resolved for search" = false
"prepare a retro on the checkout outage" = false
"is the search service doing fine" = false
"checkout feels sluggish this morning" = false
$ evoke "checkout is failing in eu-west"
1 errors service="checkout" region="eu-west" 0.95 · outage 1
2 deploys service="checkout" region="eu-west" 0.95 · outage 2
3 logs service="checkout" region="eu-west" 0.95 · outage 3
4 suspect 0.95 · takes errors from 1, deploys from 2, logs from 3 · outage 4
5 rollback service="checkout" region="eu-west" · takes release from 4 · outage 5
6 post channel="#incident" 0.95 · outage 6
7 status component="checkout" 0.95 · outage 7 · service as component
outage service="checkout" region="eu-west" · destructive · weakest: route 0.95
Run the outage plan for checkout? [y]es [n]o [t]each > y
Every line of the plan ends with the playbook and the step that wrote it. Then comes the sentence's own line, the decision that picked the playbook, and one question over the whole plan. A step that takes a result waits for the steps it takes from, as in any sentence of several steps (Weaving).
A plan of sentences
A playbook is fetched, locked, tested, overlaid and shown like any reflex. What differs is the body: steps
instead of run, and none of a body's keys, [needs], [config], [yields], returns, platforms or an
argument's takes. evoke check refuses each of them. A plan holds at most 24 steps. It lists the situation's
steps in the order they happen: rule 8. Its effect is your claim about
the whole; add says when a step reaches a tighter one. The claim is the worst effect its steps reach:
rule 10.
evoke new --playbook <name> writes one to start from: <name>/reflex.toml alone, a plan of three steps from the
template, which checks clean.
Slots
A slot, {service}, is an argument of [args]: a vocabulary word, an option or a pick, never a flag. It is
filled from the sentence, or asked before the plan prints, and written into the step as the person typed it, an
option as its key. A word beyond the slots fills nothing. A value is never the playbook's own: a word the person
lacks joins their vocabulary with [+] add one.
$ evoke "we have an outage"
1 outage · asks service
Which service is down? [1] checkout [2] payments [3] search [+] add one > 2
1 errors service="payments" 0.95 · outage 1
…
Words in square brackets go only with the slot inside them: [ in {region}] is written when a region was stated
and dropped when it was not. An optional argument stands only in a bracket, and a required one never does:
evoke check refuses a slot in the wrong place. It also refuses a slot that is a flag or no argument at all, and
an argument that no step names. A slot goes in every step whose reflex reads its value:
rules 25 and 26.
Every step is decided when the plan is made
A step is decided by the classifier over what the person has installed, from its words alone, as a typed
sentence would be. So each step is one action, worded as a person would type it:
rules 24 to 29. evoke check refuses a step that reads as a call with a value,
errors service={service}. Lint reports a step that holds a connective or a comma, states a channel or an
address, or opens with check that. It also reports a step whose words are all the summary's or an example's.
Diagnostics lists each line.
A step may reach another playbook, once: a plan inside a plan inside a plan is refused. A step that reaches the playbook it stands in is refused too. A step no reflex matches refuses the whole plan before anything runs:
7 "update the status page for checkout" · no reflex · outage 7
[2]
evoke add shows what each step reaches on the set it joins, and evoke test fails a step that reaches
nothing (Examples and tests).
One yes over the whole plan
Nothing runs before the whole plan is printed and one yes is given over it: your confirm when the plan is your
playbook's alone, Run the plan as it stands? when the sentence holds more. A step's own confirm, a destructive
one's above all, is asked again at its turn, every time. [t]each at the whole-plan question teaches the route
to your playbook; at a step's turn it teaches the step's reflex from the filled sentence.
A result picks among the steps you listed
A step may run only under a value an earlier step yields. Write it as a table, say the sentence and when
one field with the value it runs under, as text, and list the alternatives together right after the step
whose result picks among them:
steps = [
"drain connections off the primary",
"fail over to the replica",
"check whether writes land on the new primary",
{ say = "tell the ops channel the failover is done", when = { landing = "yes" } },
{ say = "fail back to the old primary", when = { landing = "no" } },
]
A result never writes a step: it picks among the steps you listed, and the whole list prints before anything
runs. The field is one that step's reflex declares under [yields] as one value, never a list. The plan refuses
before anything runs when the step reaches a reflex that yields no such field, or the value is one the field
cannot hold. evoke check refuses a when on step 1, where no step comes before it, and a when on two fields.
The step's line ends then 4 on "yes", 5 on "no", and each alternative's if 3 yields landing "yes". At that
step's turn the result is compared with each value as text. The step it picks runs, the others are skipped clean
and say so at the end, and a value no step lists picks nothing, the plan going on. A step without when runs
either way. A step that may not run is one step: a later step cannot take its result, and its words may not
reach another playbook. A check whose result picks is a reflex of its own that yields one field:
rule 9.
$ evoke "drain the primary"
1 drain 0.95 · runbook 1
2 failover · destructive · weakest: route 0.95 · runbook 2
it cannot be undone, so it always waits for a yes
3 verify 0.95 · runbook 3 · then 4 on "yes", 5 on "no"
4 tell channel="#ops" 0.95 · runbook 4 · if 3 yields landing "yes"
5 failback · destructive · weakest: route 0.95 · runbook 5 · if 3 yields landing "no"
it cannot be undone, so it always waits for a yes
runbook · destructive · weakest: route 0.95
it cannot be undone, so it always waits for a yes
Run the failover runbook? [y]es [n]o [t]each > y
primary drained
2 failover · destructive · weakest: route 0.95
it cannot be undone, so it always waits for a yes
Promote the replica now? [y]es [n]o [t]each > y
replica promoted
writes landing on the new primary
told #ops
5 failback · destructive · weakest: route 0.95 · runbook 5 · if 3 yields landing "no" · skipped · not chosen: step 3 yielded landing "yes"
it cannot be undone, so it always waits for a yes
What the person adds to the sentence
A part of the sentence that repeats a step of the plan folds into that step, and the plan says so:
folded "show me the error rate" into 1. A part that states the situation a second time, «payments keep timing
out, customers can't pay», folds into the plan the first part opened, so the plan holds each step once. Its
folded line names that plan's first step: folded "customers can't pay" into 1. A part that says what not to
do beside a plan is refused, and so is a part that opens with a condition, if, unless or in case. evoke
judges no condition, so the line it prints says to ask for the check first, then what to do.
What is contract
steps is contract: an overlay may reword the description, the confirm and the asks, never a step. A step
added, removed, reworded or reordered is a major version, and evoke check says so: steps the steps moved.
A slot added or renamed rewords a step, so it is major too, and so is a when added, moved or changed.
add, test and show
add decides every step of a newcomer playbook over the set it joins, runs nothing, and prints what each
reaches under its row; a step that reaches nothing installs and is reported, since the plan refuses when it is
made. Two lines more may follow. claims write; its steps reach destructive says a step reaches a tighter effect
than the playbook claims. step 3 reaches logs, which lacks the tag outage says a request narrowed with
--tag outage would leave that step without its reflex. Every reflex the steps reach carries the playbook's
tag: rule 40. add also prints also fits for the examples of the
reflexes the steps reach, since the plan does hold their action. Typed alone, such a sentence still reaches its
own reflex: Exceptions.
test decides the playbook's records as any reflex's, then its steps, each filled from the first record whose
reading fills every slot it holds; a step no record fills is untested. Both say when the step before a branch
reaches a reflex that yields no field the branch waits on, and test reports a claim the steps pass too. show
prints the steps after confirm, numbered, a step that may not run ending in its when.
$ evoke test outage
outage 16 passed · 7 steps route
In the SDK
playbook(manifest) hands a playbook as code, steps in its manifest and no body; project.steps(input, { ask }) answers a slot the sentence lacks before the plan is made (Decisions). A playbook
is never called by name: run, handle and evoke run <name> refuse it with say it in a sentence.
Next: Examples and tests.
Examples and tests
Two tables, one shape. [examples] are sent to the classifier: they teach. [tests] are held out, never sent:
they check. One line is documentation, tuning and test at once:
"kill the lights" = { state = "off" }
A user's overlay adds lines of the same shape.
The shape
[examples]
"turn on the kitchen lights" = { state = "on" } # asserts the route and one argument
"lock the screen" = {} # asserts the route alone
'timer for 3 minutes called "eggs"' = { duration = "3 minutes", label = "eggs" }
"grab part of the screen to the clipboard" = { area = "selection", clipboard = true }
"take a screenshot" = { area = false } # asserts that area is unstated
[tests]
"make it darker in here" = { state = "dim" }
"light a candle" = false # never this reflex
| Assertion | Means |
|---|---|
{} | The input routes here; nothing said about arguments |
{ arg = "key" } | An option: its key |
{ arg = "span" } | A pick: the exact span, which must occur in the utterance. For quoted, the text between the quotes |
{ arg = false } | The argument is unstated |
{ flag = true } | The flag is raised |
false | Never this reflex. Such an example teaches the no side, alongside not_for |
An argument a record does not name is not asserted. A shipped manifest may not assert a vocab argument,
because the words are the user's. A user's overlay may.
Identity
Records are keyed by the utterance as written, and the classifier reads that spelling. Two records can still be
the same utterance under identity. Identity means NFC, lower-cased, whitespace collapsed and trimmed, and a
trailing . ! ? or … dropped. Such records are one record, and duplicates in one file are refused. A
user's line naming a shipped utterance replaces it. So the highest layer decides each utterance's table and value.
Which records to write
The rules say which records to write, and why:
- At least three examples, as people type the request: rule 14.
- Each near neighbour, a sentence that sounds like the reflex's request and is not, as a
falsetest in words no example uses: rule 15. - For each option or pick argument, one record that leaves it out,
{ area = false }: rule 15. - No record that opens with a negation or a condition: rule 15.
- For a playbook, records that state the situation it handles, never one of its steps: rule 6.
evoke test
$ evoke test
lights 3 passed · 1 failed
"make it darker in here" state: expected "dim", read "off"
timer 8 passed
"ping me in 2 hours" passes, but would wait for a yes: the call holds all of it at 0.20, and a call runs at 0.30 or more
1 of 12 cases failed → evoke test
[1]
Every example and every test of every active reflex is decided over the whole installed set. The classifier sees the other reflexes too, so a near neighbour installed next to yours is part of the test. It never goes through the cache, and it is never logged. A case passes on its route and on each asserted argument:
- A
falsecase passes on abstain, or on another reflex. - An ask reads its missing arguments as unstated.
- A confirm is judged by its call, not by whether it would have run.
- An argument the record does not name is not compared.
A case that passed at the last run and fails now is decided twice more. Two of three failing marks it a
regression. test exits 1 when a case failed. It never blocks an install.
A case that passes while its call would wait for a yes, because the call holds less than the case's own words, is named under its reflex, with how far the call holds them. It counts among the passed and fails nothing. The cure is the summary: write it so that the case reads as what the reflex does (rule 11).
A playbook's steps are tested after its records (Playbooks): each filled from the first record
whose reading fills every required slot it holds, decided over the whole set, and passed when it reaches a reflex
other than its own playbook. Its line counts them, outage 14 passed · 7 steps route, and a step that reaches
nothing, or its own plan, fails and prints under it. These print under it too, and never fail:
- A step no record fills, marked untested.
- The step before a branch, when its reflex yields no field the branch waits on.
- A step that reaches a tighter effect than the playbook claims.
Lint
Records count against lint: at most 40 per table, and 200 characters per utterance. Lint also reports fewer than
three examples that are not false, an option or pick argument that no record leaves out, and a quoted argument
that no example shows in quotes. Lint reports at add and check.
Next: The rules.
Rules for reflexes, playbooks and collections
The keys are defined on The manifest, Arguments and Playbooks. This page says how to write them so that what people type reaches the right reflex, is asked only for what it left out, and never runs a step nobody asked for.
Everything the classifier knows about your reflex is your manifest, read word for word with every sentence a person
types. It chooses one reflex or none: the route. It reads each argument's value, or answers unstated. The
gate then turns those answers and the effect into an outcome: a call runs unasked only when every value it
holds clears the floor its effect sets and the call holds all that was typed, and otherwise evoke asks for the
value, or for a yes. When a reflex is added, the classifier also answers fits for every example installed:
does the new reflex do what that sentence asks? The person reads the call or the plan, and gives the yes.
Neither the classifier nor the person knows what you meant and did not write. So the careful thinking is yours,
done once, in the words of the file.
Four words come up throughout. A lookup is a reflex whose effect is read. A change is one whose effect
is write or destructive. A record is one line of [examples] or [tests]. A yield is one field of a
body's result that a later step may take, declared under [yields] with the kind that reads it; a whole result
travels instead by the name returns gives it and takes asks for.
Each rule gives its reason. Where the tool holds a rule for you, a word in italics after it says what: (check)
refuses the manifest, (lint) reports at check and add and never refuses, (add) reports when a reflex is
installed, (plan) refuses the sentence before anything runs. A rule with no such word is yours to keep. Each
section ends with a line that keeps its rules, one that breaks them, and what the terminal prints.
Everything you write has a reader:
| What you write | Read by |
|---|---|
| The reflex's name, the last segment of its directory or repository | The classifier, as one of the route's choices. The person, first on every call line |
description, not_for, [examples] | The classifier, with every sentence. The person, in evoke show and in the file |
An ask | The classifier, as the argument's question. The person, at the prompt |
| An option's key | The classifier, as the choice's key. The person, at the prompt and on the call line. The body |
| An option's meaning, a vocabulary word's meaning | The classifier, as an answer to the ask |
| A vocabulary word's value | The body, and [needs] where it names the argument. Never sent |
steps | The classifier, each step as a typed sentence over the set it runs on. The person, as the plan's lines |
effect | The gate, as the floor a call must clear before it runs unasked. The person, when a call confirms |
confirm | The person, before the action |
[needs], platforms | The kernel, which holds the body to them. The person, at add |
[tests] | evoke test and evoke calibrate. Never sent |
tags | --tag, which narrows a request. Never sent |
| The result line | The person, after the run |
1. One reflex, one action
- One reflex is one action, with one effect and one confirm, since the effect sets how sure
evokemust be before every call of the reflex: a gentle action bundled with a harsh one confirms like the harsh one. The states of one action are the options of one argument, likescreenshot'sarea, so a sentence that names no state still reaches the reflex. Split the states into one reflex each, and that sentence reaches one of them by a narrow margin, or none. - When a plan runs a lookup and then the change it serves, make them two reflexes:
devicefinds the laptop andlocklocks it. A lookup runs without asking on weaker evidence than a change, and one lookup can feed several changes. Where people also type the change alone, give the change a word of the sentence to read, «Which colleague's laptop?», since a reflex that only takes a whole result is refused when nothing before it returns one. (plan) - Give a reflex only the arguments its body uses, since each argument is one more answer and a call's confidence is the weakest of them. An argument the body cannot run without is required, and so is a value the confirm must read back, since a confirm may name only required values. Make an argument optional only where running without it is what the person expects, since an optional value the classifier misses is dropped, never asked. When a value of one kind plays two roles, a day of departure and a day of return, give each role its own argument, named by the role, since one question cannot read two answers. When people state one value in two forms, a new time or a shift, «to 4pm» or «15 minutes later», make the form the confirm can show the one required argument, since no key makes one of two required; the other form then stops the call at confirm (rule 4).
- A value from an open set is a pick, a switch is a flag, and options are for a closed set that covers every value people mean, since a value with no key is asked. When people type a kind of value beside the action and no argument reads it, the call holds less than they typed, and stops at confirm where that shows: so give a value an argument when the body can use it, and always when the body acts on a quantity.
# kept: three states of one action, one effect, one confirm
[args.area]
ask = "The whole screen, a window, or a selection?"
options.screen = "The whole screen."
options.window = "One window, chosen by clicking it."
options.selection = "A rectangle you drag out."
optional = true
A screen, a window and a selection reflex each read «fire off a quick screencap», cost three descriptions on
every request, and share the route between them, so the call stops at confirm or nothing runs.
2. When to write a playbook
Playbooks has the keys. A playbook is a reflex whose body is a plan: steps, one sentence each,
in place of run. A situation is what a person faces, typed in one sentence: a problem, «checkout is failing in
eu-west», or a routine, «close the month».
- Write a playbook for a situation people type in one short sentence and your team handles with the same steps each time, since a sentence that states the situation reaches the playbook, and one that asks for the steps reaches their reflexes instead: such a request seldom opens the playbook, and is refused whole when a step takes what the plan's steps return. Steps that differ each time are the person's own sentence, not a playbook.
- Its summary, its examples and its tests state the situation, never a step's action, since a sentence that names a step reaches that step's reflex and the playbook is never reached. Its test is the short sentence a person types when the situation happens, in words no example holds.
- Its
not_fornames the steps people type alone, in their words, «draining the primary on its own», and the situations beside it that it does not handle, so one step typed alone reaches its own reflex. - A plan holds one situation's steps in the order they happen, the lookups a change rests on first, since a person reads the whole plan before one yes; keep it to the steps the situation needs, since 24 is a cap and not a target. Every step keeps its own gate at its turn, a destructive one asking again, so a plan may be long: each step still asks what it must.
- When what happens next depends on what a step found, make that check a reflex of its own that yields one field,
and list the alternatives right after it, one per value, each under
when. A step that runs either way has nowhen. A step may reach another playbook once; a playbook reached from that one refuses the plan. (plan) No step holds a condition, a loop or a computed value, since evoke judges no condition and computes no value of yours. - Its
effectis the worst its steps reach, sinceaddprints a claim its steps pass. (add)
# kept: the situation, no step named
description = """
Handle a service that is down or failing.
Checks its errors, deploys and logs, finds the release behind the failure, rolls it back, tells the incident channel and updates the status page; the rollback asks again before it runs."""
not_for = ["rolling back one release on its own", "a slow service that still answers"]
An example «check checkout's errors» on the playbook reaches errors, never the playbook; a step «check whether
errors dropped, then roll back» is reported: step 4 holds "then"; one step is one action.
3. Words that reach the right reflex
The manifest has the keys.
- The summary, the first line of
description, says the action and what it acts on, in the words people type, 100 characters at most, since a stranger reads it first. (lint) It says the action with what a person gives it,Look up a new hire by name, with their role and start date, never by what comes back alone,Look up a new hire's role and start date, since a call is read back against these words, andlook up mariamust read as what the reflex does.evoke testnames an example whose right call waits for this. The second line draws the boundary: what the reflex covers, what it touches, and what cannot be undone, since a reader takes the effect from those words. - Describe the action and never address the classifier, since one manifest serves every engine and every reader. (lint)
not_fornames the near neighbours: what a person might type that sounds like your reflex and is not. Write each line in the manifest of the reflex that must not take the phrase, since a line guards only the reflex that carries it. Add a line for a phrase you have seen taken, or a neighbour's example your reflex would take, since a line also turns away your reflex's own sentences near it. Never a generic phrase: every line is read with every request, and a generic one holds nothing. (lint) reports more than eight lines.- Write at least three examples, as people type the request, since every example is read with every sentence
and costs on every request. Take them from sentences people typed; varying the verb for its own sake buys
nothing. Each asserts the options and flags its words state, which
evoke testchecks, and a pick as the exact span the sentence holds. (check) refuses a span the sentence does not hold; (lint) reports fewer than three examples. - Write the near neighbours as
falsetests, in words your examples do not use, since a test proves a boundary only where the classifier never saw its words. Cover each option and each pick. For every option or pick argument, write one record that leaves the value out and asserts the argument unstated,{ label = false }, since a required one is then asked. (lint) No record opens with a negation or a condition: typed, a negated part is left out and a conditional one refuses the request. - Guard a change with a required value and an honest effect, never with wording alone, since only an ask or a confirm stops a wrong route. A question about the change that states its value passes the ask and the confirm alike, so a collection with a change also holds the lookup that answers such questions (rule 39).
# kept: the action, then the boundary; the near neighbours as false tests in new words
description = """
Set a timer that rings after a duration.
From seconds to hours, named in quotes if you like, "like this"; it rings with a notification and a sound."""
not_for = ["keeping the laptop awake for a while", "an alarm or a reminder at a time of day", "asking what time it is"]
[examples]
"set a timer for 12 minutes" = { duration = "12 minutes" }
[tests]
"give me a two hour countdown" = { duration = "two hour", label = false }
"ring at 6:30 am" = false
not_for = ["anything unrelated"] holds nothing and is read with every request; "set a timer" = { duration = "12 minutes" } is refused: asserts duration = "12 minutes", which is not in the utterance.
4. Words that read each value
Arguments has the keys.
- Each ask is one question, as a person would ask it, since the classifier reads it word for word and the
person sees it at the prompt. Never ask whether a value was given: every question already offers unstated,
and none of these for a word the list lacks, which
evokethen asks for by the person's own word. Keep the question short, since a longer one lost a word the list holds. - An option's key is a word a person knows at a glance, since the prompt and the call line print keys alone:
[1] restart [2] shutdown. Its meaning answers the ask in one short line, since every request carries every meaning. (lint) reports a meaning that repeats the ask. - A value only the user knows, a folder, a service, a person, is a vocabulary, named by its published name below, since reflexes share a list only by naming it alike. Your records assert nothing about a vocabulary word, since the words are the user's; a user's overlay, the wording file kept over a shipped manifest, may.
- A value people type is a pick of its kind,
number,duration,email,url,quoted,date,time,amountorcode. A recognizer reads the forms its grammar knows and never invents one: a code as it is typed,TP1043ortp1043; a quoted value between double quotes. A form it does not read is asked for. Arangebounds a number or a duration, so a value outside it asks with the reason. quotedis for text people quote. Typed without quotes, a quoted value is read from the person's own words where they answer the ask,set a timer for 10 minutes called tea, and the call waits for a yes; where no words answer it, an optional one is dropped and a required one is asked. So text people type freely as the whole of the sentence is never aquotedargument: the body reads it frominput. For a typed sentence,inputholds the person's own words; when a plan runs the step, it holds the step's words. A message a playbook sends is therefore written into the step. (lint) reports a quoted argument that no example shows in quotes.- Where people point at a value an earlier command found,
recenton the pick names the field a lookup yields, so the ask lists the values this session found and the person chooses. - The confirm reads the call back, its action and each required value,
Roll back {release}?, and nothing else, since when a call asks it is the last line before the action and must say what this call will do. NeverAre you sure?. (check) refuses a confirm naming an optional argument, a flag or a taken result; (lint) reports one that names no required argument, or asks Are you sure.
# kept: one question, a pick, a range
[args.level]
ask = "How loud, in percent?"
pick = "number"
range = [0, 100]
ask = "Did they say how loud, and in percent or not?" is two questions, one of them already answered;
confirm = "Set the volume to {level}, {muted}?" is refused: confirm names {muted}, a flag.
5. How to word a step
A step is one sentence of a playbook's plan. A slot, {service}, is an argument written into a step.
- One step is one action, written as a person would type it, never as a call,
check {service}'s errorsand noterrors service={service}, since each step is decided over whatever is installed, as a typed sentence is. A step may hold a verb that is also a reflex's name; only the call form is refused. (check) refuses the call form; (lint) reports a connective. - Write the slot into every step whose reflex reads its value, required or optional,
list {service}'s deploys. A vocabulary word the person typed also reaches a step written without its slot,list its deploys, since the plan carries a stated vocabulary word into a step that lacks it; a pick or an option is never carried, so its slot is always written. A step without the slot must not hold a word of that vocabulary,search its logsbeside a service namedsearch: the plan leaves such a step to read its own value, it reads none, and asks. - Put an optional slot in a bracket with the words that go only with it,
[ in {region}], and leave a required slot bare, since the bracket drops whole when the slot is unstated. (check) - Describe a channel, «post to the incident channel», since the step then reaches whichever word of the person's
channelsvocabulary means that. Never write an address into a step, since a value is never the playbook's own; make the recipient a slot the sentence fills. A required slot the sentence leaves out keeps the plan shut until it is answered, and in a longer request a part that takes what the plan's steps return then has no source and the whole request is refused. (lint) reports a channel or an address written into a step, and calls both a slot: describe the channel, and make the address a slot. - A step that takes a value an earlier step found refers to that step with
it,themor its noun,wipe it,roll {service} back from that release, since a found value binds only through a reference; neverthey, which evoke does not read as one. Word a checkcheck whether, nevercheck that, sincethatbefore a noun names an earlier step. (lint) - Word each step apart from the situation, its summary and its examples, since a step near the playbook's own sentence reaches no reflex, or its own plan, and the plan refuses. (plan) (lint) reports a step whose words are all the summary's or an example's.
# kept: one action each, the slot where it goes
steps = [
"check {service}'s errors[ in {region}]",
"list {service}'s deploys[ in {region}]",
"roll {service} back from that release[ in {region}]",
"post to the incident channel",
]
"errors service={service}" is refused: step 1 reads as a call: errors service={service}; a step is a sentence.
"post to #incident" is reported: step 4 states "#incident"; a word another team would change is a slot, and the
fix is to describe the channel.
6. How results connect
- Declare under
[yields]every field a later step may take, by its kind, named by the noun a person would type for it, since a step takes only a declared field, andthat releasetakes the field of that name from the step it refers to. A found value fills only a pick of the same kind, never an option or a vocabulary word. A lookup yields the value its change reads, in the change's kind and in its absolute form, a day as2026-05-06and a time as09:30, two digits each on the 24-hour clock, since a step andrecenttake a yielded value only in that form, and9:30is nothing. - A lookup that finds several things yields the one its change acts on, under that thing's noun,
job = "number", whenever a plan's step takes it. The list,jobs = { each = { id = "number" } }, may stand beside it under another noun, forrecentand forthem, which runs the next step once per item. Over a list,that jobstops the whole plan before anything runs,step 2 takes one job from step 1, which finds several — which one?, since nothing is picked for you. - Give a whole result one noun across the collection, and one shape under it, since a reflex that takes it binds by the name alone and checks the shape itself. A reflex never returns a name it takes, and a destructive reflex takes a field its confirm can show, never a whole result. (check)
# kept: the field the next step takes, by kind and noun
[yields]
release = "code"
A deploys reflex with no [yields] gives «roll back that release» nothing to refer to, so the release is asked
before anything runs: the plan's line ends · asks release.
7. What a person reads
- Always write
effect, since left out it means destructive, and a reader sees that only where the description says the change cannot be undone. (lint)readobserves.writechanges what a later call can put back as it was, the volume, a light, a message to your own team.destructiveends or spends what no call brings back: a wiped disk, a purchase, a message to someone outside. Adestructivecall asks before it runs, every time; areador awriteruns without a question when the classifier is sure of every answer. A change that also sends a notice takes the notice's effect, judged by the widest audience the call's values can name, since a notice is never taken back. Claim the effect the action has, never a worse one to be safe, since a question before every harmless call teaches the yes a destructive one needs. - Name a reflex by its directory, which
addinstalls it as: the word people use for its action or its object, noun or verb alike, short, unique in the project it joins, since the name opens every call and plan line and the classifier reads it too. Name a playbook by its situation. - The result line says what happened in one lowercase line, with the value that lets a person check it,
volume 40%,locked, neverdone. A lookup that found several things counts them first,3 jobs queued, then one line each. - Keep the keys in the order
evoke showprints them, afterreflex = 1:description,not_for,tags,effect,confirm,stepsorrun,platforms,returns, then the tables,[needs],[config], each[args.<name>]with itsaskfirst and its source next, the asked arguments before the taken,[yields],[examples],[tests]. In TOML a plain key written below a table header belongs to that table, so the plain keys come first; and a stranger then reads the file as the tool shows it: what it does and will not take, what it changes and asks before acting, what it touches, what it will ask, what it was tested on.
# kept: the effect written, the keys in show's order
reflex = 1
description = """
Roll a service back from a release to the one before it.
Everywhere or in one region; live traffic moves to the older release, and only a new deploy brings the newer one back."""
effect = "destructive"
confirm = "Roll back {release}?"
run = "rollback.mts"
With effect left out the tool still treats the rollback as destructive, and a reader who sees no word of undo
takes it for a write; a confirm written below [args.x] belongs to that argument, and check says confirm is required.
8. How to design a collection
A collection is the reflexes and playbooks you publish together: one repository, versioned by one tag.
- Write the collection's shape before any wording: each reflex's name, effect, arguments with their sources, and what it returns and takes, since each reflex's words depend on its neighbours. The shape is the contract, what a user cannot override: the run or the steps, the arguments and their sources, the option keys, what a reflex yields, returns and takes. A reflex's words move freely under one tag; a playbook's steps are contract too.
- When the collection acts on things the user names, a service or a printer, its topic is one vocabulary every
reflex that acts on them names,
servicesacross an outage collection, since a word typed once reaches the steps that lack it only through that name. Things found by a pick, a meeting by its time or a job by its number, are no vocabulary: coin none just to have a topic. A vocabulary that exactly one installed reflex requires, a playbook among them, is that reflex's own while it stays the only one, and a typed word then reaches no other step through it. - Add a reflex only for an action the collection lacks, since every installed reflex is read with every request
and two that cover one action take each other's sentences and stop each other at confirm. Where people ask
about what a change changes, hold the lookup that answers, so «what does the status page say?» has a home and
never runs
status. - Every reflex a playbook's steps reach carries the tag the playbook carries, one tag per domain shared by the
reflexes that serve it, since a request narrowed with
--tagstarves a step whose reflex lacks it and the plan refuses. (plan) (add) reports a step that reaches a reflex without the tag. - Install the collection beside the reflexes it will live with, and read every
stealsandalso fitsline: each names a sentence two reflexes both claim. Fix a theft in thenot_forof the reflex that steals. (add) Two reflexes of one name are installed under different names, withadd --as. - The README holds one table, a row per reflex with what it does, its effect, each argument's source, what it yields, returns and takes, and what the user sets, since a stranger chooses a collection there and it is the collection's shape.
$ evoke add ./outage # kept: the playbook installed last, every step reaching
+ outage ./outage destructive a plan of 7 steps
1 check {service}'s errors[ in {region}] → errors · read
…
A newcomer restart for a service prints restart: steals "restart this computer" from power, a sentence two
reflexes both claim. Under --tag outage, a step whose reflex lacks the tag prints "list checkout's deploys" · no reflex, and the plan refuses.
9. What a body must do
- Declare in
[needs]the tightest reach that runs, since the kernel holds the body to it and a person consents to it atadd. What differs per machine is a[config]setting, a secret only as the name of a variable, andplatformsis named when the body runs on one system, so elsewhere the reflex is inactive and says why. - The body detaches what outlives the run, stops when
signalaborts, fails by throwing in its own words, runs programs without a shell, and is one file, since the deadline of thirty seconds a decision, the kernel and a reviewer hold it to that. The body has the rest.
# kept: one program, named; the system it runs on
platforms = ["macos"]
[needs]
runs = ["lpstat"]
runs = ["sh"] reaches everything a shell can; a program this machine lacks refuses at check, whatever platforms
names: [needs] runs names lpstat, which is not on PATH.
10. How to work
- Work in the order that finds each fault first.
evoke newwrites a reflex that checks clean, andevoke new --playbooka playbook.evoke check, after every edit, refuses what cannot load and reports lint, without the classifier, on a machine that runs the body.evoke trustonce; then give your own project a word in each vocabulary the reflex requires, since a reflex over an empty vocabulary is inactive andtestskips it.evoke addinto that project beside the neighbours, which names each of their examples your reflex would take.evoke tryone sentence per outcome you expect: a run, an ask, and one that is not yours.evoke test, which decides every record over everything installed.evoke teacha miss, and copy its line into[examples], dropping the vocabulary argument from what it asserts.evoke calibrateover what the project gathers.
- Read the contract diff before the tag, since a reworded step, a removed option or an argument made required is
major. Rename an argument with
was, so every user's overlay follows.
$ evoke check # kept: nothing refused, the lint weighed
timer write runs timer.mts
0.4.0 same
A manifest with a key evoke does not read prints unknown timer: args.label.not_for is not a key this evoke reads
and exits 0: the key is lost, and the manifest loads as if it were absent, so read every line.
A reflex handed as code
A reflex or a playbook handed to the SDK's load as code, reflex({…}) or playbook({…}), keeps every rule above
except those about the body's files: its name is its key under reflexes, it has no [needs], [config] or
platforms, and its body runs in your process and honours signal. load refuses what evoke check refuses in
a manifest, and the plan refuses as it would for any reflex; nothing lints it, nothing names what it steals, and a
key evoke does not read is dropped with no line printed. So run its records through a project's evoke test before
you ship it, and read it with the checklist in hand.
What evoke checks for you
You need not remember these. The tool holds them itself.
evoke check refuses
- A body that is missing or does not load; a program
[needs]names that is not onPATH; an argv element no argument fills; a[needs]entry naming no argument or setting, or naming a flag; a host other than*; a path this machine lacks. - An argument with no source or two; a pick of no known kind; an argument with no
ask; arangeon anything butnumberandduration;recentbeside anything but a pick; a reserved argument name; a retired argument name back in use, or one dropped fromwas. - No
confirm; a confirm naming an optional argument, a flag, a taken result or no argument. - A record asserting a span the sentence does not hold, a key not offered, a taken argument or a vocabulary word in a shipped manifest; two records with one utterance.
- A step that reads as a call with a value; more than 24 steps; an optional slot outside a bracket or a required one
inside; an argument no step names; a
whenat step 1 or on two fields. takesbeside another key, on an argv body or a destructive reflex, or of a name the reflex returns;returnsor[yields]on an argv body; a body's key on a playbook.
evoke check reports and never refuses
- A key it does not read, which is lost.
- A summary over 100 characters; a description over 1,000; more than 8
not_forlines; more than 24 options on one argument; more than 40 records in a table; a sentence or a step over 200 characters. - A phrase that addresses a model.
effectleft out; fewer than three examples; an option or a pick that no record leaves out; aquotedargument that no example shows in quotes; a confirm that names no required argument, or asks Are you sure; an option's meaning that repeats its ask.- A step holding a connective, stating a channel or an address, or opening with
check that; a step whose words are all the summary's or an example's.
The plan refuses before anything runs
- A step no reflex matches; a step that takes a result no step before it returns, or two return.
- A branch on a field its source does not yield, or a value no reader reads; a
whenstep that reaches a playbook, or that a later step takes from. - A plan past 24 steps or three deep; a step reaching its own playbook.
- A part of the person's sentence that opens with
if,unlessorin case; a part that says what not to do, beside a playbook's plan. - It stops rather than guess: a reference several fields could satisfy, or one item of a list, stops the plan before anything runs, naming them.
- Every request stops while one question of the project offers more than 255 choices, unstated counted: a vocabulary past 254 words, an argument past 254 options, a project past 254 reflexes.
add and test report and go on
stealsandalso fits, each with the phrase and its owner; a claimed effect its steps pass; a step that reaches a reflex without the playbook's tag; a branch source that yields no such field; a step that reaches nothing here.testfails a record that misses, and a step that reaches nothing or its own plan. It names a record that passes while its call would wait for a yes, since the call holds less than the record's own words.
Exceptions
- A change that needs nothing.
trashandscreenshotneed no value from the sentence, so only the gate stands before them. Their confirm names the action alone,Empty the trash?, and theirnot_fornames what people type nearby, «logging out». (Rules 16, 23.) - A reflex whose only required values are taken.
locktakes the laptopdevicefound; its confirm names the action alone,Lock the laptop?, and its examples are the words people type for it inside a longer sentence. (Rules 2, 23.) - A gentler state beside a harsher one. States of one reflex share its effect, so the gentler one confirms as
the harsher one does. Accept that price only where people type the action without its state; where they name it
every time, the gentler state is a reflex of its own:
sleep, a write, stands besidepower, which restarts or shuts down. (Rule 1.) - A playbook fits its own steps.
addprintsalso fitsfor the sentences of the reflexes a playbook's steps reach, since the plan does hold that action, and nonot_forline takes that away. Such a sentence typed alone still reaches its reflex. (Rules 7, 41.) - One verb, two things. «restart the payments service» and «restart the laptop» share a verb across two
collections; each names the other's object in
not_for. (Rule 13.) - One exact sentence a neighbour owns. When one whole sentence keeps reaching a reflex past its
not_for, it becomes afalseexample,"mute the sound" = falseinvolume. (Rule 13.) - What every team calls the same. A step may name a thing no team would rename, «update the status page for {service}». (Rule 27.)
- A lint line is weighed. Lint flags the first word a step would be split on, so
nextin «find the next flights to {city}» is flagged though the step is one action. (Rule 24.)
Checklists
A line is answered from the files, unless it ends with what answers it: a command, the neighbours or your users. A no sends you to the rule it names.
Before a tag
evoke checkrefuses nothing, and you weighed every lint line. (45)effectis written, and it is the effect the action has. (33)- The second line agrees with the effect. (11)
- The summary says the action with what a person gives it, in the words people type. (11)
- Each argument is one the body uses, with one ask and one source. (3)
- Each optional argument has a default people mean. (3)
- Options cover every value people mean; an open value is a pick; a switch is a flag. (4)
- No
quotedargument reads text people type without quotes. (21) - The confirm names each required value and nothing else. (23)
not_fornames the near neighbours, none of them generic. (13)- Three examples at least, as people type them, each asserting the options, flags and pick spans it states and never a vocabulary word. (14, 19)
- Each near neighbour is a
falsetest in words no example uses. (15) - One record leaves out each option or pick argument. (15)
- No record opens with a negation or a condition. (15)
[needs]is the tightest that runs;platformsis named when the body runs on one system; a secret is a variable's name. (43)evoke testpasses beside the neighbours. (45)evoke addprinted nostealsand noalso fits. (41)- Inside the repository,
evoke check's last line,1.2.0 → 1.3.0 minor, names the version you tag; a first tag has none. (46)
Before a playbook's tag
- The situation comes back, and your team handles it with the same steps each time. (5, your users)
- The summary and every example state the situation, and none names a step. (6)
- The test is the short sentence people type for it, in words no example holds. (6)
not_fornames its steps taken alone, in the words people type for them. (7)- Each step is one action, as typed, with no condition. (24, 9)
- No step writes in a channel or an address. (27)
- No step without its slot holds a word people give that vocabulary,
searchfor a service. (25, your users) - Each slot stands in every step whose reflex reads it, an optional one in a bracket. (25, 26)
- The lookups a change rests on come first. (8)
- A later step refers to what an earlier one found with
it,themor its noun. (28) - Each check is worded
whether, its reflex yields the field, and one step per value follows it. (9, 28) - No step reads like the situation or like an example. (29)
effectis the worst step's. (10)- The confirm names the required slots. (23)
evoke addshows every step reaching the reflex you meant. (45)evoke testfills every step from a record. (45)
A collection's shape
- One line per reflex: its name, its effect, each argument's source, what it returns and takes. (37)
- One reflex per action, named by the word people use, unique in the projects it joins. (1, 34)
- Each change's lookup is its own reflex where a plan needs it, and yields what the change takes. (2, 30)
- Each change people ask about has a lookup that answers. (39)
- Every vocabulary name is on the list below, or names a kind of thing none of them holds. (19)
- The things the user names are one vocabulary, or the collection has none. (38)
- The near neighbours are listed, inside the collection and toward the collections it will join. (13)
- No reflex covers an action a neighbour already covers. (39)
- Every reflex a playbook's steps reach carries the playbook's tag. (40)
- Each situation people type has a playbook, or a reason for none. (5)
- The README's table holds the shape. (42)
Vocabulary names
A vocabulary is the user's list of words for one kind of thing, kept in vocab/<name>.toml. You name the list
in vocab = "<name>"; the user writes its words, their meanings and, where the body needs one, their values. A
collection never ships one. Reflexes share a list only by naming it alike, so use these names, and coin a new one
only for a kind of thing none of them holds: a plural noun for what its words are, like vehicles or printers.
| Name | Its words are | A word's value, when the body needs one |
|---|---|---|
places | folders on this machine | the folder's path |
sites | web sites | the URL |
rooms | rooms of a home | an id the lights know |
people | people you work with | an address or an account id |
teams | teams | an id |
channels | chat channels, written #incident | the channel's id |
services | services a team runs | the service's id |
regions | where a service runs, eu-west | an id |
cities | cities | a code |
months | months, september | none |
shipments | shipments on their way | a tracking id |
suppliers | suppliers | an account id |
A day, a time, an amount and a code are never a vocabulary. They are picks, read by code from what the person
typed, tomorrow and 3pm among them.
A word's meaning:
- names what the word is, in the words people use for it, since a sentence or a step that describes the thing
reaches the word through its meaning:
"#incident" = "Where an outage is handled."; - holds only what the word is, since every extra word draws near phrases to it:
den = "The TV room downstairs; also 'the snug'."also draws «the living room», a room the list lacks; - holds what stays true,
september = "September.", never «last month», since the meaning is read in every month; - never holds the value, since the meaning is read by the classifier and the value is not.
Drafting with a model
A model may write a first draft of a manifest, a playbook or a whole collection. A person reads every line before it is saved, and decides. Once saved, a manifest is words: the drafting model takes no part in any decision.
Give the model what the body does, what it touches and what goes wrong when it runs by mistake; the collection's
shape, with each neighbour's summary and not_for; sentences your users typed for the action, so the examples
start from real words; these rules and the checklists; what evoke check printed for its last draft.
Never give the model your tests, since a test proves a boundary only when its words were out of view while the manifest was written; nor a user's vocabulary values, a setting's value or a secret.
Then read the draft as its owner. The effect, the contract and the tag are your claims, never the model's: set
effect yourself, since it sets the floor every call must clear. Check each example against the body: the values
it asserts are the values the body would receive. Write the tests yourself, after the draft, in words the draft
does not use. Run check, add and test on the draft as on any manifest, and read the lint line that names text
addressed to a model. Read it with the checklist in hand: a fluent draft is easy to approve unread.
Next: The manifest, key by key; Arguments; Examples and tests; Playbooks; The body; Publishing.
Publishing
Publishing is a git tag. A reflex's identity is its location, owner/repo[/dir]. Its version is the tag it was
fetched at. There is no account, no registry API, no build step, and no upload.
A repository of reflexes
radhi/home a repository is a collection
├── lights/{reflex.toml, lights.mts, reflex.d.ts}
└── blinds/{reflex.toml, blinds.mts, reflex.d.ts}
evoke add radhi/homeinstalls every directory directly under it that holds areflex.toml.evoke add radhi/home/lightsinstalls one. Directories may nest: name a deeper directory, or the one above it.- A repository may also be a single reflex at its root. An author may keep a project inside that directory:
evoke.toml,evoke.lock,overlays/,vocab/,evoke.d.ts. None of it ships. Those five names are skipped when a reflex is fetched and hashed. - Symlinks and submodules are refused at fetch. Everything a body needs is a committed file in its directory.
- A
LICENSEat the listed root is needed for a Hub listing. The collection uses MIT, since its scripts are meant to be copied into reflexes of your own.
Design a collection before you word its reflexes: rules 37 to 42.
Tags and versions
$ git tag v1.2.0 && git push --tags
- Tags are
X.Y.ZorvX.Y.Z. The newest is the highest. A repository without a version tag cannot be installed. - Published tags never change. The lock records the tag, the commit and the content hash.
syncrefuses a tag that moved. Publish a fix as a new tag. - One tag covers the whole repository. Every reflex in a collection moves together, and
evoke checkreads the contract diff for each from the same series.
What the next version must be
Run evoke check in a reflex directory inside a git repository. It diffs the manifest's contract against the
repository's newest version tag, and says what the next tag must carry:
$ evoke check
lights write runs lights.mts
lint lights: args.power has no record that leaves it out; assert power = false once
lint lights: args.brightness has no record that leaves it out; assert brightness = false once
lint lights: examples holds 2 requests; write three at least
+ reflex.d.ts
1.2.0 → 2.0.0 major
args.state renamed power
Each lint line reports a rule the manifest breaks. Lint never refuses:
What evoke checks for you.
| Level | When |
|---|---|
same | Wording only: description, not_for, tags, confirm, any ask, option meanings, records. Also a declaration narrowed |
minor | Additions, like an argument, an option, a config key, a field under [yields], a returns name or a platform. Also a config key removed, an argument made optional, a secret made plain, or a declaration widened |
major | Something a user's files or calls may not survive: an argument or option removed or renamed, a source, a range or recent changed, run or steps changed, an argument made required, a config key made secret, a yield or a returns name removed or changed, a taken argument added, removed or changed, a platform dropped |
A was violation is refused outright. That means a retired name returning, or a name dropped from the list.
With no repository, no tag, or no reflex at this directory in the tag, no diff prints.
Read this diff before every tag: rule 46. The checklists list what else to check before a tag.
Renaming safely
was lists an argument's former names, which stay listed forever:
[args.power]
was = ["state"]
Every user's overlay and call follows at merge time, and update tells them so. Rename an argument with was:
rule 46. An option key has no was, and removing one is major.
What users see at update
evoke update never prompts, never blocks and never rewrites a user's files. It reports your rewritten
description as taken, unless they overrode it. It reports each contract change, and what they override that you
changed. It reports a loosened effect, which they keep until they accept it. A tightened effect applies at once.
What add checks
Each install lints the manifest, as evoke check does. It also routes every installed example over the new set.
A steals line names an example your reflex would take from the reflex that owns it. An also fits line names
an example your reflex also fits: two reflexes claim the sentence, and one worded like it may reach either. For a
playbook, add names
each step whose reflex lacks the playbook's tag. None of these lines stops the install.
Before a tag, install your reflexes beside the ones they will live with, and read every such line: rules 40 and 41.
The Hub
The Hub is where reflexes are found. Until about twenty third-party reflexes exist, it is the first-party
collection, evoke-build/reflexes, reviewed and published as one repository. After that, it becomes one index
repository. A listing is a one-line pull request. Static pages at evoke.build show a preview, a diff, and a
search over examples. No install ever depends on it. add and sync reach git directly.
The Hub publishes no per-engine pass rates. An engine's terms may forbid them.
Next: The collection, or the SDK.
Getting started with the SDK
@evoke-build/evoke loads a project of reflexes, decides what an input asks for, and runs it. It does this inside
your application, with the same rules as the CLI. The rules run in core.wasm, the CLI's own core. The SDK adds
files, the network and a process.
Six things to learn, in order: reflex() · load() · handle() · a Decision · run() · replay().
Install
$ npm install @evoke-build/evoke
You need Node 24.5 or newer, and ES modules: "type": "module" in your package.json, or .mts files. The
package ships five entries, so the main one imports no engine:
| Import | Holds |
|---|---|
@evoke-build/evoke | load, reflex, the types, the errors |
@evoke-build/evoke/jev | jev(), the first adapter |
@evoke-build/evoke/openjev | openjev(), the same model through OpenJEV, an independent service |
@evoke-build/evoke/clef | clef() and clefFlash(), Cloudflare's Clef on Workers AI |
@evoke-build/evoke/testing | replay(), the recorded adapter for tests |
The first hour
One file. A reflex handed as code, a project with nothing on disk, and the whole loop in one call:
// app.ts · export TYPESAFE_API_KEY=… · node app.ts "timer for 10 minutes"
import { load, reflex } from "@evoke-build/evoke"
import { jev } from "@evoke-build/evoke/jev"
const timer = reflex({
description: "Start a countdown timer.",
effect: "write",
confirm: "Start a {duration} timer?",
args: { duration: { ask: "How long?", pick: "duration" } },
examples: { "timer for 10 minutes": { duration: "10 minutes" } },
}, async ({ duration }) => { // duration: number — seconds, typed from the manifest
setTimeout(() => console.log("ring"), duration * 1000)
return `ringing in ${duration} s`
})
const project = await load({ reflexes: { timer }, adapter: jev() })
const handled = await project.handle(process.argv[2] ?? "", {
confirm: async d => { console.log(d.prompt.template); return true }, // "Start a 10 minutes timer?"
ask: async d => ({ duration: "10 minutes" }), // d.missing says what is missing and why
})
console.log(handled.outcome === "ran" ? handled.result.text : handled.outcome)
reflex()takes the manifest as an object, and the body. The object is the file's shape, withoutrunandconfig. The body's argument types are inferred from the manifest literal.load()builds a project. Here it builds one from code alone, with the adapter passed in.handle()runs the whole loop with your two handlers: decide, ask until nothing is missing, confirm, run. It returnsran,abstained,declinedorunanswered.
With files
Next to an app, evoke add evoke-build/reflexes writes evoke.toml, evoke.lock and evoke.d.ts, exactly as
it does in your home project. Overlays and vocabularies under the same root tune the wording. That includes a
reflex handed as code.
import type { Reflexes } from "./evoke.d.ts"
const project = await load<Reflexes>({ root: import.meta.dirname }) // the adapter evoke.toml names
const d = await project.decide("kill the lights in the den")
if (d.outcome === "run" && d.reflex === "lights") console.log(d.values.room, d.values.state) // typed by evoke.d.ts
A Decision is a union. Narrow it by outcome, one of run, confirm, ask and abstain, then by reflex.
Tests, offline
import { replay } from "@evoke-build/evoke/testing"
const record = process.env.RECORD ? jev() : undefined // RECORD=1 asks Jev once and writes the file
const project = await load({ reflexes: { timer }, adapter: replay("answers.toml", { record }) })
Every run after the first is offline, and always gives the same answers.
Next: Projects in code.
Projects in code
load() reads a project once and compiles it. A project is the owned files under a root, the reflexes handed as
code, and who answers. load() never fetches and never writes.
load(options)
const project = await load<Reflexes>({ root, reflexes, adapter })
| Option | Meaning |
|---|---|
root | The project directory: evoke.toml, evoke.lock, overlays/, vocab/, local reflexes. Absent: no files at all. A root without evoke.toml is the default project, adapter = "jev", nothing installed |
reflexes | Reflexes handed as code, by local name. A name evoke.toml also lists is an error. Overlays under root apply to them too |
adapter | Who answers. Absent: the adapter evoke.toml names, resolved from the SDK's own subpath and built under [adapters.<name>]. Required when there is no root |
platform | "linux" or "macos": the platform a manifest's platforms is judged by, this machine's when absent. A reflex named for another platform is inactive; name one to decide or test as that machine would. A body still runs here |
Remote reflexes come from the store, by the lock's content hash, and are hashed again at load. A missing entry
fails, and the fix is evoke sync. The SDK never checks trust, since the root is yours to choose. It keeps no cache.
Reflexes as code
const timer = reflex({
description: "Start a countdown timer.",
effect: "write", // absent means destructive
confirm: "Start a {duration} timer?", // required
args: { duration: { ask: "How long?", pick: "duration" } },
examples: { "timer for 10 minutes": { duration: "10 minutes" } },
}, async ({ duration }, { input, signal }) => startTimer(duration))
The manifest is the file's shape without run and config, yields and returns included. A body closes over
what it needs. Its types are inferred from the literal: an option key union, a word or a quoted pick as string,
a number or a duration as number, a flag as true, an optional argument as optional, and an argument written
{ takes: "deploys" } as unknown, an earlier step's whole result the weave hands the body. So the body's args
are typed with nothing generated. A manifest that does not read throws at load, since it is your own code. Its problems end in
reflex(<name>). The body runs in-process. It is your application's code, unscrubbed, and nothing can end it from
outside. So it honours the deadline's signal.
playbook(manifest) hands a playbook the same way: steps in the manifest and no body
(Playbooks). Its steps are decided over the project's reflexes when a sentence reaches
it, and it reports runs: "plan".
Generated types
evoke add, update and remove write evoke.d.ts next to evoke.toml. It holds one Reflexes interface,
with a member per installed reflex, and its arguments as a decision carries them. Hand it to load:
import type { Reflexes } from "./evoke.d.ts"
const project = await load<Reflexes>({ root: import.meta.dirname })
Now d.reflex narrows d.args and d.values. A project with reflexes both installed and handed as code writes
load<Reflexes & ReflexesOf<typeof own>>({ root, reflexes: own }). TypeScript infers all type arguments or none.
What a project knows
project.reflexes // per name: { active: true, effect, runs: "inline" | "file" | "argv" | "plan" } | { active: false, problems }
project.plan // the digest of the compiled set, "h1:…"; every decision carries it
An inactive reflex is left out of every decision. The causes are an empty vocabulary, a setting unset, or an
overlay that does not parse. Its problems each end in the command that fixes it.
with({ vocab }): a tenant's words
A vocabulary binds a project, not a call. with compiles the same set over vocabularies of its own, in the
file's form. It is synchronous and takes milliseconds:
const tenant = project.with({ vocab: { rooms: await db.rooms(user) } })
const d = await tenant.decide(input)
if (d.outcome === "run") await tenant.run(d)
decide, fill and run on that project cannot disagree about the plan the decision was made under. So a
multi-tenant server stays correct. A decision made on one tenant's project is refused by another's.
Running bodies from the SDK
A reflex the project names, by a git ref or a directory, runs in a child process, through the SDK's own loader,
under the SDK's Node, held to what its manifest declares under [needs] as far as the package can: under Node's
permission model on both systems, and under Seatbelt on macOS. An argv reflex is spawned directly, under the same
layers. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held
and an argv body unheld there, and the result's contained says which, in the shape
--json prints. A reach past the declaration ends the run with what was
reached and the key, as it does in the CLI. Nothing starts before the decision. The body's deadline is 30
seconds, less what the adapter call took.
Next: Decisions.
Decisions
One input decided is a Decision. It is a union, narrowed by outcome and then by reflex. decide makes one.
fill answers an ask. run runs a chosen call. handle does all of it.
decide(input, { tags?, signal? })
const d = await project.decide("kill the lights in the den", { tags: ["home"] })
Asks the adapter, reads its answers, and gates them. Runs nothing. tags offers only the reflexes carrying one
of them. signal aborts the adapter call, and decide rejects with the signal's own reason.
The four shapes
outcome | Carries |
|---|---|
"run" | reflex, args, values, call, effect, confidence, weakest, judgments, contenders, runner_up?, whole? |
"confirm" | All of the above, plus prompt: { own, reason, template } and because: Cap[]: every reason it stopped, in order |
"ask" | reflex, partial args and values, unconsumed, missing: Missing[], and the judgments so far |
"abstain" | contenders, the ranking, and judgments |
Every decision also carries three more things. input is the sentence as decided. plan is the digest it was
decided under. trace has one entry per adapter call: { adapter, questions, ms }.
switch (d.outcome) {
case "run": await project.run(d); break
case "confirm": if (await ui.confirm(d.prompt.template)) await project.run(d, { confirmed: true }); break
case "ask": project.fill(d, await ui.pick(d.missing)); break
case "abstain": ui.say(d.contenders); break
}
args and values
args is what the classifier read, typed: { type: "option", key }, { type: "word", word, value? },
{ type: "pick", span, value }, { type: "flag" }. values is what the body receives: the key, the word's
value or else the word, the number or text, true. With Reflexes from evoke.d.ts, or a manifest handed as
code, both are typed per reflex once d.reflex is known:
if (d.outcome === "run" && d.reflex === "lights") {
d.values.room // string
d.values.state // "on" | "off" | "dim"
}
An app that wants a reflex's wording with a body of its own calls decide, switches on d.reflex, and reads d.values.
Confirm
prompt.own is evoke's line: the call, the effect and the weakest judgment. prompt.reason says in one
line why the call waits, a phrase for each cap, as the CLI prints it under the call. prompt.template is the
reflex's own question, filled in. because lists why it stopped: destructive,
no_gate, under_floor, one_view, respelt, text_read, more, cut, quoted, text_left, whole, detail,
named, pointed (Outcomes). A confirm decision runs only with run(d, { confirmed: true }).
decide reads the input as one call. When its words say the call is wanted once more, like too or the same for, the decision is a confirm with more, since there is no second step to be the other time. steps reads
every step of a sentence.
Ask and fill(d, given)
missing says, per argument, its ask, why it is missing, and what it may be. Missing means never stated, or a
pick out of range. What it may be is the options, the vocabulary's words, or which recognizer reads it, with
choices.recent listing the values recalled from the results the application handed decide, handle or
weave as recent: [{ reflex, data }], newest first, when the pick's manifest names the field. Where the
input's words say a pick's value aloud and read in more than one way, words holds them and choices.readings
lists what they read as: offer those in place of the recalled values. For a word from a vocabulary, likely
names the listed word to offer for a yes before the choices, which keep their order. fill takes what a person answered, by
argument name: an option's key, a word, or the text a pick reads, a recalled value or a reading among them. It
types the answer and gates again, synchronously:
const filled = project.fill(d, { room: "den" }) // a Decision again: run, confirm, or still an ask
A text that does not read is a DiagnosticError naming the argument and what it may be:
room: "attic" is not one of den, office. A flag is never asked.
run(d, { signal? })
Runs the chosen call's body and resolves to { text, data? }, with contained when a file or an argv ran:
whether this machine held it to its declaration, in the shape --json
prints. A reflex handed as code runs in-process. A file runs in a
child. An argv is spawned. A body's failure is a FailureError. signal aborts the body, and run
rejects with the signal's reason. A decision made under another plan is refused as misuse, with a TypeError. A
reflex that takes an earlier step's whole result is refused with a DiagnosticError: only weave hands one, so
a host with its own loop learns at run that decide's decision was a taker's. A playbook is refused the same
way, <name> is a plan of steps; say it in a sentence: only steps and weave make its plan.
handle(input, { confirm?, ask?, tags?, signal? })
The whole loop: decide, then ask and fill until nothing is missing, then confirm, then run.
const handled = await project.handle(input, {
confirm: d => ui.confirm(d.prompt.template), // true runs, false declines
ask: d => ui.pick(d.missing), // answers by name, or undefined to decline
})
handled.outcome | When |
|---|---|
"ran" | The body ran: decision and result |
"abstained" | none won, or the route was under its floor |
"declined" | confirm returned false, ask returned undefined, or an answer changed nothing |
"unanswered" | A confirm or an ask was reached and no handler was given. The decision itself is the answer |
An answer that does not read is asked again once. Twice is a decline. An empty input is refused before the adapter is asked. A decided reflex that takes an earlier step's whole result is refused before any confirm, and so is a playbook. Only a diagnostic, a fault or a failure throws.
steps(input, { tags?, signal? }) and weave(input, { confirm?, ask?, proceed?, tags?, signal? })
A sentence that asks for several things is a weave (Weaving). steps reads one into its
plan, runs nothing, and returns it sealed as a plan file — the sentence, the plan, every answer the classifier
gave, and the pins it was decided under — the object evoke try --save writes and evoke run <file> reads
(The plan file):
const pinned = await project.steps("look up dana's address and email them")
const plan = pinned.weave
plan.steps // [{ n: 1, text, decision, reflex: "contact", effect: "read", ... }, { n: 2, ... }]
plan.binds // [{ from: 1, to: 2, arg: "to", field: "email", kind: "email", via: "fill" }]
plan.stages // [[1], [2]]: a stage's steps run together; stages run in order
plan.verdict // { outcome: "run" | "ask" | "confirm" | "refuse", because?: [...] }
pinned.set // the plan digest it was decided over; pinned.answers, every answer the plan took
Each step carries its decision, the Decision decide would have made of its words alone; shared names
the words the sentence said once for several steps that reached it, { service: { word: "checkout", via: "fill" } }; repair says how a part that was not decided on its own became a step, "narrowed", "spliced",
"merged", "split", "corrected", "named", "first", "verb", "again" or "apart"
(The JSON line). A binding whose via is "takes" is a whole result by
the name its source returns, with no kind; a plan with no source for one, or two, has verdict.outcome
"refuse" and no_source or several_sources among its because. A step a playbook wrote carries from, the playbook, its step and the
slots the sentence filled (Playbooks); folded names the parts of the sentence that
repeat one of its steps; and the verdict's because holds reviewed per playbook, with the sentence's own
prompt. steps(input, { ask }) answers a slot the sentence lacks before the plan is made, as weave's ask
does; such a plan is a sheet, and weave refuses it as a file, since a file holds no one's answers. weave
settles what the plan asks first, then runs every step under handle's handlers, each told which step and round
asks, a Turn:
const woven = await project.weave(input, {
ask: (d, turn) => ui.pick(d.missing, turn.step), // before anything runs, for a required argument no step provides
confirm: (d, turn) => ui.confirm(d.prompt.template), // at the step's turn
proceed: plan => ui.confirm("Run the plan as it stands?"), // a step refers to another it takes nothing from
})
woven.status // the worst step's: "ran", "failed", "declined", "refused", "unanswered"; or why nothing ran
woven.steps // per step: { step, status, why?, bound, rounds: [{ round, input, decision, status, result? }] }
A confirm handler may wait: hand the decision, plain JSON, to a queue a second person reads, and resolve
with their answer; the weave goes on where it waited, and nothing that ran runs twice. The whole plan may travel
instead: JSON.stringify(pinned) is the file, and weave(pinned, { proceed, confirm?, ask?, signal? }) runs it
on the same project, here or on another machine — its pins checked first, a DiagnosticError naming the first
that moved and the call that makes the plan again; the plan made again from the file's answers under its gate
and refused when it does not read the same; then proceed(pinned), required whatever the verdict, since a
file's numbers are whoever wrote them; then every step under the handlers, a value bound at run time decided by
this project's adapter. A plan that asks or refuses runs nothing.
const checker = await load({ root, adapter: jev() })
const woven = await checker.weave(JSON.parse(file) as Pinned, {
proceed: pinned => ui.confirm(`Run the plan of ${JSON.stringify(pinned.input)}?`),
confirm: (d, turn) => ui.confirm(d.prompt.template),
})
``` A body's failure is the
step's, with `status: "failed"`, never a throw. A step after one that stopped is `skipped`, with `why: { type:
"earlier_step" }`. A step bound to a list of records runs once per record, one
round each. A step that takes whole results receives them in its body's `args`, handed by the weave alone; its
`bound` entries name each argument, its source and the name, with no `value`.
`signal` aborts the adapter's call while the plan is made, as it does in `decide`, and cancels the run once it
is under way: every body is ended, its own `signal` aborted first, nothing starts after, and every step that did
not finish reads `skipped` with `why: { type: "cancelled" }`. `weave` then rejects with the signal's reason, the
record on it as `woven`:
```ts
try {
await project.weave(input, { signal: controller.signal })
} catch (error) {
if (error === controller.signal.reason) console.log(error.woven.steps) // what ran, and what was cancelled
else throw error
}
Next: Adapters.
Adapters
An adapter is the classifier behind a decision. It is an object that answers typed questions with probabilities.
The core names no engine. A project names an adapter, and only your machine resolves the name. Two built-in
adapters reach Jev: jev(), through TypeSafe AI's own API, and openjev(), through OpenJEV, an independent
service. They send the same questions and ship the same bars. Two more reach Clef, Cloudflare's model: clef()
and clefFlash(). They send the same questions and ship no bars.
jev(): the first adapter
import { jev } from "@evoke-build/evoke/jev"
const project = await load({ reflexes, adapter: jev() })
const project = await load({ reflexes, adapter: jev({ key, gate: { write: 0.95 } }) })
| Option | Meaning |
|---|---|
key | The API key. Absent: TYPESAFE_API_KEY from the environment, read when jev() is called |
gate | Floors over the defaults of route 0.5, fits 0.3, read 0.8, write 0.9 and whole 0.3. The same as [adapters.jev] gate in evoke.toml |
When load resolves jev by name from evoke.toml, it is built under the file's [adapters.jev] table. An
adapter passed in replaces both. The transport keeps one connection alive for the process, through the proxy
HTTPS_PROXY names when one is set; a proxy needs Node 24.5 or newer, and jev() says so on an older one. It
retries once after a connect error or a server error, and never after a client error, except a 429 that says
how long to wait: that wait is waited out, and the request sent again, within the deadline. No key is a
DiagnosticError ending in export TYPESAFE_API_KEY=<value>.
openjev(): the same model, through OpenJEV
import { openjev } from "@evoke-build/evoke/openjev"
const project = await load({ reflexes, adapter: openjev() })
const project = await load({ reflexes, adapter: openjev({ key, gate: { write: 0.95 } }) })
| Option | Meaning |
|---|---|
key | The API key. Absent: OPENJEV_API_KEY from the environment, read when openjev() is called |
gate | Floors over the same defaults as jev(). The same as [adapters.openjev] gate in evoke.toml |
OpenJEV is an independent service, not TypeSafe AI's. It forwards each request to Jev and
returns Jev's answers. evoke is affiliated with neither, and vouches for neither. A key from OpenJEV is on
OpenJEV's terms, and whether it may offer Jev is a matter between OpenJEV and TypeSafe AI. As of this release it
publishes no terms and no privacy policy, so weigh that before you choose it for an application. The transport
is jev()'s, with 3 seconds once connected instead of 1.5, since the service forwards the request onward. It
names the model by version, jev-1.13.0, as jev() does, so its id is Jev's own. No key is a
DiagnosticError ending in export OPENJEV_API_KEY=<value>.
clef() and clefFlash(): Clef, on Cloudflare's Workers AI
import { clef, clefFlash } from "@evoke-build/evoke/clef"
const project = await load({ reflexes, adapter: clef() })
const project = await load({ reflexes, adapter: clefFlash({ key, account, gate }) })
| Option | Meaning |
|---|---|
key | The API token. Absent: CLOUDFLARE_API_TOKEN from the environment, read when the adapter is called |
account | The account's id. Absent: CLOUDFLARE_ACCOUNT_ID from the environment, read at the same time |
gate | The floors route, read and write, with fits and whole if you want them. The same as [adapters.clef] gate or [adapters.clef_flash] gate in evoke.toml |
Clef and Clef-flash are Cloudflare's models, with
weights published under Apache-2.0. clef() reaches Clef and clefFlash() the smaller Clef-flash. Both run on
Cloudflare's Workers AI, in the account the token belongs to. Cloudflare's terms cover the token and each request.
These adapters ship no floors, so every decision confirms. To let calls run, pass a gate with route, read
and write, and measure the numbers you choose on your own records: Calibrating. A
gate that lacks one of the three is a DiagnosticError.
The account's id goes into the address. So it must be 32 hex digits in lowercase, and anything else is refused
before a request is made. The transport is jev()'s, with 6 seconds once connected for clef() and 3 for
clefFlash(). Workers AI takes 64 questions a request, so a longer request is sent in parts and answered as one.
It names no version of a model, so each id carries a date: clef-2026-10-03 and clef-flash-2026-10-03. No
token or no account is a DiagnosticError ending in the export line for its variable.
The contract
interface Adapter {
id: string // opaque; changes whenever answers could; compared, never parsed
limits?: { options?: number; tokens?: number } // options: a plan over it is refused at load; tokens: declared, not enforced
gate?: { route: number; fits?: number; read: number; write: number; whole?: number } // each number means P(correct)
plan?: string // a recording's plan digest; another plan refuses it
answer(state: { request: string }, questions: Record<string, Question>, signal: AbortSignal): Promise<Raw>
}
type Question =
| { type: "choice"; ask: string; options: Record<string, Text>; otherwise?: string } // the key meaning "none of these"
| { type: "yesno"; ask: string; yes: Text; no: Text } // answers { yes: p }
type Text = string | { what: string; not_for?: string[]; examples?: string[] }
type Raw = Record<string, Record<string, number>> // per question, a number per key
- State is only
{ request }. An adapter sees the input and the questions, nothing else. - A choice answer is a distribution over the keys offered, summing to 1. An omitted key reads as 0. A yes/no
answers
{ yes: p }. The core validates every answer and fails closed. A missing question is a fault. otherwisemarks the sentinel,noneorunstated, in the structure. So an engine may abstain its own way.gateis the adapter's own calibration. The core never rescales. Without one, every decision confirms.gate.wholeis the floor of a call read back against the input; without it no call is read back.gate.fitsis read when a reflex is added: a new reflex that fits another's example at or above it is named.answeris stateless and may be called with any subset of a plan's questions.signalaborts it at the deadline. An adapter that ignores its signal is raced against it anyway.idcovers everything that could shift answers: model, version, prompt rendering, calibration. It is pinned in the lock with the project's adapter name.
Writing one
const mine: Adapter = {
id: "my-classifier-2",
gate: { route: 0.5, read: 0.8, write: 0.9, whole: 0.3 },
async answer(state, questions, signal) {
const raw: Raw = {}
for (const [id, q] of Object.entries(questions)) raw[id] = await classify(state.request, q, signal)
return raw
},
}
const project = await load({ root, adapter: mine })
Any engine that can answer a closed choice with probabilities fits. That could be a hosted model that spells them
out, a local model with log-probabilities, or a zero-shot classifier. An embedding router cannot fill arguments
on its own. A throw that is not evoke's own becomes a transport FaultError. The caller's abort passes through
as its own reason.
Next: Testing.
Testing
replay() is an adapter over a recording. The recording is the CLI's own answers.toml, read and written through
the core. With record, an utterance the file lacks is asked of a real adapter once, and written back. So one run
records a test suite. Every run after is offline, and always gives the same answers.
replay(file, { record? })
import { load, reflex } from "@evoke-build/evoke"
import { jev } from "@evoke-build/evoke/jev"
import { replay } from "@evoke-build/evoke/testing"
const record = process.env.RECORD ? jev() : undefined
const project = await load({ reflexes: { timer }, adapter: replay(new URL("answers.toml", import.meta.url), { record }) })
const d = await project.decide("timer for 10 minutes")
assert.equal(d.outcome, "run")
RECORD=1 node --testasks Jev for every utterance the file lacks. It writes the file whole, through a rename, so a reader never sees half a recording.- Without
record, a missing utterance is aFaultErrornaming it, with the call that records it. - The recording carries the adapter's declaration:
id,limits,gate. So the gate in tests is the gate in production. Aplanline, when present, pins the installed set the answers were recorded against. A project over another set refuses it.
The file
Answers are keyed by utterance identity: lower-cased, whitespace collapsed, trailing punctuation dropped.
Each question is keyed by its id: route, <reflex>.<argument>, and weave.<name>, a question evoke asks
on its own account. fits.<reflex> is asked when a reflex is added.
id = "replay"
[gate]
route = 0.5
fits = 0.3
read = 0.8
write = 0.9
whole = 0.3
[answers."kill the lights in the den"]
route = { lights = 0.95, timer = 0.01, volume = 0.01, none = 0.03 }
"lights.room" = { den = 0.92, office = 0.03, unstated = 0.05 }
"lights.state" = { off = 0.94, on = 0.03, dim = 0.02, unstated = 0.01 }
"lights.brightness" = { unstated = 1.0 }
"timer.duration" = { unstated = 1.0 }
"timer.label" = { unstated = 1.0 }
"volume.level" = { unstated = 1.0 }
"weave.view_lights__room" = { den = 0.92, office = 0.03, unstated = 0.05 }
"weave.view_lights__state" = { off = 0.94, on = 0.03, dim = 0.02, unstated = 0.01 }
"weave.is_lights__state_1" = { yes = 0.95 }
"weave.left_lights_0_2" = { action = 0.9, courtesy = 0.1 }
"weave.whole_693291516" = { whole = 0.9, less = 0.05, other = 0.05 }
A pick's candidates are keyed <start>-<end> by their character offsets in the input. A recording made with
record holds every question. You can write one by hand too: a question the file lacks is named by the fault,
weave.whole_693291516 was not answered, and its answer is one line more.
The CLI on a recording
The same file drives the CLI. Set adapter = "replay" in evoke.toml, and the file's path in EVOKE_ANSWERS.
That is how evoke's own transcripts run offline. It is also how a CI job can exercise a project without a key.
$ EVOKE_ANSWERS=answers.toml evoke try "kill the lights in the den"
Testing bodies
A body is a function. Import it and call it with a context of your own. No adapter is involved:
import note from "../note/note.mts"
const context = { input: "", config: { file: "notes/today.txt" }, signal: new AbortController().signal }
assert.equal(await note({ text: "buy milk" }, context), 'noted "buy milk" in ~/notes/today.txt')
Next: Errors.
Errors
Every error evoke raises is data with a fix attached. Its message is the CLI's own line: lights: vocabulary "rooms" is empty → evoke vocab rooms add <word> "<meaning>".
import { DiagnosticError, EvokeError, FailureError, FaultError } from "@evoke-build/evoke"
| Class | kind | Means | CLI exit |
|---|---|---|---|
DiagnosticError | "diagnostic" | Something a person fixes in the files or the environment: problems, each { reflex?, at?, message, fix, command } | 3 |
FaultError | "fault" | The adapter failed, or its answers did not validate: fault, one of transport, status, refused, retired, unanswered, malformed, unrecorded | 4 |
FailureError | "failure" | A body or the machine failed: what, why, fix; a body's own error is the cause, and the frames of an error a file body threw are frames | 1 |
All three extend EvokeError, which carries command. That is the literal line that fixes it. It is empty when
the only fix is to try again. fix is the structured value, and command is its rendering. Render nothing yourself.
try {
await project.handle(input, handlers)
} catch (error) {
if (error instanceof DiagnosticError) for (const p of error.problems) log(`${p.message} → ${p.command}`)
else if (error instanceof FaultError) retryLater(error.fault)
else if (error instanceof FailureError) log(error.message)
else throw error
}
- Misuse is a plain
TypeError. That means running a decision that is notrunorconfirm, running a confirm without{ confirmed: true }, or handing a decision to a project other than the one that made it. - Aborts reject with the signal's own reason, never wrapped; a cancelled
weaveputs its record on it aswoven. - A bug in
evoke, a trap in the core, is a plainErrornaming it.
Next: the Reference.
Commands
Every command, flag and exit code, as evoke --help prints them and with what each one does. The three groups
are use, tune and install; the SDK has its own reference.
evoke 0.21.0 · you invoke a function; you evoke a reflex
use
evoke "<input>" decide, gate, run
evoke the REPL; a piped line is one input
evoke try "<input>" decide only, and show every judgment
--json one JSON line per input, for a filter
--tag <tag> only the reflexes carrying the tag
--save <file> the plan as a file, for run
-- the rest is input, even a command word
evoke why the last sentence, explained
evoke run <call> by name, without the classifier
evoke run <file> a saved plan, run as it stands
--json one JSON line: the call and its result
<call> is <name> [<arg>=<value> | <flag>]…
install
evoke add <ref>… [--as <name>] fetch, lint, lock, install
evoke remove <name>
evoke update [<name>] each remote reflex to its newest tag
evoke update --accept <name> a moved contract or effect, accepted
evoke sync the lock realised on this machine
evoke trust this project, trusted at its content
tune
evoke show [<name>] the installed reflexes, or one as used
evoke teach ["<utterance>"] <call> the utterance means this call
evoke teach ["<utterance>"] not <name> the utterance is not this reflex
--forget <name> the overlay's line for it, removed
["<utterance>"] omitted means the last input
evoke vocab <name> the words and their meanings
evoke vocab <name> add <word> "<meaning>" [--value <v>]
evoke vocab <name> remove <word>
evoke config <name> <key> <value> a setting; a secret as --env <VAR>
evoke test [<name>] every example and test, judged
evoke calibrate [<name>] the confidence measured on the records
--repeat <k> each input decided k times: the spread
--json one JSON object, for a script
author
evoke new <name> a working reflex from the template
--playbook a playbook instead: a plan of steps
evoke check lint, types, contract against its tag
exit 0 ran · 1 failed · 2 declined · 3 needs a human · 4 adapter failed
manual https://evoke.build/manual/
Grammar
- The first argument selects a command only when it is exactly a command word. Otherwise every argument is the
input, bare words joined by one space.
--forces input. Lines from stdin are always input. --help,-horhelp, and--versionor-V, are flags in the first place. They print to stdout and exit 0.--helpor-hright after a command word prints the help too.- A command word after a flag,
evoke --json try "…", is refused with the way to write it. It is never decided. - What you asked to see goes to stdout: a reflex's result, the
--jsonline, and the answer ofshow,vocab,why,try,testandcheck. Whatevokesays about a run goes to stderr: the call and its confidence, prompts, the line of a write, the ranking after an abstain, and every diagnostic. - Commands validate every argument before acting. Arguments that spell no command end in
evoke --help. The reserved wordsedit,search,publishandadapterare refused by name until they exist. - A call is
name arg=value…. A value is bare or a JSON string, likeduration="10 minutes". A flag is its bare name.runandteachtake a call as one argument or as separate words. An argument torunwhose first word is no reflex name,month.plan.jsonor~/month.plan.json, is a plan file.
Use
| Command | Does | Exit |
|---|---|---|
evoke "<input>" | Decides, gates, runs. Prints the call and confidence on stderr, and the result on stdout. Logged | 0 · 2 · 3 · 1 · 4 |
evoke | On a terminal, the REPL: > , line editing, history. Piped, a filter: one input per line, and the first non-zero exit is kept | as each line |
evoke try "<input>" | Decides only, and prints the sentence as it was read: how it was read as a whole, then each step with which reflex, each value with what it stands on, the call and what would become of it, and why; a sentence of several steps shows the plan first. Last, how much the adapter was asked. Never logged: Outcomes | 0 · 3 · 4 |
evoke try --save <file> "<input>" | The plan's lines, then the plan written as a file for run <file>: the sentence, the classifier's answers and the pins it was decided under, in clear. A plan that asks is refused with the step's question, exit 3; one that refuses, exit 2; nothing written either way. Never logged: Weaving | 0 · 2 · 3 · 4 |
evoke why | The last logged sentence, shown as try would show it, and what became of each step: ran, ran at your yes, declined, asked, refused, failed, cancelled, skipped, unanswered; from <file> where a plan ran from one; a playbook's own block first where one wrote the plan | 0 · 3 |
evoke run <call> | Runs the call by name. No classifier. The effect policy is kept, so a destructive call confirms with [y]es [n]o. A reflex that takes an earlier step's result is refused: only a sentence of several steps hands one. So is a playbook: a sentence reaches it. --json prints the call and its result as one line. Not logged | 0 · 2 · 3 · 1 |
evoke run <file> | Runs a plan try --save wrote, exactly: a file, since its first word is no reflex name. A pin that moved refuses it with the line that names it and the command that mends it; so does a file that does not read the same as its answers. The plan prints, one yes over it is asked whatever its verdict, then each step at its turn as a sentence's is. Every step's line names the file. Logged | 0 · 1 · 2 · 3 · 4 |
| Flag | With | Does |
|---|---|---|
--json | input, try, run | One JSON line per input, per step of a weave, or per call; try prints a weave's plan whole: The JSON line |
--tag <tag> | input, try | Only reflexes carrying the tag; repeatable |
--save <file> | try | The plan written as a file for run <file>; one input, never stdin |
-- | input | The rest is input |
Install
| Command | Does |
|---|---|
evoke add <ref>… [--as <name>] | Fetches each ref at its pin or newest tag, once per repository; reads a local ref, ./dir, where it is. Lints, tests for stolen phrases, writes evoke.toml, the lock and evoke.d.ts, and records the runtime; for a playbook, decides each step over the set it joins and prints what it reaches, and any reflex reached that lacks the playbook's tag. --as names a single ref |
evoke remove <name> | Drops the reflex from evoke.toml and the lock. Keeps your overlay, vocabularies, settings and the store's copy |
evoke update [<name>] | Moves each unpinned remote reflex, or one, to its newest tag. A pinned one moves to its pin. Reports, or prints up to date. Never prompts or rewrites your files |
evoke update --accept <name> | Takes on an effect upstream loosened, or a declaration it widened, at the current tag |
evoke sync | Places every locked reflex in the store at its locked tag, and records the runtime, or prints up to date. Never changes the lock |
evoke trust | Trusts the project here, at the content of its four owned paths |
A ref is owner/repo[/dir][@tag] on GitHub, <git url>[#dir][@tag] over https or ssh, or ./dir for a
local reflex: Installing reflexes.
Tune
| Command | Does |
|---|---|
evoke show | Every installed reflex: name, ref and tag or ./dir, effect, what it runs or a plan of 7 steps. Then the inactive lines |
evoke show <name> | The effective manifest as TOML, with + in the gutter of every line that is yours; for a reflex that takes whole results, which installed reflexes return each; for a playbook, its steps after confirm, numbered, a step that may not run ending in its when. Then its inactive lines |
evoke teach "<utterance>" <call> | Writes the example to overlays/<name>.toml, with only what the call asserts |
evoke teach "<utterance>" not <name> | Writes "<utterance>" = false to the reflex's overlay |
evoke teach --forget "<utterance>" <name> | Removes the utterance's line from the reflex's overlay, an example or a test |
evoke teach <call> · evoke teach not <name> | The same, for the last input the log holds; of a weave, the step the lesson's reflex decided. A first word is the utterance when a call follows it, unless it names an installed reflex |
evoke vocab <name> | Lists the words. Empty exits 3, with the add line |
evoke vocab <name> add <word> "<meaning>" [--value <v>] | Adds or replaces a word |
evoke vocab <name> remove <word> | Removes one. A word that is not there is refused |
evoke config <name> <key> <value> | Sets a declared setting under [config.<name>] |
evoke config <name> <key> --env <VAR> | Names the variable a setting is read from. The only way to set a secret |
evoke test [<name>] | Decides every example and test of every active reflex, or of one, without the cache; a playbook's steps too, each filled from a record, 7 steps route or the step that reaches nothing, or reaches a reflex that yields no field a branch waits on. A case that passes while its call would wait for a yes, holding less than its words, is named under its reflex and fails nothing. Exits 1 when a case failed; nothing to test says so |
evoke calibrate [<name>] [--repeat <k>] [--json] | Decides every record once more, each distinct input once or k times, and reports what the confidence meant: the whole call right by bins with their counts and intervals, the wrong calls at or over each bar per thousand, the spread over repeats, the log's lines apart. --json prints one object. Exits 1 on a call wrong at or over its bar, or a bin of a hundred calls over-confident: Calibrating |
None of these needs the classifier's key but test and calibrate, which ask it, or a recording.
Author
| Command | Does |
|---|---|
evoke new <name> | Writes ./<name>/reflex.toml, <name>.mts and reflex.d.ts from the template. Refuses a directory that exists |
evoke new --playbook <name> | Writes ./<name>/reflex.toml alone, a playbook of three steps from the template |
evoke check | In a reflex directory: the manifest's lines to fix, the body found and loaded, lint, reflex.d.ts rewritten when it changed, and the contract diffed against the repository's newest tag |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Ran, or the command did what it said |
| 1 | A body or the machine failed. Also evoke test with a failing case, and evoke calibrate with a call wrong at or over its bar |
| 2 | Abstained, or declined at a prompt |
| 3 | Needs a human: a missing key, nothing installed, an untrusted project, a prompt without a terminal, a line to fix |
| 4 | The adapter failed |
Files
Everything evoke reads or writes in a project, and who owns each file.
<project>/
├── evoke.toml yours the adapter, the reflexes, their settings
├── evoke.lock evoke's written whole by add, update and remove; realised by sync
├── evoke.d.ts evoke's written with the lock: Reflexes, for load<Reflexes>
├── overlays/<name>.toml yours your wording for one reflex, by local name
└── vocab/<name>.toml yours your words, by vocabulary name
<reflex>/
├── reflex.toml the author's the manifest
├── <body>.mts the author's the file run names; nothing for an argv, or for a playbook, whose body is steps
└── reflex.d.ts evoke's written by check: Args, Config, Context, Result, Reflex
evoke edits your files in place, keeping comments and order. It verifies each write by reading it back. On
failure it writes nothing, and prints the line. The lock and the two .d.ts files are rendered whole. A file that
is a symlink is written through; the link stays.
evoke.toml
adapter = "jev" # required; a name resolved against evoke's built-ins, never a path
[reflexes] # local name = ref
lights = "radhi/home/lights" # owner/repo[/dir]: unpinned, update moves it
timer = "radhi/timer@1.0.1" # @tag pins it
clock = "https://git.example.com/clock.git#clock@2.0.0" # any git host; https or ssh; #dir and @tag optional
hello = "./hello" # a local directory, relative to this file; never locked
[config.lights] # per reflex, the keys its [config] declares
bridge = "10.0.0.2" # plain
token = { env = "HUE_TOKEN" } # from a variable; the only form a secret may take
[adapters.jev] # per adapter, under its name; inert unless selected
gate = { route = 0.5, fits = 0.3, read = 0.8, write = 0.9, whole = 0.3 } # each a probability; read ≤ write
The built-in adapters are jev, openjev, clef, clef_flash and replay; [adapters.openjev] takes the same
gate table as [adapters.jev]. clef and clef_flash ship no thresholds, so a gate table of theirs gives
route, read and write. Local names match [a-z][a-z0-9_]*. The names none, unstated, fits and weave are reserved. A missing evoke.toml
at a root means the default project: adapter = "jev", nothing installed.
evoke.lock
lock = 1
evoke = "0.1.0"
[adapter]
name = "jev"
id = "jev-1.13.0"
[reflexes.lights]
ref = "radhi/home/lights"
tag = "1.2.0"
commit = "7a498d11d5375f3cb65c575c3186bc64ecac7f52"
h1 = "h1:a30b1bbcf52390c4b1311685ceee9f886b81b7699cde1374040979ad00c25d3a"
effect = "write"
Per remote reflex: the ref without its pin, the tag, the commit, the content hash of the reflex directory, and the
effect you consented to. Local reflexes are not locked. h1 is a SHA-256 over sha256sum's own lines for every
file in the directory. Files are taken in byte order of path, and the project's five names are skipped:
$ find . -type f | sed 's|^\./||' | LC_ALL=C sort | xargs sha256sum | sha256sum
overlays/<name>.toml
description = "…" # replaces whole
not_for = ["…"] # replaces whole
tags = ["…"] # replaces whole
effect = "destructive" # may only tighten
confirm = "…" # placeholders name required arguments
[args.state] # by its current name, or a former one declared with was
ask = "…"
options.dim = "…" # new wording for an existing key only
[examples] # sent; may assert vocab arguments
"kill the lights" = { state = "off" }
[tests] # held out; a shipped example named here stops being sent
"light a candle" = false
A contract key in an overlay makes the reflex inactive: run, config, yields, and an argument's source, range,
flag or optional. A key that addresses nothing the reflex has, an argument or an option key of its own, is
orphaned: skipped, and reported by update.
vocab/<name>.toml
den = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }
A word is one clean line, trimmed, spaces allowed, and unique under identity. none and unstated are reserved.
what is what the classifier reads. value is what the body receives instead of the word.
evoke.d.ts
// Generated by evoke add, update and remove; do not edit.
// What a decision's args carry, per installed reflex: load<Reflexes> narrows them by reflex.
export type Option<K extends string> = { type: "option"; key: K }
export type Word = { type: "word"; word: string; value?: string }
export type Pick<T extends string, V> = { type: "pick"; span: { start: number; end: number; text: string }; value: { type: T; value: V } }
export type Day = { type: "offset"; days: number } | { type: "weekday"; weekday: "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; which?: "next" | "this" | "last" } | { type: "day"; day: number } | { type: "calendar"; year?: number; month: number; day: number }
export type Flag = { type: "flag" }
export interface Reflexes {
timer: {
/** How long? */
duration: Pick<"duration", number>
/** What is the timer for? */
label?: Pick<"quoted", string>
}
}
A date is Pick<"date", Day>, a time Pick<"time", string>, an amount Pick<"amount", { amount: number; currency: string }>, a code Pick<"code", string>. A reflex whose manifest does not read is left out. The file imports nothing.
reflex.d.ts
// Generated by evoke check; do not edit.
export interface Args {
/** How long? */
duration: number
/** What is the timer for? */
label?: string
}
export interface Config {}
/** What the body receives beside its arguments. */
export interface Context {
/** The person's own words, or the step's words in a plan. Empty when `evoke run` calls it by name. */
input: string
config: Config
/** Aborted at the deadline, on Ctrl-C or SIGTERM, when an app cancels the run, and when evoke ends: stop. */
signal: AbortSignal
}
/** What the body returns: the text a person reads, and data an app may use. */
export type Result = string | { text: string; data?: unknown }
/** The body: the default export of the file `run` names. */
export type Reflex = (args: Args, context: Context) => Result | Promise<Result>
Written for a file body only, and only when it changed.
A plan file
evoke try --save <file> writes one; evoke run <file> runs it. It is yours, wherever you put it; <name>.plan.json
is the recommended name. Written whole as JSON, its second line says what it holds:
{
"note": "Written by evoke try --save: the sentence and the classifier's answers, in clear. evoke run runs it exactly, or says which pin moved.",
"plan": 1,
"input": "pull september's bank transactions, invoices, card expenses and payroll, reconcile them, post the closing entries to the ledger, then send the report to cfo@example.com",
"weave": { "input": "…", "steps": [ { "n": 1, "text": "pull september's bank transactions", "decision": { "call": "bank month=\"september\"", … } }, … ], … },
"evoke": "0.10.1",
"adapter": { "name": "jev", "id": "jev-1.13.0" },
"gate": { "route": 0.5, "fits": 0.3, "read": 0.8, "write": 0.9, "whole": 0.3 },
"set": "h1:…",
"reflexes": { "bank": { "item": "h1:…", "path": "./bank" }, "ledger": { "item": "h1:…", "ref": "acme/books/ledger", "tag": "1.2.0", "h1": "h1:…" }, … },
"vocab": { "months": "h1:…" },
"answers": [ { "text": "pull september's …", "raw": { "weave.split_0": { "yes": 0.9 }, … } }, … ]
}
The file holds, in clear, the sentence, the words it used, and every number the classifier answered; it holds no
setting's value, no variable and no key, and no time and no name. A remote reflex is pinned by the lock's h1; a
local reflex's body is not pinned, as it is not locked. grep '"call"' lists the calls.
Machine-local files
These live outside every project, under XDG paths. The store, the answer cache and the test baselines are in
~/.cache/evoke/. The log, trust, the runtime path and the REPL's history are in ~/.local/state/evoke/.
Environment lists each.
The JSON line
--json prints one line per input. The line holds the input, the decision's fields flattened next to it, the
adapter calls made, and the result or the error when a body ran. It is the filter format. The log holds the same
line, with the adapter's raw answers and the input's candidates next to it. why reads that. The SDK's
Decision is the same object, plus values and plan.
Shape by outcome
| Field | run | confirm | ask | abstain | Holds |
|---|---|---|---|---|---|
input | ● | ● | ● | ● | The sentence as decided |
outcome | ● | ● | ● | ● | "run", "confirm", "ask", "abstain" |
reflex | ● | ● | ● | The winner's local name | |
args | ● | ● | ● | Per argument, a typed value; partial for an ask. A pick read from words that spell it out carries typed, the value as it is typed | |
basis | ○ | ○ | ○ | Per value read from the input, what it stands on, by by: ask, its own question; views, two questions that agree; view, one of them and the words that hold it; either carries anchored where those words, asked alone, name the value; words, the words and a yes; spelled, words that spell it out and a yes, with shape where the value was held against the argument's examples: whole, completed by a case or a separator the examples share, or open; only, the one candidate of its kind and a yes; text, a text typed without quotes, with the others it could have been; shared, a value another part of the sentence states, from its words, and a yes | |
call | ● | ● | The call on one line: lights room="den" state="off" | ||
effect | ● | ● | "read", "write", "destructive" | ||
confidence | ● | ● | ● | The weakest judgment's probability | |
weakest | ● | ● | ● | { question, top, p } | |
judgments | ● | ● | ● | ● | Every choice read: { question, top, p } |
contenders | ● | ● | ● | ● | The ranking: { reflex, route } |
runner_up | ○ | ○ | ○ | The second reflex, when there is one | |
prompt | ● | { own, reason, template }: evoke's line, why the call waits in one line, and the reflex's question | |||
left | ○ | ○ | ○ | The words of the input that no value holds, each with what it does: { words, does, p }, does one of action, answers with its arg, result, nothing, more; cut is true where the words follow a value and may be part of it, again where they say the call is wanted once more, read where a value of the call was read from them | |
whole | ○ | ○ | ○ | How far the call holds all the input says, where the call was read back against it | |
quotes | ○ | ○ | ○ | The texts in quotes that no value took, each with its marks, where the reflex takes a text in quotes | |
because | ● | Why it stopped, in order: destructive, no_gate, under_floor, one_view, respelt, text_read, more, cut, quoted, text_left, whole, detail, named, pointed | |||
unconsumed | ○ | ○ | ● | Typed spans no argument took | |
missing | ● | Per missing argument: arg, ask, because (unstated, out_of_range, not_offered, unsettled or unread), words when the input holds words that answer the ask, choices; a pick's choices.recent lists the values recalled from this session's results, when any, and its choices.readings what the words that answer the ask read as, when they say a value aloud; likely, the listed word made ready for a yes, when there is one | |||
trace | ● | ● | ● | ● | One entry per adapter call, a round of questions each: { adapter, questions, ms }. Empty when the answers came from the cache |
result | ○ | { text, data? } when the body ran | |||
contained | ○ | Whenever the body ran, with result or error: whether this machine held it to its declaration, { "type": "full" }, or "partial" or "none" with why, one sentence | |||
error | ○ | The failure's message when it did not. The line is all that prints; the exit is 1 | |||
frames | ○ | With error, the frames of the error a JavaScript body threw, from its first: at body (throws.mts:1:1) | |||
cancelled | ○ | true when Ctrl-C ended the body; a step of a weave says it as status and why instead |
A sentence read as several steps (Weaving) prints one line per step, as each runs, the
line of its decision with these fields more: step and steps first, the step's number and the count; bound
after trace, where a value came from another step, [{ arg, from, field, value }], value absent for a whole
result, which stays on its source's line as result.data; shared, where the sentence said a word once for
several steps and this one took it, { "<arg>": { "word", "via" } }, via fill for a required argument and
rewrite for an optional one written into the step's words; beside, where a day of the month was read beside
the day of a step this one takes from, { "<arg>": <step> }; from, where a playbook wrote the step,
[{ playbook, step, slots }], outermost first; when, where the step runs only under a value an earlier step
yields, { step, field, is }; and status last, with why when the step stopped. A plan a
playbook wrote prints the sentence's own line first: step 0, steps the count, the decision that picked the
playbook, playbook and slots, and no status, so a filter counting lines per input sees one more. A request
that is only what not to do prints one abstain line over the whole input, with no judgments and no contenders,
and because [{ "type": "nothing_to_do" }]; a request refused before its plan was made prints the same line
with the reason, conditional with its text. A plan stopped before any step ran prints every step's line at
once, its status refused, skipped, declined or unanswered. A step run from a plan file carries pinned too, { "file", "id" }: the
file as shown, ~/month.plan.json, and the SHA-256 of what was read, h1:…. evoke try --json prints the plan
whole instead, on one line: input, steps with each step's decision, binds, stages, verdict, exclusive,
excluded, splits, count, and trace, every adapter call the plan took.
| Field | Holds |
|---|---|
status | "ran", "failed", "declined", "refused", "skipped", "unanswered" |
why | { "type": "earlier_step" }, { "type": "nothing_to_take", "from" } and { "type": "too_large", "from" } with the source step, found_nothing, { "type": "not_chosen", "from", "value" } for a step another value picked past, no_reflex, cancelled, { "type": "read_as", "reflex" }, or { "type": "said", "message" }: a prompt's own line, a failure, a question no one answered |
In the plan evoke try --json prints, a step's refs count the steps they may name from 0; step, after,
stages and binds count from 1. A step carries shared and beside as the line does, typed where the plan wrote its
words anew, the words as typed, and repair where it was not one part decided on its own: narrowed or
spliced, an item of its neighbour's task; merged, a part read with its neighbour; split, a part of a joint
the classifier read as one thing; corrected, a part that says what not to do read with its step; named, a part
that matched nothing alone, read as the reflex the whole sentence gives its words, its decision judging the reflex
by that question, weave.span_<start>_<end>, in place of route. spliced is also a part put in the words of
the step before it: one that holds only values, or one that says the same for another value. So is each step
written for one of the things that words like both point at. Its decision's because holds pointed,
{ "type": "pointed", "arg", "words" }: the argument, and those words as typed. first and verb are two verbs
before one thing: the first read without the second, and the second a step of its own that takes from it.
apart is a step read for the one value its call holds, with its words as typed in typed. again is the step
before it written again for another value its words name, with that value in place. Its decision judges the
reflex by the question whether the request asks for that value as well,
weave.again_<start>_<end>_<reflex>__<argument>, in place of route. The answers are also where it does,
instead where it means the value in place of the one read, out where it rules the value out, and nothing
where the words say something else. A binding's
via is fill, rewrite or takes, a whole result by the name its source returns, with no kind; the
verdict's because names no_source and several_sources where such a result stops the plan. Each of
splits carries cut where the plan cuts the sentence there; count, where the sentence could be cut, holds
one, how surely it asks one thing, and as_one where it stands as one step for it. A plan carries folded,
[{ text, into, gave, picked }], the parts of the sentence folded into a step — a call typed again, or a step a
playbook wrote — with the arguments the part gave and the word of it that picked the step among several; and
asides, [{ text, remark, does, by }], the parts that ask for nothing: a remark set aside, or words that may add
a detail to a step, does the share of each answer, { aside, detail, asks }, and by where the part was set
aside by its words alone, own for your own action or courtesy. A plan a playbook wrote
carries from on each of its steps and when on a step that may not run; its verdict's because holds
reviewed per playbook, with the step, the
sentence's text and its prompt, and names what refused it: nested, too_deep, too_long, conditional,
excluded, or, for a branch, no_field, bad_value, maybe_source or branch_into_plan.
A call by name, evoke run --json, prints the same line with nothing judged: input is empty, there is no
confidence, weakest, judgments or contenders, trace is empty, and result or error says what the body
did. A destructive call carries prompt and because like any confirm. Nothing was decided.
The plan file
evoke try --save <file> writes the plan whole as JSON, readable without a tool
(Files): note, fixed text; plan, 1; input, the sentence as typed; tags, when
any; weave, the plan as try --json prints it less trace; evoke, the version that wrote it; adapter, the
lock's { name, id }; gate, the adapter's floors, absent when it declares none; set, the plan digest;
reflexes, per active reflex item, the hash of its wording, consent and settings, with path for a local
one, or ref, tag and h1 for a remote one, or nothing more for one handed to the SDK as code; vocab, each
vocabulary's hash; answers, every classifier answer the plan took, { text, raw } each, the whole sentence's
own questions first. run <file> refuses the file when any of these differs here, with the line and the fix.
Values
{ "type": "option", "key": "off" }
{ "type": "word", "word": "office", "value": "group-7" }
{ "type": "pick", "span": { "start": 18, "end": 28, "text": "10 minutes" }, "value": { "type": "duration", "value": 600 } }
{ "type": "flag" }
A pick's value is what a recognizer read: the number, the seconds, the text; a date's reading, { "type": "offset", "days": 1 }, { "type": "weekday", "weekday": "friday", "which": "next" }, { "type": "day", "day": 14 } or { "type": "calendar", "year": 2027, "month": 5, "day": 4 }, resolved to a day only when the body runs;
a time as "17:30"; an amount as { "amount": 1200, "currency": "EUR" }; a code as its text. Its span is where it was read,
in character offsets of the input.
Examples
A run, from evoke try --json:
{"input":"kill the lights in the den","outcome":"run","reflex":"lights","args":{"room":{"type":"word","word":"den"},"state":{"type":"option","key":"off"}},"call":"lights room=\"den\" state=\"off\"","effect":"write","basis":{"room":{"by":"views","ask":0.92,"reader":0.92},"state":{"by":"views","ask":0.94,"reader":0.94,"yes":0.95}},"left":[{"words":{"start":0,"end":15,"text":"kill the lights"},"does":"action","p":0.9}],"whole":0.9,"confidence":0.92,"weakest":{"question":"lights.room","top":"den","p":0.92},"judgments":[{"question":"route","top":"lights","p":0.95},{"question":"lights.room","top":"den","p":0.92},{"question":"lights.state","top":"off","p":0.94}],"runner_up":{"reflex":"timer","route":0.01},"contenders":[{"reflex":"lights","route":0.95},{"reflex":"timer","route":0.01},{"reflex":"volume","route":0.01}],"trace":[]}
An ask, from a filter with no terminal. The line stands for the prompt that could not be shown, exit 3:
{"input":"kill the lights","outcome":"ask","reflex":"lights","args":{"state":{"type":"option","key":"off"}},"basis":{"state":{"by":"views","ask":0.74,"reader":0.74,"yes":0.95}},"left":[{"words":{"start":0,"end":15,"text":"kill the lights"},"does":"action","p":0.9}],"unconsumed":[],"confidence":0.74,"weakest":{"question":"lights.state","top":"off","p":0.74},"judgments":[{"question":"route","top":"lights","p":0.95},{"question":"lights.state","top":"off","p":0.74}],"runner_up":{"reflex":"timer","route":0.01},"contenders":[{"reflex":"lights","route":0.95},{"reflex":"timer","route":0.01},{"reflex":"volume","route":0.01},{"reflex":"power","route":0.0}],"missing":[{"arg":"room","ask":"Which room?","because":{"type":"unstated"},"choices":{"type":"vocab","words":{"den":"The TV room downstairs; also 'the snug'.","office":"The upstairs study."}}}],"trace":[]}
Questions
A question id is route, fits.<reflex>, <reflex>.<argument>, or weave.<name>: a question evoke asks on
its own account, beside a reflex's, and no reflex is named weave. A choice's keys are option keys, vocabulary
words, <start>-<end> for a pick's candidates, yes and no for a flag, local names for the route, and the
sentinels none and unstated. On an argument's question, none says the input states a value that is not
among the choices.
evoke calibrate --json
One object, the report of Calibrating: adapter; records, reflexes, inputs,
repeats; outcomes with run, confirm, ask, abstain; bins, each { lo, hi, calls, right, interval, claimed, thin, over_confident }, a bin with no call left out; unknown; abstained as { count, right, interval }; bars with read and write where the effect has a complete call, each { bar, wrong, calls, per_thousand, at_most, near } and near a list of { at, run, wrong }, and whole where a call over its bar
was read back against the input, { bar, calls, run, wrong, per_thousand, at_most, held_right }; questions,
each { kind, judgments, right, interval, claimed } with kind one of route, options, vocab, pick, flag; brier with
brier, reliability, resolution, uncertainty, absent without a call; misses, each { case, outcome, reflex?, confidence?, mismatch, wrong }; under --repeat, variance with flips, verdict_flips, spread
as { median, p90, max }, straddling, wrong_at_bar, and moved, each { utterance, confidence?, route, winners, outcomes, wrong }; and log with adapter, decisions, ran, confirmed_ran,
confirmed_stopped, asked, abstained, failed, skipped, stopped_by_confidence as [{ lo, hi, count }],
records as { count, right, interval }, and unread. An interval is [low, high]; a rate is per thousand.
Rules of the wire
Keys are snake_case. An absent optional is omitted, never null. A tagged value carries type. A decision
carries outcome. Numbers are numbers: 0.85 and 0.850 are one value.
Environment
The variables evoke reads, and the paths it writes under. Nothing is read from a file it does not own, and
nothing is sent anywhere but the adapter you chose.
Variables evoke reads
| Variable | Read by | Means |
|---|---|---|
TYPESAFE_API_KEY | The jev adapter, when deciding | The classifier's key, from typesafe.ai. Never written to a file |
OPENJEV_API_KEY | The openjev adapter, when deciding | The classifier's key, from openjev.sh. Never written to a file |
CLOUDFLARE_API_TOKEN | The clef and clef_flash adapters, when deciding | The classifier's token, from dash.cloudflare.com. Never written to a file |
CLOUDFLARE_ACCOUNT_ID | The clef and clef_flash adapters, when deciding | The account the token belongs to: 32 hex digits in lowercase, which go into the address. Anything else is refused |
EVOKE_ANSWERS | The replay adapter | A recording to answer from, with adapter = "replay" in evoke.toml |
EVOKE_PLATFORM | Every decision, add, show, test | linux or macos: the platform a manifest's platforms is judged by, this machine's when unset. Set it to decide or test as another machine would; a body still runs here |
EVOKE_TODAY | A body's run, check | YYYY-MM-DD: the day a date argument resolves against, your machine's local day when unset. A value that is no day stops the run |
<VAR> of a --env setting | A body's run | A config value, most often a secret, resolved for that run only |
NO_COLOR | The terminal | Set and not empty: no colour. The spinner and the line editor stay |
TERM | The terminal | Unset, empty or dumb: no colour, no spinner, no line editor |
HOME, XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_CACHE_HOME | Paths | Where the project and the machine-local state live |
PATH | add, sync, every run | Where node is found and recorded, and where a declared program or an argv's program is found at each run; git too |
GIT_* | Fetching | Your git configuration and credentials apply; GIT_TERMINAL_PROMPT=0 is set |
HTTPS_PROXY, NO_PROXY | The classifier's connection | A proxy the connection goes through, in the CLI and the SDK, and the hosts that bypass it; https_proxy and no_proxy win over the upper-case names. An address that is not http or https is refused, never bypassed. The endpoint itself never moves |
Paths
| Path | Holds | Written by |
|---|---|---|
$XDG_CONFIG_HOME/evoke/ · ~/.config/evoke/ | The home project | you, add, teach, … |
$XDG_CACHE_HOME/evoke/store/<h1>/ | Fetched reflex directories, by content hash | add, sync |
$XDG_CACHE_HOME/evoke/answers/<plan>/ | The adapter's answers, per installed set, utterance and questions asked | every decision, try |
$XDG_CACHE_HOME/evoke/baselines/<plan>.json | evoke test's last verdicts per installed set | test |
$XDG_CACHE_HOME/evoke/runtime.json | Whether the node a file body runs under can keep it off the network, as Node 25 and later can; asked once per node binary | a file body's run, check |
$XDG_STATE_HOME/evoke/log.jsonl | One JSON line per decision; why reads the last | every decision |
$XDG_STATE_HOME/evoke/trust.toml | Trusted project roots and their content digests | trust, every write |
$XDG_STATE_HOME/evoke/runtime | The real path of node a file body runs under | add, sync |
$XDG_STATE_HOME/evoke/history | The REPL's lines, the last 1 000 | the REPL |
$TMPDIR/evoke-…/ | A body's private temporary folder, for one run | every run; removed after it |
$XDG_STATE_HOME defaults to ~/.local/state, and $XDG_CACHE_HOME to ~/.cache. Everything under cache can
be deleted. sync and the next decision rebuild it. No key and no config value is ever written under either. The
log and the history hold what you typed and what a body returned. Only you can read them.
What a body sees
A file body runs under a scrubbed environment of five variables: PATH, HOME, TMPDIR, LANG, TERM, with
TMPDIR a private folder made for the run. It receives its arguments, the input, its config and a signal
through the call, and touches what its manifest declares under [needs]: The body.
An argv body gets the same five, plus:
| Variable | Holds |
|---|---|
EVOKE_CONFIG_<KEY> | Each [config] key, upper-cased, as its value; a plain one under the home expanded |
EVOKE_INPUT | The person's own words; the step's words when a plan runs the reflex; empty under evoke run |
Secrets reach a body only this way, for the length of one run.
Time
One decision has 30 seconds, shared by the adapter's answer and the body's run. A prompt never counts. The jev
adapter gives a request 1.5 seconds once connected, and openjev 3 seconds, since OpenJEV forwards the request
onward. clef gives it 6 seconds and clef_flash 3. Each retries once after a connect error or a server error,
and waits out a 429 that says how long to wait, within the deadline.
Diagnostics
Every line that needs something from you has one shape: what is wrong, then the literal command that fixes it. It exits 3:
lights: vocabulary "rooms" is empty → evoke vocab rooms add <word> "<meaning>"
~/app is not trusted → evoke trust
Under --json, the decision line stands for a prompt that could not be shown. In the SDK, the same lines are the
message of a DiagnosticError. Each problem carries its fix and command.
The fixes
The set of fixing commands is closed. Each line ends in one of these:
| Fix | The problem it answers |
|---|---|
export <VAR>=<value> | A key or a --env setting's variable is not set |
evoke add evoke-build/reflexes | Nothing is installed |
evoke add <ref> --as <name> | A local name is taken, or a [reflexes] line names something not locked |
evoke vocab <name> add <word> "<meaning>" | A vocabulary is empty, or a word is not in it |
evoke vocab <name> add <word> "<meaning>" --value <path> | A word's value is not the path a declaration takes it for |
evoke vocab <name> remove <word> | A vocabulary offers more words than the classifier takes in one question |
evoke vocab <name> | A word to remove is not in the vocabulary |
evoke add <ref>… | The rest of a collection one taken name refused; a ref without a tag it has not, or without --as |
evoke config <reflex> <key> <value> | A declared setting is not set |
evoke config <reflex> <key> --env <VAR> | A secret is not set, or was set plain |
evoke show [<reflex>] | Something answered by looking: an unknown argument, an option not offered, an undeclared key, a reflex not installed |
evoke teach "<phrase>" not <reflex> | A newcomer steals a phrase an installed reflex claims, or fits it over the floor |
evoke update [<reflex>] | A name is already locked, a tag moved or vanished, or a retired adapter |
evoke update --accept <reflex> | An effect upstream loosened, or a declaration it widened, since you consented |
evoke sync | A reflex is not in the store, or no runtime is recorded |
evoke trust | The project is not trusted, or changed since it was |
evoke remove <reflex> | A local reflex's directory or manifest is missing; a fetched reflex reached past its declaration |
evoke check | A body does not exist, does not load, or exports no function |
evoke new <name> | No reflex.toml here, or the directory already exists |
evoke test | The theft test at add did not finish; the install stands |
mkdir -p <path> | A folder a body's declaration writes into is not there |
evoke --help | The arguments spell no command |
<file>:<line>:<column> | A line of an owned file to edit: the schema, a contract key in an overlay, a loosening effect, a local reflex's declaration to widen, or one naming a path that is not there |
| the command you ran | Try again once the reason on the line is addressed: a prompt with no terminal, a value out of range |
A body refused past its declaration, or a declared path the machine lacks, is a failure, exit 1, whose line
still ends in its fix: the manifest's line for a local reflex, evoke remove <reflex> or
evoke update --accept <reflex> for a fetched one, the value's source for a {name}, mkdir -p for a folder
the body writes into.
What lint reports
At evoke check and evoke add, lint reports and never refuses. Each line starts with lint, names the reflex
and says what to change. The text is yours to weigh:
| Line | Means |
|---|---|
the summary is 120 characters; the cap is 100 | A size passed its cap: the summary, the description, not_for, the options, the records, a sentence or a step |
description addresses the model: "you must" | A phrase speaks to a model instead of describing the action |
effect is absent, which means destructive; write it | The manifest leaves effect out, and a reader cannot see what that means |
confirm reads no value back; name {release} | The reflex has a required argument, and the confirm names none |
confirm asks "Are you sure"; say what the call will do | The confirm asks for a yes without saying what it covers |
args.state.options.on repeats the ask; a meaning answers it | An option's meaning is the question again |
args.label reads text in quotes, and no example shows them | A quoted argument, and no example holds a quotation mark |
args.level has no record that leaves it out; assert level = false once | No example or test asserts the option or the pick unstated |
examples holds 2 requests; write three at least | Fewer than three examples state a request of the reflex |
step 4 holds "then"; one step is one action | A step holds a connective or a comma |
step 6 states "#incident"; a word another team would change is a slot | A step writes in a channel or an address |
step 3 says "check that writes"; a step refers by "that <noun>": say "check whether" | A step opens with check that |
step 1's words are the summary's or an example's; word a step apart from the situation | Every word of a step that says something is in the playbook's own summary or examples |
evoke add prints two lines more for a playbook, once its steps are decided over the set it joins:
| Line | Means |
|---|---|
claims write; its steps reach destructive | A step reaches a reflex with a tighter effect than the playbook claims |
step 3 reaches logs, which lacks the tag outage | A request narrowed with --tag outage would leave that step without its reflex |
Why a reflex is inactive
An inactive reflex is left out of every decision, and show lists why, one line each:
| Line | Means |
|---|---|
runs on macOS only | The manifest's platforms leave this machine out |
vocabulary "rooms" is empty | An argument names a vocabulary with no words |
config "bridge" is not set | A declared setting has no value |
config "token" is a secret; it is set from a variable | A secret was given plain |
HUE_TOKEN is not set | A setting names a variable the environment lacks |
| an overlay line, with its file and position | A contract key, a loosening effect, or a parse error in your overlay |
| a manifest line | The shipped manifest does not read |
A missing store entry or runtime is not inactive but a stop. The fix is evoke sync. An effect upstream loosened
is not inactive either. The reflex runs under the effect you consented to, until you accept.
Failures and faults
A body that throws, exits non-zero, or overruns the deadline is a failure. Its message prints, exit 1. A
thrown error's frames are kept for its author: evoke why shows them under the decision, and --json carries
them as frames. An adapter that cannot be reached, returns an error status,
or answers something the core cannot validate is a fault, exit 4. A key the classifier refuses is one, and its
line ends in the export that replaces the key. The core fails closed rather than guess. Both name what to do
next. A path under your home prints as ~/….
Glossary
Every term evoke uses, one line each: the outcomes of a decision, the parts of a reflex, the files of a project, the
adapter, and the words of a weave.
| Term | Meaning |
|---|---|
| Abstain | The outcome when none wins the route, or the winner is under the route floor. Exit 2 |
| Account | What each run of the input's words that no value holds does: says what to do, answers an argument's ask, says what is wanted from the result, asks for nothing, or asks for another thing. why names the words that answer an ask, those a call leaves out and those about the result; the JSON line carries them as left |
| Adapter | The classifier behind a decision, as an object answering typed questions with probabilities. jev and openjev reach Jev, through TypeSafe AI's API or through OpenJEV; clef and clef_flash reach Clef on Cloudflare's Workers AI; replay answers from a recording |
| Argument | A question about the input and a value for the body: options, vocab, pick or flag |
| Ask | The outcome when a required argument is missing: evoke asks the argument's own question |
| Binding | A value of one step's result taken by a later step, by kind: filled into a required argument, or written into the words for an optional one; or the whole result, by the name its source returns, handed beside the decision |
| Body | What run names: a .mts/.mjs file exporting a function, or a program with its arguments; a playbook has steps instead |
| Branch | Steps of a playbook that run only under a value an earlier step yields, { say = "…", when = { landing = "yes" } }, listed right after that step; the plan prints every one, the result picks, the rest are skipped clean |
| Call | A reflex with its arguments filled, on one line: lights room="den" state="off" |
| Cap | A reason a complete call stops at confirm: destructive, no gate, under the floor, a value read from your words another way, words that ask for more, a call that holds less than you typed |
| Change | A reflex whose effect is write or destructive: it changes something, where a lookup only reads |
| Collection | A repository of reflex directories |
| Confidence | The lowest probability among the route and every value the call holds |
| Confirm | The outcome when a call is complete but capped: [y]es [n]o [t]each |
| Contract | What a user cannot override: run or steps, argument names and sources, option keys, ranges, config keys, what a reflex yields, returns and takes |
| Effect | What running a reflex does: read, write or destructive. Absent means destructive, so a manifest always writes it |
| Effective manifest | The shipped manifest with your overlay merged in; what show <name> prints |
| Fits | The yes/no question asked when a reflex is added: does it do what another reflex's example asks? At or above its floor, add names the example |
| Flag | A yes/no argument. The body receives true or nothing |
| Floor | The bar a decision must clear. A threshold the adapter ships, meaning the probability this is right: route, read, write, whole, and fits at add |
| Fold | A part of a sentence that repeats a step a playbook wrote: removed from the plan and named under it, folded "…" into <n> |
| Gate | The step that turns confidence and effect into an outcome |
| h1 | The content hash of a reflex directory, h1:<sha256>, in the lock and the store |
| Home project | ~/.config/evoke/: the project used when no evoke.toml is found upward |
| Hub | Where reflexes are found; today the collection evoke-build/reflexes |
| Identity | How utterances are keyed: NFC, lower-cased, whitespace collapsed, trailing punctuation dropped |
| Inactive | A reflex left out of every decision until a problem is fixed: an empty vocabulary, an unset setting, an overlay that does not read |
| Local name | The key under [reflexes]: what the classifier reads and what the overlay file is named after |
| Lock | evoke.lock: per remote reflex, the ref, tag, commit, h1 and consented effect. Also the adapter |
| Lookup | A reflex whose effect is read: it finds or shows something, and changes nothing |
| Manifest | reflex.toml: what the classifier reads and what the body receives |
| Near neighbour | What a person might type that sounds like a reflex's request and is not: named under not_for, and written as a false test |
| Overlay | overlays/<name>.toml: your wording for one reflex, merged over the shipped one |
| Pick | An argument read from an exact span of the input: number, duration, email, url, quoted, date, time, amount, code |
| Pin | A question evoke asks the classifier on its own account beside a reflex's own, keyed weave.<name> in a recording: a second view of a listed word, a yes on a word of the input, what a run of words does, whether a call holds all the input says. Also what a plan file is checked against before it runs: the evoke version, the adapter's id and gate, the set's digest, each reflex and vocabulary by hash, a remote reflex's h1 |
| Plan | The installed set compiled to questions. Its digest keys the cache, the baselines and every decision |
| Plan file | A weave saved by try --save: the sentence, the classifier's answers and the pins it was decided under; run <file> runs it exactly, or names the pin that moved |
| Playbook | A reflex whose body is a plan: steps, one sentence per step with {slots}, reached by a short sentence and decided step by step over the installed set |
| Project | A directory of files you own: evoke.toml, evoke.lock, overlays/, vocab/ |
| Recalled | A value an earlier body of this session returned under the field a pick's recent names, listed at the ask and chosen by you |
| Record | One line of [examples] or [tests]: `"utterance" = { assertions } |
| Recording | An answers.toml: an adapter's declaration and its answers by utterance identity |
| Ref | Where a reflex comes from: owner/repo[/dir][@tag], a git URL, or ./dir |
| Reflex | A directory: a manifest and the file or program it names. Plural: reflexes |
| Returns | The name a body's whole data goes by, returns = "deploys", for a later step of one sentence to take |
| Route | The one choice over every installed reflex plus none |
| Run | The outcome when a call clears its floor. Also the key naming the body |
| Runtime | What runs a body: Node for a file, nothing for an argv |
| Situation | What a person faces, typed in one sentence, a problem or a routine: checkout is failing in eu-west, close the month. A playbook is reached by its situation |
| Slot | An argument named in a step of a playbook, {service}: filled from the sentence or asked before the plan prints; in square brackets, written only when stated |
| Span | An exact piece of the input, by character offsets |
| Step | One part of a weave, decided as one input is, a playbook's sentences among them; numbered as the plan prints it, run at its turn |
| Store | ~/.cache/evoke/store/<h1>/: fetched code, re-hashed before every run |
| Tag | A word under tags for --tag to narrow by. Also a git version tag |
| Takes | An argument an earlier step's whole result fills by that name, takes = "deploys": never asked, never stated, the plan alone hands it |
| Trust | A project's four owned paths bound to their content. Required outside home |
| Unstated | The answer that an argument was not given in the input |
| Utterance | A sentence a record is keyed by |
| View | One of two questions about a listed word, the argument's own and a second one in other words; a value both give stands, and why says asked a second way. Where they differ, the input's own words decide, or the call waits for a yes |
| Vocabulary | vocab/<name>.toml: your closed list of words, shared by every argument that names it |
| Weave | A sentence read as several steps: each decided on its own, ordered by the words, a result threaded into a later step, the plan shown before anything runs |
| Wording | What a user may override: descriptions, questions, option meanings, records |
| Yield | A field of a result's data a later step may take, declared under [yields] with the kind that reads it |
The collection
evoke-build/reflexes is the first-party collection: fifteen reflexes for what a Mac does at a word. Each
reflex is one directory, with reflex.toml and the file it runs, where it runs one. There is nothing to build.
Thirteen of the fifteen run a file and need Node 24 or newer on your PATH; open and trash run a program
the Mac has. Every one follows the rules, so each is a manifest to copy from.
$ evoke add evoke-build/reflexes
| Reflex | Does | Effect | Reads from your words | Yields | Yours to set |
|---|---|---|---|---|---|
awake | Keeps the laptop awake for a duration, or until pkill caffeinate | write | duration, a duration, optional | ||
download | Saves a URL's file to ~/Downloads, or to a place you name | write | url, a URL; to, one of your places, optional | path | places |
lock | Locks the screen | write | |||
mail | Starts an email in your mail app | write | to, an address; subject, text in quotes, optional | ||
note | Appends a dated line to your notes file | write | text, text in quotes | file | |
open | Opens one of your folders | read | place, one of your places | places | |
power | Restarts or shuts down | destructive | action, restart or shutdown | ||
screenshot | Captures the screen, a window or a selection | write | area, screen, window or selection, optional; clipboard, a flag | path | |
sleep | Puts the laptop to sleep | write | |||
sound | Mutes, unmutes, or turns the volume up or down | write | state, off, on, up or down; by, a number of points, optional | ||
timer | Counts down, then rings | write | duration, a duration; label, text in quotes, optional | ||
trash | Empties the trash | destructive | |||
visit | Opens one of your sites, in a private window on request | read | site, one of your sites; incognito, a flag | sites | |
volume | Sets the output volume | write | level, a number from 0 to 100 | ||
wifi | Turns Wi-Fi on or off | write | state, on or off |
Yours to set
A reflex that needs your words or a setting for a required argument stays inactive until it has them. evoke
says which line gives it what it needs. download runs without places; with it, a place you name. Two
vocabularies and one setting cover the collection:
$ evoke vocab places add desktop "The desktop." --value "~/Desktop" # a word per folder; the value its path
$ evoke vocab sites add github "GitHub." --value https://github.com # a word per site; the value its URL
$ evoke config note file "~/notes.txt" # a path under your home, or absolute; a file that exists
What they pass on
A reflex may declare what its result holds, for a later step of one sentence to take (Weaving).
download and screenshot yield path, the file they saved to, a quoted text: a later step with a quoted
argument your words left empty can take it, as note does in download https://evoke.build/llms.txt and note it.
No reflex in the collection returns a whole result under a name yet, and none takes one.
What they say back
One lowercase line that says what happened: volume 40% · muted · locked · sleeping · saved ~/Desktop/Screenshot 2026-09-20 at 10.31.05.png · copied to the clipboard · eggs: 3 minutes, rings at 10:34 AM · awake for 2 hours ·
noted "buy milk" in ~/notes.txt · new mail to ana@example.com about "friday" · opened https://github.com in a private window · saved report.pdf to ~/Downloads (1.2 MB).
How they are built
- macOS, one self-contained file each: every manifest names it, so on Linux each reflex is inactive, and
evoke addsays so.openandtrashare argv reflexes and need no runtime. Each manifest declares what its body touches under[needs], andevoke addprints it under the row:screenshotwrites~/Desktopand runsscreencapture.wififinds the Wi-Fi device by its port's name:en0on a laptop, often not on a desktop with Ethernet. - What outlives a run detaches and returns at once.
awakeleavescaffeinaterunning.timerleaves a script that waits, then notifies with a sound. sleepsleeps withpmset.powersends the login window's own restart and shutdown events. So apps are asked to quit, and no second dialog appears, since the decision was confirmed already.lockopens the system's lock screen and needs no Accessibility permission.downloadnever writes over a file already there. A download the 30 s deadline cuts short is removed, and the run says so. A body that runs a program reports the program's own words when it fails.visitopens a private window through the default browser's own flag, in Chrome, Brave, Vivaldi, Edge or Firefox. Safari opens none from a script.- Every reflex writes its
effect, carries at least three examples, and has its near neighbours asfalsetests, in words no example uses. One record leaves out each option and each pick. The reflexes name each other undernot_for:timeris not for keeping the laptop awake, andawakeis not for timers.
Licence and changes
The collection is MIT, since its scripts are meant to be copied into reflexes of your own. Issues and changes go to evoke-build/evoke. The collection is developed and published from there.
Security
A fetched reflex runs as you, held to what its manifest declares. evoke add and evoke update are the trust
decisions. Everything below is what the tool guarantees around them, and what it does not.
What runs, and as whom
- A body runs with your user, under a scrubbed environment of five variables:
PATH,HOME,TMPDIR,LANG,TERM.TMPDIRis a private folder, made for the run and removed after it. The body receives secrets only through its config, for one run. The scrubbing prevents leaks. Read what you install, and its declaration, the next point, with it. - A body is held to its declaration.
[needs]in the manifest names the paths the body reads and writes, whether it reaches the network, and the programs it runs: What the body touches. Leaving the table out is the tightest declaration: the body's own directory,TMPDIR, and nothing else. Two layers hold the body there. The kernel's: on Linux a Landlock ruleset,no_new_privsand a seccomp filter that closes the network, applied to the process before the body starts; on macOS Seatbelt, throughsandbox-exec, with a profile that denies everything the declaration does not name. And, for a file body, Node's permission model, which refuses a read past the declaration with the path before the kernel is asked. A program named inrunsruns under the kernel's layer too, so it reaches no further than the body; what it asks the system to do — Finder emptying the trash, the login window restarting — happens outside the declaration, soruns osascriptis read beside the effect.evoke checkimports a body under the same hold, with nothing configured. - A reach past the declaration ends the run. The line names what was reached and the key:
~/secret.txt is not in [needs] reads,example.com is not in [needs] hosts,[needs] runs names no program. Its fix depends on where the declaration is written: the manifest's line for a local reflex; for a fetched one,evoke update --acceptwhen upstream's own declaration already allows it, elseevoke remove. The run exits 1, a failure. - The status is printed, never assumed. A machine that holds only part of a declaration, or none of it — a
Linux kernel without Landlock or before its third version, a Mac without
sandbox-exec— still runs the body, and says so: one line ataddand atshow,not contained <why>;· partly containedor· not containedat the end of the lineevokeprints before every body runs; andcontainedin--json. - No code runs at install time.
addfetches a tree, reads a manifest, lints it, and writes files. A body runs only at a decision, atevoke run, or whenevoke checkimports it to see that it exports a function. - A reflex handed to the SDK as code is your application's own. It runs in its process, unscrubbed and unheld.
It must honour its
signal, since nothing can end it from outside. A reflex the SDK loads from a project runs held as far as the package can: under Node's permission model on both systems, and under Seatbelt on macOS. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held and an argv body unheld there, and the result'scontainedsays which.
What the classifier cannot do
- State is only
{ request }. The adapter sees the sentence and the questions built from installed manifests. Nothing else of yours. - Arguments are closed sets, exact spans, or an earlier step's result. A value is an author's option key, one of your vocabulary words, a piece of the input read by a recognizer and checked against its range, or, in a sentence of several steps, a field of an earlier step's result or that step's whole result, bound by the plan and shown on its line before anything runs; or, at a prompt, a value a body of this session returned under a field the author named, listed for you to choose and read by the recognizer. Injected text can choose a call. It can never mint a value. A date reaches a body as a day of your machine's calendar, never as a word.
- The effect gate covers what it chooses. A destructive reflex always confirms, and there is no
--yes. Unattended use is a threshold in a file you own and trust. Areadorwritereflex over its floor runs without asking. Whatever it does with a span it is handed, like a URL or a quoted text, an injected sentence can make it do. So its effect is the author's promise about exactly that. Awritereflex that takes an earlier step's whole result runs with it over its floor, unasked; the plan printstakeson its line first, and a destructive reflex takes none. - Prompts cannot be repainted. Control characters and bidi overrides are refused in every manifest, overlay
and vocabulary string, and in every span.
evoke's own line, with the call, the effect and the weakest judgment, prints before the reflex's template. - An input over 2 000 characters is refused, and one decision has 30 seconds.
What a manifest cannot do
- Wording is linted and conflict-tested at
add. Text that addresses a model is named. Every phrase a newcomer would steal from an installed reflex is reported, with its one-line fix. - Effect and needs only tighten. The lock holds the effect and the declaration you consented to. Upstream
may tighten either at any tag. Loosening waits for
evoke update --accept, which says what widened. Your overlay may tighten the effect, never loosen it, and cannot touch[needs]. - The contract cannot be overridden. An overlay that tries makes the reflex inactive, not looser.
- Tags are only for
--tag. No label, category or claim in a manifest changes what may run.
What the files guarantee
- Trust binds to content. A project outside home decides only after
evoke trust. The digest of its four owned paths is checked at every run. Agit pullthat changed an overlay is a stop, not a surprise. A local reflex,./dir, is outside those paths. Its directory is yours, so a pull that changes it runs what it now holds, under the effect its manifest now claims. - The lock binds to content. Every remote reflex is pinned by tag, commit and
h1. The store's copy is hashed again every timeevokestarts. A tag that moved is refused. - Fetching is narrow.
gitis called with a strict ref grammar,--end-of-options, and an allow-list ofhttpsandssh. Symlinks and submodules are rejected. There is no checkout, no hook, and no clone-time script. - Secrets are referenced, never stored.
evoke.tomlnames a variable. The value exists in your environment, and reaches a body for the length of one run.evokewrites no key and no config value under the cache or the state directory. The log and the REPL's history hold what you typed and what a body returned. Only you can read them. - Adapter names resolve on your machine only, against the tool's built-ins, never from a project directory.
So a cloned repository cannot point your decisions at a classifier of its choosing. The endpoint is built into
the adapter, whose id keys the cache and the lock, so no file and no variable moves it. The
clefandclef_flashadapters put one thing from a variable into their address, your account's id at Cloudflare. It must be 32 hex digits and nothing else, so it picks an account at that one service and never another address. A proxy named in your environment carries the connection there and sees ciphertext: the CLI trusts Mozilla's roots, never your platform's, and the SDK trusts what your Node trusts. - The SDK never searches.
loadtakes an explicit root, never fetches, and never writes. It hands each tenant their own vocabulary. So a server cannot be hijacked by whatever project a working directory holds. - A plan file holds the sentence and the classifier's numbers, in clear, and no setting's value, variable or key. It can choose among your installed reflexes and your words, never mint a value, and it runs only after your yes over the whole plan and each step's own confirm; a file whose pins moved is refused before anything runs.
- A playbook reaches only what you installed. Its steps are sentences, decided over your reflexes as typed ones are, under the same gate; it fills no value of its own, and it runs only after your yes over the whole plan, a destructive step confirming again at its turn.
Check it yourself
Nothing on this page needs a key to test. evoke run <call> runs a reflex by name and calls no classifier. The
replay adapter answers every decision from a file you write: set adapter = "replay" in evoke.toml and name
the file in EVOKE_ANSWERS. Testing shows the file. An answer that
tries to mint a value, a key no argument offers or a number that is not a probability, ends the decision with a
fault, and nothing runs. The repository's spec/ holds the transcripts and vectors behind the claims above, and
mise run test runs them without a key. The contained transcript shows a body refused past its declaration,
and a declared path the machine lacks refused before the body runs.
Reporting
Report a vulnerability privately, through GitHub's report form, never in a public issue.