Imported from marioaulima/agent-loop-setup (
AGENTS.md). Install upstream withnpx skills add marioaulima/agent-loop-setup. Copyright stays with the author.
Ultracite Code Standards
This project uses Ultracite, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
Quick Reference
- Format code:
pnpm dlx ultracite fix - Check for issues:
pnpm dlx ultracite check - Diagnose setup:
pnpm dlx ultracite doctor
Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
Pagination is mandatory for all lists
Never fetch an unbounded list from the database. Every list endpoint must be paginated. Violating this blocks PR merge.
Choose the right strategy
| Signal | Use cursor | Use offset |
|---|---|---|
| Real-time feed, new items appear at top | ✓ | |
| Infinite scroll / "load more" UX | ✓ | |
Ordered by immutable column (created_at, id) |
✓ | |
| Large or unbounded dataset (chats, events, logs) | ✓ | |
| User needs "jump to page N" / numbered pages | ✓ | |
Sorted by mutable column (price, name, status) |
✓ | |
| Admin table with visible total count | ✓ | |
| Small, bounded dataset (< ~500 rows at all times) | ✓ |
When in doubt: feeds and activity lists → cursor. Tables, search results, admin grids → offset.
Cursor pagination contract
// Input
{ tenantId: string; limit?: number; cursor?: string } // cursor = last item's sort value (ISO date or UUID)
// Query pattern
WHERE tenant_id = :tenantId AND created_at < :cursor
ORDER BY created_at DESC
LIMIT :limit + 1 // fetch one extra to detect hasNextPage
// Output
{ items: T[]; nextCursor: string | null }
// nextCursor = items[limit - 1].createdAt if items.length > limit, else null
Frontend: use useInfiniteQuery (React Query) — never useQuery for cursor-paginated lists.
Offset pagination contract
// Input
{ tenantId: string; limit?: number; offset?: number }
// Query pattern
WHERE tenant_id = :tenantId
ORDER BY <sort_column> <direction>
LIMIT :limit OFFSET :offset
// Output
{ items: T[]; total: number }
// total drives page count; offset drives current page
Frontend: use useQuery with { limit, offset } in the query key. Refetch on page change.
Defaults
- Default
limit: 20. Maxlimit: 50. Reject values above max. - Never default to
limit = 9999or omit the limit entirely.
Type Safety & Explicitness
- Prefer
unknownoveranywhen the type is genuinely unknown - Use const assertions (
as const) for immutable values and literal types
Modern JavaScript/TypeScript
- Use optional chaining (
?.) and nullish coalescing (??) for safer property access
Error Handling & Debugging
- Remove
console.log,debugger, andalertstatements from production code - Throw
Errorobjects with descriptive messages, not strings or other values - Use
try-catchblocks meaningfully - don't catch errors just to rethrow them - Prefer early returns over nested conditionals for error cases
Never show raw technical/enum values to the user
Backend enum values, status/kind codes, and identifiers (appointment_conflict, no_response, seller_handoff, FORBIDDEN, raw error codes, etc.) are internal vocabulary. They must never reach the rendered UI — not in labels, badges, table cells, toasts, or error states.
Every place that renders a value pulled from a closed set (status, kind, reason, role, category, source, temperature, ...) must go through a translated label map (Record<string, string> with Portuguese, user-facing text). This has caused real bugs: reason codes like appointment_conflict and no_response leaked straight into the conversation and lead UI because a label map was missing entries and fell back to the raw value.
Rule for fallbacks: never ?? rawValue. If a label map might be missing an entry (free-form/growing enum), fall back through humanizeEnum(), which turns no_response into No response instead of leaking the literal token. This is a last resort — the correct fix is always to add the missing entry to the label map. If the set is fully closed and typed (e.g. a TS union), the map should already be exhaustive and the fallback is purely defensive.
This applies to error toasts too: toast.error(err.message) is only safe when every error message on that path is an Error message we authored ourselves in Portuguese — never surface a raw driver/HTTP/enum error to the user.
Agent skills
Issue tracker
Issues live in Linear (team: odiniy) via the Linear MCP. See docs/agents/issue-tracker.md.
Triage labels
Default label vocabulary (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See docs/agents/triage-labels.md.
Issue readiness
Never apply ready-for-agent to an implementation issue until an ATDD plan has been attached to that issue. Newly created implementation issues that still need acceptance-test planning must use needs-atdd, not ready-for-agent. After the ATDD plan is attached with the repo's ATDD planning skill (plan-atdd-to-issues / atdd-plan-for-issue), the issue can be promoted to ready-for-agent.
Backlog drain
Use the drain-ready-issues skill, or the /drain-ready-queue shortcut when available, to sequentially complete eligible Linear ready-for-agent issues. It composes take-next-issue, execute-ready-issue, PR merge, Linear Done, and the next issue handoff. For fresh context per issue, use an external runner that launches a new agent process/session per issue and invokes /drain-ready-queue RUN_CONTEXT=local WORKSPACE_MODE=same-thread MERGE=true MAX_ISSUES=1. Do not continue into another issue in the same session when MAX_ISSUES=1; the runner parses the final sentinel to decide whether to continue. The runner does not select issues. take-next-issue remains the only issue selector. In Conductor, call /drain-ready-queue RUN_CONTEXT=conductor WORKSPACE_MODE=per-issue; workspace-per-issue is preferred only when the host actually supports programmatic workspace creation. If unavailable, stop with DRAIN_ABORT rather than asking the user to manually create a workspace. In normal local sessions, refresh with git checkout main and git pull origin main. Stop on human-only issues, missing ATDD, blocked work, failed checks, unmergeable PRs, dirty unrelated work, or unavailable Conductor workspace creation.
Domain docs
Multi-context repo — CONTEXT-MAP.md at root points to per-context CONTEXT.md files. See docs/agents/domain.md.
Database migrations: never use db:push
db:push diffs the live database and applies changes directly, bypassing the migration history entirely. This repo hit real damage from it: migration files drifted out of sync with schema.ts for months (a duplicated migration, a column with no migration at all), because someone used db:push for schema changes instead of generating a migration. db:migrate replaying history from scratch then failed on a fresh database.
Always use db:generate + db:migrate for schema changes:
pnpm db:generate # generates a new migration file from schema.ts changes
pnpm db:migrate # applies pending migrations, tracked in drizzle.__drizzle_migrations
Never run pnpm db:push (or drizzle-kit push) against any environment, including local dev. If a migration file needs correcting before it has been applied anywhere, edit the file directly and regenerate; don't reach for push as a shortcut.
Required quality gates
For database integration tests, run pnpm test:integration:podman instead of pnpm test:integration. The local development environment uses Podman, and the Podman script configures Testcontainers correctly for the ephemeral PostgreSQL database.
If the change is user-facing, the agent must also run the highest-practical smoke/e2e check:
- UI: Chrome DevTools MCP happy path against the running app when available. If Chrome DevTools MCP is unavailable, fall back to Playwright/browser automation MCP. This means external browser automation tooling by default, not the repo's Playwright test suite, unless the issue or
### Required commandsexplicitly calls for project Playwright tests. - Backend: HTTP against running service with real test DB
Do not chase coverage percentage. Coverage is not a goal. Behavioral confidence is the goal.