This guide walks an AI agent through implementing a module end-to-end using only slash commands and the MCP dev server. Human involvement: review + approve the PR.
pnpm setup:agenthas been run (Docker up, DB migrated, dependencies installed).- The
oss-devMCP server is registered in.mcp.jsonand your agent launches it automatically (stdio). No separate process to start. Verify:claude mcp list(Claude Code), or check MCP settings in Cursor/Windsurf. Seedocs/mcp-setup.mdfor per-editor setup.
read-agents-md section="Mission"
read-agents-md section="Architecture pillars"
read-agents-md section="Where does X go?"
Read the output before proceeding. Do not skip this.
list-modules
If the module is listed, use describe-module <name> to understand its current state.
/scaffold-module <name>
This creates packages/addons/<name>/ (a new add-on package) with all required files and registers it in extensions.config.ts.
Edit packages/addons/<name>/src/schema/index.ts (core modules: packages/core/src/<domain>/<module>/schema/index.ts - no src/ segment). Add pgTable definitions following these rules:
- No FK references to tables owned by other modules (use plain ID columns). Within your own module,
.references(() => table.id)is fine. - Table names are snake_case (
pgTable('tournament_entry', ...)); the exported const is camelCase; the row type istypeof <const>.$inferSelect.
Check for table-name collisions before adding:
propose-table-change table=<snake_case_name>
Inspect the existing DB shape any time with get-drizzle-schema (pass module=<name> to scope). Then generate the migration:
/regen
Edit the module's contract/ dir (packages/addons/<name>/src/contract/index.ts; core modules: packages/core/src/<domain>/<module>/contract/index.ts). It owns the route contract plus the request/response schemas and their z.infer'd types. Check if a shared schema already exists:
schema-get name=<EntityName>
query-openapi keyword="<entity>"
If a shared schema exists in @openora/core/contracts, re-export it instead of duplicating.
Edit packages/addons/<name>/src/service/<name>.service.ts (core modules: packages/core/src/<domain>/<module>/service/<name>.service.ts). Business logic only. Rules:
- Take
DrizzleService(from@openora/core/server) +EventBusas constructor arguments (the module plugin builds the service from the container). Query withthis.drizzle.db.select().from(<table>).where(eq(...)); import operators (eq,desc,sql) fromdrizzle-ormand tables from../schema/index.js. - Throw domain errors via
createDomainError(...)from@openora/core. - Never call external HTTP APIs directly - use an adapter interface from
@openora/core/contracts.
For each operation, add a route:
/scaffold-route <name> GET /<name>s
/scaffold-route <name> POST /<name>s
/scaffold-route <name> GET /<name>s/:id
Check existing routes first to avoid duplicates:
list-routes module=<name>
Edit packages/addons/<name>/src/plugin.ts (core modules: packages/core/src/<domain>/<module>/plugin.ts). Confirm the service is added to ctx.providers and the router is added to ctx.routers. The registry surface is: providers, controllers, routers, slots, events, mcp, imports.
The platform is headless backend only - pages, components, and styling live in the downstream consumer repo. What you can add to the OSS:
- A new data hook (eg
useAdminUsers,usePlayerWallet) ->packages/core/src/react/src/hooks/.@openora/core/reactis the supported frontend consumption surface (data hooks, auth, realtime transport - no components).
Edit packages/addons/<name>/AGENTS.md (core modules: packages/core/src/<domain>/<module>/AGENTS.md). Fill in:
- What the module does (one paragraph).
- Extension points (ports, events emitted/consumed, UI slots, routes).
- Do / don't list.
- Sample diff showing how to add a route.
- A "Done when:" checklist the next agent can self-verify against.
/verify --filter @openora/core
Fix any typecheck, lint, boundary, or test failures before considering the work done.
Start the full stack:
pnpm dev
Call a route manually to confirm end-to-end wiring:
curl -X POST http://localhost:3001/<name>s -H "Content-Type: application/json" -d '{}'
- Forgetting
pnpm regenafter editingsrc/schema/index.ts- the migration and generated types will be stale. - Importing from another module directly - use events or read its tables via the
@openora/core/<domain>/schemasubpath. Both boundary gates reject it: the oxlintoss-boundaries/*plugin (per-edit, specifier strings) and the whole-graphpnpm boundariesgate (catches transitive / re-export / dynamic-import / relative-path dodges too). - Introducing an import cycle - rejected by
import/no-cycleand the whole-graphno-circulargate. Break it by extracting a shared module, inverting a dependency, or moving the type to a contracts package. - Declaring
interface- usetype(lint-enforced). - Defining schemas inline in handlers - they must live in the module's
contract/dir. - Hand-editing the generated migrations - they are produced by
pnpm regen(drizzle-kit). The source of truth is each module'ssrc/schema/index.ts. - Opening a PR with a failing
pnpm verify- CI will reject it.