Guides
Task guides
Five things you will actually do with EvoPet, each one with what it needs first, the steps in order, what a correct run looks like, and the specific way it fails. Every command and number is taken from the project's own files, which are named at the end of each guide.
-
Choose your evolution gates
Pick the levels your pet changes form at, and predict what that costs in turns.
-
Publish a pet
Mint a key, publish a package, and know which status your pet lands in.
-
Install across all profiles
One command for the default profile and every named profile on the machine.
-
Float the pet on the desktop
Mirror the pet into the desktop shell, and know what the mirror does not do.
-
Read your pet’s state
Where the ledger lives, what `status` reports, and what it will not tell you.
Choose your evolution gates
Pick the levels your pet changes form at, and predict what that costs in turns.
Before you start
- A checkout of the pet tree. The pet needs Python and nothing else: Pillow ships with Hermes.
- Clear on who chooses: EvoPet fixes the ladder, and you fix how many times your pet changes form and at which levels.
Steps
-
1. Pick levels, not XP amounts
A gate is a level your pet reaches; the XP it costs is whatever the fixed curve says that level is worth. One to four gates, strictly increasing, levels 1–99. Keep the first gate above level 1: a gate at level 1 fires at 0 XP, so the pet starts already hatched and its first form is never drawn.
-
2. Price the gates in turns
xp_for_level(L) = round(100_000 × ((L − 1) ÷ 98)²), and a good turn is about 18 XP. Gates at 20, 40 and 60 cost 3,759, 15,837 and 36,245 XP — about 209, 880 and 2,014 turns. Gates below level 30 feel fast; gates above level 50 do not. -
3. Declare them in the manifest
In the
pet.jsonthe install will replace:"evopet": { "evolutionGates": [20, 40, 60] }. Form names are positional and not yours to choose, starting ateggand runninghatchling,child,teen,adult. -
4. Install, which is when the gates take effect
./hermes/install-hermes.sh --line toast --machine aurora. The installer absorbs the manifest it is about to replace into the ledger (pet_compiler.sync_ledger_gates), so the newpet.jsoncarries the ledger's gates and the running pet evolves on the ledger's copy, not on the manifest. -
5. Check the table before you ship the pet
The gate control on the mechanics page recomputes the table for 1–4 gates from the published curve, and
levels.gate_report(gates)prints the same table from the same formula.
Gates at 20, 40 and 60 give four forms — egg, hatchling, child, teen. Three gates cannot reach adult, and that is the contract rather than a bug. The default pet's gates, 11/23/32/45, are 1,041, 5,040, 10,006 and 20,158 XP, which the gate control on the mechanics page reproduces exactly. Gates installed at a level become a floor in the ledger, and XP only rises, so a pet cannot regress out of a form.
A block that is present but invalid is refused at install, and the error names the file:
pet.json: evolution gates must strictly increase; 20 follows 40. Out of range
(>99), not increasing, zero gates, or more than four gates are all refused by the same
validator. Silence is not a declaration, so "evopet": {} leaves installed
gates alone. Editing a manifest does not retroactively move a growing pet: the change lands
on the next install, not on the next event. And nothing cross-checks your gates against
your spritesheet when you declare them — a line with no form for a stage your gates
imply fails later, when the pet grows into it.
Source. docs/evopet/levels-and-evolution.md, tamahermes/levels.py
Publish a pet
Mint a key, publish a package, and know which status your pet lands in.
Before you start
- A library service to publish to; its routes are listed at the endpoints.
-
An owner key, minted once with
npm run key:issue -- --email you@example.com --username you. -
A package:
pet.json(≤16 KB),spritesheet.webpor.pngat exactly 1536×1872 (8×9 cells of 192×208), and optionally a thumbnail (≤256 KB, ≤512×512).
Steps
-
1. Mint the key, and keep the refresh slot empty
A key looks like
evk1.<id>.<secret>and is stored hashed; the secret is never written, never logged and never returned again. The command prints a paste-ready blob that writes{"access_token":"evk1…","refresh_token":""}. Leaverefresh_tokenempty: a library key in the refresh slot is a credential leak to a third party. -
2. Declare capability as an object
"evopet": { "evolutionGates": [11, 23, 32, 45] }. Evolution capability is the object-valuedevopetkey and nothing else; declared gates are validated exactly as the pet validates them. -
3. Publish
POST https://evopet.askaconsult.com/api/pets/publish # multipart/form-data, bearer key GET https://evopet.askaconsult.com/api/manifest # the public index, approved pets onlyThe publish is
multipart/form-datawith a bearer key: partspet.json,spritesheet.webp, an optionalthumbnail.webp, and an optionalpublic=false. -
4. Read the response
{slug, status, url, capable, evopet, thumbnailUrl, version, replaced, notes}. With no flag the status isapprovedand the package is listed immediately. -
5. Confirm it is listed
GET /api/manifest— public, no auth, approved pets only, sorted by slug. The same index this site's library page reads.
The packet is in /api/manifest with capable: true and its parsed
evopet block, and it renders in the library page and in the desktop app's own
library panel. public=false is the only thing that produces
pending; nothing in the default path does. Deleting is
DELETE /api/pets/<slug>, owner or operator, and it removes the index
record and every package object — after that the record is a 404, not a
tombstone.
Validation happens before anything is stored: a manifest that does not parse, an
id that is not a legal slug ([a-z0-9-], 1–64 bytes, no
leading or trailing dash), a displayName over 96 bytes, or a sheet that is not
the documented atlas all fail the publish and nothing is written. A string
"evopet": "yes" is refused outright rather than stored as something the
app would call incapable. A request for a pending package's bytes from
someone who is not its owner returns 404, not 403, so a slug
cannot be used to probe for hidden pets.
Source. evopet-library/README.md
Install across all profiles
One command for the default profile and every named profile on the machine.
Before you start
- A checkout of the pet tree on the machine that runs Hermes.
-
HERMES_HOMEunderstood: unset means~/.hermes(the default profile); inside a named profile it is<home>/.hermes/profiles/<name>.
Steps
-
1. Install once, in the profile you are in
./hermes/install-hermes.sh --line toast --machine aurora, thenhermes plugins enable tamahermes,hermes pets select tamahermesandhermes pets doctor. -
2. Then cover every profile at once
./hermes/install-all-profiles.sh— the default profile plus~/.hermes/profiles/*. -
3. Narrow it if you need to
--only name1,name2installs into some profiles, and--keep-selectioninstalls the pet without changing which pet is active. -
4. Or one home at a time
HERMES_HOME=~/.hermes/profiles/you ./hermes/install-hermes.sh, andHERMES_HOME=~/.hermes ./hermes/install-hermes.shfor the default profile.
Each profile owns its own <HERMES_HOME>/pets/tamahermes/pet.json, its own
compiled atlas and its own ledger, and hermes pets doctor reports the pet
ready. Be clear about what that means: the pet is profile-scoped, so “all
profiles” is one pet per profile. Making those many pets one creature is the
combined ledger's job, which is the last guide here.
If hermes plugins enable dies with
TypeError: list indices must be integers, that profile still has the legacy
bare-list form of plugins: in its config.yaml; rewrite it to the
mapping form and retry. An install pointed at a non-live home — which is what tests
and staging do — installs the pet but deliberately does not run
hermes pets select, so it cannot change the config of the profile you are
actually running. And if you move the checkout, the recorded repo root at
<HERMES_HOME>/tamahermes/repo-root goes stale: re-run the installer, or
set TAMAHERMES_REPO_ROOT.
Source. README.hermes.md, hermes/install-all-profiles.sh
Float the pet on the desktop
Mirror the pet into the desktop shell, and know what the mirror does not do.
Before you start
- The desktop shell installed on this machine. It is a local build today: there is no notarized download, and a bundle built on one Mac is rejected by Gatekeeper on another.
- The pet installed in the profiles whose work you want the floating pet to reflect.
- The desktop app closed — this is not optional.
Steps
-
1. Quit the app first
The app rewrites its settings file from memory when it quits, so an activate pass against a running app is clobbered a moment later.
-
2. Mirror the pet into the desktop home
./hermes/install-hermes.sh --petdex-activatepins it as the pet on screen;--petdexalone adds it to the rotation without pinning it. -
3. Do it from every profile
./hermes/install-all-profiles.sh --petdex-activate. The mirror is machine-wide while ledgers are per-profile, so installing from every profile is what keeps the floating pet current from all of them. -
4. Or drive the mirror directly
python -m tamahermes petdex --petdex-home ~/.petdex --force --activate.
~/.petdex/pets/tamahermes/ holds pet.json and
spritesheet.webp — the same 8×9, 192×208 atlas Hermes
renders, because both read the one sheet. With --activate, the app's
active_pet points at it and every other key of that settings file, including
its order, is preserved. Once the opt-in marker is recorded at
<HERMES_HOME>/tamahermes/petdex-home, every growth refresh re-mirrors
the sheet, so the floating pet evolves with the terminal one instead of freezing at
whatever it looked like on install day.
Activating against a running app appears to work and is then overwritten. The mirror is a
copy, not a second pet: it never grows on its own, and an unmanaged mirror shows whichever
profile last changed. If hooks look dead, check the hook server's /health
first: the shell exits 0 by design when its hook server fails to bind — for example
because another instance is already running — and then keeps running with every hook
silently doing nothing.
Source. README.hermes.md, PROVENANCE.md
Read your pet’s state
Where the ledger lives, what `status` reports, and what it will not tell you.
Before you start
- An installed pet, from the previous guide.
Steps
-
1. Ask the pet
python -m tamahermes --target hermes status, or add--jsonfor the machine-readable form. It reports the display name and the form it resolves to, the stage and level, XP, the five stats, the traits, and the visual state the atlas is drawn from. -
2. Read the profile's ledger
<HERMES_HOME>/tamahermes/state.json. This is the pet's own source of truth, and it is also where the installed gates live — which is why editing a manifest needs an install to take effect. -
3. Read the combined ledger
~/.evopet/state.json: one pet's worth of XP from every source, with the per-profile ledgers kept as the components they are. Its top-level fields include the mirror claim, which is what makes exactly one writer own the desktop mirror. -
4. Fill it with the drain
python -m tamahermes.evopet_drainis a dry run by default;--applywrites the combined state and prunes the consumed spool, and--no-mirrordrains the ledgers without touching the desktop mirror. The defaults are the spool at~/.petdex/runtime/evo-queue, the state at~/.evopet/state.jsonand consumed events under~/.evopet/consumed, each overridable byEVOPET_SPOOL,EVOPET_STATEandEVOPET_CONSUMED.
A dry run reports what it would absorb and changes no bytes; a second run with no new activity reports zero deltas, which is the honesty check on the whole thing. The Hermes share of the combined ledger's XP equals the sum of the per-profile ledgers, and the clamped stats — energy, health, bond, mess, satiety — are merged by maximum rather than summed, because a delta on a clamped value means nothing. On a first run the cursor is zero, so every profile's whole history lands in the combined ledger exactly once.
Nothing is written without --apply. A spool event with no
session_id is ignored: those are the shell-hook posts, display-only and
un-attributable, so they can be neither counted nor deduped safely. A spool event that came
from Hermes is ignored too, because the profile ledger already counted it — each
source is counted on exactly one of the two routes. The ledger carries numbers only: never
your prompts, never a path, never a transcript, and that restriction is checked by test
rather than by convention. One honest gap: every non-Hermes event seen on the machine so
far has been a fixture, so until Claude Code and Codex really post to the hook server, the
combined ledger earns from the Hermes route and little else.
Source. README.hermes.md, docs/evopet/W1-combined-ledger.md