Project-specific rules and conventions for AI assistants and contributors.
Absolutely forbidden: inferring, guessing, or "reasoning about likely behavior" of any agent CLI (claude, codex, gemini, opencode, hermes, aionrs, …) — its wire protocol, message shapes, field semantics, timing, defaults, or capabilities — from a CLI's name, a plausible mental model, prior training knowledge, or how you think it probably works. Every claim about an agent CLI's behavior MUST be backed by one of these approved sources, cited explicitly by path:
- Captured real data — actual sampled wire traffic under
~/aion/protocols/samples/(e.g.codex-cli/<ver>/,claude-cli/<ver>/,codex-acp/,opencode/,capture/). This is ground truth for what the CLI actually emitted. - The ACP library source —
agent-client-protocol(main crate +agent-client-protocol-schema), vendored at~/.cargo/registry/src/*/agent-client-protocol-*— for the canonical ACP wire types and semantics we translate to. - An official adapter's code — the codex
app-servermachine-generated JSON schema under~/aion/protocols/samples/codex-cli/<ver>/schema-full/(ground truth from the codex binary itself), the official claude-code / claude-code-acp adapter source, or an equivalent first-party adapter — for inferring a CLI's contract from the reference implementation.
Additional reliable sources, when a claim can be grounded in them: the CLI binary's own
--help / self-describing schema output (run it and read it), and our own passing
integration/live-e2e fixtures that were recorded against the real CLI (not hand-authored
mocks). If none of these can substantiate a claim, the honest answer is "not yet verified —
need a capture/schema", and the next step is to capture or read a source — NOT to guess.
When you state any agent-CLI behavior, cite the source inline: verified: samples/codex-cli/0.137.0/schema-full/ClientRequest.json
or verified: agent-client-protocol-schema-0.12.0/src/session.rs. A claim with no such citation
is a guess and violates this rule. This is a non-negotiable, standing constraint — it outranks
convenience and applies to every statement, plan, commit message, and design doc.
This rule exists because of a repeated, costly failure mode: forming a confident conclusion from a proxy for the truth instead of the truth itself, then reporting it to the user as fact. Concrete instances that must never recur:
- Trusting a sub-agent's conclusion without checking its evidence. A spawned agent reported "the frontend has NO question renderer; the break is in the frontend." That was false — the frontend renders whatever
options[]the backend sends; the real bug was the backend hard-coding[Allow, Reject]options for an AskUserQuestion. The conclusion was relayed to the user verbatim. A sub-agent's report is a lead, not a fact. Before you repeat any load-bearing claim from an agent, open the cited file:line and confirm it says what the agent said. - Declaring equivalence/correctness from a thin test. A frame-by-frame A/B was run with one trivial prompt ("Reply PONG") that only exercised
start/text/finish, then "core turn flow is equivalent" was declared. The prompt never triggered tool-output streaming, subagents, plans, permissions, AskUserQuestion, or mode-switch — where all the real divergences were. A green result on inputs that don't exercise the behavior is not evidence about that behavior. Before claiming a class of behavior works, confirm your test actually produces that class of event.
Enforced behaviors:
- Cite from primary source. Any claim about what code does must be backed by a file you (this agent) have read this session — not a sub-agent's summary, not memory, not inference from names. Sub-agent findings must be spot-checked against the code before being surfaced.
- Verify the negative before asserting absence. Never say "X has no Y / feature Z doesn't exist / the frontend can't do this" until you have grepped for it AND read the relevant handler. Absence is a strong claim; a single missed file falsifies it (e.g.
sideQuestion.ts,MessageAcpPermissionrendering dynamicoptions[]). - Match the test to the claim. When verifying behavior, the test input must exercise the exact events/paths the claim covers. Trivial/happy-path inputs prove only the trivial path. When you cannot exercise a path, say so explicitly rather than implying it passed.
- Trace to the break, don't guess the layer. For a cross-layer bug (backend→wire→frontend), follow the actual data through every link and locate where it diverges from expected. Do not attribute the break to a layer by plausibility.
- Calibrate language to evidence. Say "verified: file:line", "not yet checked", or "a sub-agent claims X (unverified)". Never launder an unverified lead into a flat assertion.
See also the standing discipline in the root AGENTS.md / memory aioncore-verification-blindspot-g6: self-consistent-all-green ≠ correct — verify outward (against a real agent) AND against the old/reference implementation, not just against your own happy path.
When planning or changing a critical path or hard-to-observe flow, evaluate whether logging needs to change. In implementation plans for such changes, briefly state whether logs will be added, existing observability is sufficient, or logs are intentionally unnecessary. Do not add logs for simple refactors, test-only changes, UI copy/style changes, or when existing tests, errors, metrics, or logs already provide enough observability.
Add structured logs only when they help make production behavior diagnosable or provide extra development detail. Production normally runs at info, while development runs at debug; therefore, information needed to troubleshoot production issues must be available at info, warn, or error, not only debug.
Use log levels as follows:
debugfor high-frequency or detailed development-only flow details and state transitionsinfofor low-volume production-diagnostic lifecycle boundaries, important state changes, and non-sensitive correlation contextwarnfor malformed or unexpected data that is safely handlederrorfor contract violations or failed operations
Production-visible logs must not include sensitive payloads such as prompts, tool input/output, file contents, command bodies, tokens, secrets, or raw provider requests/responses. If such payloads are needed for local debugging, they must be behind explicit development-only guards and never enabled by default.
For detailed background and design decisions, see ARCHITECTURE.md.
Cargo workspace organized in four layers: Foundation → Capability → Domain → Composition. Dependencies flow strictly downward.
- ✅ Upper layers may depend on lower layers (including cross-layer)
- ✅ Same-layer interaction through trait abstractions only
- ❌ No lower-layer depending on upper-layer
- ❌ No circular dependencies
- Changes to foundation crates require impact assessment
Every domain crate must follow:
lib.rs— module exports only, no business logicroutes.rs— exportdomain_routes(state) -> Router, handlers do request/response transformation onlyservice.rs— sole location for business logic, must not import axumstate.rs—#[derive(Clone)]RouterState holding Arc-wrapped dependencies
- Route prefix:
/api/ - Resource names: kebab-case
- Response format:
ApiResponse<T>(success) /ErrorResponse(failure) - All request/response types defined in
aionui-api-types aionui-api-typesmust NOT depend on axum/tower or any HTTP framework- Use
aionui_common::ApiErroronly at API/HTTP boundaries such as routes and middleware. Service/domain code must prefer crate-owned errors (ConversationError,TeamError, etc.) and map them toApiErrorin route modules.
- Format:
domain.camelCaseAction(two-level structure) - Message type:
WebSocketMessage<T>(name + data) - Existing kebab-case or three-level names are legacy — new events must follow the convention
- Repository traits in
aionui-db, prefixed withI - Concrete implementations prefixed with
Sqlite - Row models in
aionui-db/src/models/ - Params objects co-located in repository files
- Migrations:
NNN_descriptive_name.sql, no manual DB modifications - Services depend on traits, never on concrete implementations
AppServicesis the sole service construction center- Domain crates only define RouterState, never construct their own dependencies
- All assembly happens in
aionui-app'sbuild_*_state()functions
- New endpoints must be evaluated for auth middleware requirement
- State-changing operations must be CSRF-protected
- Sensitive operations should have rate limiting
- Error responses must not leak internal details
- Secrets must never be hardcoded
- Rust 2024 edition, stable toolchain (pinned in
rust-toolchain.toml) - Comments in English, commit messages in English
- Each
.rsfile follows single responsibility — one module, one concern - Target under 1000 lines per
.rsfile; exceeding it is a signal to split into submodules, not a hard limit (test files exempt)
New subprocess spawn sites must go through aionui_runtime's spawn Builder — never raw tokio::process::Command. See ARCHITECTURE.md § Runtime Infrastructure for the correct constructor and details.
Always use just push instead of git push.
It runs the full pre-push gate (migration check, lint, format, tests) before pushing, preventing CI failures.
Supports the same arguments as git push (e.g. just push -u origin feat/branch).
- Request/response types →
aionui-api-types/src/{domain}.rs - Handler function →
crates/aionui-{domain}/src/routes.rs - Business logic →
crates/aionui-{domain}/src/service.rs - Register route in
domain_routes()function - Add test →
crates/aionui-{domain}/tests/orcrates/aionui-app/tests/
- Next number →
ls crates/aionui-db/migrations/ - Create
NNN_descriptive_name.sqlwithIF NOT EXISTS
- Event type →
aionui-api-types - Emit via
event_bus.broadcast()in service - Naming:
domain.camelCaseAction
| Location | What goes there |
|---|---|
Inline #[cfg(test)] in each .rs file |
Unit tests for that module's internals |
crates/<crate>/tests/ |
Integration / E2E tests for that crate |
- Database tests use
init_database_memory() - Prefer real in-memory DB over mocks; mock only to isolate unneeded dependencies
- New features must include tests
Happy Path (Critical Paths)
Every new or modified feature must have integration tests covering its normal flow. Critical paths that always require test coverage:
- Authentication flow (login, token refresh, permission checks)
- Message sending and retrieval
- Agent session creation and interaction
- File upload/download
- WebSocket connection and event delivery
Bad Path (Error Paths)
New endpoints or business logic must include tests for these scenarios:
- Invalid input (missing fields, wrong types, oversized content)
- Resource not found (404)
- Insufficient permissions (unauthenticated, accessing another user's resources)
- Business rule violations (duplicate creation, operations not allowed in current state)
Bad path tests must assert specific error codes or error messages — asserting merely "not success" is not acceptable.
Security Tests
Endpoints involving authentication, authorization, or data isolation must include security tests:
- Unauthenticated requests are rejected (401)
- Cross-user data isolation (user A cannot access user B's resources)
- State-changing requests are rejected when CSRF token is missing or invalid
- Sensitive fields (passwords, tokens) never appear in responses
WebSocket Event Tests
New WebSocket events must verify:
- The event is emitted after the correct business operation
- Event payload conforms to
WebSocketMessage<T>structure - Events are only delivered to authorized subscribers (no leakage to unrelated users)
When a test fails, do NOT modify the test to make it pass. First determine:
- Test assertion still represents correct behavior → fix implementation, not the test
- Requirements/interface intentionally changed → may update test, but must confirm:
- The change is intentional (not an unintended side effect)
- New assertions still validate meaningful behavior
- Uncertain → stop, trace back the change, clarify before proceeding
Prohibited:
- ❌ Deleting failing tests to "fix" the problem
- ❌ Weakening specific assertions to vague ones (e.g.,
assert_eq!(status, 201)→assert!(status.is_success()))
⚠️ When to run what:
- During development: only test the crate you're working on →
cargo test -p aionui-<crate>- After implementation complete: full verification →
cargo test --workspace- Do NOT run
cargo test --workspaceat the start of a task.
⚠️ Performance:
cargo clippy --workspacetakes several minutes — userun_in_background: true.cargo test --workspacetakes 10+ minutes. MUST userun_in_background: truewhen calling via Bash tool, otherwise it will timeout.cargo clippy -p aionui-<crate>andcargo test -p aionui-<crate>typically complete in under 1 minute.
cargo test -p aionui-<crate> # Test the crate you changed
cargo clippy -p aionui-<crate> -- -D warnings # Lint the crate you changedcargo fmt --all -- --check # Format gate (instant)
cargo clippy -p aionui-<crate1> -p aionui-<crate2> -- -D warnings # Lint affected crates
cargo test -p aionui-<crate1> -p aionui-<crate2> # Test affected cratesjust push # full pre-push gate, then push