# Agent Instructions - Stario App This repo is a Stario 4 Python web project. Read this file before editing code. For a new repo, copy this file to the repo root first; it is the shortest useful context for a human or an AI agent to start correctly. - Docs: [stario.dev/docs](https://stario.dev/docs) - Extended LLM context: [llms-full.txt](https://stario.dev/llms-full.txt) - Product-shaped example: [examples/chat-room](https://github.com/bobowski/stario/tree/main/examples/chat-room) ## Start A Project Use Python 3.14+ and `uv`: ```bash uv init --app my-app cd my-app uv add "stario>=4,<5" uv add --dev ruff pyright pytest pytest-asyncio curl -o AGENTS.md https://stario.dev/AGENTS.md ``` Scaffold the product layout from the first commit. `examples/hello-world` and `examples/tiles` teach mechanics; `examples/chat-room` is the model for a real app tree. ```text app/ main.py bootstrap; composition root assets.py AssetManifest + href constants, when static files exist config.py env-first Config, when needed db.py thin database core, when needed common/ shared shell, markup, identity, helpers features/ / urls.py UrlPath constants handlers.py handler factories + register_(app, ...) views.py pure data -> HTML models.py optional domain dataclasses data.py optional schema + queries signals.py optional Datastar signal reader subjects.py optional Relay subject helpers static/ tests/ test_.py ``` Every app can start with one feature folder. The second feature should add another folder, not force a late split from a flat `handlers.py`. When building the first feature, place it under `app/features//`. Use `UrlPath` constants in `urls.py`, route registration in `handlers.py`, pure markup views in `views.py`, and `TestClient` tests under `tests/`. Run `uv run ruff check .`, `uv run pyright`, and `uv run pytest` before calling the work done. ## Stario Invariants Stario is not ASGI. Do not mount it in Uvicorn, Hypercorn, FastAPI, or Starlette, and do not return framework response objects. Run Stario with a bootstrap spec: ```bash uv run stario watch app.main:bootstrap uv run stario serve app.main:bootstrap ``` Handlers have this shape and complete responses through `Writer`: ```python async def handler(c: Context, w: Writer) -> None: responses.html(w, view(...)) ``` Use these imports by default: ```python from stario import App, Context, Span, UrlPath, Writer import stario.responses as responses from stario.markup import html as h ``` Rules: - `bootstrap` is an async generator with exactly one `yield`. - Register routes, static assets, and long-lived dependencies before `yield`; clean up after `yield`. - Use `UrlPath` constants and `.href()` in markup and redirects. Do not invent `url_for`. - Use `c.alive(...)` for long-lived loops. Do not call `Writer.alive()`. - Use `c.app.create_task(...)` for background work tied to app shutdown. - Treat Datastar signals as request input, not authoritative state. - Use `HttpException`, `RedirectException`, `responses.redirect`, or `responses.empty` for control flow; do not half-write a response and then try to replace it. ## Bootstrap And Routes `app/main.py` wires the application. It should not own feature internals. ```python from app.assets import ASSETS from app.features.home.handlers import register_home from stario import App, Span, StaticAssets async def bootstrap(app: App, span: Span): span.attr("app.name", "my-app") static = StaticAssets(ASSETS) static.register(app) register_home(app) yield ``` Each feature owns its URL constants and route registration: ```python # app/features/home/urls.py from stario import UrlPath HOME = UrlPath("/") SUBSCRIBE = UrlPath("/subscribe") SAVE = UrlPath("/save") ``` ```python # app/features/home/handlers.py def show_home(): async def handler(c: Context, w: Writer) -> None: responses.html(w, home_view()) return handler def register_home(app: App) -> None: app.get(HOME, show_home()) ``` Handler factories close over shared dependencies created in `bootstrap`. Views receive plain values and return Stario markup trees. Put domain logic in `data.py`, service modules, or plain functions that do not need `Writer`. ## Datastar And Realtime Realtime is optional. Use it when the UX needs push updates; otherwise normal request/response is simpler. The usual live UI shape is: 1. `GET /page` renders the first document. 2. The page opens `GET /subscribe` with `data.init(at.get(SUBSCRIBE.href(), retry="always"))`. 3. `POST`, `PATCH`, or `DELETE` handlers validate input, mutate server state, publish a Relay notification, and usually return `204` or redirect. 4. The subscribe handler receives the notification, re-reads authoritative state, and sends HTML through `SSE(w)`. Start with broad morphs. Re-render the whole page, or a large stable fragment, and let Datastar morph the live DOM: ```python sse.patch_elements(home_view(state)) ``` This is the preferred first version because it keeps `ui = f(state)` obvious. Datastar morphing and HTTP compression make broad patches practical for many apps. Only switch to dedicated target patches when you have observed a real cost or need to protect a shell that owns signals, focus, or a long-lived subscription. For multi-feature apps, use a stable outer shell and one live fragment: ```python # views.py LIVE_SELECTOR = "#home-live" def home_view(state): return page( h.Div( {"id": "home"}, data.signals({...}, if_missing=True), data.init(at.get(SUBSCRIBE.href(), retry="always")), home_live_view(state), ) ) def home_live_view(state): return h.Div({"id": "home-live"}, ...) ``` ```python # subscribe handler sse.patch_elements(home_live_view(state), selector=LIVE_SELECTOR, mode="outer") ``` Use targeted `selector=` patches and `patch_signals` after the broad version is correct and a smaller patch is clearly better. Relay is an in-process notification bus. It is not durable and does not cross workers. For more than one process, keep the same command/subscribe shape and use Redis, NATS, Postgres notifications, or another broker. ## Tests And Checks Prefer integration tests against the production bootstrap: ```python from stario.testing import TestClient async with TestClient(bootstrap) as client: response = await client.get("/") assert response.status_code == 200 ``` Use these commands from the project root: ```bash uv sync uv run ruff check . uv run ruff format . uv run pyright uv run pytest uv run stario watch app.main:bootstrap ``` Stario does not load `.env` files. Use `STARIO_*` environment variables such as `STARIO_HOST`, `STARIO_PORT`, `STARIO_TRACER`, and `STARIO_TRACERS_SQLITE`. ## Common Mistakes | Avoid | Prefer | | --- | --- | | Flat `routes.py` / `handlers.py` at `app/` root | `app/features//` from day one | | FastAPI, Starlette, ASGI response objects | `App`, `UrlPath`, `Context`, `Writer`, `responses.*` | | `bootstrap` without `yield` | One async-generator bootstrap with startup before `yield` | | `url_for(...)` | `HOME.href()`, `ITEM.href(id=x)` | | Bare `asyncio.create_task(...)` | `c.app.create_task(...)` | | `Writer.alive(...)` | `c.alive(...)` | | Datastar signals as database | Server state in memory, SQLite, or a real datastore | | Tiny targeted patches before the UI works | Broad page or fragment morphs first, targeted patches after measurement | ## Further Reading - [AI-assisted development](https://stario.dev/docs/how-tos/ai-assisted-development) - [Structuring apps](https://stario.dev/docs/how-tos/structuring-apps) - [The go-to architecture](https://stario.dev/docs/explanation/go-to-architecture) - [Datastar](https://stario.dev/docs/reference/datastar) - [Testing](https://stario.dev/docs/reference/testing)