agentleFS
seaaveyseaavey/SRouter/AGENTS.md

SRouter

AGENTS.md24 starsChanged 16 days agoMITView on GitHub

SRouter — a self-hosted, multi-provider LLM gateway ("Multi-Provider OpenAI & Anthropic Compatible LLM Gateway", server/src/app.rs:54). It exposes OpenAI-style (/v1/chat/completions, /v1/chat), Anthropic-style (/v1/messages), and image (/v1/images/generations) endpoints, authenticates callers, routes each model id onto a per-vendor provider driver, streams SSE back, logs every request to SQLite, and serves an operator API plus an optional SPA dashboard. Two independently-toolchained halves in one repo, sharing only generated TypeScript bindings: There is no workspace root manifest. The root carries the container entry points (Dockerfile,…

  • Reads credentials

We split this AGENTS.md into the rules it gives an agent

7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read this before touching a file.

    line 32
  2. Distinctive:

    Do the requested change, completely, and nothing else: No unrequested features, refactors, dependencies, or "while I'm here" tidy-ups.

    line 36
  3. Very specific:

    Verify by running, not by reasoning: Every behavioural change gets exercised on the real path — the handler, the CLI, or the browser.

    line 56
  4. Distinctive:

    State exactly what you ran: Never claim coverage you did not exercise, never describe an untested path as working, and never report success …

    line 57
  5. Distinctive:

    Report a blocker; do not route around it: If the requested change cannot be made safely, say which invariant it breaks and what you tried — …

    line 58
  6. Very specific:

    Run from the repo root. Server (verbatim from CONTRIBUTING.md:12-37):

    line 129
  7. Very specific:

    All client-visible strings/codes/headers come from server/src/constants.rs — never inline literals in handlers.

    line 165

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Repository Guidelines

Project Overview

SRouter — a self-hosted, multi-provider LLM gateway ("Multi-Provider OpenAI & Anthropic Compatible LLM Gateway", server/src/app.rs:54). It exposes OpenAI-style (/v1/chat/completions, /v1/chat), Anthropic-style (/v1/messages), and image (/v1/images/generations) endpoints, authenticates callers, routes each model id onto a per-vendor provider driver, streams SSE back, logs every request to SQLite, and serves an operator API plus an optional SPA dashboard.

Two independently-toolchained halves in one repo, sharing only generated TypeScript bindings:

HalfStackNotes
server/Rust edition 2024, axum 0.8, tokio, sqlx 0.9 (SQLite only), spectaThe real product; behaviorally ported from a deleted Node apps/api
client/Bun + Vite + React 19, TanStack Router/Query, Tailwind v4, shadcn (base-mira on @base-ui/react)Early-stage SPA: auth spine (login/first-run setup), provider management, request logs, and usage/quota are live; keys and settings are still static mockups

There is no workspace root manifest. The root carries the container entry points (Dockerfile, .dockerignore, docker-compose.yml, Makefile), and .github/workflows/ holds the CI plus the manual GHCR publish. cargo is the whole server build system.

Architecture & Data Flow

Boot (server/src/main.rs): dotenvy::dotenv() → telemetry::init() → APIConfig::from_env_map → AppDatabase::connect (runs migrations to SCHEMA_VERSION = 7) → bootstrap_admin_account_from_env → ProviderRegistry::with_database + register_custom_providers → AppState::with_security(...).with_database(...) → spawns catalog warm-up and a 60 s OAuth token sweeper → listeners::serve_main(create_router(state), 0.0.0.0:PORT).

Request: axum router (server/src/app.rs create_router) → per-mount guards → feature handler → model resolution → provider driver → UpstreamClient (reqwest) → SSE/JSON back out; interception/logging.rs writes the request log and settles quota best-effort.

Composition root / DI: manual, no container. AppState + SecurityState (server/src/state.rs) hold Arc<dyn Trait> stores; middleware attaches Extension<APIPrincipal>; handlers extract State, Extension, Path/Query, Request. Empty* store impls + SecurityState::unconfigured() keep a DB-less boot coherent.

Gateway pipeline (server/src/features/gateway/): routes.rs → chat.rs / messages.rs / images.rs → body parse (protocol/model.rs) → ensure_model_allowed_any (api_keys/access.rs) → token_saver → reserve_api_key_quota → ProviderRegistry::resolve(model) → ProviderAdapter/ProviderExecutor → upstream call. Streaming uses mpsc + ReceiverStream; provider failures become in-stream SSE error events (adapter.rs), never stream errors. interceptor.rs intercepts built-in search tool calls the client did not declare.

Router layering rules (critical, in app.rs): guards are applied per mount — read surfaces get api_key_auth, operator surfaces get require_admin_session + CSRF, OAuth callbacks are public, admin-auth routes enforce session per handler. /v1 and /v1/v1 nests each own a JSON 404 fallback so they never fall through to the SPA. Global layers, last-added = outermost: log_access → log_failed_requests → security_headers → cors. Root route + SPA fallback must be registered before the layers (Router::layer only wraps existing routes).

Bindings flow: Rust wire types derive Serialize + specta::Type → server/src/bindings.rs registry → cargo run --bin export_ts → writes both server/bindings.ts and client/src/generated/typed.ts. The client imports them through client/src/api/types.ts.

Non-Negotiable Rules

Read this before touching a file. Nothing here is enforced by tooling; it is enforced by review. Standard is a senior engineer who owns the code for the next six months, not one who ships the fastest diff.

Scope

  1. Do the requested change, completely, and nothing else. No unrequested features, refactors, dependencies, or "while I'm here" tidy-ups. Reduce scope only with explicit approval; never silently.
  2. A diff is the unit of work. It compiles, runs, and is verifiable on its own. Rename a symbol → every callsite lands in the same change, and the obsolete version leaves with it. No shims, re-exports, or deprecation aliases.
  3. Leave no scaffolding. No TODO/FIXME marker, stub, mock, placeholder, or unreachable branch in shipped code. server/src and client/src contain zero today — keep it that way. (Four comments in features/provider_auth/ cite a TODO.md deleted in 233b7a6; that is a stale pointer, not a marker — the same class as the dead docs/*.md citations, and not a pattern to copy.)

Design

  1. No over-engineering. The entire server defines 5 traits (APIKeyStore, APIKeyRepository, SessionStore, AdminAuthRepository, ProviderExecutor) and no DI container; the client has 19 runtime dependencies. Do not add an abstraction, generic parameter, trait, registry, plugin point, feature flag, or dependency until a second concrete caller exists. Three repeated lines beat a premature helper.
  2. Reuse the vocabulary that exists. client/src/components/ui/ (shadcn/Base UI), APIError + constants.rs on the server, TestDatabase in tests, create_*_router for mounts. A second convention for a job that already has one is the defect — even when the new one is nicer.
  3. Match the file you are in. Same naming, module layout, error style, and comment style as its neighbours. Read the surrounding 50 lines before adding to anything.
  4. Comments explain why, never what. Record the trap that motivated the line (see client/vite.config.ts on changeOrigin, state.rs on unconfigured). No narration, no section banners, no restating the next line.

Correctness

  1. No unwrap() on a production path. The codebase has exactly one (providers/registry.rs:282) and clippy will not flag yours. Propagate with ? into APIError; use expect("...") only where failure is provably impossible (static parse, literal) and always with a message. Lock poisoning is recovered, not unwrapped: .unwrap_or_else(|e| e.into_inner()).
  2. One error type. APIError with builders, propagated by ?. No anyhow, thiserror, Box<dyn Error> outside main, and no per-module error enums.
  3. Every client-visible string, code, and header name comes from server/src/constants.rs. Never inline a message in a handler; interpolated messages become functions.
  4. Frozen behaviour stays frozen. Node-parity semantics, generated files, [profile.dev] debug = 1, per-mount guard placement, global layer order, and the frozen test contracts are all deliberate. If a change seems to require altering one, stop and report it rather than "fixing" it.

Evidence

  1. Verify by running, not by reasoning. Every behavioural change gets exercised on the real path — the handler, the CLI, or the browser. Tests alone are not proof. A green cargo check or tsc proves only that it compiles.
  2. State exactly what you ran. Never claim coverage you did not exercise, never describe an untested path as working, and never report success from a command you skipped. Unverified is a valid answer; "probably fine" is not.
  3. Report a blocker; do not route around it. If the requested change cannot be made safely, say which invariant it breaks and what you tried — do not silently weaken a guard, widen a scope, or delete a test to make the change fit.

For server work, skills/srouter-server/SKILL.md is the detailed companion to the rules above; for client work, the companion is skills/srouter-frontend/SKILL.md.

Workflow: Superpowers Skills (Mandatory)

All code work in this repo runs through the superpowers set installed at ~/.agents/skills (15 skills, symlinked into the Hermes skills root as ~/.hermes/skills/<name>). Load the skill with skill_view(name=...) when its trigger fires. Do not paraphrase the process from memory, and do not skip a phase because the diff "looks small".

The Hermes-side router for this file is the skill seaavey-project-workflow: it maps phase → skill and project → repo skill. This section is the in-repo copy of the same mandate, so a non-Hermes agent reads the same rules.

TriggerSkill
Starting a task, or unsure which process appliesusing-superpowers
Any work that produces or changes something — feature, behaviour, doc, configbrainstorming (load it first, every time; it decides whether a conversation is needed)
Any *.tsx file, created, edited, reviewed, or refactoredvercel-react-best-practices (filename trigger, before the first edit, no exception)
client/src/api/**, queryOptions, useQuery, useMutation, QueryClienttanstack-query
client/src/routes/**, routeTree.gen.ts, createFileRoute, validateSearch, useSearchtanstack-router
Data grid, form, long-list virtualization, debounce, devtools paneltanstack-table / tanstack-form / tanstack-virtual / tanstack-pacer / tanstack-devtools (only the one in play)
Multi-step work, before implementingwriting-plans
Executing a written planexecuting-plans
Plan has independent tasks worth parallelisingdispatching-parallel-agents
Plan tasks handed to fresh subagentssubagent-driven-development
Any implementation or bugfix, before product codetest-driven-development
Any bug, test failure, or unexpected behaviour, before proposing a fixsystematic-debugging
A superpowers run itself went wrongdiagnosing-superpowers
Work complete, before mergerequesting-code-review
Review feedback arrivedreceiving-code-review
About to claim "done", "fixed", or "passing"verification-before-completion
Branch ready to merge or open a PRfinishing-a-development-branch
Long or risky work that needs isolation from mainusing-git-worktrees
Authoring or editing a skillwriting-skills

Hard gates, in order:

  1. brainstorming is loaded before any work that produces or changes something. Loading is unconditional and comes before the first edit or the first reply that commits to an approach; the skill itself decides whether the request is complete enough to just build (make the icon cornflower blue) or needs a conversation. The only exits are the ones it defines: the human opts out of the questions, or asks for a spike. "It's a small change" is not an exit.
  2. No production code before a failing test from test-driven-development. Where this repo genuinely has no runner for the surface (the client has none), say so explicitly and substitute bun run typecheck && bun run lint && bun run build plus a browser pass — never a silent skip.
  3. No fix before systematic-debugging Phase 1 has a root cause. Three failed fixes mean the architecture is questioned (Phase 4.5), not a fourth attempt.
  4. No "done" claim before verification-before-completion. Rule 12 (verify by running) is the evidence; the skill is the checklist.
  5. No merge without requesting-code-review on the exact SHA range, with Critical and Important findings resolved.
  6. Passing a superpowers phase is not passing this repo's gates: cargo fmt --check, clippy -D warnings, the full suite, and the client checks still apply.

The bundled Hermes copies of requesting-code-review, systematic-debugging, and test-driven-development are parked in /root/.hermes/skills-disabled/bundled-superpowers-duplicates/. Leave them disabled: a second skill with the same name makes skill_view refuse to resolve the name at all.

<!-- antislop:start -->

antislop (UI, copy, comments, mobile)

Interface, copy, accessibility, mobile-layout, or code-comment work also loads the antislop filter. On Hermes Agent the skills are antislop (core, always loaded with one of the below) plus antislop-ui, antislop-copywriting, antislop-human, antislop-layoutmobile, antislop-code. ui-ux-pro-max adds design intelligence and never replaces the filter.

Resolve the mode before the first edit: an explicit choice in the conversation, else ~/.config/antislop/settings.json, else ask DURING or AFTER and stop. Announce antislop active: <mode> (<source>). once. Run the Delivery Gate before handing anything over: a PASS/FAIL line per rule, each PASS backed by evidence, and a report containing a FAIL is never shipped.

<!-- antislop:end -->

Key Directories

PathContents
server/src/app.rsThe only router assembly; ApiInfo, /health, route_not_found
server/src/state.rsAppState, SecurityState, builders
server/src/error.rsAPIError + IntoResponse → OpenAI ErrorEnvelope
server/src/constants.rsEvery client-visible message, code, error_type, header name
server/src/features/One dir per feature: gateway/ (chat, messages, images, translation, interception, search), providers/ (registry, executors, per-vendor drivers, custom/, management/, rotation), catalog/, api_keys/, admin_auth/, provider_auth/, logs.rs, settings.rs, database_transfer/
server/src/http/middleware/ (api_key_auth, admin_session, rate_limit, csrf, cors, body_limit, access_log, failure_log, security_headers), listeners.rs, static_files.rs
server/src/infrastructure/database/ (sqlite handle, migrations, stores), upstream/ (client + SSRF guards), telemetry.rs
server/src/protocol/Leaf wire types: model, usage, sse, image
server/migrations/NNNN snake_case.sql, DDL only, embedded via include_str!; no sqlx-cli
server/tests/46 integration suites + the 3.5k-line support/ harness
client/src/routes/TanStack file routes; protected screens belong under _authenticated/
client/src/api/The only network layer (client.ts wrapper + queryOptions factories)
server/src/benchmark/k6 scenarios — external k6 binary, not part of cargo test

Development Commands

Run from the repo root. Server (verbatim from CONTRIBUTING.md:12-37):

cargo run --manifest-path server/Cargo.toml                     # listens on :3000
cargo test --manifest-path server/Cargo.toml --test chat_completions   # one suite
cargo test --manifest-path server/Cargo.toml --locked           # everything
cargo fmt --manifest-path server/Cargo.toml -- --check
cargo clippy --manifest-path server/Cargo.toml --all-targets --all-features --locked -- -D warnings
cargo run --manifest-path server/Cargo.toml --bin export_ts     # regenerate bindings (commit both files)
cargo test --manifest-path server/Cargo.toml --test opencode_live -- --ignored --nocapture  # network test

Client, with Bun from client/:

bun run dev        # vite; proxies /v1 → SROUTER_API_URL ?? http://127.0.0.1:3000, changeOrigin:false
bun run typecheck  # tsr generate && tsc --noEmit
bun run lint       # eslint .
bun run format     # prettier --write "**/*.{ts,tsx}"
bun run build      # tsr generate && tsc -b && vite build

Two runtime gotchas:

  • dotenvy reads .env relative to the current working directory, so run the server with cwd server/ (or export vars) for server/.env to apply.
  • resolve_web_dist does not search client/dist; serving the built SPA needs WEB_DIST_PATH=client/dist (dev builds proxy instead, so this only matters for production-ish runs).

Pricing snapshot maintenance (Python 3 stdlib, not a required dev step): python3 server/scripts/update_models_dev_pricing.py --update|--check.

Code Conventions & Common Patterns

Rust (server/) — 4-space indent, rustfmt defaults; no rustfmt.toml, so cargo fmt output is the law.

  • Errors: one custom APIError { status, message, error_type, code, param } with builders and impl IntoResponse; propagate with ? in Result<_, APIError>. No anyhow/thiserror (zero hits). Box<dyn Error> only in main.
  • Wire fields are snake_case, both directions. All 248 fields in client/src/generated/typed.ts are snake_case and the client depends on it. Do not add #[serde(rename_all = "camelCase")], and use #[serde(rename)] only for a name that is not a valid Rust identifier (error.type). Enum values mirror their protocol instead (UPPERCASE method, lowercase object kind, kebab-case SSE tags) — follow the enum next to yours. Regenerate with export_ts and git diff the generated file so a camelCase field cannot slip through: a Rust type change is a wire contract change.
  • Request bodies are wire shapes too. A POST/PATCH body type derives specta::Type on the server and reaches the client through export_ts (CreateAPIKeyInput, AdminSetupInput, AdminLoginInput); the client never declares its own request shape. A body that carries a secret derives no Debug/PartialEq, so it cannot be printed by accident.
  • All client-visible strings/codes/headers come from server/src/constants.rs — never inline literals in handlers; interpolated messages become functions. error.code is the ErrorCode enum there, so APIError::with_code takes a variant and the generated client sees a closed union instead of string.
  • Async: tokio multi-thread. Traits stay dyn-compatible by returning BoxFuture<'_, …> instead of async fn (see the rationale in providers/executor.rs). Shared state is Arc<..> + RwLock/Mutex with poison recovery; streams flow through mpsc/broadcast.
  • Provider drivers: implement ProviderExecutor in the vendor module, register one constructor — no central dispatch match. File set per vendor: mod.rs + executor.rs/translate.rs/types.rs/auth.rs/catalog.rs/tests.rs. Runtime-registered drivers are Arc<dyn>-based; catalog order lives in SEED_PROVIDERS.
  • Naming: snake_case modules grouped by feature with mod.rs re-exports; handlers are verbs (create_completion, list_quota); router factories are create_*_router; stores are SQLx*Store with traits in store.rs/repository.rs and Empty* fallbacks.
  • Docs: module-level //! comments explain why and cite the Node apps/api/src/...:line oracle. Match that style; a docs/ directory does not exist — comments citing docs/api-v1-contract.md etc. are stale pointers from the Node port, and the contract really lives in the Rust types plus the route tests.
  • JSON shapes: pair #[serde(skip_serializing_if)] with #[specta(optional)]; bindings.rs renders a custom WireShapes format that strips the skip attributes so no _Serialize/_Deserialize splits appear.

TypeScript (client/) — prettier: semi: false, double quotes, printWidth: 80, trailingComma: es5, tailwind class sorting with tailwindFunctions: [cn, cva].

  • strict + noUnusedLocals/noUnusedParameters + verbatimModuleSyntax + erasableSyntaxOnly → use import type / inline type imports.
  • Path alias @/* → client/src/* (Vite + tsconfig). Files kebab-case; components PascalCase, named exports.
  • Type discipline. A client shape is a view of a generated wire type — Pick/Omit/Partial/Exclude/Record, never a re-declared field list. Lookup tables are keyed by their union (Record<FontFamily, string>), not by string. No enum: erasableSyntaxOnly bans it, so use a literal union plus a const object joined with satisfies Record<Union, V> (the generated ErrorCode is exactly that). A switch over a union ends in a never assignment. Full table: skills/srouter-frontend/references/conventions.md.
  • No any and no unknown in client/src. any disables the checker; unknown defers every decision to a later instanceof. Take the narrowest true type instead — Error | null for a TanStack Query error, object for a JSON request body, the generated wire type for a response. never is exempt: it is the exhaustiveness mechanism, not an escape hatch. The four unknowns that existed were removed (validateSearch infers its parameter; describe takes Error | null; body is object; the parsed response is object | null).
  • All network access goes through request<T>() in client/src/api/client.ts (credentials: 'include', verbs limited to GET/POST/PATCH/DELETE, no PUT). The verb union is RequestMethod, derived from the generated HTTPMethod via Extract — the log type also carries PUT/HEAD/OPTIONS/CONNECT/TRACE/OTHER, none of which the operator API accepts. Never add a cross-origin API base URL or strip the Origin header: the game is same-origin + HttpOnly SameSite=Lax cookie + server-side Origin CSRF guard.
  • Server state via TanStack Query queryOptions factories declared next to their endpoint (client/src/api/<domain>.ts); the shared ['admin','status'] cache entry is load-bearing for the auth guard and login page. Local state via useState; no Redux/Zustand/immer, no form library, no toast library.
  • cn is re-exported from client/src/lib/utils.ts (the cn package, not clsx+tailwind-merge); UI primitives live in client/src/components/ui/ (shadcn generated over @base-ui/react).

Never hand-edit generated files: client/src/generated/**, server/bindings.ts (regenerate via export_ts; server/tests/bindings.rs fails on drift) and client/src/routeTree.gen.ts (tsr generate / the Vite plugin; committed on purpose).

Important Files

  • server/src/main.rs — composition root; server/src/lib.rs — module list + flat re-exports.
  • server/src/app.rs — create_router(); the single place route guards and layer order are decided.
  • server/src/config.rs — APIConfig::from_env_map, the only env parser; its Debug redacts secrets.
  • server/src/bindings.rs + server/src/bin/export_ts.rs — specta registry and writer.
  • server/src/infrastructure/database/migrations.rs — hand-written runner, SCHEMA_VERSION = 7, PRAGMA user_version carrier, refuses newer versions.
  • server/tests/support/mod.rs — TestDatabase, fake upstreams, fixture stores, request builders; every new suite should start here.
  • client/src/api/client.ts, client/src/api/admin.ts, client/src/routes/__root.tsx (queryClient singleton), client/src/routes/_authenticated.tsx (auth gate).
  • server/.env.example — the authoritative env surface; CONTRIBUTING.md, SECURITY.md — process and security rules.

Runtime/Tooling Preferences

  • Server: Rust stable (edition 2024 ⇒ ≥1.85), pinned only to the channel by server/rust-toolchain.toml; commit server/Cargo.lock and use --locked for checks. No cargo features table; all dep features are inline. [profile.dev]/[profile.test] debug = 1 with dependencies at debug = false is a deliberate disk-footprint decision — do not "fix" it to 2.
  • Client: Bun is the package manager (client/bun.lock is the only lockfile); no engines/packageManager pins, no VITE_* vars — the only client env var is SROUTER_API_URL (Vite config). Never add npm/pnpm/yarn lockfiles.
  • Database: SQLite is the only supported backend. DATABASE_URL is refused at boot (owner ruling 2026-10-08) and must stay unset. All SQL is runtime sqlx::query(...).bind(...) — no query! macros, no .sqlx offline cache, so no database is needed to build or test.
  • Server env vars (names only; parsed once at boot, grouped by concern): listener/storage PORT, DATABASE_PATH (default $HOME/.srouter/srouter.db), WEB_DIST_PATH; URLs/auth SROUTER_PUBLIC_URL, SROUTER_CORS_ORIGINS, SROUTER_SECURE_COOKIES, SROUTER_ADMIN_PASSWORD, SROUTER_TRUST_CF_CONNECTING_IP; runtime/logging NODE_ENV (development is refused at boot: production is the only mode the binary boots), SROUTER_ACCESS_LOG, SROUTER_FILE_LOG, RUST_LOG, SROUTER_UPSTREAM_CONCURRENCY; search fallbacks BRAVE_API_KEY, TAVILY_API_KEY, SERPER_API_KEY, SEARXNG_URL; read outside APIConfig: CLAUDE_OAUTH_CLIENT_ID, CODEBUDDY_PRODUCT_JSON, HOME.
  • Runtimes needed anyway: Python 3 only for the pricing script; Node is not needed at all; k6 only for the optional benchmark scenarios.
  • History lives on branches backup/pre-packages-removal and backup/pre-apps-api-removal (the removed apps/, packages/*, CLI, docs site); CONTRIBUTING.md still claims "no Node toolchain" — stale, client/ is live.

Testing & QA

  • Framework: plain cargo test only. Single dev-dependency: tower (util, for oneshot). No nextest, serial_test, testcontainers, wiremock, insta, or coverage tooling anywhere.
  • Shape: 46 integration suites in server/tests/*.rs (552 test fns) + 78 in-crate #[cfg(test)] modules for provider translation/middleware/db units. Requests are driven either through Router::oneshot with an injected ConnectInfo(SocketAddr) peer (most suites) or over a real 127.0.0.1:0 socket with reqwest (logs.rs, pricing.rs).
  • Isolation: every suite builds its own temp SQLite file via TestDatabase and fakes upstreams (Fake{Upstream,Qoder,CodeBuddy,Cline,Grok,Antigravity,Claude,Codex} in tests/support/, all bound to random loopback ports and aborted on Drop). Tests must never open ~/.srouter/srouter.db or reach a real provider. The only global lock is EVENT_TEST_LOCK in logs.rs around process-global log-event broadcasts; prefer dependency injection over new globals.
  • Expectations: no coverage thresholds; CI (.github/workflows/ci.yml) re-runs fmt, clippy, and the suite on main pushes and pull requests into it, but run the full suite yourself before pushing (cargo test --manifest-path server/Cargo.toml --locked), then fmt --check and clippy -D warnings. Client-side verification is bun run typecheck && bun run lint && bun run build, plus the Playwright suite in client/e2e (bun run e2e: four viewport projects against a throwaway gateway built by e2e/support/stack.ts).
  • Frozen contracts a change must keep green: server/tests/bindings.rs (generated TS), server/tests/schema.rs (user_version = 7, newer refused), server/tests/http_runtime.rs (six frozen response headers incl. X-Version), server/tests/database.rs (DATABASE_URL refused before any SQLite file is touched). Route tests pin status codes and error bodies, so the contract is the Rust types + tests — there is no separate contract document to update.
  • Stated policy (CONTRIBUTING.md): Conventional Commits (feat:, fix:, docs:, refactor:, test:, perf:, chore:), imperative subject, and say what you ran in the PR description.
  • Security constraints to respect (SECURITY.md): provider credentials are cleartext JSON inside the SQLite file and in exports — treat the DB like a .env; gateway keys store only sha256 + prefix (unrecoverable); request logs keep metadata only, never prompt/completion text; report vulnerabilities privately to security@srouter.web.id, never via a public issue.

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository3 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.