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
./dir, ../dirA local directory, relative to evoke.toml. Never fetched, never locked

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.

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>

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. Symlinks and submodules are refused. The tree is kept in the store.
  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 phrase a newcomer wins prints as <thief>: steals "<phrase>" from <owner> → evoke teach "<phrase>" not <thief>. The add still proceeds. The fix is one line in your overlay.
  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, with the --as line that fixes it.

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. For each move, it prints the level of the change and one line per detail:

LineMeans
same, minor, majorWording only; additions; 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 destructive upstream; write kept until you acceptUpstream loosened it; you keep what you consented to

The lock holds the effect you consented to. Upstream may tighten it at any time. It loosens only through evoke update --accept <name>, which takes the looser effect at the current tag and says so. 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 locked reflex at its locked tag, checks that its tree hashes to the lock, and records the runtime. It prints a + row per reflex placed. It never changes the lock. A tag that moved or vanished is refused, with evoke update <name>.

$ evoke sync
+ lights  radhi/home/lights 1.2.0  write  runs lights.mts

trust

Your home project is trusted by construction. 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 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 evoke sync and exit 3, after its rows. An argv reflex needs no runtime.

Next: Tuning.