Agent Quickstart
Walk an AI agent through implementing a module end to end, using only slash commands and the MCP dev server.
Introduction
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 and approve the pull request.
It assumes pnpm setup:agent has run, and that the oss-dev MCP server is registered in .mcp.json so your agent launches it automatically over stdio. Verify with claude mcp list, or check the MCP settings in Cursor or Windsurf. See MCP server setup.
Step 1: Understand the platform
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.
Step 2: Check if the module already exists
list-modules
If it is listed, use describe-module <name> to understand its current state.
Step 3: Scaffold the module
/scaffold-module <name>
This creates packages/addons/<name>/, a new add-on package, with all required files, and registers it in extensions.config.ts.
Step 4: Define the Drizzle tables
Edit packages/addons/<name>/src/schema/index.ts. Core modules live at packages/core/src/<domain>/<module>/schema/index.ts, with no src/ segment.
- No foreign-key references to tables owned by another module - use plain ID columns. Within your own module,
.references(() => table.id)is fine. - Table names are snake_case; the exported const is camelCase; the row type is
typeof <const>.$inferSelect. - Always
timestamptz, never baretimestamp.
Check for collisions before adding, then generate the migration:
propose-table-change table=<snake_case_name>
/regen
Step 5: Define schemas
Edit the module's contract/ directory. It owns the route contract plus the request and response schemas and their inferred types. Check whether a shared schema already exists first:
schema-get name=<EntityName>
query-openapi keyword="<entity>"
If one exists in @openora/core/contracts, re-export it instead of duplicating. Derive with .pick, .omit, .partial, .extend and .merge rather than re-typing fields.
Step 6: Implement the service
Business logic only.
- Take
DrizzleServiceandEventBusas constructor arguments - the plugin builds the service from the container. Import operators fromdrizzle-ormand tables from../schema/index.js. - Throw domain errors via
createDomainError(...)from@openora/core/server. - Never call an external HTTP API directly. Use an adapter interface from
@openora/core/contracts.
Step 7: Add routes
list-routes module=<name>
/scaffold-route <name> GET /<name>s
/scaffold-route <name> POST /<name>s
Admin routes must resolve the AdminGuard and call await adminGuard.assert(context) as the handler's first line.
Step 8: Wire the plugin
Edit the module's plugin.ts. The ModuleRegistry surface you get as ctx is:
| Member | What it does |
| -------------------------- | --------------------------------------------------------- |
| provide(token, factory) | Bind an adapter. Last registration wins. |
| provideSealed(token, fn) | Bind a sealed token. Owner-only, once, never rebound. |
| routers.add(ns, factory) | Mount an oRPC router under a namespace. |
| events.on(topic, fn) | Subscribe to a domain event. |
| jobs.worker(reg) | Register a background job handler. |
| slots.fill(name, comp) | Fill a typed UI slot (unused - the platform is headless). |
| mcp.tool(def) | Expose an MCP tool. |
Step 9: Add a frontend consumption layer
The platform is headless backend only - pages, components and styling live in the consumer repo. What you can add here is a new data hook, in packages/core/src/react/hooks/ or that domain's own react/ directory.
Hand-write useMemo and useCallback for any stability-contract return: the consumer's React Compiler skips node_modules.
Step 10: Update AGENTS.md
Fill in what the module does, its extension points (ports, events emitted and consumed, routes), a do/don't list, a sample diff showing how to add a route, and a "Done when:" checklist the next agent can self-verify against.
Step 11: Verify
/verify --filter @openora/core
Fix every typecheck, lint, boundary and test failure before considering the work done.
Every state-changing action must leave an entry in the append-only,
hash-chained audit log - either by emitting a declared domain event the audit
module subscribes to, or by resolving AUDIT_WRITER and calling record(...)
directly.
Common pitfalls
- Forgetting
pnpm regenafter editingschema/index.ts- the migration and generated types go stale. - Importing another module directly. Use events, a command port, or its read-only
/schemasubpath. Both boundary gates reject it: the per-edit oxlint plugin and the whole-graphpnpm boundariescheck, which also catches transitive edges, re-export laundering, dynamic imports and relative-path dodges. - Introducing an import cycle. Break it by extracting a shared module, inverting a dependency, or moving the type into contracts.
- Declaring
interface- usetype, lint-enforced. - Defining schemas inline in handlers - they belong in the module's
contract/. - Hand-editing generated migrations - the source of truth is
schema/index.ts. - Opening a PR on a failing
pnpm verify- CI will reject it.