8.6 KiB
AGENTS.md
Guidance for AI coding agents working in this repository. Each component directory has its own AGENTS.md; read it before working there.
1. Think before coding
State your assumptions. If several interpretations exist, present them rather than picking one silently. If a simpler approach exists, say so. If something is unclear, stop and ask.
2. Simplicity first
The minimum code that solves the problem: no features beyond the ask, no abstractions for single-use code, no unrequested configurability, no error handling for impossible cases. If 200 lines could be 50, rewrite.
3. Surgical changes
Touch only what the request needs. Do not improve adjacent code, comments or formatting, do not refactor what is not broken, and match the existing style. Mention unrelated dead code; do not delete it. Remove only the imports, variables and functions your own change made unused.
Leave no containers running. Stop every stack or container you started
before finishing, including after a failure or interruption (make e2e-*
tears its own down). Leave up only a stack that was already running or that
the user asked to keep.
4. Comments serve the reader, not the requester
A comment explains the code as it stands. Never address it to whoever asked for the change ("as requested", "deliberately without X"), never narrate what changed or moved, never justify at paragraph length. Comment genuine traps: a non-obvious invariant, an upstream bug, an ordering that looks arbitrary and is not. Test: does it still earn its lines for someone reading the file a year from now with no idea what was asked? The same test applies to the prose in these AGENTS.md files.
Keep comments as simple and short as possible, so one read is enough: short sentences, plain words, no nested clauses or dashes. Good:
// the simulation ends once the metric has data, but the cache may
// still hold an empty result from before. Refetch, but only when the
// query can run, because refetch does not check that itself.
Bad: "the cached answer is keyed by the time range and not by the window it resolves to, so the one waiting here was taken while the metric was still missing — which is what the block has just been told it no longer is."
5. Goal-driven execution
Turn the task into a verifiable goal ("fix the bug" becomes a test that reproduces it, then passes). For multi-step work, state a short plan with a check per step and loop until every check passes.
6. AGENTS.md itself
- If a request contradicts this file or a nested one, name the conflict and ask which side wins. Once the new direction is confirmed, update the affected section in the same task.
- These files hold only what cannot be learned from the code, the READMEs or git history: conventions, invariants, traps, and where things live. Before adding a line, check that it is not already stated, not readable from the code it describes, and not the story of one change. Write the rule; a reason only where the rule would otherwise look arbitrary.
- Some tools load at most 32 KiB of AGENTS.md per session: this file plus
every AGENTS.md on the path to the working directory. This file plus any
component's file stay under 32 KiB together; a package-level file below a
component stays under 5 KiB (
wc -c). When a file grows, cut before adding.
Repository map
Oxynote is a collaborative documentation product. One repo, four buildable components:
web/— Nuxt 4 + Vue 3; ships as a web app (SSR) and an Electron desktop app. Own pnpm workspace. web/AGENTS.md.server/core/— Go API server, modulegithub.com/oxynote/oxynote/server/core. Architecture in server/AGENTS.md, Go standards in server/core/AGENTS.md.server/auth-realtime/— Node service: Better Auth and Hocuspocus (Yjs) in one Hono process. pnpm. server/auth-realtime/AGENTS.md.datagen/— demo-data generator, separate Go module.e2e/— Playwright suite plus the two compose stacks it drives,dev(built from this repo) andprod(the all-in-one image); neither is an unnamed default. e2e/AGENTS.md.docker/— dev compose stack, Caddyfile,env/(committed*.example.envtemplates;make setupcopies them to the gitignored*.local.envthe stack reads),demo/data configs.docker/prod/— the production all-in-one image, its Caddyfile, the TS launcher and the example compose. docker/prod/AGENTS.md.scripts/— helpers the root Makefile calls.
One .gitignore, at the repository root. Nested rules carry their path
(e2e/test-results/).
Common commands
From the repository root:
make deps / setup / setup-force # deps; + env files from templates; + overwrite them
make run / start / stop # dev stack foreground / background / stop
make dev # backend containers + web dev server on :3000
make check-env / sync-env # report / reconcile *.local.env against templates
make lint / check-lint # fix / verify lint, format and types everywhere
make test # unit tests + coverage gates everywhere
make check-all # check-lint, test, e2e-dev, e2e-prod; run before a commit
make prod-build / prod-run[-no-email] / prod-stop # all-in-one image on :8080 (mailpit :8025)
make e2e-{dev,prod}[-stack-build|-stack-stop] # dev stack :18080, prod image :19080 (no email :19081)
make prod-publish is release-only (.github/workflows/release.yml).
Component commands are in the nested files.
Cross-component contracts
- Front door: Caddy on
:8080./core/*is path-stripped to core (:8080),/auth-realtime/*to auth-realtime (:8081), everything else to web SSR. The frontend usesNUXT_PUBLIC_CORE_API_BASE_HTTP_URLandNUXT_PUBLIC_AUTH_REALTIME_API_BASE_*_URL. - Trust boundary:
/api/...needs a session. Core's/api/x/...and auth-realtime's/api/internal/...have no auth, exist for service-to-service calls, and are blocked by the Caddyfile; never expose them. Fork, branch update, merge and branch delete are/api/xroutes that are also session-authed and reachable only through auth-realtime (see server/AGENTS.md)./api/apps/...is public and sessionless (GitHub/Slack webhooks and OAuth callbacks, proven by signature or encrypted state)./api/mcptakes OAuth 2.1 bearer tokens issued by auth-realtime and validated against its internal MCP session endpoint. - Session validation: core calls auth-realtime's
/api/auth/get-session; auth-realtime owns the Better Auth schema. - Yjs invariant: the Hocuspocus
documentNameis"<documentId>-<branchIdentifier>", split on the first-. Never seed or merge one Y.Doc from another withY.applyUpdate; usereplaceYdocContent(server/auth-realtime/src/ydocument.ts). Read the document-storage section of server/AGENTS.md before touching branch content. - System writes: a protected branch accepts only core's own persists,
marked through
edit.Client.Apply'ssystemargument and recognised byonStoreDocumentonly while every change came from core. Protection is enforced at the connection (onAuthenticatemakes it read-only), because a persist carries whole-document state. - Env naming: core reads
OXYNOTE_CORE_*(viabuildinfo.Getenv), auth-realtimeOXYNOTE_AUTH_REALTIME_*, the frontendNUXT_PUBLIC_*. _DSNvs_URL: a connection string carrying credentials ends in_DSN; a plain address ends in_URLand any credential is its own variable (CHANGEDETECTION_API_URL+_API_KEY). The suffix says whether the value is a secret and never changes across the trust boundary.
Code style (TS/JS — web and auth-realtime)
- Comment only complex implementations, edge cases and places that would confuse a future reader, under section 4 above.
//comments only, never/* */, wrapped at 80 characters.- Never reference AGENTS.md or another agent-instruction file from a comment; inline the rule instead. Links to real docs (READMEs, upstream issues) are fine.
- Within a comment the first sentence starts lowercase and later sentences
uppercase (
// strip the leading "; " that getCookie emits. Downstream code expects a bare pair.), unless the first word is an identifier. - A blank line before and after each logical block (function,
if/else, loop,try/catch,switch), not between every statement. Aniforswitchfollows the declaration of the value it checks directly, with no blank line between them.
Go style and Vue conventions live in the nested files.