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, or nothing for an argv
└── 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.
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.6, write = 0.8 } # each a probability; read ≤ write
Local names match [a-z][a-z0-9_]*. The names none, unstated and fits 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 makes the reflex inactive. That means run, a new argument, a new option key, a source, a range,
or config.
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 Flag = { type: "flag" }
export interface Reflexes {
timer: {
/** How long? */
duration: Pick<"duration", number>
/** What is the timer for? */
label?: Pick<"quoted", 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 input as it was typed. */
input: string
config: Config
/** Aborted at the deadline or on a decline: stop, never guess. */
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.
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.