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.