--- name: "postxl" description: "[skaile-development] Use when building, modifying, or extending a PostXL-generated app inside skaile-dev — `platform/backend`, `platform/frontend`, and (in progress) `store/backend`. Covers schema authoring (`postxl-schema.json`, `schema/*.model.json`, `schema/*.enum.json`), the regenerate workflow (`bun run generate` → `pxl generate` + `tsr generate`), the three modes for code changes (schema edit / custom blocks / new module), `postxl-lock.json` and ejection semantics, the dual backend+frontend verify loop, the `pxl doctor`/`status`/`info` toolkit, and the `@postxl/ui-components` primitives. Activates whenever you see `postxl-schema.json`, `postxl-lock.json`, `generate.ts`, or `// @custom-start` markers, or when the user mentions PostXL, regenerate, eject, or custom block." metadata: tags: - "postxl" - "code-generation" - "schema" - "nestjs" - "react" - "prisma" - "typescript" - "scaffolding" - "platform" - "store" - "skaile-development" source: "MERGED" stage: "beta" prerequisites: files: - path: "postxl-schema.json" gate: soft description: "Project-root schema (PostXL-generated app). When present, this skill applies." - path: "postxl-lock.json" gate: soft description: "Tracks per-file checksums and ejection status for generated outputs." - path: "schema/" gate: soft description: "Optional split-schema directory with per-model / per-enum JSON files." - path: "platform/CLAUDE.md" gate: soft description: "Read first when working inside platform/ — it owns the project-specific conventions on top of PostXL (Frontend Action Pattern, dev modes, lint stack)." - path: "store/CLAUDE.md" gate: soft description: "Read first when working inside store/ — store/backend is being scaffolded onto PostXL." produces: [] reads: - path: "postxl-schema.json" - path: "schema/*.model.json" - path: "schema/*.enum.json" - path: "postxl-lock.json" - path: "generate.ts" - path: "platform/CLAUDE.md" - path: "store/CLAUDE.md" - path: "node_modules/@postxl/ui-components/CLAUDE.md" --- # PostXL App Development You are working on an application generated by **PostXL** — a schema-driven full-stack TypeScript framework. The app is **not** a hand-written codebase; most of `backend/`, `frontend/`, and `e2e/` is regenerated from declarative inputs. ## Skaile-Specific Context In the skaile-dev monorepo, **three** projects are PostXL-generated: | Project | Path | State | Notes | |---|---|---|---| | Skaile platform backend | `platform/backend/` | Live | NestJS + Fastify + Prisma + tRPC. Schema at `platform/postxl-schema.json`. | | Skaile platform frontend | `platform/frontend/` | Live | Vite + React 19 + TanStack Router. Routes regenerated by `tsr generate`. | | Public catalog backend | `store/backend/` | **In progress** | `postxl-schema.json` is in place; `postxl-lock.json` and `generate.ts` are not yet wired up. Don't blindly run `bun run generate` here until scaffolding is complete. | **Read first when working in either project:** - `platform/CLAUDE.md` — platform-specific conventions on top of PostXL (Frontend Action Pattern, dev modes, lint stack, agent gateway). - `store/CLAUDE.md` — store-specific conventions (when present). **Skaile uses Bun**, not pnpm. Every command in this skill is `bun run …` / `bunx …`. The platform's own `bun run generate` script chains `pxl generate` + `tsr generate` (TanStack Router file-based-route regeneration) — **don't bypass it with a bare `pxl generate`**, or your frontend route tree will go stale. ## Use This Skill When - The user asks to add/modify a model, enum, action, or auth rule in the platform or store. - The user asks to extend behavior in any file under `platform/backend/libs/`, `platform/frontend/src/`, `store/backend/libs/`, or `e2e/specs/`. - You see `postxl-schema.json`, `postxl-lock.json`, `generate.ts`, or `// @custom-start` markers in a project. - The user mentions "regenerate", "eject", "custom block", "drift", or "PostXL". - The user reports a confusing diff after running `bun run generate` — usually an ejected file or a missing custom block. ## When NOT to Use - Pure platform feature work that doesn't touch generated paths or the schema (e.g., editing `agent-framework/*` from inside platform). Use `implement` directly. - Routine NestJS / React work in **non-generated** files (e.g., `platform/backend/apps/api/src/main.ts`, ad-hoc utility modules outside `libs/`). - Documentation-only changes — use `doc`. - Adding a new package outside `platform/` and `store/` — those are not PostXL-generated. --- ROLE PostXL guidance — picks the right mode (schema / custom block / new module), drives the regen + dual-typecheck verify loop, and keeps the agent off the rails of editing generated files directly. READS ! /postxl-schema.json — primary schema ? /schema/*.model.json — split-model schemas ? /schema/*.enum.json — split-enum schemas ! /postxl-lock.json — generated-file checksums + eject markers ? /generate.ts — generator pipeline configuration ! platform/CLAUDE.md — when project = platform/ ? store/CLAUDE.md — when project = store/ ? node_modules/@postxl/ui-components/CLAUDE.md — UI primitive catalog ? node_modules/@postxl/cli/docs/postxl-for-agents.md — full schema grammar reference WRITES /postxl-schema.json — schema edits (Mode 1) /schema/*.{model,enum}.json — split-schema edits /backend/libs/**, frontend/src/** — only inside `// @custom-*` blocks (Mode 2) /backend/apps//**, frontend/src/{routes,components,lib}/** — new modules outside generated paths (Mode 3) MUST read `platform/CLAUDE.md` (or `store/CLAUDE.md`) before any change inside that project — it has the cross-cutting rules MUST prefer Mode 1 (schema edit) when the change is expressible as a schema field, validation, auth rule, or faker rule MUST use `// @custom-start[:name] … // @custom-end[:name]` for in-file custom logic (Mode 2) MUST run the dual verify loop (BE typecheck + FE typecheck) after any schema change or regen MUST run `bun run generate` from the **project root** (the directory containing `postxl-schema.json`) — not from `backend/` or `frontend/` MUST run `bunx prisma migrate dev` after any schema change that altered model fields MUST use `@postxl/ui-components` primitives for any new UI element — do not recreate buttons, inputs, dialogs, or tables NEVER edit a file listed in `postxl-lock.json` outside a `// @custom-*` block — that ejects it permanently NEVER run `bunx pxl generate` directly in `platform/` — use `bun run generate` so `tsr generate` also runs NEVER run Biome inside `platform/` (it's Prettier + ESLint there) — see the monorepo CLAUDE.md NEVER create a new barrel `index.ts` in `platform/backend/libs/` — only PostXL-generated barrels (tracked in `postxl-lock.json`) are allowed; for new code, import via subpath alias NEVER use the `@Optional()` NestJS decorator — it silently swallows DI resolution failures and hides broken wiring; for test setup, provide a mock or stub in the test module instead NEVER access the database directly (raw Prisma client, raw SQL) in custom blocks or new modules — use `modelViewService` methods (or `.data`) for reads and `modelUpdateService` methods (or dispatch mutation actions) for writes NEVER use pnpm — skaile-dev is bun across the board ## Mental Model ``` Inputs (you edit these) Outputs (PostXL generates these) ┌─────────────────────────┐ ┌──────────────────────────────────┐ │ postxl-schema.json │ │ backend/libs/* (NestJS + tRPC) │ │ schema/*.model.json │ ──▶ │ frontend/src/* (React + Vite) │ │ schema/*.enum.json │ │ e2e/specs/* (Playwright) │ │ generate.ts │ │ Prisma schema, Docker, CI files │ └─────────────────────────┘ └──────────────────────────────────┘ Tracked in postxl-lock.json ``` Re-running the generator is **idempotent**. The lockfile tracks each generated file's checksum so the generator can detect manual edits. ## The Three Modes for Code Changes When the user asks for a change, decide which mode applies before touching anything: ### Mode 1 — Schema change (preferred when possible) If the change is "add a model", "rename a field", "change validation", "tweak auth", "add a faker rule" — **edit the schema, not the generated code**. 1. Edit `postxl-schema.json` (or the matching `schema/.model.json` / `schema/.enum.json` if the project uses split files). 2. `bunx pxl validate` first — fails fast on schema errors (sub-second). 3. `bun run generate` from the project root. In `platform/` this runs `pxl generate` **and** `tsr generate` (TanStack Router routes); skipping it via bare `pxl generate` will silently leave the frontend route tree stale. 4. Run the verify loop (below). 5. If schema fields changed, also run `bunx prisma migrate dev` from the project root to apply the DB migration. ### Mode 2 — Custom logic in a generated file If the change is "add a method to UserService", "inject a custom logger", "wrap an action with extra logic" — **use custom blocks**. These survive regeneration: ```typescript import { Injectable } from '@nestjs/common' // @custom-start:additionalImports import { CustomLogger } from './logger' // @custom-end:additionalImports @Injectable() export class UserService { // ...generated methods... // @custom-start:auditHook private logAudit(event: string) { this.logger.log(event) } // @custom-end:auditHook } ``` Rules: - Marker styles: `// @custom-start[:name]` … `// @custom-end[:name]` (line) or `/* @custom-start[:name] */` … `/* @custom-end[:name] */` (block). - Names are optional but recommended — they help repositioning when surrounding code shifts. - Place blocks **after** an identifiable anchor line (a method signature, an import group). If the anchor disappears, the block is appended at the end with a warning. - Each named block must have matching start/end and be unique within the file. ### Mode 3 — New project-specific code If the change is a brand-new feature with no obvious generated counterpart — write it **outside** generated paths: - Frontend: `frontend/src/components/`, `frontend/src/routes//`, `frontend/src/lib/`. - Backend: a new NestJS module outside `backend/libs/` (e.g., `backend/apps/api/src//`). - E2E: new spec files in `e2e/specs/`. The generator never touches files outside its known output paths. ## When in Doubt — Don't Edit Generated Files Directly Editing a generated file outside `// @custom-*` blocks **ejects** it: the generator stops re-emitting that file (the lockfile records `"ejected"` instead of a checksum). Once ejected, the file is yours to maintain forever, and bug fixes/features in upstream generators won't reach it. Eject only deliberately, not as a workaround. **Find drift before it bites:** `bunx pxl status` lists every generated file's eject/drift state in one shot. `bunx pxl doctor` runs the full preflight (schema + drift + custom blocks + env + migrations) — this is the single best command to run when something feels off. If you ejected by accident: `bun run generate -f -p ''` forcibly regenerates the file (overwriting your edits), or remove the file's entry from `postxl-lock.json` and regenerate. Verify the result with `bunx pxl status`. ## Schema Authoring Cheat Sheet `postxl-schema.json` and per-model split files share the same field shapes. Common pieces: - **Field types**: `String`, `Int`, `Float`, `Boolean`, `DateTime`, `Json`, plus relations (`"type": "Post"` or `"Post?"` for optional), enum references, and inline `DiscriminatedUnion` (with `commonFields` and `members`). - **Validations**: `maxLength`, `min`, `max`, `int`/`float`, `isUnique`, `hasIndex`. - **Auto-fields**: `isCreatedAt`, `isUpdatedAt`. Or use `standardFields: ["id", "createdAt", "updatedAt"]` for the common set. - **Auth**: per-model `read`/`write`/`create`/`update`/`delete`/per-action rules; `auth.adminUi.visibleFor` controls Admin UI visibility. - **Repository**: `repository.type`: `DatabaseDirect` | `DatabaseCached` | `InMemory` | `NoRepository`. - **Mock data**: `faker.seed` + `faker.items` for count, plus per-field `faker` expressions like `lorem.slug`, `internet.email`. - **Standard models**: opt into built-ins via top-level `standardModels` (`User`, `Action`, `ActionOperation`, `File`, `TableView`, `Comment`, `Config`). For the full grammar, read `docs/postxl-for-agents.md` §3 in the framework repo, or the Zod decoders under `packages/schema/src/` (npm-installed at `node_modules/@postxl/schema/`). ### Schema Splitting (Multi-File Schemas) Large schemas can be split into per-model / per-enum files under `schema/`: - `schema/.model.json` → model ``. Example: `schema/country.model.json` → `Country`. - `schema/.enum.json` → enum ``. - File contents are the same JSON shape as an inline model/enum, **minus** the `name` field (the filename is the source of truth). - Inline `models` / `enums` and split files are merged at parse time. Defining the same name on both sides is an error. - Override the default globs via top-level `modelFiles` / `enumFiles` arrays in `postxl-schema.json`. See `docs/postxl-for-agents.md` §12 for the full reference. ## The Verify Loop (Run After Every Schema Change or Regen) PostXL generates both backend and frontend code. **Type-checking only one half misses real failures.** ```bash # From the project root (the dir with postxl-schema.json — e.g., platform/) # 1. Validate the schema (fast — sub-second) bunx pxl validate # 2. Regenerate (in platform/, this also runs tsr generate) bun run generate # 3. Type-check both apps bun run typecheck # platform-root: runs --filter '*' test:types # or, per-package if you want isolated output: cd backend && bun run test:types cd ../frontend && bun run test:types # 4. If schema changed DB shape: cd .. # back to project root bunx prisma migrate dev ``` For a one-shot health check across schema, drift, custom blocks, env, and migrations, run `bunx pxl doctor` — it's the canonical preflight before any larger change. For faster iteration on a single model, scope the regen via `pxl generate` directly (you lose the `tsr generate` step, so re-run the full `bun run generate` once before committing): ```bash bunx pxl generate -m Country City # regenerate only these models (no tsr) ``` ## UI Components — Do Not Reinvent The frontend uses `@postxl/ui-components` (~60 primitives: Button, Input, Dialog, DataGrid, Form components, etc.). Before creating any UI element: 1. Check the package's `CLAUDE.md` (under `node_modules/@postxl/ui-components/CLAUDE.md` or in the framework repo at `packages/ui-components/CLAUDE.md`). 2. Use existing components — never recreate generic primitives (button, input, modal, table). 3. For tables, use `DataGrid` (sorting, filtering, inline edit, saved views via the `TableView` standard model). ## Skaile Cross-Cutting Rules That Interact With PostXL These come from the monorepo `CLAUDE.md` and `platform/CLAUDE.md`. They constrain how you work on PostXL output inside Skaile: - **Frontend Action Pattern (platform only).** Every new user-facing action — including ones added in custom blocks or new `frontend/src/routes//` modules — must register a command palette action via `useCommandPaletteActions()`. See `platform/CLAUDE.md` § Frontend Action Pattern. This is mandatory — Cmd+K and the in-app AI agent are how features get discovered. - **Lint stack: Prettier + ESLint, NEVER Biome.** Even after you eject a file or write a Mode-3 module, format it with the package's `bun run lint` (ESLint) and Prettier — do not run `bun run format` (Biome) anywhere inside `platform/`. The dev-shell `CLAUDE.md` has a hard prohibition. - **Barrel-file rule.** `platform/backend/libs/` forbids new `index.ts` re-export barrels (they break NestJS DI). The exception is exactly the PostXL-generated barrels tracked in `postxl-lock.json`. If your Mode-2 custom block or Mode-3 module creates a barrel-shaped import, restructure it into direct subpath imports (`@credential/credential.service`, not `@credential`). The deeper reason: barrel imports — and even direct subpath imports of services that are themselves transitively re-imported by the consumer — create circular **file-level imports** that JavaScript resolves to `undefined` at module-load time. Nest then reports `UndefinedDependencyException` against an unrelated consumer (we hit this with `ClaudeCodeTokenRefreshService.dispatcher` at parameter index [3] — the reported service is innocent; the actual cycle was elsewhere). Use `import type {...}` (erased at compile time) when you only need the type. See § Custom Action Handlers below and § Diagnosing UndefinedDependencyException. - **`@Optional()` is forbidden.** Never annotate NestJS constructor parameters with `@Optional()`. The decorator silently drops DI resolution failures — broken wiring becomes invisible at runtime. When a dependency is unavailable in test context, provide a mock or stub in the test module setup instead. - **Data access layer.** Custom blocks and new modules must never call the Prisma client or raw SQL directly. For reads, use the relevant `modelViewService` methods or its `.data` property. For writes, call `modelUpdateService` methods or dispatch a mutation action. Direct access bypasses caching, authorization checks, and event hooks generated by PostXL. - **Submodule commit workflow.** `platform/` and `store/` are git submodules of skaile-dev. After your change: 1. Commit inside the submodule (`platform/` or `store/`). 2. Then bump the submodule pointer with a `chore(submodules): bump …` commit in the dev shell. The `git` skill handles this — use it, don't roll your own. - **Two CLAUDE.md sources of truth.** `platform/backend/libs//CLAUDE.md` files describe lib-specific conventions on top of PostXL — read them when your custom block or Mode-3 module touches that lib. ## Custom Action Handlers — Service Injection Rules PostXL generates one `UpdateService` per model in `backend/libs/update/`. Each handler is dispatched through `DispatcherService` (in `ActionsModule`), which already depends on `UpdateService` (forwardRef'd) which depends on every `UpdateService` (forwardRef'd). This means **any service that itself depends on `DispatcherService` cannot be injected into a `UpdateService` constructor** — it closes a cycle that JavaScript module-load can't resolve. `forwardRef`, `@Optional()`, and `ModuleRef` all happen *after* module load, so none of them break this cycle. Common offenders in skaile-platform: - `SkaileConfigService` (uses DispatcherService for skaileConfig.update dispatches) - `SessionLifecycleService` (uses DispatcherService for session field updates) - Any custom service that calls `this.dispatcher.dispatch(...)` internally The right pattern when a custom action handler needs data from such a service: 1. **In the tRPC route**, pre-resolve the data via `ctx.` — the route layer is outside the cycle because `TrpcPlugin` injects these services directly. 2. **Add the pre-resolved data to the action payload** as a new field on the custom action's Zod decoder (e.g. `zForkDecoder`). 3. **In the handler**, consume `data.`. The handler stays pure — no cross-module class injection, no runtime `import` of the cycle-causing service. If you still need the *type*, use `import type { ... }` (erased at compile time, no module-load cycle). The same applies to preconditions that require reading from such a service: do the check in the route and throw `TRPCError({ code: 'BAD_REQUEST', ... })` before dispatching. **Worked example (Session.discard / Session.fork in `platform/backend/`):** ``` // session-actions.route.ts (route layer — outside the cycle) discard: procedure .use(authMiddleware) .input(z.object({ sessionId: zSessionId })) .mutation(async ({ input, ctx }) => { const config = await ctx.skaileConfigService.getEffectiveConfig(input.sessionId) if (mountHasAutosync(config.mounts[0])) { throw new TRPCError({ code: 'BAD_REQUEST', message: '...' }) } return ctx.dispatch({ scope: 'session', type: 'discard', payload: { sessionId: input.sessionId } }) }), // session.update.service.ts (handler — pure, no SkaileConfigService import) import type { SkaileConfigData } from '@session/skaile-config.service' // type-only, no cycle public async discard({ data, execution }) { // state precondition only; mount precondition lives in the route if (session.status !== 'Closed') throw new BadRequestException(...) await this.sessionLifecycle.destroy(...) await this.delete(...) } ``` **Symptom of getting this wrong:** backend boot fails with `UndefinedDependencyException` pointing at an *unrelated* service (we hit `ClaudeCodeTokenRefreshService`). Nest gave up on the cycle and injected `undefined` into the first consumer of the deadlocked provider it tried to construct. See § Diagnosing UndefinedDependencyException. ## Diagnosing UndefinedDependencyException at Backend Boot The reported service usually isn't the cause — it's the first consumer of a deadlocked provider Nest tried to construct. 1. Identify the unresolved parameter (`?` in the constructor signature in the error message). That's the provider that couldn't resolve, not the reported service. 2. Grep the codebase for both `@Inject()` and value-level `import { }` (not `import type`). 3. Walk the import graph: does any file that runtime-imports the provider get re-imported (directly or transitively) by the provider's own dependency chain? That's the cycle. 4. If yes: the cycle is at the JavaScript module-load level. Neither `forwardRef`, `@Optional()`, nor `ModuleRef.get(...)` will fix it — they all happen later than the failing module load. Fix by removing the runtime import (switch to `import type {...}`) or restructuring the call site to a layer outside the cycle (see § Custom Action Handlers above). 5. If no cycle is visible: check decorator-parameter union types. `private readonly foo: Foo | null` emits `Object` as `design:type` metadata; Nest can't resolve the parameter unless you add explicit `@Inject(Foo)`. ## Common Pitfalls | Mistake | What to do instead | |---|---| | Editing in `backend/libs/` or `frontend/src/components/admin/` directly | 95% chance you should edit the schema or use a custom block. Check `postxl-lock.json` — if listed, it's generated. | | Forgetting `bunx prisma migrate dev` after a schema change | Backend boots, but DB queries fail at runtime with table/column-missing errors. | | Skipping the frontend typecheck after adding a field | Frontend imports BE-derived types and breaks silently until built. Run `bun run typecheck` from the project root. | | Running `bunx pxl generate` directly in `platform/` | TanStack Router routes go stale. Always run `bun run generate` (chains `pxl generate` + `tsr generate`). | | Custom blocks without an anchor line | Placing `@custom-*` at the top/bottom with no identifiable anchor above is fragile — the block lands at the end with a warning after future regens. Anchor against a method signature or import group. | | Putting business logic in `generate.ts` | `generate.ts` only configures *which generators run*. Custom behavior goes in `// @custom-*` blocks or new modules. | | Running generate in the wrong directory | Must be run from the project root (the directory with `postxl-schema.json`), not from `backend/` or `frontend/`. | | Using `pnpm` commands | Skaile-dev is bun. Use `bun run …` and `bunx …`. | | Running `bun run format` in `platform/` | That's Biome, which is forbidden here. Use `bun run lint` (ESLint) per-package and Prettier for formatting. | | Committing changes inside `platform/` without bumping the submodule pointer | The dev shell's `platform` ref stays at the old commit. Use the `git` skill so the submodule bump is generated for you. | | Running `bun run generate` in `store/backend` | Not yet wired up (no `generate.ts`/`postxl-lock.json` as of this writing). Verify the project is fully scaffolded before regenerating. | | Using `@Optional()` on a NestJS injectable | Remove the decorator; DI failures are silently swallowed and hide broken wiring. In test modules, provide a mock or stub for the missing dep instead. | | Reading data via the raw Prisma client in custom code | Use `modelViewService.data` or its read methods — direct Prisma access bypasses PostXL's caching, auth enforcement, and event hooks. | | Writing data via the raw Prisma client in custom code | Use `modelUpdateService` methods or dispatch a mutation action — direct writes skip authorization and event propagation. | | Injecting a `DispatcherService`-dependent service (e.g. `SkaileConfigService`, `SessionLifecycleService`) into a custom action handler in a `UpdateService` | That closes a circular module-load chain. Pre-resolve the data in the tRPC route via `ctx.` and pass it through the action payload. Type-only imports (`import type { ... }`) are safe in the handler. See § Custom Action Handlers. | | Diagnosing `UndefinedDependencyException` by focusing on the reported service | The reported service is usually innocent — Nest gave up on a cycle elsewhere and injected `undefined` into the first consumer of the deadlocked provider. Identify the `?` parameter, walk the import graph for cycles, look for value-level `import { ... }` of services that re-import the consumer transitively. See § Diagnosing UndefinedDependencyException. | ## Quick Reference All commands assume you are in the project root (`platform/` or `store/`) unless stated otherwise. | Task | Command | |---|---| | Validate schema | `bunx pxl validate` | | One-shot project orientation | `bunx pxl info` | | Composite preflight (schema + drift + custom blocks + env + migrations) | `bunx pxl doctor` | | Show eject/drift status of every generated file | `bunx pxl status` | | List/validate custom code blocks | `bunx pxl custom-block` | | Regenerate everything (in `platform/`: pxl generate + tsr generate) | `bun run generate` | | Regenerate one model (no tsr step) | `bunx pxl generate -m ModelName` | | Force-regen specific file (un-eject) | `bun run generate -f -p ''` | | Skip ejected files during regen | `bun run generate -i` | | Watch & regen on schema change | `bun run generate:watch` | | Apply DB migration | `bunx prisma migrate dev` | | Reset DB | `bunx prisma migrate reset` | | Typecheck whole project (root) | `bun run typecheck` | | BE typecheck (per-package) | `cd backend && bun run test:types` | | FE typecheck (per-package) | `cd frontend && bun run test:types` | | BE lint (ESLint — never Biome) | `cd backend && bun run lint` | | FE lint (ESLint — never Biome) | `cd frontend && bun run lint` | | Run BE in dev (stateless, no auth) | `cd backend && bun run dev` | | Run FE in dev | `cd frontend && bun run dev` | | Run E2E (stateless) | `cd e2e && bun run e2e:stateless` | ## Operational Modes The generated backend supports several runtime modes — pick the right one for the task: - `dev` — in-memory repos, no auth. Fastest. Use for FE iteration. - `dev:auth` — in-memory + Keycloak. - `dev:stateful` — Prisma + Postgres, no auth. - `dev:stateful:auth` — full prod-like stack. - `e2e:stateless` / `e2e:stateful` — for Playwright runs. Same code, different repository wiring. ## Pointers for Deeper Reference - `docs/postxl-for-agents.md` (in the framework repo or under `node_modules/@postxl/cli/`) — full reference for schema grammar, what gets generated, authorization model, and project archetypes. - `node_modules/@postxl/ui-components/CLAUDE.md` — full UI component catalog with usage rules. - `docs/generators.md` — covers the generator-development workflow (only relevant if extending the framework itself, not consuming it). - `projects/demo/postxl-schema.json` (in the framework repo) — canonical example schema with most features exercised. - `platform/CLAUDE.md` — Skaile platform conventions on top of PostXL (Frontend Action Pattern, dev modes, Protocol v2 capability surface, agent gateway). - `platform/backend/libs//CLAUDE.md` — per-lib conventions for any backend area you're touching. CHECKLIST - [ ] Read the project's `CLAUDE.md` (`platform/CLAUDE.md` or `store/CLAUDE.md`) before any edit - [ ] Picked the right mode: schema (Mode 1), custom block (Mode 2), or new module (Mode 3) — Mode 1 first if expressible - [ ] `bunx pxl validate` passed before `bun run generate` - [ ] Used `bun run generate` (not bare `bunx pxl generate`) so `tsr generate` ran in `platform/` - [ ] Custom blocks anchored against an identifiable line and have matching `@custom-start` / `@custom-end` markers - [ ] Did NOT edit a file listed in `postxl-lock.json` outside a custom block - [ ] `bunx prisma migrate dev` run if model fields changed - [ ] Both backend and frontend typechecks pass (`bun run typecheck` from project root) - [ ] `bunx pxl status` shows no unintended drift / ejection - [ ] Lint clean via `bun run lint` (ESLint) in each touched package — Biome never invoked - [ ] No new `index.ts` barrel under `platform/backend/libs/` (PostXL-generated barrels excepted) - [ ] No `@Optional()` decorator on any NestJS injectable — test modules provide mocks/stubs for unavailable deps - [ ] Custom action handlers in `UpdateService` do NOT inject any service that depends on `DispatcherService` (e.g. `SkaileConfigService`, `SessionLifecycleService`) — cross-module data is pre-resolved in the tRPC route and threaded through the action payload - [ ] Custom blocks and new modules read data via `modelViewService` methods / `.data` (not raw Prisma/SQL) - [ ] Custom blocks and new modules write data via `modelUpdateService` methods or mutation action dispatch (not raw Prisma/SQL) - [ ] Any new user-facing UI feature in `platform/frontend/` registered as a command palette action (Frontend Action Pattern) - [ ] Used `@postxl/ui-components` primitives for new UI — no hand-rolled buttons/inputs/dialogs/tables - [ ] Submodule pointer bumped in the dev shell after committing inside `platform/` or `store/` (use the `git` skill) --- ## Integration - **Called by:** `implement` (when target package is `platform/backend`, `platform/frontend`, or `store/backend`); user directly when working on a PostXL-generated project. - **Calls:** `git` (commits + submodule pointer bump), `test` (post-change verification), `audit` (scope=diff after changes), `doc` (when public surface changed), `verify-ui` / `e2e-platform` (when UI changed). - **Reads:** `/postxl-schema.json`, `/postxl-lock.json`, `/generate.ts`, `platform/CLAUDE.md`, `store/CLAUDE.md`, `node_modules/@postxl/ui-components/CLAUDE.md`. - **Writes:** schema files, custom blocks inside generated files, brand-new modules outside generated paths. Never modifies generated files outside `// @custom-*` blocks.