Reference
Docs
Everything EvoPet fixes, everything a pet creator chooses, and the parts that are still being built. Where a number appears here it was computed from the code that produces it, and where something does not work it is listed as a defect.
Quick start
The short version, for a machine that already runs a Hermes agent. It installs the pet for one profile and selects it.
git clone <the pet tree> # from a checkout of the pet tree
cd <the pet tree>
./hermes/install-hermes.sh --line toast --machine aurora
hermes plugins enable tamahermes
hermes pets select tamahermes
hermes pets doctor # should report ✓ ready hermes pets doctor should report the pet ready. The long version — every profile
at once, the installer flags, and floating the pet on the desktop — is in Getting started below.
Getting started
Install the pet, enable the plugin, and put it on the desktop.
Install the pet
EvoPet is the pet and the desktop shell, and they install separately. You can run the pet alone, in the terminal, with no desktop app at all. From a checkout of the pet tree:
git clone <the pet tree> # from a checkout of the pet tree
cd <the pet tree>
./hermes/install-hermes.sh --line toast --machine aurora EvoPet's own repository is public as of 11 September 2026, and the pet tree is a separate tree, so these instructions assume a checkout on your machine. The pet needs Python and nothing else — its only dependency is Pillow, which already ships with Hermes.
That one command installs the package, builds the pet into your Hermes home, copies the plugin, and selects the pet.
Enable the plugin
The plugin is how growth events reach the ledger. It is a native Hermes plugin, so it runs in process, nothing needs allowlisting, and hook callbacks only enqueue a payload — the atlas rebuild is coalesced, so a burst of tool calls triggers one rebuild rather than forty. A failed rebuild is logged and dropped, because a mascot must never break an agent turn.
The alternative is shell hooks: put the block from hermes/hooks.snippet.yaml into <HERMES_HOME>/config.yaml,
replacing __REPO__ with your checkout path, then allowlist each entry with hermes hooks list. Pick one. Wiring both feeds the same ledger
twice.
pets select
Selecting the pet is what makes it the active one for that profile, and it happens once per install:
hermes plugins enable tamahermes
hermes pets select tamahermes
hermes pets doctor # should report ✓ ready pets doctor
hermes pets doctor is the check, and it should report the pet ready. If it does
not, start with the two things that go wrong most: the plugin was copied but never enabled,
or the install was pointed at a home whose config.yaml still uses the legacy
bare-list plugins: form.
Per-profile install
Hermes homes are profile-scoped: the pets directory is <HERMES_HOME>/pets,
and HERMES_HOME is <home>/.hermes/profiles/<name>
inside a named profile (unset — that is, ~/.hermes — for the default profile).
So each profile owns its own pet and its own ledger, and you install once per profile you
actually use.
Pointing an install 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.
install-all-profiles.sh
One command covers the default profile and every named profile on the machine. It also takes --only name1,name2 and --keep-selection (install the pet without
changing which pet is active).
# every Hermes profile on this machine: the default one and ~/.hermes/profiles/*
./hermes/install-all-profiles.sh
# or one at a time
HERMES_HOME=~/.hermes/profiles/you ./hermes/install-hermes.sh
HERMES_HOME=~/.hermes ./hermes/install-hermes.sh Installer flags
| Flag | Effect |
|---|---|
--line LINE_ID | which companion line to install. Default: toast. |
--machine aurora|pulse | which outer shell. Default: aurora. |
--form FORM_ID | install one exact form, grown or not. |
--display-name NAME | the name shown for the pet. |
--reset | start over from a fresh egg. |
--shell-hooks | print the config.yaml hook block instead of copying the plugin. |
--petdex | also mirror the pet into the desktop home so it floats (adds it to the rotation). |
--petdex-activate | with --petdex, also make it the active desktop pet. |
--petdex-home PATH | the desktop home to use. Default: ~/.petdex. |
Float it on the desktop
The pet renders in the terminal by default. Mirroring it into the desktop shell's pet directory makes it float there too, off the same ledger:
./hermes/install-hermes.sh --petdex-activate # close the desktop app first
./hermes/install-all-profiles.sh --petdex-activate
The app rewrites its settings file from memory when it quits, so an activate pass against a
running app is clobbered. Only --petdex-activate touches which pet is on
screen, and it preserves every other key of that file, including their order.
The mirror is a copy, not a second pet. 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.
One honest caveat: the desktop pet is machine-wide while growth ledgers are per-profile, so an unmanaged mirror shows whichever profile last changed. Installing from every profile is what keeps it current from all of them, and the combined ledger is what makes the ambiguity go away.
Environment
None of these are needed for a normal install. They exist so tests, staging and unusual layouts can point the same code somewhere else.
| Variable | Purpose |
|---|---|
HERMES_HOME | the target Hermes home. Inside a named profile it is <home>/.hermes/profiles/<name>; unset, it is ~/.hermes. |
TAMAHERMES_HOME | overrides HERMES_HOME for the hook script and the plugin. |
TAMAHERMES_PETDEX_HOME | the desktop home to mirror into. Unset means no desktop mirror. |
TAMAHERMES_REPO_ROOT | lets the plugin and the hook script import the package without installing it. |
TAMAHERMES_PY | the interpreter the shell-hook wrapper runs. |
TAMAHERMES_SYNC=1 | run the plugin inline instead of on its worker thread (tests). |
TAMAHERMES_LINE / TAMAHERMES_MACHINE | pin the companion line and the shell. |
TAMAHERMES_CATALOG_DIR | a custom rendered catalog directory. |
What is kept, and where
| Path | What it is |
|---|---|
<HERMES_HOME>/pets/tamahermes/pet.json | the manifest Hermes reads, including the evolution gates |
<HERMES_HOME>/pets/tamahermes/spritesheet-<hash>.webp | the compiled atlas, 8 columns by 9 rows of 192×208 cells |
<HERMES_HOME>/tamahermes/state.json | the profile’s growth ledger |
<HERMES_HOME>/tamahermes/hermes-hook-state.json | per-turn bookkeeping (turn ids only) |
<HERMES_HOME>/plugins/tamahermes/ | the native plugin the installer copies |
~/.evopet/state.json | the combined ledger: one pet’s worth of XP from every source |
~/.evopet/settings.json | the auto-upload switch. See the library docs |
Only numbers are stored: XP, counters, stats and traits — never your prompts, never tool output, never a file path. The combined ledger's restriction is enforced by its own test rather than by convention.
The desktop shell
The shell is the app that floats the pet and taps every agent's hook payload, so Claude Code, Codex, opencode and Gemini CLI can feed the same creature. It is a Zig build with a pinned SDK, and today it is a local build: there is no notarized download, and a bundle built on one Mac is rejected by Gatekeeper on any other.
Because of that, the honest install path for the shell right now is to build it from the app tree on the machine that will run it. It installs as its own app, leaving the stock one in place so you can revert by quitting and reopening the other.
The shell half descends from Petdex, the desktop app shell lineage. An
earlier repository published as ahrazzle/petdex is that lineage's older
home; this site links the lineage as Petdex, and the older mirror is named here as prose
rather than offered as a second button.
The shell exits 0 by design when its hook server fails to bind — for example because another
petdex is already running — and then keeps running with every hook silently doing nothing.
Check the hook server's /health first.
Source. README.hermes.md, hermes/install-hermes.sh; tamahermes/paths.py and tamahermes/pet_compiler.py for what an install writes.
Mechanics
The loop, the ladder, the gates, and the gauges that are watched.
The loop
- An agent does something; Hermes fires a hook event (
prompt_sent,task_success, …). - The event is applied to that profile's ledger as XP plus stat changes, at the turn boundary.
- The compiler redraws the pet's atlas and installs it. Rebuilds are coalesced, so a burst of tool calls is one redraw.
- Every surface renders that one sheet: the terminal pet and the desktop mirror.
XP is awarded at turn boundaries, never per tool call. A turn in Codex has to be worth a turn in Hermes; per-tool accounting would inflate the pet twenty to forty times over against the ladder.
XP events
| event | XP | fires on |
|---|---|---|
task_success | 14 | a turn ends successfully, once per turn |
recovery | 6 | a success after a failure in the same turn |
task_failure | 5 | a turn fails |
review_opened | 5 | a review or critique is opened |
prompt_sent | 4 | every prompt you send |
care | 3 | you tend the pet yourself |
session_start | 2 | an agent session begins |
rest, token_usage, idle_minute, drag | 0 | these move stats and energy only |
A good turn is about 18 XP — a prompt and a success — and a failing turn about 9 to 11.
The ladder
Every pet in every package climbs the same ladder, so a level means the same amount of work in every pet. It is fixed by EvoPet, cheap at the start and long at the end:
xp_for_level(L) = round(100_000 × ((L − 1) ÷ 98)²),
that is CAP_XP = 100,000, MAX_LEVEL = 99, EXPONENT = 2.0.
| level | cumulative XP | what this level costs |
|---|---|---|
| 1 | 0 | — |
| 2 | 10 | 10 |
| 20 | 3,759 | 385 |
| 50 | 25,000 | 1,010 |
| 70 | 49,573 | 1,426 |
| 99 | 100,000 | 2,030 |
XP is cumulative and never decreases, so a level once reached is never lost. Half the cap, 50,000 XP, lands inside level 70, and the climb from level 50 to level 99 costs 75,000 XP.
The bar fills against the level, not the form. level_progress(xp)
reports the level and the position between its floor (xp_for_level(L)) and its
ceiling (xp_for_level(L + 1)), so the bar fills about a hundred times in a pet's
life and resets at every level-up — but the sprite changes only when a gate is crossed,
because a gate is the only thing that changes the form. At level 99 the ceiling is None, the bar reports 100%, and the drawn value snaps to 5% buckets.
Evolution gates
How many evolutions a pet has, and at which levels, is the creator's choice, declared in the package's own manifest:
"evopet": { "evolutionGates": [11, 23, 32, 45] }
A gate is a level the pet reaches: when cumulative XP first equals or passes xp_for_level(gate), the pet moves to the next form. There is always one more
form than gate, so one to four gates means two to five forms. The default pet declares (11, 23, 32, 45), which on the fixed ladder is:
| gate level | cumulative XP | from | to | XP this step costs |
|---|---|---|---|---|
| 11 | 1,041 | egg | hatchling | 1,041 |
| 23 | 5,040 | hatchling | child | 3,999 |
| 32 | 10,006 | child | teen | 4,966 |
| 45 | 20,158 | teen | adult | 10,152 |
When a declaration takes effect
-
Declare
"evopet": { "evolutionGates": [11, 23, 32, 45] }in thepet.jsonthe install will replace, or edit the installed one afterwards. -
Install. The installer absorbs the manifest it is about to replace into the ledger
(
pet_compiler.sync_ledger_gates), then builds, so the newpet.jsoncarries the ledger's gates. -
The change takes effect on the next install, not on the next event. Nothing
re-reads a manifest mid-run, and it is the ledger's own gate list —
state.evolution_gates(state)— thatmaybe_evolveandstage_progressread.
A manifest that declares nothing leaves the ledger alone: silence is not a declaration.
What a bad gate list is told
levels.validate_gates refuses, and names the problem. These are its messages,
verbatim:
evolution gate 120 is outside 1..99evolution gates must strictly increase; 10 follows 30a pet needs at least one evolution gateat most 4 gates supported (5 forms); got 5
A block that is present but invalid is refused at install, and PetCompileError
names the file: pet.json: evolution gates must strictly increase; 20 follows 40. A ledger with no evolutionGates key at all behaves exactly as it always has, on the default pet's
gates — a growth event that raised would strand a user mid-run.
Two properties make a reached gate safe to rely on. The gate level becomes a floor in the ledger's threshold table, and XP only rises, so a pet cannot regress out of a form. And the terminal form is absent from that table, so the last form has nothing to grow into.
Forms and branches
Form names are positional and not the creator's to choose, taken from STAGE_ORDER = ("egg", "hatchling", "child", "teen", "adult"). Three
gates give you egg, hatchling, child and teen — and your pet never reaches adult.
teen and adult also carry branch variants that the runtime picks
from stats and traits rather than from a gate:
- teen —
resilient(resilience ≥ focus + 3),restless(restlessness ≥ focus + 4), otherwisefocused. - adult, checked in order —
sleepy(energy < 28, or 120+ quiet minutes),worker(≥ 4 completed runs and focus ≥ resilience),resilient(resilience ≥ focus and ≥ 2 failures),quiet(restlessness ≤ 2 and 60+ quiet minutes), otherwisecalm.
Branches are sticky: once chosen they persist, and they are restored after dormancy. A branch
is a record of how you worked, not a choice you make. Form ids read <line>:<branch>, for example child:root or teen:focused.
Energy and dormancy
Energy is 0–100 and is the only stat that gates behaviour: it is the awake budget.
| direction | what moves it |
|---|---|
| drains | session_start −1 · prompt_sent −1 · idle_minute −1 · task_failure −4 |
| restores | task_success +2 · recovery +1 · care +5 · rest +10 per 10 quiet minutes, to a maximum of
48 blocks (8 hours)
|
| falls asleep | energy ≤ 4, or health ≤ 12, or 240 idle minutes |
| wakes up | energy ≥ 35 and health ≥ 35 |
| bins | ≤20 critical · ≤45 low · ≥80 full · otherwise ok |
While asleep the pet draws flat sleeping artwork — a 24×11 source union against roughly 23×24 for every other form, so it looks vertically compressed and is not — and its bar shows real progress toward the form it will wake into rather than a pinned zero.
The five trackers
| tracker | derived from | bins | reads |
|---|---|---|---|
| energy | stats.energy | ≤20 critical, ≤45 low, ≥80 full, else ok | Full is good. The only tracker that gates behaviour. |
| satiety | totalTokens // 100, else a mix of prompt chars, tool-output chars, runs and token samples | <35 hungry, ≥70 fed, else ok | Full is good: how much work you have fed it. |
| health | stats.health (−4 on failure, +2 recovery, +4 care, +3 rest) | ≤35 weak, else ok | Full is good. |
| bond | stats.bond (+1 session, +2 success, +1 recovery, +3 care) | <25 new, <65 warm, ≥65 attached | Full is good. |
| mess | mess + failedRuns×6 + careMistakes×4 + min(24, idleMinutes ÷ 10) + unresolved×3, where unresolved = workRuns − completedRuns − reviews | <25 clean, <60 dusty, ≥60 messy | Inverted: high is bad. See D1 and D2 below. |
Also tracked but never shown: mood (which affects nothing) and the traits focus, resilience and restlessness, which decide the
branch.
What the shipped status strip draws
The top row is silent unless something needs you. Energy is the one escalation: at 45 or below the bolt and its gauge appear, amber while energy is low and red once it is critical (20 or below). The alert glyph still shows a failure or a review, and the health warning shows while health is 35 or below. The bottom row is always drawn: satiety as the bowl, growth as the centre bar, bond as the heart — which stays hidden until the pet is past "new". A direction decision for a richer HUD is still open and is not shown here.
Known limitations
These are the defects the project has recorded in its own notes and, where named, in the code. None of them is documented as working anywhere on this site.
| id | defect |
|---|---|
| D1 | Mess can never fall. failedRuns and careMistakes are never decremented, and task_failure
increments both, so mess_score gains +10 per
failure monotonically and clamps at 100. Any used profile
reads "messy" forever, which means a cleanliness gauge cannot work until failures
decay.
|
| D2 | careMistakes is mis-modelled. It is meant to say "the
owner neglected care", but it is incremented by agent turn failures — double-counting
every failure in the mess score.
|
| D3 | Dormancy is easy to fall into. Energy drains on every prompt and session start and restores only over quiet time, so a long working session can pin the pet asleep. |
| D4 | mood is dead weight. Tracked, sits near 100, displayed
nowhere and affects nothing.
|
| D5 | level is computed and shown nowhere. |
| D6 | opencode cannot earn turn XP. It tracks its phase internally but does
not send it, so its payload carries no phase and its turns are
indistinguishable from its tool calls.
|
Three further gaps are documented rather than fixed, and they matter if you are about to rely on them:
-
Nothing cross-checks your gates against your spritesheet when you declare them.
validate_gateschecks the list's shape only, so a line that lacks a form for a stage your gates imply fails later, in the catalogue lookup, when the pet grows into it —CatalogError: no form for line …. -
A manifest's
"forms"list is ignored: you cannot name your own forms. -
Gates are levels only. There is no XP amount, no branch, no condition such as three failed
runs; the
teenandadultbranches still come from stats.
levels-and-evolution.md — the pet's own contract — describes the 99-level
ladder with gates at 11/23/32/45. An older mechanics write-up
describes stage thresholds of 120/320/900/1800 and a level of 1 + xp // 25. Both cannot be true. This site publishes the gates model, which
the shipped levels.py implements, and treats the older write-up as stale; that
conflict is flagged rather than hidden, and regenerating the stale note is open work.
Source. tamahermes/levels.py, state.py, visual_state.py; MECHANICS.md. Sleep and wake thresholds, the tracker formulas and
the bins were read out of tamahermes/state.py and tamahermes/visual_state.py; the defects are the project's own recorded list.
Library & publishing
The public index, what makes a pet capable, and how publishing works.
The library service is written, and passes its own end-to-end round trip. It is not deployed: the base URL the site calls is a placeholder until someone chooses the host, which also has to be chosen before the first publish because every URL in the index is absolute and issued from that value. Until then the library page says it cannot reach the library, which is the honest answer rather than an empty shelf.
Endpoints
One service, no database, no auth provider: Node and TypeScript, deployed as a single function,
with one runtime dependency (the platform's own blob client). Public and owner-only bytes are
separated deliberately — an approved pet's files are served to anyone, while a pending or rejected one's are served only to its owner or an
operator, and an unauthorised request gets 404 rather than 403 so a
slug cannot be used to probe for hidden pets.
| method | path | auth | purpose |
|---|---|---|---|
| GET | /api/health | — | liveness, storage mode, public base URL |
| GET | /api/desktop/library | bearer | the app’s library response |
| POST | /api/pets/publish | bearer | publish a package (multipart) |
| GET | /api/manifest | — | the public index, approved pets only |
| GET | /api/pets/<slug> | — / owner | the index record |
| GET | /api/pets/<slug>/pet.json | — / owner | the published pet.json, verbatim |
| GET | /api/pets/<slug>/spritesheet.webp|png | — / owner | the published atlas |
| GET | /api/pets/<slug>/thumbnail.webp|png | — / owner | the preview image |
| POST | /api/pets/<slug>/catch | bearer | record “downloaded from the catalogue” |
| POST | /api/admin/pets/<slug>/status | operator | set pending | approved | rejected |
| DELETE | /api/pets/<slug> | owner / operator | remove a package, bytes included |
| GET | /api/admin/deletions | operator | the deletion audit log |
The index the library page reads is GET /api/manifest: public, unauthenticated,
approved pets only, sorted by slug, and carrying generatedAt, total, capableTotal, assetBase and a row per pet with slug, displayName, thumbnailUrl, capable, evopet, petJsonUrl, spritesheetUrl, version and updatedAt.
Capability
Evolution capability is the evopet block, and nothing else. If a pet.json carries an object-valued evopet key the pet is capable and
the parsed block is stored with the record and served in the index; otherwise it is a stock
package and evopet is null. That is the desktop app's own marker, so
the service refuses "evopet": "yes" rather than storing something the app would
call incapable, and it asserts that its own flag agrees with the app's scanner before storing.
Declared evolutionGates are validated like levels.validate_gates: 1–4 gates, levels 1 to 99, strictly
increasing. The index serves both the boolean and the block on purpose — the block is the whole
truth, and the boolean is the convenience — so a client never has to re-derive the answer and
never has to trust one that cannot be checked.
Package requirements
POST /api/pets/publish takes multipart/form-data only:
| part | requirement |
|---|---|
pet.json | ≤ 16 KB — the window the app reads capability from |
spritesheet.webp | spritesheet.png | exactly 1536×1872: 8 × 9 cells of 192×208, the atlas the app's sprite loader and EvoPet's compiler share |
thumbnail.png | thumbnail.webp | optional; ≤ 256 KB and ≤ 512×512 |
public=true|false | optional; the off switch described below |
Validation happens before anything is stored: the manifest must parse, id must be
a legal slug ([a-z0-9-], 1–64
bytes, no leading or trailing dash — it becomes a directory name under ~/.petdex/pets), displayName must fit 96 bytes, the sheet must be the documented atlas of the documented
kind, and spritesheetPath must agree with the uploaded sheet.
The response is a record of what landed, not a promise:
{slug, status, url, capable, evopet, thumbnailUrl, version, replaced, notes} Auth v1
One bearer key per user, minted by the operator CLI and stored hashed with scrypt. The secret
is never written, never logged, and never returned again. Format evk1.<id>.<secret>: the id is the public lookup path, the secret is 32
random bytes. Issuing is a CLI rather than an HTTP endpoint on purpose — the first operator key
would otherwise need a bootstrap secret in the environment that mints credentials, which is
worse to hold than the storage token the CLI already needs.
npm run key:issue -- --email you@example.com --username you --display "owner key"
npm run key:revoke -- --id <id> Publishing with a key:
curl -X POST "https://<host>/api/pets/publish" \
-H "Authorization: Bearer <key>" \
-F "pet.json=@pet.json" \
-F "spritesheet.webp=@spritesheet.webp" \
-F "thumbnail.png=@thumbnail.png" \
-F "public=true"
The desktop app stores its session in the login keychain as a JSON blob, and refresh_token must be empty. On a 401 the app posts a
non-empty refresh_token to a third party's token endpoint — so a library key
sitting in the refresh slot is a credential leak to a service that has nothing to do with
this library. Write the blob, which is what key:issue prints:
security add-generic-password -U -s dev.petdex.desktop-native -a oauth \
-w '{"access_token":"<key>","refresh_token":""}'
PETDEX_LIBRARY_URL=https://<host>/api/desktop/library
With refresh_token empty, a 401 surfaces as a sync error and nothing leaves the
machine.
Publication policy
Publishing is auto-upload: with no flag a package appears in the index immediately with the
status the app already understands for a listed pet, approved. The off switch is public=false, which is the only thing that produces pending — and it
is the only setting in this whole system that persists a choice, written to ~/.evopet/settings.json.
| item | value |
|---|---|
| file | ~/.evopet/settings.json |
| document | {"autoUpload": true} or {"autoUpload": false}, written whole on every change |
| default | on. Only an explicit false turns publishing off: a missing key, a missing file, an unreadable file or a value that is not a bare boolean all resolve to the default |
| where it is set | the desktop app: Settings → Pet library |
| copy in the app | “A newly hatched pet is published to the EvoPet library so others can find it; turn this off to keep every pet local” |
pending and rejected exist because the app already has both statuses,
because the off switch has to leave an unlisted package somewhere honest, and because a
maintainer needs to suppress a pet without destroying it. Nothing in the default path produces
them, and the rejection path is a status change, not a queue.
| from | to | by |
|---|---|---|
| — | approved | publish, no flag (the default) |
| — | pending | publish with public=false |
| pending | approved | owner re-publishing, or an operator |
| approved | pending | owner re-publishing with public=false, or an operator (unpublish) |
| pending / approved | rejected | operator |
| rejected | pending / approved | owner re-publishing, or an operator |
| any | gone | DELETE, owner (own pet) or operator (any pet) |
The catalogue is stored one object per pet, not one catalogue document. A single document would be a read-modify-write with no compare-and-swap behind it, so two concurrent publishes would silently lose one of the two pets — exactly while the library is growing. Per-pet keys make a publish a single write to a key nobody else is writing; the price is that listing is a list plus N reads, which is fine at this scale and honest about it.
Deletion and takedown
DELETE /api/pets/<slug> removes the index record and every package object.
No tombstone serves anything afterwards, and the slug becomes a 404. An owner can
delete their own pet; an operator key can delete any pet, optionally with a
reason, and that reason is kept for maintainer deletions only, in an operator-readable audit
entry.
curl -X DELETE "https://<host>/api/pets/<slug>?reason=<complaint%20ref>" \
-H "Authorization: Bearer <operator key>" A third-party complaint needs an address a rights holder can write to, and there is not one: the interim posture is that removal is a manual operator action. Any hosted public use should add a monitored address, a response-time commitment, and a repeat-infringer rule for publishing keys. This site states that plainly rather than implying a process that does not exist, and makes no claim about how fast a takedown would be answered.
What this site does not do
- No authenticated flow of any kind. No accounts, no OAuth, no keys, no uploads. The library page reads one public endpoint and nothing else.
- No hosted claim. The service's deploy steps live in its own repository; this site neither performs nor asserts a deployment, and it does not choose the public base URL.
- No install path that does not exist. The app reads its catalogue manifest
from a compiled constant and trusts asset URLs under its own host, so
petdex://<slug>installs still resolve to petdex.dev. Only the library URL is overridable. A link here would imply otherwise.
Source. evopet-library/README.md, src/manifest.ts; evopet_settings.zig and settings_view.zig for the auto-upload switch, quoted from its own copy.
Reference
Constants, functions and the files they live in.
Constants
All of these live in tamahermes/levels.py. Change them there and nowhere else.
| name | value |
|---|---|
MAX_LEVEL | 99 |
CAP_XP | 100,000 |
EXPONENT | 2 |
MANIFEST_KEY | "evopet" |
STAGE_ORDER | ("egg", "hatchling", "child", "teen", "adult") |
DEFAULT_EVOLUTION_GATES | (11, 23, 32, 45) |
Function list
In the order you will want them:
| function | what it answers |
|---|---|
xp_for_level(L) | cumulative XP needed to reach a level; level 1 is 0, level 99 is the cap |
level_for_xp(xp) | the level a pet with that much cumulative XP is in (1–99) |
level_progress(xp) | the level band the bar fills against: floor, ceiling, percent |
validate_gates(gates) | checks a creator’s gate list and raises ValueError, naming the problem |
gates_from_manifest(manifest) | the creator’s gates, or the default pet’s when it declares none |
forms_for_gates(gates) | the positional form names implied by a gate list |
thresholds_for_gates(gates) | {form: the XP it begins at} |
ledger_thresholds(gates) | {stage: the XP that ends it}, with the terminal form absent |
stage_for_xp(xp, gates) | the form a pet is in at that XP |
gate_report(gates) | the creator-facing gate table: level, XP, from, to |
curve_report(levels) | cumulative XP at chosen levels, plus what each level costs |
Code map
| file | what lives there |
|---|---|
tamahermes/levels.py | the whole contract above: the curve, the gates, the validation, and the reports |
tamahermes/state.py | EVENT_DELTAS — one table holding every XP value and every stat change — plus STAGE_THRESHOLDS, maybe_evolve, the teen and adult branch choosers, and passive rest |
tamahermes/visual_state.py | the bin functions, mess_score, satiety_score, alert_bin, and derive_visual_state |
tamahermes/pet_compiler.py | draws the atlas: the floating layout, the growth bar, the status strip, the icons |
tamahermes/hermes_events.py | maps a Hermes hook event onto a pet event |
tamahermes/evopet_drain.py | the cross-agent drain: absorbs per-profile XP into the combined ledger, consumes spooled foreign turns at turn boundaries, and prunes what it absorbed |
hermes/install-hermes.sh | installs the pet and plugin for one Hermes home |
hermes/install-all-profiles.sh | the same, for the default profile and every named profile |
The contract is covered by the pet's own suite: the ladder, the gates and the reports are exercised from the same formula this site renders, so a constant moved in one place and not the other is a failing test rather than a wrong page.
Source. tamahermes/levels.py and the module list it names. Constants read from the module, not from prose.
A note on this documentation
Some of the project's own notes have fallen behind the code, which is normal and not worth hiding. The mechanics write-up still describes an older ladder and an older notion of when a pet evolves; where a note and the source disagree, the source wins, and Known limitations names the specific differences.
There is no documentation search at v1, deliberately: the site is four routes with anchors inside them, and a search box over this much text would be decoration rather than a tool.