Imported from yuri-os/YuriOS (
AGENTS.md). Install upstream withnpx skills add yuri-os/YuriOS. Copyright stays with the author.
YuriOS Agent Guide
Specification
SPEC.mdis the normative specification: RFC-2119 language, cited from code asSPEC §ncomments (hundreds of them). When changing specified behavior, updateSPEC.mdin the same change; when code and spec disagree, the spec is authoritative by convention — treat the mismatch as a bug.- Section numbers are stable and never renumbered — code, tests, and scripts cite them, so only ever append or revise within a section.
- A bare
SPEC §nmeans this spec and must resolve —tests/test_spec_citations.pyfails on a citation to a section that doesn't exist, or to one SPEC.md marks superseded. A predecessor build's spec carries its prefix instead (B1/B2/B4, perPROVENANCE.md's map of which package came from which build). docs/spec-map.mdis the inverse index — section → the code that implements it — generated from those citations bypython scripts/spec_map.py. Use it to go from a section to its modules; the file's own docstring goes the other way. Regenerate it withpython scripts/spec_map.pyafter adding or moving a citation —tests/test_spec_citations.pyfails while it is stale, so this is the suite's rule rather than a habit, and--checkis the same answer outside pytest.- The citation test catches a
SPEC §nthat does not exist and one that lands on a superseded stub. It cannot catch the third case — a citation that resolves perfectly, to the wrong section, which is what a predecessor build's number looks like once it has lost itsB1/B2/B4prefix.python scripts/spec_map.py --auditnarrows where to look by flagging the odd package out of each section's citers; it is a review list, not a verdict. Run it after moving code between packages. docs/test-map.mdis the other generated index: module → the test files that actually ran its lines, measured with coverage contexts bypython scripts/test_map.py(--checkverifies it,--reuserebuilds without rerunning). Use it before running the whole suite — for the 100-odd modules with notest_<name>.py, it is the only way to derive the cheap subset. It is not a gate stage: the instrumented run is serial and takes minutes.docs/is the plain-language companion;SPEC.mdwins on any disagreement. Don't treatdocs/as a source of truth for behavior.
Toolchain and verification
- Support Python
>=3.11,<3.14; Python 3.12 is the installer target. Use the project interpreter when present:.venv/bin/python. - Run the offline suite with
.venv/bin/python -m pytest -q -n 8— 1,744 tests in ~80s across eight workers, minutes serial, so pass-n 8. Focus a change with.venv/bin/python -m pytest -q tests/test_file.py::test_name, and drop-nwhen you need a readable traceback (xdist interleaves eight workers' output). Don't raise the worker count much past 8: most workers pay a one-off torch import (~5s warm, ~20s cold), and-n automeasured slower than-n 8on a 20-core box. - Tests deliberately replace dotenv loading and use fake voice, tools, image, model, and clock seams. Do not require a configured model, API key, GPU, or live service for test coverage.
./scripts/check.shis the gate:ruff check,mypy, pytest, and the web suite, each stage running even when an earlier one failed. Use--fastto skip pytest while iterating,--releaseto add the install smoke test. It needspip install -e ".[dev]". There is no CI; this is the whole gate.- Lint and typecheck are configured in
pyproject.tomland are green — keep them that way. Ruff ischeckonly, neverformat(the formatting is hand-set). Mypy skips the 30 modules listed in its overrides, which predate it: that list may only shrink, and new modules are checked from the start. - For
install.sh, runbash -n install.sh(andshellcheck install.shwhen available). - The browser app is a separate Vite build. After changing
web/, run(cd web && npm ci && npm run build); FastAPI serves the ignoredweb/dist/output at/. Use(cd web && npm run dev)only for Vite development. web/tests/holds the frontend suite (npm test, vitest). Scope it to modules that decide something, not to the three.js room: a test that mocks WebGL asserts only that the mock was called.- After changing a dependency in
pyproject.toml, regenerate the pins with./scripts/pin_deps.shand run./scripts/smoke_install.sh— the resolve check is the only thing that sees a version ceiling breaking, and pytest never will. scripts/live_check.pyis the other thing the gate cannot see, and for the same kind of reason: every mind test builds its loop withmake_mind, which handsMindLoopaPostRecorderwhere the host hands itRuntime.post_message— so the whole delivery chain below the mind (the transcript ring,conversation.jsonl, themessageevent, the inbox) is mocked by the entire battery and exercised by none of it. A picture that reachedpost_messageand got no further was, to 1,900-odd green tests, indistinguishable from one that landed in the chat. The rig builds one realRuntimefrom the house.envthe way the host does, creates a scratch character under.yurios/live-check/, takes the heartbeat out so every tick is one it asked for, and asserts from the files afterwards.--listnames the scenarios,--keepleaves the character to pick through,--hourplaces her day inside Gate 2's quiet hours. It wants a model, a spawned tool server and half a minute of wall clock, so it is not a gate stage — run it after changing anything the mind delivers through, and add a scenario rather than a fake when the thing you broke lives belowpost_message.sentence-transformersis the one core dependency the suite cannot see: her memory is built on it, it has no fake by design, and every test usesFakeEmbedderinstead — so a new major can land inconstraints.txt(whichpin_deps.shonly checks resolves) and the gate stays green. After moving it,transformers,huggingface-hubortorch, runpython scripts/check_embedder.py— and--coldtoo, which exercises the first-ever download rather than the offline path a warm machine always takes. Last verified: sentence-transformers 6.0.0.
Runtime and configuration
yuriosis the console entry point and defaults to starting the daemon. Invoke it from the project root: it treats the current directory as the installation and reads its.env,.yurios/, and.venv.- Use
yurios start --foregroundfor attached server logs;yurios status,stop,restart, andlogmanage the background daemon.yurios configuresaves the house model choice to.env; restart before the daemon uses it. yurios startlaunches the supervisor (yurios/daemon.py), which owns.yurios/yurios.pidas a held lock, restarts the server when it dies (with backoff and a crash-loop stop), and records.yurios/last-exit.json. Treat the lock as the only answer to "is she running" — never a bare pid check.python -m yurios.worldis the server entry point. Normal startup migrates legacy single-character state intoDATA_DIRbefore creating the character host.- The
.envhouse configuration is read at boot. Per-character model and loop overrides live inDATA_DIR/characters.jsonand may be applied live by the host; do not conflate those two configuration paths. .env,.yurios/,data/,vault/,models/,traces/,corpus/,tool-logs/, and Vite output are ignored personal/generated state. Do not add them to commits or use them as fixtures.
Architecture constraints
yurios/worldhosts FastAPI, character routing, event/channel plumbing, MCP tools, and the voice-facing runtime;yurios/mindowns autonomous ticks;yurios/appowns the SOUL, Vault, memory, and model providers;yurios/charactersowns registry and card import/export;yurios/forgeowns image backends;yurios/desktopowns STT/TTS/VAD and native-window support.yurios/kernelsits below all of them and holds the three primitives they share — the injected clock, thecorr_id, theEventHub. It imports the standard library and nothing fromyurios;tests/test_layering.pyenforces that. Put something there only if every layer above could need it, and never let it grow a dependency on a package that imports it.- The same test refuses a new import cycle. A function-local import is not an escape from one — it makes a cycle work at runtime while hiding it, which is how the two in
KNOWN_CYCLESsurvived. That list follows the mypy overrides' rule: it may only shrink, and the test fails both on a new cycle and on a listed one that has been fixed but not struck off. - Optional heavyweight backends are lazy, replaceable seams with fakes. Preserve that property: avoid importing model, voice, camera, or GPU dependencies at module import time, and retain a testable fake/degraded path when changing a seam.
- What a brain is, is written down:
world/brain_protocol.pynamesConversationalBrain(a room's needs) andAutonomousBrain(a mind's, on top). Check the shape withisinstanceagainst those rather thanhasattron one method, and when you add or rename a seam, change the Protocol in the same edit —tests/test_brain_protocol.pycompares signatures as well as names, which is what stops a fake drifting silently. - The Vault commits at most once a day (
vaultgit.COMMIT_INTERVAL_S, SPEC §2.1). Writes still land immediately and atomically; only the history entry waits, andgit add -Aon the far side sweeps up everything since. Sovaultgit.commit(...)returning without a new sha is the normal case, not a failure — never "fix" a caller by working around it. A test asserting that a write reaches history takes theopen_vault_windowfixture; the window itself is pinned intests/test_vault_commit_noise.py. - That window paces the machine, not the user. A commit whose message names something a person just did — a studio or switchboard edit, a dream job rewritten, a memory she was asked to forget — passes
now=True(MindVault.commit_if_dirtyforwards it), because waiting does not delay that entry, it destroys it: the far-side sweep files the edit under whichever tick or turn trips the window next. A message naming a tick, a turn or a night waits. The line is what the message names. - Nothing in
yurios/may call a git-shellingvaultgitfunction from anasync def. A host is one process holding every character on the node, so a subprocess on the event loop stops all of their rooms; wrap it asawait asyncio.to_thread(vaultgit.commit, …).vaultgit.BLOCKINGlists the functions andtests/test_vault_off_loop.pyreads that tuple and fails on a direct call — add a new git-shelling function to it in the same edit. - Building a character is slow and blocking — LM Studio seating her chat model, and (the first time in a process) a cold embedding model — so
CharacterHost.startdoes it on a worker thread. The embedder itself then loads off-thread and MUST NOT hold up the rest of that start (SPEC §2.4). The MCP tool server is the same idea on the event loop: spawn is kicked off and MUST NOT be awaited bystart_async(SPEC §7.2). A host is one process holding every character on the node: anything that takes seconds inside an async host or route handler stops the whole node, including the other characters' rooms.tests/test_host.py::test_starting_a_character_does_not_freeze_the_rest_of_the_nodebeats a heartbeat through it. - Her hands run in a spawned process, and the only thing that reaches them is a dict of strings.
world/tools/spawn_env.pyowns that wire end to end — the key names, the types, and what each means when absent. Add a tool setting there and use it from both sides; neitherworld/main.pynorworld/tools/server.pymay name an env key of its own, andtests/test_spawn_env.pyfails if one does. - There are two
/ws/voiceroutes —world/routes/voice_ws.py(the browser) anddesktop/routes/voice_ws.py(the native window) — and they differ on purpose. The wire under them does not: the connection cap, the hello exchange, the size ceilings and the STT session aredesktop/voice/ws_session.py, anddesktop/voice/ws_limits.pyholds the numbers. Change a wire invariant there, not in a route;tests/test_voice_handshake.pyruns every one of them against both apps. mind/loop.pyis the tick's own machinery — SENSE, APPRAISE, DECIDE, the_actswitchboard, REFLECT, REGULATE. What a tick does lives beside it:mind/acts.py(one function per intention, each returning the acted/trace/journal triple),mind/goalwork.py(the only act that may reach for a hand),mind/prompts.py(the persona blocks and the utility call),mind/housekeeping.py(rollover, maintenance, bootstrap). Add an intention as a function inacts.pyplus one line in_act; put it inloop.pyonly if the tick's own sequence changes.- The mind relies on injected time. Do not add direct wall-clock reads or bare sleeps in
yurios/mind; use the runtime clock so VirtualClock tests remain deterministic. - The browser is a thin client: host-to-client updates use the shared SSE
EventHub. Add cross-surface state as typed events rather than a frontend-specific polling path.
Commits
- Commit subjects use scope prefixes (
mind:,world:,forge:,studio:, …) — match that style. No AI-attribution trailers.
