Skip to content

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 ​

ToolFiles it reads
Claude CodeCLAUDE.md; skills in .claude/skills/ (new-feature-autopilot, shadcn-ui-skills)
Codex CLIAGENTS.md (read automatically); skills in .agents/skills/
Cursor, Windsurf, GitHub Copilot and othersAGENTS.md (they all read it automatically)
AI reading the docs siteThe site's /llms.txt (entry index; source website/public/llms.txt)
MCP clientsapps/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 (admin or component_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 the admin domain, cc_<name> for the component_center domain, with _add / _edit / _delete / _export / _import appended 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:

text
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 ​

  1. Generate the module with pnpm scaffold -- --spec <file> (list the files first with --dry-run).
  2. 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.
  3. Polish the frontend page (translations of page-specific text, extra validation), or rebuild it after the chosen page pattern.
  4. With menu in the spec, scaffold has already written the menu and button permissions into seed-rbac.ts; otherwise add them by hand. Then run pnpm seed:rbac -- --incremental.
  5. Review the newly generated migration SQL, run pnpm db:migrate, and confirm the table really exists with psql -d <database> -c '\d <table>'.
  6. API docs: pnpm scaffold has already written the module's endpoints into docs/apifox-full.openapi.json. If you change the generated routes, fields or validation, or add routes, update the entries from the code per AGENTS.md "OpenAPI writing rules". This is required: the openapi_sync check of pnpm verify fails 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):

bash
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                   # generate

Without a spec, a field list works too (English placeholder labels, no rules or menu):

bash
# 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 ​

OptionDescriptionDefault
--nameResource name in snake_case, e.g. customer_orderRequired
--domainDomain: admin or component_centeradmin
--fieldsField list in the form field:type,field:typename:str
--specDescribe the module in a JSON file instead of --name / --fields: Chinese labels, required, unique, defaults, options and the menu; see Spec files below—
--dry-runOnly print what would be generated; no files written, nothing registered, no migrationOff
--validate-onlyWith --spec: only check the spec and say which endpoints, permissions, table and menu it would generate; writes nothing, lists problems and exits 1 if anyOff
--write-schemaRegenerate docs/spec.schema.json from the scaffold's current field types and rulesOff
--skip-migrationDon't call drizzle-kit to generate a migrationOff
--data-scopeWith --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 / --helpPrint usage—

What gets generated ​

Existing files are skipped, never overwritten.

Generated fileDescription
apps/api/src/db/schema/<domain-dir>/<name-kebab>.tsTable definition + toDict
apps/api/src/modules/<domain-dir>/<name-kebab>/{schema,repository,service,routes}.tsThe four backend layers
apps/api/test/<admin|cc>-<name-kebab>.test.tsBasic API tests (CRUD, search, 404, export, import template, import)
apps/web/src/modules/<module>/api/<name>.tsFrontend API client, typed from the module's OpenAPI entries (row type ApiItem<'/api/admin/<name-kebab>s'>)
Frontend list page index.tsxIn 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}.jsonTranslations 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.ts and apps/api/src/modules/<domain-dir>/router.ts
  • Writes the module's endpoints into docs/apifox-full.openapi.json and regenerates the frontend API types (apps/web/src/shared/api/openapi.d.ts) from it
  • With menu in the spec: adds the menu and button permissions to apps/api/scripts/seed-rbac.ts and their English / Japanese names to apps/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 ​

TypeDrizzle columnForm componentNotes
strvarchar(100)FormInput
str20varchar(20)FormInput
str50varchar(50)FormInput
str500varchar(500)FormInput
texttextFormTextarea
intintegerFormNumber
floatnumeric(10, 2)FormNumberReturned by the API as a string, e.g. "12.50"
boolbooleanFormSwitch
datedate (string mode)FormDateYYYY-MM-DD
datetimetimestamp (string mode)FormDateTime
filevarchar(36) holding a file-center idFormFileUpload"View" link in the list; the reference is registered on save
imagevarchar(36) holding a file-center idFormImageUploadThumbnail in the list; the reference is registered on save
enumvarchar(50) holding the option valueFormSelectFixed 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
dictvarchar(100) holding the dictionary item valueFormSelectOptions 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 descriptionType
name, title, person's name, emailstr
code, identifier, number (as in an ID or serial number)str50
mobile, phone, colorstr20
status, type, level with fixed optionsenum (options in the spec; str20 with --fields)
category, source, industry with options admins maintaindict (a data dictionary code in the spec)
URL, link, address (external)str500
image, avatar, cover, photoimage
attachment, file, contract, scanfile
description, remarks, summary, content, body, tags (JSON string)text
amount, price, fee, costfloat
quantity, count, progress, percentage, sort order, weightint
date (without time)date
timedatetime
is/whether, enabled, disabled, togglebool

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:

json
{
  "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": { "设备台账": "設備台帳" } }
}
bash
pnpm scaffold -- --spec device.spec.json --validate-only   # check first and see what it would generate
pnpm scaffold -- --spec device.spec.json
  • title and every field's label are required; a misspelled key (such as requried) 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 in docs/examples/specs/
  • required: the column is NOT NULL, an empty value on create / edit returns 400 <label>不能为空, and the form marks and checks it; image / file can't be required
  • unique: the column is UNIQUE, duplicates return 400; text and number types only
  • default: the column default, used when a new record leaves the field empty and prefilled in the form
  • label / title: the Chinese text of the page, headers, imports / exports and errors; i18n holds 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) to apps/api/scripts/seed-rbac.ts, under the top-level 「业务管理」 (Business) group by default (ID 1000, created with the first module; modules from 1001); a component_center module goes under the gallery's 「页面模板」 (Page patterns) directory instead (IDs 4301–4399, API under /api/admin/component-center/); parentId picks another directory. Menu names in English and Japanese go to apps/web/src/locales/menus/
  • dataScope: true makes the module follow data scope, like --data-scope does for --fields
  • options[].tone: the badge colour of that option in the list (neutral by 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 --fields alone there are no required / unique / default values and the title and labels are English placeholders — use --spec for 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.

bash
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):

CheckWhat it checks
typescript_compiletsc --noEmit over apps/api (including scripts and test), apps/mcp and apps/web (including its tests)
no_local_has_permissionRoutes files must not define their own hasPermission
migration_chainThe drizzle migration journal is linear, the snapshot chain is complete, every entry has SQL, and there is no stray SQL
migration_appliedCompares the journal with drizzle.__drizzle_migrations in the database and confirms the module's table exists
openapi_syncEvery 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_pathsWhether paths referenced in the AI context docs exist (warning only by default; blocking with --strict-docs)

Module checks (run when --module is passed):

CheckWhat it checks
backend_fileBackend routes / repository / service files exist
data_scope_filterA module whose schema.ts declares DATA_SCOPE must filter with dataScopeWhere in its repository; skipped otherwise
frontend_pageThe frontend page file exists
frontend_apiThe frontend API file exists
router_registrationRoutes are registered in src/router.ts or the domain router.ts
schema_registrationThe table definition is registered in db/schema/index.ts
rbac_seedseed-rbac.ts contains the module's menu or permission codes

Build and tests (can be skipped):

CheckWhat it checksSkip flag
frontend_buildFrontend Vite build--skip-build
frontend_testsFrontend Vitest--skip-frontend-tests
api_testsBackend Vitest (needs the test database)--skip-api-tests

Other options ​

OptionDescription
--skip-dbSkip migration_applied (no database connection)
--run-rbac-syncAlso run seed:rbac --incremental once (check rbac_sync)
--database-url <url>Database connection used by migration_applied
--strict-docsMake 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.

ToolPurpose
get_project_contextReturns the full text of AGENTS.md and the current module structure; call it before implementing a new feature
get_menu_treeReturns the menu tree from the database, for picking parent_id and a free ID
get_spec_guideReturns the spec JSON Schema and the requirement → spec examples; call it before writing a spec
validate_specChecks a spec (parameter spec) and says what it would generate; writes nothing
scaffold_featureCalls pnpm scaffold with spec (recommended) or name, domain, fields; dry_run only previews
check_openapiChecks the API document against the OpenAPI rules and lists the operations that break them
run_verifyCalls pnpm verify --json and returns the result (parameters module, skip_build)
init_rbacCalls pnpm seed:rbac -- --incremental
run_migrationRuns db:generate + db:migrate (parameter message is used as the migration description)
list_templatesLists the templates under docs/templates/

Example Claude Desktop config (claude_desktop_config.json):

json
{
  "mcpServers": {
    "castor": {
      "command": "pnpm",
      "args": ["--dir", "/path/to/castorjs", "-s", "mcp"]
    }
  }
}

Or build it first and run it directly with node:

bash
pnpm --filter @castorjs/mcp build
node /path/to/castorjs/apps/mcp/dist/index.js

By default the MCP Server derives the repo root from its own location; override it with the CASTOR_KIT_ROOT environment variable.

Released under the MIT License.