All five agents died mid-flight (three on session limits, two on 529s), but
their worktrees held real work — 17 files. Salvaged everything, wrote the
missing pieces, and finished the integration by hand.
PROFILES + RANK
User gains display_name, bio, gender, avatar_form, avatar_hue and
profile_public — all nullable, so every existing row including the guest
`wanderer-` accounts stays valid with no backfill. The avatar is procedural
(a GhostForm plus a hue, drawn by the same GhostGlyph that renders
entities): no uploads means no moderation surface, no EXIF and no blob
storage, and an `avatar_url` still slots in later without changing anything.
rank.py converts encounters, essence and favor into one "standing" currency
and maps it onto six one-word titles. An encounter is worth ten points to
ten essence's one, because contact is what the app is about — a seeker who
only buys unlocks climbs very slowly. Negative essence and favor floor at
zero rather than subtracting, so a bad judgment can never demote you: rank
is a record of what you have done. Level 1 costs exactly one encounter, so a
new hunter sees the bar move after their first séance.
Privacy invariants, verified live rather than assumed:
- `email` is returned by GET /api/profile/me and by nothing else. Confirmed
against the running server: zero occurrences in both public payloads.
- A hidden profile 404s rather than 403s — confirming the account exists
would leak exactly what hiding it was meant to prevent.
WHISPERS BETWEEN HUNTERS
Plain text, no attachments, no editing. Guests can RECEIVE but not send:
that gives registering a felt purpose beyond keeping a codex, and closes the
obvious spam vector since guest accounts are free and automatic. Verified
live: alice→bob delivers, a guest send returns 403, and a third party's
conversation list comes back empty — no cross-user leak.
Message bodies are rendered as text nodes, never as HTML, and wrap with
overflow-wrap:anywhere so a long unbroken string can't blow out the layout.
THE ENCOUNTER RECORD
The Codex already knew all of this — Entity.discovered_by has always been
recorded and every contact was already an entity_sightings row. Nobody ever
showed it. Now an entity page names its summoner and lists every hunter who
has met it. Hunters who opted out of a public profile are still COUNTED but
not linkable: an anonymous contact is still a contact, so a spirit's history
stays honest without exposing anyone.
Live on production data: Mabel Crump, discovered by Charly, 1 encounter;
Charly ranks channeler (level 2) from 5 real sightings — all computed from
data that was already sitting there.
385 frontend tests pass; i18n parity holds across both languages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements Workstream B of the character-depth-ghost-log spec:
ritual_start/ritual_step/judgment WS handlers, the pure judgment.py
logic module, and the User.favor / Entity.at_peace columns + migration.
- app/judgment.py: pure ritual success roll (base 65%, floored at 30%,
driven by an entity's power+deceptiveness difficulty), the "stuck
spirit" cross_over rule (alignment >= 0.5 and volatility > 0.6, ~20%
of entities), judgment correctness/favor-delta/essence-delta/
consequence resolution for all four verdicts, favor clamping, the
favor-to-trait-roll bias applied at mint time, and tell-line
generation (opaque behavioral flavor text, never a raw stat).
- app/ws.py: wires ritual_start/ritual_step/judgment frames, emits
ritual_complete/tell/judgment_result/item_drop per the spec's
Contract; traits are added to serialize_entity for internal
server-side use but stripped from the outbound `entity` frame via a
new _public_entity helper so hidden ground truth never reaches the
client outside ritual_complete; _summon excludes at-peace entities
from signature re-contact and mints a fresh (salted-signature) entity
instead; new entities' traits are nudged by the discovering user's
favor before being persisted.
- models/user.py, models/entity.py, main.py: User.favor and
Entity.at_peace columns plus their idempotent ADD COLUMN IF NOT
EXISTS migration lines in lifespan, alongside the existing ones.
- tests/test_judgment.py, tests/test_ws_ritual_judgment.py: 56 new
tests covering the ritual/judgment correctness matrix, favor
clamping/bias, essence crediting, at_peace persistence + re-contact,
and the entity-frame trait leak guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the backend REST surface and WS wiring for
docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md's
Workstream C:
- New models: UnlockRecord (unlocks), InventoryItem (inventory_items),
Sigil (sigils) — brand-new tables, picked up by main.py's existing
create_all.
- New app/inventory.py: unlock price table, item drop table/odds,
essence economy constants, sigil design validation, and an atomic
(row-locked) purchase_unlock() that guards against double-spend races.
- New app/routes/inventory.py: GET unlocks/items/sigils, POST sigils
(validates the placeholder {points, rune} shape, points capped at 12),
POST unlocks/{unlock_key} (402 on insufficient essence, 404 on unknown
key, idempotent re-buy).
- GET /auth/me now includes unlocks: list[str] and essence: int.
- ws.py: wires essence trickle + item_drop rolls into the one trigger
point that exists in this worktree today (_handle_summon, covering
every successful summon plus high-rarity summons); the other two
contract trigger points (correct judgment, successful ritual) belong
to Workstream B's not-yet-landed ritual/judgment WS handlers, which
should call app.inventory's same helpers once they land.
- User.essence: int added (Workstream B owns this column per the spec;
added here per orchestrator instruction so this workstream is
independently testable — merge controller reconciles the duplicate
edit).
Also fast-forwarded this worktree's branch onto master (it had fallen
behind several commits) so the files this workstream depends on
(shop.py, ws.py, entities.py, etc.) were actually present to build
against.
Tests: 109 passed (drop-roll statistical sanity with seeded RNG,
inventory/sigil CRUD, purchase success/insufficient-funds/idempotency/
unknown-key paths, /auth/me shape, ws summon-trickle and item-drop
wiring).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>