Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

FormMeans
owner/repoEvery reflex directory in the repository, on GitHub
owner/repo/dirOne reflex, the directory dir of the repository; dir may nest
owner/repo/dir@1.2.0Pinned: 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.0Any git host over https or ssh. #dir and @tag are optional
ssh://git@github.com/owner/repoThe same over ssh, the user before the host
./dir, ../dirA 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:

  1. 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 to evoke.toml as a path relative to that file. It is never fetched and never locked.
  2. 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_for entries, 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.
  3. 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, and evoke test runs it again.
  4. Writes the [reflexes] lines to evoke.toml, the lock, and evoke.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:

LineMeans
same, minor, majorWording only, or a declaration narrowed; additions, a declaration widened among them; something your files or calls may not survive
· code changedA file other than the manifest changed
description rewritten, takenYou have no override, so upstream's new text is used. Otherwise: yours kept
args.state renamed power; your 1 example followsThe author declared the rename; your records follow it at merge time
staleYou override something upstream changed: yours wins, both are shown once
orphanedA line of yours addresses nothing any more: skipped, the rest applies
effect tightened to writeUpstream tightened the effect; it applies
effect write upstream; destructive kept until you acceptUpstream loosened it; you keep what you consented to
needs narrowed: hosts * droppedUpstream narrowed what the body touches; it applies
needs widened: writes ~/notes upstream; none kept until you acceptUpstream 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.

FAQPrivacyGitHub