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.

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. 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. 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. 3. Declare them in the manifest

    In the pet.json the install will replace: "evopet": { "evolutionGates": [20, 40, 60] }. Form names are positional and not yours to choose, starting at egg and running hatchling, child, teen, adult.

  4. 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 new pet.json carries the ledger's gates and the running pet evolves on the ledger's copy, not on the manifest.

  5. 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.

What a correct run gives you

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.

How it fails

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.webp or .png at exactly 1536×1872 (8×9 cells of 192×208), and optionally a thumbnail (≤256 KB, ≤512×512).

Steps

  1. 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":""}. Leave refresh_token empty: a library key in the refresh slot is a credential leak to a third party.

  2. 2. Declare capability as an object

    "evopet": { "evolutionGates": [11, 23, 32, 45] }. Evolution capability is the object-valued evopet key and nothing else; declared gates are validated exactly as the pet validates them.

  3. 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 only

    The publish is multipart/form-data with a bearer key: parts pet.json, spritesheet.webp, an optional thumbnail.webp, and an optional public=false.

  4. 4. Read the response

    {slug, status, url, capable, evopet, thumbnailUrl, version, replaced, notes}. With no flag the status is approved and the package is listed immediately.

  5. 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.

What a correct run gives you

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.

How it fails

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_HOME understood: unset means ~/.hermes (the default profile); inside a named profile it is <home>/.hermes/profiles/<name>.

Steps

  1. 1. Install once, in the profile you are in

    ./hermes/install-hermes.sh --line toast --machine aurora, then hermes plugins enable tamahermes, hermes pets select tamahermes and hermes pets doctor.

  2. 2. Then cover every profile at once

    ./hermes/install-all-profiles.sh — the default profile plus ~/.hermes/profiles/*.

  3. 3. Narrow it if you need to

    --only name1,name2 installs into some profiles, and --keep-selection installs the pet without changing which pet is active.

  4. 4. Or one home at a time

    HERMES_HOME=~/.hermes/profiles/you ./hermes/install-hermes.sh, and HERMES_HOME=~/.hermes ./hermes/install-hermes.sh for the default profile.

What a correct run gives you

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.

How it fails

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. 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. 2. Mirror the pet into the desktop home

    ./hermes/install-hermes.sh --petdex-activate pins it as the pet on screen; --petdex alone adds it to the rotation without pinning it.

  3. 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. 4. Or drive the mirror directly

    python -m tamahermes petdex --petdex-home ~/.petdex --force --activate.

What a correct run gives you

~/.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.

How it fails

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

Steps

  1. 1. Ask the pet

    python -m tamahermes --target hermes status, or add --json for 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. 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. 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. 4. Fill it with the drain

    python -m tamahermes.evopet_drain is a dry run by default; --apply writes the combined state and prunes the consumed spool, and --no-mirror drains the ledgers without touching the desktop mirror. The defaults are the spool at ~/.petdex/runtime/evo-queue, the state at ~/.evopet/state.json and consumed events under ~/.evopet/consumed, each overridable by EVOPET_SPOOL, EVOPET_STATE and EVOPET_CONSUMED.

What a correct run gives you

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.

How it fails

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