DocumentationAgent Quickstart
DOCS

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 bare timestamp.

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 DrizzleService and EventBus as constructor arguments - the plugin builds the service from the container. Import operators from drizzle-orm and 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.

No audit entry = not 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 regen after editing schema/index.ts - the migration and generated types go stale.
  • Importing another module directly. Use events, a command port, or its read-only /schema subpath. Both boundary gates reject it: the per-edit oxlint plugin and the whole-graph pnpm boundaries check, 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 - use type, 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.
Updated 1 day ago