argos
AGENTS.md657 starsChanged 11 days agoMITView on GitHub
Monorepo: apps/backend (Express + GraphQL + Objection/Knex), apps/frontend (React + Vite + Apollo), shared code in packages/. Local setup (Docker, DB seeding, dev server) is documented in CONTRIBUTING.md. Run pnpm run static-checks. It is the same turbo task CI runs (check-types, check-format, lint, knip), so it is the only thing that tells you CI will pass. Fix everything it reports, including unused code found by knip. While iterating, use the fast local checks on what you touched: oxfmt <target>, tsrs…
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 31,439 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 repository9 other files it gives its agents.
- .agents/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- argos-cliSkill
- argos-pr-reviewSkill
- argos-uploadSkill
- frontend-designSkill
- lucide-style-iconsSkill
- security-auditSkill
- upgrade-depsSkill
- vercel-react-best-practicesSkill
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
14 rules. About 0 are common, 4 are distinctive, and 10 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.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 78
When the explanation keeps growing, the code usually wants a named function instead.
- Very specific: line 83
Every human correction — a requested change on a pull request, a comment pinned to an Argos screenshot, a "no, do it this way" in the …
- Very specific: line 109
Never use ! (non-null assertion).
- Very specific: line 117
Avoid as type assertions; prefer proper typing, type guards, or satisfies.
- Very specific: line 131
Update the Objection model in src/database/models/: add the column to jsonSchema.properties (nullable → { type: ["string", "null"] }, dates …
- Distinctive: line 140
Idempotent inserts: never gate an insert on a prior existence check — two concurrent requests can both pass it and race on the primary key.
- Distinctive: line 154
Keep resolvers thin, put business logic in services, and use DataLoader to avoid N+1 queries.
- Very specific: line 164
Mutations: only use useMutation when you actually render from its result tuple (loading / data / error).
- Very specific: line 181
If possible, put some screenshots or videos using /argos-upload skill.
- Very specific: line 202
Never trust the client for anything derived from content (e.g.
- Very specific: line 237
Use apps/backend/src/database/testing/factory.ts for fixtures, and test.extend (prefer one per file, creating models inside fixtures).
- Distinctive: line 239
Keep fixtures small, reusable, independent, and split by concern so tests run in parallel.
- Very specific: line 241
Test API endpoints with createTestHandlerApp + supertest.
- Very specific: line 248
If db:reset reports database "test" is being accessed by other users:
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 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.
Files
Argos
Monorepo: apps/backend (Express + GraphQL + Objection/Knex), apps/frontend
(React + Vite + Apollo), shared code in packages/. Local setup (Docker, DB
seeding, dev server) is documented in CONTRIBUTING.md.
Before finishing any change
Run pnpm run static-checks. It is the same turbo task CI runs
(check-types, check-format, lint, knip), so it is the only thing that
tells you CI will pass. Fix everything it reports, including unused code found
by knip.
While iterating, use the fast local checks on what you touched:
oxfmt <target>, tsrs --noEmit, oxlint <target>.
Type checking is tsrs, a Rust port of
the TypeScript compiler. typescript is pinned to the exact TypeScript build
tsrs ports (tsrs --version prints it), so the editor and every tool that peers
on typescript see the compiler CI checks with: bump the two together.
Formatting is oxfmt, configured
once in .oxfmtrc.json at the repository root — there is no per-workspace config
and no per-workspace check-format script. It formats the whole monorepo in a
couple of seconds, so pnpm run check-format at the root is the only command you
need. Keep it that way: per-workspace scripts leave root-level sources like
tests/ and vitest/ unchecked, because no workspace owns them. It also owns
import sorting and Tailwind class sorting, which used to be Prettier plugins.
Linting is oxlint, configured once
in .oxlintrc.json at the repository root — there is no per-workspace config and
no per-workspace lint script. It lints the whole monorepo in well under a
second, so pnpm run lint at the root is the only command you need. The rule set
is oxlint's correctness category across the typescript, unicorn, oxc,
react and vitest plugins, plus the deviations listed in rules /
overrides. Two project-specific guards live there:
no-restricted-importskeepssonnerout of the frontend (import from@/ui/Toasterinstead).argos/no-module-relative-paths— a local plugin intools/oxlint-plugin-argos.js, wired up throughjsPlugins— rejectsimport.meta.dirname/__dirnamein the bundled backend. Oxlint has nono-restricted-syntaxequivalent, so the rule is hand-written.
Suppress with // oxlint-disable-next-line <rule>. Oxlint still honours
eslint-disable comments, but do not write new ones.
Commits
Conventional Commits, with one
rule that decides the type: feat and fix are for what a user would
notice — a capability they gain, a bug they hit. Judge it from their side of
the screen, not from how much code moved or how hard it was to find. Everything
else is chore, however substantial: refactors, tests, tooling, docs,
dependency bumps, and internal fixes no user could ever observe. Do not use
style — the formatter owns formatting, so the type has nothing left to
describe.
Comments
A comment has to say something the code cannot. A constraint coming from elsewhere in the system, a race, a workaround, a decision whose better-looking alternative is wrong — those earn their lines, and the codebase is full of them on purpose. Everything else is noise a reader wades through and a maintainer has to keep true.
So do not write:
- a restatement of the line below it —
// Review the next buildover a button that reads "Review next build"; - a defence of what is not in the code. An approach considered and dropped belongs in the pull request, where it is read once, not in the file, where it is read forever;
- an explanation of a name that is already clear, or a banner announcing a section the structure already shows.
Comment the part that is actually surprising, not the whole block around it. When the explanation keeps growing, the code usually wants a named function instead.
Learning from review
Every human correction — a requested change on a pull request, a comment pinned to an Argos screenshot, a "no, do it this way" in the session — means these instructions were missing something. Fix the instance, then fix the cause, so the next agent does not make the same mistake.
- Write down the rule that would have prevented it, not the instance: the convention behind "rename this", not the rename. Keep it as narrow as the review; a rule broader than what the reviewer objected to forbids things nobody asked to forbid. When the intent is unclear, ask in the thread first.
- Put it where it gets enforced, strongest first. A lint rule
(
.oxlintrc.json, ortools/oxlint-plugin-argos.jsfor a custom one), a type or a test fails CI on the next attempt; a sentence can be skimmed past. Otherwise add it to the matching section of this file, or to the skill in.agents/skills/when only that workflow needs it. - Edit, don't append. Fold the lesson into the section it belongs to, and replace a rule the review contradicts instead of keeping both. Record the rule and why it holds, not the story: the pull request and who asked are in git history.
- Ship it in the same pull request as the fix, so the reviewer checks the rule as well as the code.
Skip what will not recur: a typo, a one-off judgment call, a question answered without a change.
TypeScript
- Never use
!(non-null assertion). Useinvariantfor required values. - Program by assertion, not by defense: when a value is expected to be present
(a context provided by an ancestor, a required param, an invariant of the call
site), assert it with
invariant(or a hook built on it, e.g.useNonNullable/useProjectPermission) and use it directly. Do not paper over the expectation with optional chaining or?? <fallback>— that silently degrades instead of surfacing a broken assumption. Reserve?./??for values that are genuinely optional. - Avoid
astype assertions; prefer proper typing, type guards, orsatisfies.as constis fine.
Database
Schema lives in apps/backend/db/, managed with Knex migrations + Objection
models. To add or change a column:
pnpm run --filter @argos/backend db:migrate:make <short-description>, then writeasyncup/downin the generated file. Useknex.raw(...)for raw constraint changes, withexport const config = { transaction: false }when needed.pnpm run --filter @argos/backend db:dump— migrates and regenerates the committeddb/structure.sql. Commit the migration andstructure.sql.- Update the Objection model in
src/database/models/: add the column tojsonSchema.properties(nullable →{ type: ["string", "null"] }, dates are ISO strings) and the typed class field.createdAt/updatedAtcome from the baseModel. - Reset the test DB before running the Playwright tests:
NODE_ENV=test pnpm run --filter @argos/backend db:reset.pnpm test:integrationneeds nothing — it rebuilds its own databases wheneverstructure.sqlchanges.
Idempotent inserts: never gate an insert on a prior existence check — two
concurrent requests can both pass it and race on the primary key. Insert
atomically with onConflict([...]).ignore() (or .merge(...)), and read the
.returning(...) rows when you need to know whether the row was actually
created (empty array = conflict ignored, e.g. don't send the notification).
GraphQL
- The schema is the source of truth. After editing any
typeDefsinapps/backend/src/graphql/definitions/, runpnpm run codegenfrom the repo root — it writes the backend resolver types (src/graphql/__generated__/,schema.gql) and the frontend types (apps/frontend/src/gql/). Import generated enums/types instead of usingany, and add mappers in/codegen.tswhen needed. - Keep resolvers thin, put business logic in services, and use DataLoader to avoid N+1 queries.
- Expose per-object permissions as an enum field (
permissions: [XxxPermission!]!) computed fromctx.auth?.user. Put the logic in a pure helper (seesrc/comment/permissions.ts) so the same function backs both thepermissionsresolver and the guard inside the mutation. Always enforce the check in the mutation — never trust the client.
Frontend
Mutations: only use useMutation when you actually render from its result
tuple (loading / data / error). It re-renders the component while the
request is in flight even if you never read that state. Otherwise call
useApolloClient().mutate({ mutation, variables, ... }) directly.
Dialogs: build them with Modal + Dialog from @/ui. Modal owns a
pending state (ModalActionContext) that blocks dismissal while an action is in
flight — never track that with a local loading boolean.
- Form dialogs: wrap the body in
<Form>with a<FormSubmit>.Formflags the modal pending during submit and routes server errors throughhandleFormError. Close on success withuseOverlayTriggerState().close(). - Action dialogs (confirm/delete/revoke): drive the mutation with
useModalAction()to get the same pending-blocks-dismiss behavior. Userole="alertdialog",variant="destructive"for destructive actions, andtoast.success(...)on completion.
If possible, put some screenshots or videos using /argos-upload skill.
Notifications
Two-stage async pipeline in apps/backend/src/notification/:
sendNotification({ type, data, recipients }) → notificationWorkflowJob
(applies opt-outs) → notificationMessageJob (renders and sends the email).
To add a type, model it on an existing handler (e.g.
handlers/review_submitted.tsx) and register it in handlers/index.ts — the
NotificationWorkflowType union and typed sendNotification props derive from
that array, and no migration is needed (data is a JSON column). recipients
is an array of user ids you compute; the system never discovers them.
Rich-text comments (TipTap)
The TipTap extension list is duplicated and must stay in sync between
apps/backend/src/comment/schema.ts (drives validateCommentJson and email
HTML rendering) and apps/frontend/src/ui/Editor/. A node the client produces
but the backend doesn't register is rejected with Invalid comment body.
Never trust the client for anything derived from content (e.g. mentioned
users) — re-parse the stored JSON server-side and check permissions (see
src/comment/mentions.ts). A mention node persists only the account id,
never the label; labels are resolved at render time on both sides.
What to test
Coverage is not the goal. Every test is code someone has to keep working, so it has to earn its place by protecting something the project actually depends on.
- Test what matters to the whole project, not edge cases. Billing, permissions and auth, the build lifecycle, review and comment flows, notifications — the paths where a regression reaches users or corrupts data. A test that pins one component's internal state buys little and costs maintenance forever.
- The more end-to-end, the better. A Playwright spec exercises the real server, the real database and the real client together, which is where bugs actually live; the same assertion one layer down proves much less. When a behavior can be reached from the UI, test it there and skip the unit test.
- Avoid mocking. A mock asserts what you assumed the collaborator does, so the test passes while production breaks. If a behavior can only be reached by mocking, move the test further out — or accept that it does not need one.
- Low-level utilities are the exception worth being exhaustive about. Pure
helpers with many input shapes — parsers, validators, comparators, formatters
— are cheap to cover case by case, and that is exactly where a table-driven
*.test.tspays off. - Not every fix needs a test. A flake, a one-off rendering detail or an ordering tweak is usually fixed and left at that. Add a guard only when the behavior is worth defending on its own.
Testing (Vitest)
- Tests live next to the code.
*.e2e.test.tsrequires Redis/Postgres,*.test.tsotherwise. pnpm test:unit(no infra) /pnpm test:integration(Postgres + Redis).- Use
apps/backend/src/database/testing/factory.tsfor fixtures, andtest.extend(prefer one per file, creating models inside fixtures). - Keep fixtures small, reusable, independent, and split by concern so tests run in parallel. Avoid large shared setups.
- Test API endpoints with
createTestHandlerApp+supertest. test:integrationruns in parallel, and every test truncates the whole database, so each worker owns atest_<workerId>database, a Redis database and a queue prefix — set up byvitest/vitest.e2e.global-setup.mts(seeapps/backend/src/database/testing/workers.ts). Nothing to create by hand: worker databases are built fromdb/structure.sqland rebuilt whenever it changes. Thetestdatabase is left alone, it belongs to Playwright.If
db:resetreportsdatabase "test" is being accessed by other users:psql -h 127.0.0.1 -U postgres -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'test' AND pid <> pg_backend_pid();"
E2E / visual tests (Playwright + Argos)
Specs live in tests/*.spec.ts and run against the built app.
- Run with
NODE_ENV=test pnpm test:e2e. This is required: the test process (which seeds the DB) and the web server must both resolve to thetestPostgres DB. Without it the seeds write todevelopmentwhile the server readstest, and pages render "Page not found".- Single test:
NODE_ENV=test pnpm test:e2e --project=chromium -g "<title>".
- Single test:
- Build first (
pnpm build), and rebuild the frontend after changing it — the server servesapps/frontend/dist. Specs import seeds fromapps/backend/srcdirectly, so seed changes need no backend rebuild. Sharedpackages/*must be built too or module resolution fails. - The
setupproject truncates the DB; each test seeds its own data via theloggedTestfixtures (real session cookie), so keep seeded data isolated. - Take screenshots with the
screenshot()helper intests/util.ts. Content markeddata-visual-test="transparent"(e.g.<Time>) is neutralized automatically; use the helper'sreplacementsfor other unstable text. - Seeds live in
apps/backend/src/database/seeds.ts. Add a focused scenario function rather than bendingcreateBuildScenario, which many baselines depend on. Use a uniquekeyPrefix(e.g. the project id) forFilekeys, and reuse existing image keys (dummy-*,diff-*) so images load fromfiles.argos-ci.com/test/<s3Id>. - A guard test — when the behavior warrants one, see What to test — should fail without the fix. Verify by temporarily reverting the fix, rebuilding, and re-running before trusting it.
Seeding gotchas
createBuildScenario: build 6 = changes detected (2 failures, 6 changed, 8 added, 2 removed, 2 unchanged), build 7 = accepted, build 8 = rejected. All on branchmain, bucket namedefault.- An active Stripe subscription needs
provider: "stripe"plusstripeSubscriptionIdandsubscriberId(ausers.id, not an account id) — thecheck_stripe_fieldsconstraint. - "Previous approvals" (
branchApprovedDiffs) needs an earlier build on the same bucket name + branch with conclusionchanges-detected, an approved non-dismissedBuildReviewfor the viewer, matchingScreenshotDiffReviewrows, and matchingfingerprinton both builds' diffs (seed diffs have no fileIds, so fingerprints are the only link). - IconButtons don't expose accessible names from tooltips — locate them via the
lucide icon class, e.g.
button:has(.lucide-thumbs-up).
Verifying a change in the running app
docker compose up -d(Postgres on 5432, Redis on 6380).NODE_ENV=test pnpm run --filter @argos/backend db:migrate:latest.pnpm build(orpnpm turbo run build --filter=@argos/frontendfor a frontend-only change).NODE_ENV=test pnpm run --filter @argos/backend start-web— serves the built frontend on http://localhost:3000. Always start backend processes throughbin/start.sh(whatstart-web/start-workeruse); it loads Sentry instrumentation before the modules it patches.
For interactive development, pnpm run dev runs the watchers instead.
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
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.

