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.