AI-driven workflow
The goal of Castor: you describe a business requirement in plain language, and your AI coding tool works out the technical details on its own, delivering a feature module end to end (table, API, page, permissions, migration) that follows the project's conventions and passes the verification gate.
This page covers the four pieces the workflow relies on: the project context in AGENTS.md, the per-tool AI configuration, the scaffold generator pnpm scaffold, and the verification gate pnpm verify.
AGENTS.md: the single project context
AGENTS.md in the repo root is the complete project guide written for AI, and every AI tool treats it as the source of truth. It covers:
- Tech stack, directory layout and naming rules
- Backend layering rules, routing conventions, how to write permission checks, and cross-cutting conventions (time, numbers, errors, CSRF)
- Frontend dynamic routing, page structure, shared components, design tokens and i18n rules
- Import/export conventions, RBAC conventions, menu ID allocation rules and the current menu tree
- The field-type inference table (business description → field type)
- A list of anti-patterns and the standard delivery process
Each tool's own config file only adds to it and points back to AGENTS.md. When you change a project convention, change AGENTS.md first.
Deeper architecture notes are in docs/architecture.md; the frontend UI approach is in docs/frontend-design-system.md.
Supported AI tools
| Tool | Files it reads |
|---|---|
| Claude Code | CLAUDE.md; skills in .claude/skills/ (new-feature-autopilot, shadcn-ui-skills) |
| Codex CLI | AGENTS.md (read automatically); skills in .agents/skills/ |
| Cursor, Windsurf, GitHub Copilot and others | AGENTS.md (they all read it automatically) |
| AI reading the docs site | The site's /llms.txt (entry index; source website/public/llms.txt) |
| MCP clients | apps/mcp; see MCP Server below |
.claude/skills/ and .agents/skills/ have the same content; the backend test skills-sync.test.ts checks that they stay in sync.
Feature delivery process
The new-feature-autopilot skill (type /new-feature-autopilot in Claude Code, or just say "build an XX feature") runs the five steps below. Other tools follow the same process through their own rule files.
1. Read the context
The AI reads AGENTS.md, the code scaffold templates under docs/templates/, the existing reference modules (backend apps/api/src/modules/admin/users/, frontend apps/web/src/modules/admin/pages/users/index.tsx), and the menu tree in apps/api/scripts/seed-rbac.ts. If the requirement can be met by extending an existing module, it prefers that.
2. Infer the technical spec
The AI infers the following internally, without asking you:
- The resource name and its domain (
adminorcomponent_center) - The API path, e.g.
/api/admin/customer-orders - Field names and field types (based on the field-type inference table below)
- Permission codes:
system_<name>for theadmindomain,cc_<name>for thecomponent_centerdomain, with_add/_edit/_delete/_export/_importappended for button permissions - Frontend file paths, parent menu, migration name, and the page pattern (a plain list unless the requirement calls for another pattern)
It writes the result as a spec file and checks it with pnpm scaffold -- --spec <file> --validate-only, which also reports the API path, permissions, table and the menu ID scaffold will allocate.
3. Show a business preview
The AI shows only business-level information and waits for you to confirm or adjust:
Customers
Location: Business → Customers
Shown as: a list
Features: list, create, edit, delete, import, export
Fields:
· Customer name (required)
· Phone
· Status
Shall I go ahead, or would you like to change anything?The AI asks additional questions only when the data model has an ambiguity that would be irreversible, when external system configuration is needed, or when a permission boundary has security implications.
4. Implement
- Generate the module with
pnpm scaffold -- --spec <file>(list the files first with--dry-run). - Add the business logic in the order
db/schema → schema → repository → service → routes. The spec already produced the Chinese labels, required / unique / default rules and options; anything beyond them (relations, cross-field checks, computed values) is written here. - Polish the frontend page (translations of page-specific text, extra validation), or rebuild it after the chosen page pattern.
- With
menuin the spec, scaffold has already written the menu and button permissions intoseed-rbac.ts; otherwise add them by hand. Then runpnpm seed:rbac -- --incremental. - Review the newly generated migration SQL, run
pnpm db:migrate, and confirm the table really exists withpsql -d <database> -c '\d <table>'. - API docs:
pnpm scaffoldhas already written the module's endpoints intodocs/apifox-full.openapi.json. If you change the generated routes, fields or validation, or add routes, update the entries from the code perAGENTS.md"OpenAPI writing rules". This is required: theopenapi_synccheck ofpnpm verifyfails otherwise.
5. Verification gate
Run pnpm verify -- --module <name>. The AI fixes any failing checks and re-runs verification. Once everything passes, it outputs a delivery report that states the migration version (e.g. "migrated to 0001_customer").
Migrations must actually be applied
Generating the migration file or passing static checks is not enough. You must run pnpm db:migrate, confirm with psql \d, and the migration_applied check of pnpm verify must pass.
pnpm scaffold
pnpm scaffold generates the backend module, frontend page, API tests and migration in one go. The usual input is a spec file (Chinese labels, rules, options, menu):
pnpm scaffold -- --spec device.spec.json --validate-only # check it; preview the API, permissions, table and menu
pnpm scaffold -- --spec device.spec.json --dry-run # list the files it would write, write nothing
pnpm scaffold -- --spec device.spec.json # generateWithout a spec, a field list works too (English placeholder labels, no rules or menu):
# Preview the files to be generated without writing anything
pnpm scaffold -- --name customer --domain admin --fields "name:str,phone:str20,status:str20" --dry-run
# Generate for real
pnpm scaffold -- --name customer --domain admin --fields "name:str,phone:str20,status:str20"Options
| Option | Description | Default |
|---|---|---|
--name | Resource name in snake_case, e.g. customer_order | Required |
--domain | Domain: admin or component_center | admin |
--fields | Field list in the form field:type,field:type | name:str |
--spec | Describe the module in a JSON file instead of --name / --fields: Chinese labels, required, unique, defaults, options and the menu; see Spec files below | — |
--dry-run | Only print what would be generated; no files written, nothing registered, no migration | Off |
--validate-only | With --spec: only check the spec and say which endpoints, permissions, table and menu it would generate; writes nothing, lists problems and exits 1 if any | Off |
--write-schema | Regenerate docs/spec.schema.json from the scaffold's current field types and rules | Off |
--skip-migration | Don't call drizzle-kit to generate a migration | Off |
--data-scope | With --fields, adds data scope: dept_id / created_by columns, list / detail / edit / delete / export filtered by the caller's scope, creator and department stamped on create, plus matching API tests. With --spec, set "dataScope": true in the spec instead (the flag is ignored) | Off |
-h / --help | Print usage | — |
What gets generated
Existing files are skipped, never overwritten.
| Generated file | Description |
|---|---|
apps/api/src/db/schema/<domain-dir>/<name-kebab>.ts | Table definition + toDict |
apps/api/src/modules/<domain-dir>/<name-kebab>/{schema,repository,service,routes}.ts | The four backend layers |
apps/api/test/<admin|cc>-<name-kebab>.test.ts | Basic API tests (CRUD, search, 404, export, import template, import) |
apps/web/src/modules/<module>/api/<name>.ts | Frontend API client, typed from the module's OpenAPI entries (row type ApiItem<'/api/admin/<name-kebab>s'>) |
Frontend list page index.tsx | In pages/<name>/ for the admin domain, pages/patterns/<name>_page/ for the component_center domain; typed with the shared components (FormValues, DataTableColumn<Row>[]) |
Page locales/{en-US,ja-JP}.json | Translations the shared locale files don't have yet: the module's webhook event names (created / updated / deleted) and page text such as the title and labels |
<domain-dir> is admin or component-center; <name-kebab> is the resource name with underscores replaced by hyphens.
It also automatically:
- Registers the module in
apps/api/src/db/schema/index.tsandapps/api/src/modules/<domain-dir>/router.ts - Writes the module's endpoints into
docs/apifox-full.openapi.jsonand regenerates the frontend API types (apps/web/src/shared/api/openapi.d.ts) from it - With
menuin the spec: adds the menu and button permissions toapps/api/scripts/seed-rbac.tsand their English / Japanese names toapps/web/src/locales/menus/ - Runs
drizzle-kit generate --name <name>to generate the migration
scaffold prints the permission code prefix (Perm prefix), the menu component value and the API path in its output; use them when you add the menu by hand (--fields, or a spec without menu).
Field types
| Type | Drizzle column | Form component | Notes |
|---|---|---|---|
str | varchar(100) | FormInput | |
str20 | varchar(20) | FormInput | |
str50 | varchar(50) | FormInput | |
str500 | varchar(500) | FormInput | |
text | text | FormTextarea | |
int | integer | FormNumber | |
float | numeric(10, 2) | FormNumber | Returned by the API as a string, e.g. "12.50" |
bool | boolean | FormSwitch | |
date | date (string mode) | FormDate | YYYY-MM-DD |
datetime | timestamp (string mode) | FormDateTime | |
file | varchar(36) holding a file-center id | FormFileUpload | "View" link in the list; the reference is registered on save |
image | varchar(36) holding a file-center id | FormImageUpload | Thumbnail in the list; the reference is registered on save |
enum | varchar(50) holding the option value | FormSelect | Fixed options (options, --spec only); the list filters on it and shows the option name as a badge (colour from the option's tone), exports show the name, imports accept name or value |
dict | varchar(100) holding the dictionary item value | FormSelect | Options from the Data dictionary (dict = dictionary code in --spec); the list shows the item label |
With --fields, an unknown type is treated as str; in a spec it is an error. id, created_at and updated_at are added automatically.
Field-type inference
The AI infers types from the business description, so you don't have to specify them:
| Keywords in the business description | Type |
|---|---|
| name, title, person's name, email | str |
| code, identifier, number (as in an ID or serial number) | str50 |
| mobile, phone, color | str20 |
| status, type, level with fixed options | enum (options in the spec; str20 with --fields) |
| category, source, industry with options admins maintain | dict (a data dictionary code in the spec) |
| URL, link, address (external) | str500 |
| image, avatar, cover, photo | image |
| attachment, file, contract, scan | file |
| description, remarks, summary, content, body, tags (JSON string) | text |
| amount, price, fee, cost | float |
| quantity, count, progress, percentage, sort order, weight | int |
| date (without time) | date |
| time | datetime |
| is/whether, enabled, disabled, toggle | bool |
Spec files
--spec reads a JSON file. The AI writes the inferred spec into such a file and generates from it, so Chinese labels, required / unique / defaults, options and the menu come out right the first time:
{
"name": "device",
"title": "设备台账",
"fields": [
{ "name": "code", "type": "str50", "label": "设备编号", "required": true, "unique": true },
{ "name": "name", "type": "str", "label": "设备名称", "required": true },
{ "name": "status", "type": "enum", "label": "状态", "required": true, "default": "idle",
"options": [{ "value": "idle", "label": "闲置" }, { "value": "in_use", "label": "使用中", "tone": "success" }] },
{ "name": "category", "type": "dict", "label": "分类", "dict": "device_category" },
{ "name": "price", "type": "float", "label": "采购价格" }
],
"menu": {},
"i18n": { "en-US": { "设备台账": "Devices", "设备编号": "Device no." }, "ja-JP": { "设备台账": "設備台帳" } }
}pnpm scaffold -- --spec device.spec.json --validate-only # check first and see what it would generate
pnpm scaffold -- --spec device.spec.jsontitleand every field'slabelare required; a misspelled key (such asrequried) is an error instead of being ignored silently- The full format is in
docs/spec.schema.json(add"$schema": "<relative path>/docs/spec.schema.json"to the JSON for editor completion and hints); four requirement → spec examples with the reasoning behind every field are indocs/examples/specs/ required: the column isNOT NULL, an empty value on create / edit returns 400<label>不能为空, and the form marks and checks it;image/filecan't be requiredunique: the column isUNIQUE, duplicates return 400; text and number types onlydefault: the column default, used when a new record leaves the field empty and prefilled in the formlabel/title: the Chinese text of the page, headers, imports / exports and errors;i18nholds their English and Japanese (missing ones fall back to the field name)menu: also adds the menu and button permissions (add / edit / delete / export / import) toapps/api/scripts/seed-rbac.ts, under the top-level 「业务管理」 (Business) group by default (ID 1000, created with the first module; modules from 1001); acomponent_centermodule goes under the gallery's 「页面模板」 (Page patterns) directory instead (IDs 4301–4399, API under/api/admin/component-center/);parentIdpicks another directory. Menu names in English and Japanese go toapps/web/src/locales/menus/dataScope:truemakes the module follow data scope, like--data-scopedoes for--fieldsoptions[].tone: the badge colour of that option in the list (neutralby default;success/warning/danger… for status-like fields)- The generated API test gets a "field rules" case covering required, options, unique and defaults
Known limitations
- With
--fieldsalone there are no required / unique / default values and the title and labels are English placeholders — use--specfor those. - The table name is always the resource name plus
s, and so is the API path. Keep the plural form in mind when choosing a resource name. - Once you add business rules, keep the generated API tests up to date.
- Backend errors name fields by their Chinese label, in the English and Japanese UI as well.
If the scaffold isn't available, you can write the files by hand from docs/templates/; the substitution rules are in docs/templates/backend/README.md.
pnpm verify
pnpm verify is the delivery gate: work is done only when every check passes.
pnpm verify -- --module customer # All checks
pnpm verify -- --module customer --skip-build # Skip the frontend build (faster while debugging)
pnpm verify -- --module customer --json # Structured JSON output (stdout contains only JSON)Checks
There are 16 checks in three groups. A check that doesn't apply (for example data_scope_filter on a module without data scope) is reported as skipped.
Global checks (always run):
| Check | What it checks |
|---|---|
typescript_compile | tsc --noEmit over apps/api (including scripts and test), apps/mcp and apps/web (including its tests) |
no_local_has_permission | Routes files must not define their own hasPermission |
migration_chain | The drizzle migration journal is linear, the snapshot chain is complete, every entry has SQL, and there is no stray SQL |
migration_applied | Compares the journal with drizzle.__drizzle_migrations in the database and confirms the module's table exists |
openapi_sync | Every registered /api route is documented in docs/apifox-full.openapi.json per AGENTS.md "OpenAPI writing rules", request bodies included (runs pnpm openapi:generate -- --dry-run --strict) |
docs_paths | Whether paths referenced in the AI context docs exist (warning only by default; blocking with --strict-docs) |
Module checks (run when --module is passed):
| Check | What it checks |
|---|---|
backend_file | Backend routes / repository / service files exist |
data_scope_filter | A module whose schema.ts declares DATA_SCOPE must filter with dataScopeWhere in its repository; skipped otherwise |
frontend_page | The frontend page file exists |
frontend_api | The frontend API file exists |
router_registration | Routes are registered in src/router.ts or the domain router.ts |
schema_registration | The table definition is registered in db/schema/index.ts |
rbac_seed | seed-rbac.ts contains the module's menu or permission codes |
Build and tests (can be skipped):
| Check | What it checks | Skip flag |
|---|---|---|
frontend_build | Frontend Vite build | --skip-build |
frontend_tests | Frontend Vitest | --skip-frontend-tests |
api_tests | Backend Vitest (needs the test database) | --skip-api-tests |
Other options
| Option | Description |
|---|---|
--skip-db | Skip migration_applied (no database connection) |
--run-rbac-sync | Also run seed:rbac --incremental once (check rbac_sync) |
--database-url <url> | Database connection used by migration_applied |
--strict-docs | Make docs_paths failures blocking |
Use the skip flags to speed things up while debugging, but run the full gate once before delivering.
MCP Server
apps/mcp exposes the toolchain as MCP tools, so MCP clients (such as Claude Desktop) can run the full development flow without a command line.
| Tool | Purpose |
|---|---|
get_project_context | Returns the full text of AGENTS.md and the current module structure; call it before implementing a new feature |
get_menu_tree | Returns the menu tree from the database, for picking parent_id and a free ID |
get_spec_guide | Returns the spec JSON Schema and the requirement → spec examples; call it before writing a spec |
validate_spec | Checks a spec (parameter spec) and says what it would generate; writes nothing |
scaffold_feature | Calls pnpm scaffold with spec (recommended) or name, domain, fields; dry_run only previews |
check_openapi | Checks the API document against the OpenAPI rules and lists the operations that break them |
run_verify | Calls pnpm verify --json and returns the result (parameters module, skip_build) |
init_rbac | Calls pnpm seed:rbac -- --incremental |
run_migration | Runs db:generate + db:migrate (parameter message is used as the migration description) |
list_templates | Lists the templates under docs/templates/ |
Example Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"castor": {
"command": "pnpm",
"args": ["--dir", "/path/to/castorjs", "-s", "mcp"]
}
}
}Or build it first and run it directly with node:
pnpm --filter @castorjs/mcp build
node /path/to/castorjs/apps/mcp/dist/index.jsBy default the MCP Server derives the repo root from its own location; override it with the CASTOR_KIT_ROOT environment variable.
Related pages
- Backend: layering and API conventions
- Frontend: page structure and shared components
- Permissions (RBAC): menu and button permissions
- Commands
