module.json
Every field of a module manifest — the required five, the two enums, the regex patterns, and what reads each one.
Five fields are required — id, name, description, status, skill — and additionalProperties
is false, so a field the schema does not know is a failed gate rather than a silent no-op.
The schema is packages/module-manifest/module.schema.json and it is the authority. The
ModuleManifest interface beside it is the same document in TypeScript; the two are changed
together, and pnpm validate fails on drift.
The top level
Prop
Type
There is no routes field and no nav field. Where a module appears in the app is the shell's —
see the module contract.
What status and selection actually do
| Value | Effect |
|---|---|
status: "planned" | The module is in the tree and validated like every other, and ready() filters it out. The picker never offers it, and --modules naming it ends the run with the list of ids that are ready. |
status: "ready" | Offered. |
selection: "default" | Taken by --yes. |
selection: "opt-in" | Offered in the picker with an extra hint, and never taken by --yes — name it in --modules to get it. Two modules use it: auth and admin, both because they need credentials the wizard cannot invent. |
An app preset's module list goes through the same gate as --modules: an id that is unknown or
not ready ends the run, naming which of the two asked for it.
requires is resolved into a closure before anything is written: dependencies first, core
always, and the ids it added are printed. One module declares one — deals requires contacts.
recommends is offered in an interactive run as a second checklist, and merely logged when the
selection came from --modules, --yes or an app preset. A mutual recommendation between two
modules is legal where a requires cycle is not. The longest list is publishing, which
recommends auth, admin and automations.
hidden
A hidden module contributes no rail item and no /<id> route. The flag lives in the manifest as well
as on the shell descriptor because the wizard writes the agent handoff from manifests alone and would
otherwise print a deep link to a route that does not exist — and the two are compared, so they cannot
drift. A hidden module still gets a line in the handoff's tour, without a route.
auth is the only module that sets it: it contributes a host integration instead of a page.
railHidden, which keeps the route and only takes the listing away, is a descriptor field with no
manifest counterpart — see the module contract.
skill
Prop
Type
app.env
An array of the variables this module needs. The wizard writes the union across the selected
modules, first declaration wins, preceded by the app's own four — VITE_APP_NAME, VITE_APP_LOGO,
VITE_CHATFUEL_DASHBOARD_URL, ALLOWED_ORIGINS — which belong to no manifest. An app preset's
declarations come last, so a preset can add variables but never redefine one a module declared.
Prop
Type
Values resolve in this order: a step-resolved value, then the token for CHATFUEL_TOKEN, then the
manifest default, then ''.
resolve documents which step fills the variable; no code reads the field. The steps write into the
answer bag by variable name — the workspace picker sets VITE_CHATFUEL_WORKSPACE_ID and
CHATFUEL_WORKSPACE_ID, the auth setup sets the SUPABASE_* ones, the admin step sets
ADMIN_PASSWORD — so a resolve value that is wrong changes nothing, and a variable nobody writes
comes out empty however it is annotated.
A .env line has no escape, so the writer refuses a resolved value carrying a line break, by
variable name, before the file exists.
app.embed
What --embed copies into a host project.
Prop
Type
The wizard installs the union of npmDependencies across the selected modules, and drops an
unselected module's from the scaffold's package.json — a name a selected module also declares
stays.
permissions
Prop
Type
Nothing checks these at runtime. They are read once, when the wizard writes the handoff notes, so the
agent that continues the work knows what the token has to be allowed to do. A module may declare
none — auth does not, because it gates the app rather than calling Chatfuel — and the handoff then
prints (none declared).
What the gates check
pnpm validate runs both of these; a failure is a failed build, not a warning.
| Check | Failure |
|---|---|
A directory under content/modules/ | Every one needs a module.json — the skill lint, the migration hygiene and the codegen coverage all reach a module through its manifest, and would skip it in silence. |
| ajv against the schema | Reported per field path — a bad pattern, a value outside an enum, a property the schema does not have. |
id vs the directory | id "x" does not match directory name "y". |
skill.installAs | Already used by another module. |
| The skill directory | Declared but not in the tree. |
SKILL.md frontmatter | Its name must equal installAs. |
requires / recommends | May not name themselves, or a module that does not exist. |
requires cycles | Detected over requires edges only. |
The shell side, for a ready module with an app block | content/shell/src/modules/<id>/index.tsx exists and exports moduleDescriptor under that exact name; its hidden matches the manifest's; the id is in the shell registry; content/modules/<id>/handoff.md exists; every embed root, the playbook and the entry component resolve. |
| The shell registry | It may list nothing that is not a ready module with an app block. |
Scripts in the generated app
Every npm script the scaffolded app ships with — what it runs, when you reach for it, and what has to be true first.
The module contract
The four interfaces a module implements — ModuleDescriptor, ModuleAppProps, HostIntegration, HostRuntime — field by field, and what each one is for.