Thanks for contributing! This project is a production-grade AI Agent Platform that will evolve into a commercial SaaS product. The conventions below exist to keep the codebase testable, secure, and swappable — please follow them on every change.
# 1. Install dependencies (runs `prisma generate` automatically via postinstall)
npm install
# 2. Configure environment
cp .env.example .env # DATABASE_URL + Clerk keys are required; GROQ/OPENROUTER keys optional
# 3. Sync the schema
npx prisma db push
# 4. Run the dev server
npm run devValidation commands (run all three before opening a PR):
npx tsc --noEmit # typecheck — must be 0 errors
npm test # vitest unit suites — must be all green
NODE_ENV=production npm run build # production build must compileIf your
.envsetsNODE_ENV, always build withNODE_ENV=production— adevelopmentvalue in the environment breaks Next's production build.
New code goes in the layer it belongs to. Never put domain logic in a layer that doesn't own it.
src/
├── app/ # PRESENTATION — Next.js App Router pages + API handlers (thin)
│ ├── dashboard/ # Dashboard, skills module, executions module UI
│ └── api/ # REST route handlers — stateless HTTP adapters only
├── components/ # PRESENTATION — React components (UI + layout + feedback)
├── stores/ # PRESENTATION — Zustand stores (e.g. toast)
├── services/ # APPLICATION — domain orchestration, constructor DI
│ └── interfaces/ # ISkillService, IExecutionService, IApprovalService, IVersionService
├── repositories/ # INFRASTRUCTURE — Prisma data access
│ └── interfaces/ # ISkillRepository, IExecutionRepository, IAuditLogRepository…
├── validators/ # DOMAIN — Zod schemas (validation lives here, not in routes/services)
├── types/ # DOMAIN — shared TypeScript entities (skill, execution, approval…)
├── modules/execution/ # DOMAIN — the graph-first agent runtime
│ ├── state/ # typed AgentState + LangGraph annotation
│ ├── graph/ # LangGraph nodes + graph wiring
│ ├── planner/ # PlannerService + plan schema + node
│ ├── executor/ # ExecutionEngine, retry, timeout, cancellation, validation
│ ├── llm/ # provider selector (config-driven, testable)
│ └── tool-registry/ # ToolRegistry + PermissionChecker (no tools yet)
├── providers/llm/ # INFRASTRUCTURE — LLMProvider interface + Groq/OpenRouter + LLMRouter
├── lib/ # INFRASTRUCTURE — api handlers, logger, env config, prisma singleton
└── middleware.ts # Clerk route protection (keep this filename — Clerk 6.x does not detect Next 15.5's proxy.ts rename)
tests/unit/ # Vitest suites — mirror the src layout (validators/, services/, modules/, providers/)
prisma/schema.prisma # INFRASTRUCTURE — the single source of truth for the DB
Data flow rule: Route Handler → Service (validates + orchestrates) → Repository → Prisma. Handlers parse HTTP + auth and call one service method; services contain the business logic; repositories are the only code that touches Prisma.
- Application services depend on interfaces, not concrete classes. Each service declares its dependencies in a constructor typed with repository/service interfaces (
IExecutionRepository,ISkillRepository…). - Every service and repository exposes an interface in
src/services/interfaces//src/repositories/interfaces/— even if there's only one implementation today. This is what makes the execution engine and DB swappable later. - Wire dependencies at the composition root — route handlers instantiate repos and services at module top and call them; services must not
newtheir own dependencies (tests inject fakes instead):
// ✅ Good — route handler as composition root
const skillRepo = new SkillRepository();
const auditRepo = new AuditLogRepository();
const skillService = new SkillService(skillRepo, auditRepo);- No business logic inside route handlers. A handler should be: auth → parse → validate → call one service method → respond. If a handler grows
ifchains about domain rules, the logic belongs in a service.
- All external input is validated with Zod in
src/validators/— the schema is the contract. Field rules (min lengths, enums, JSON-object checks) live in the schema, never scattered through services. - Use
.parse()for payloads you want to throw on (the route maps Zod issues to a 400VALIDATION_ERROR), and.safeParse()for query strings where a user-supplied?status=should never 500:
const validated = createSkillSchema.parse({ ...body, userId }); // throws ZodError
const parsed = skillListQuerySchema.safeParse({ status, search }); // returns result- Pass validated, typed data into services — services should not re-validate what the validator already guaranteed.
- Reuse the shared handlers in
src/lib/api/handlers.ts:unauthorized(),forbidden(),notFound(),badRequest(),serverError(). - Response envelope (every endpoint):
{ success: true, data }or{ success: false, error, code, fields? }. - Error code mapping:
- Zod issues →
badRequest(error)→400 VALIDATION_ERROR(withfields) - Domain errors (e.g.
ExecutionErrorfromsrc/modules/execution/executor/errors.ts) →400 - "Resource doesn't exist or isn't yours" →
403 FORBIDDENfirst, then404— never leak which IDs exist - Anything else →
serverError(error)→500 INTERNAL_ERROR. Never returnerror.messagein a 500 — log the real error server-side, return a generic message.
- Zod issues →
- Ownership is non-negotiable: resolve
auth()in every handler and scope every repository query touserId(findByIdForUser,cancelExecutionForUser…). Users can never read or mutate another user's rows.
- Use the shared Pino logger (
import { logger } from "@/lib/logger") — noconsole.log. - Structured logging:
logger.info({ contextFields }, "Message"). Put the context object first, keep messages short and sentence-case.
logger.info({ skillId, userId, versionId }, "Publishing skill version");
logger.error({ err: error, executionId }, "Execution failed");- Log mutations. Skills and executions write both a structured log line and an audit row via
auditRepo.log({ userId, executionId, action, details }). Actions areUPPER_SNAKE_CASE(SKILL_CREATED,SKILL_PUBLISHED,EXECUTION_STARTED…).
- Business logic never calls Groq, OpenRouter, or any vendor directly. Every AI call goes through the
LLMProviderinterface (generate,stream,structuredOutput,complete). - New vendors = a new provider class + a model roster entry in
src/providers/llm/modelLists.ts+ nothing else. TheLLMRouterhandles failover (cooldowns: 429 → 60s, 5xx/network → 30s, 404 → 10min, auth → 5min vendor park). - The execution runtime selects providers via
src/modules/execution/llm/providerSelector.ts(config-driven, unit-testable). PrefergetLLMProvider()/the selector over constructing providers ad hoc.
- The execution runtime (
src/modules/execution/) is built around a LangGraphStateGraph— the LLM is one node's dependency, not the center of the system. - Nodes are independent and pure: they read/write the typed
AgentState(via reducers fortoolCalls/errors/results) and receive their dependencies through LangGraph'sconfig.configurable.runtime. Do not import services/repos directly into node files. - New capabilities = new node + edge in
buildExecutionGraph.ts, new field onAgentState— no changes to the engine or services.
prisma/schema.prismais the single source of truth. After editing it, runnpx prisma generate(andnpx prisma db pushlocally to sync).- Multi-step writes must be atomic — wrap dependent queries in
prisma.$transaction(e.g. skill creation + draft pointer, publish + clearing the draft pointer). Non-atomic write bugs are a recurring review finding. - Never expose raw Prisma types through the service layer — map to DTOs in
src/types/.
- New env vars are added to both
.env.exampleandsrc/lib/config/env.ts(Zod-validated). Access viaenvfrom@/lib/config/env— neverprocess.envdirectly in business code.
- Server components by default; use
"use client"only where interactivity is required. Data fetching in RSC usesauth()+ repositories directly. - Forms: React Hook Form +
zodResolver+ schemas fromsrc/validators/. Client-side JSON editors must validate JSON before submitting (invalid editors block submission). - Server state: TanStack Query for mutations/list refetching; client state: Zustand (toasts). No prop-drilling for global state.
- Styling: TailwindCSS + the existing design language (dark theme, terminal/pixel accents, status badges). Reuse
src/components/feedback/(EmptyState,LoadingSkeleton,ConfirmDialog,ErrorBoundary) — don't rebuild them. - Every data-heavy page has loading/empty/error states.
- Strict mode. No
any— type the edge (unknown+ narrowing, generics, or an explicit cast with a comment). Implicit-anyerrors fail CI. @/alias for all imports fromsrc/(@/services/…,@/validators/…). Prefer relative imports only inside a file's own folder.- Async/await, no
.then()chains;Promise.allfor independent parallel calls. - Match surrounding formatting — 2-space indent, double quotes, semicolons (ESLint enforces this via
npm run lint).
- Vitest, suites under
tests/unit/mirroringsrc/layout (tests/unit/validators/,tests/unit/services/,tests/unit/modules/,tests/unit/providers/). - Every service/domain unit gets a test. Required coverage patterns: validation rules, publish/archive/duplicate flows, unauthorized access, failover/retry logic, graph state reducers.
- Tests inject fakes (see
tests/unit/modules/helpers/stubLLM.ts,fakeExecutionRepo.ts) through constructor DI — never mock Prisma in unit tests.
- Branch per feature:
feat/<module>-<description>orfix/<description>. - Keep PRs focused — one logical change per PR. Small, reviewable diffs land faster.
- Commit messages: conventional style, imperative mood (
feat: add skill version history,fix: clear draft pointer on publish).
Before opening a PR, verify every item:
- Typecheck:
npx tsc --noEmitexits with 0 errors - Tests:
npm testis fully green, and new logic has new tests (services, validators, modules) - Build:
NODE_ENV=production npm run buildcompiles - Architecture: business logic lives in services/modules — route handlers are thin (auth → validate → call → respond)
- DI: services depend on interfaces, wired via constructors at the composition root; no
new-ing dependencies inside services - Validators-first: all external input validated with Zod schemas from
src/validators/before entering services - Errors: shared handlers used; no internal error messages leaked in 500s; domain errors → 400; ownership scoping (
forbiddenbeforenotFound) - Auth & ownership: handler resolves
auth(); every query/repo call is scoped touserId - Logging: structured
loggercalls (noconsole.log); mutations write audit entries withUPPER_SNAKE_CASEactions - Atomicity: multi-step DB writes use
prisma.$transaction - LLM: no direct vendor calls — everything through
LLMProvider/router - Env: new vars added to
.env.exampleandsrc/lib/config/env.ts - No
any: strict typing maintained; no unused imports/variables/dead code - Reuse: existing feedback/layout components used rather than reimplemented
- Self-review: re-read your diff — comments explain why, not what