You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WebJs sells full-stack type safety as a headline capability (README.md L48, the docs site /docs/typescript and /docs/server-actions), but the guidance an app-building AI agent actually reads does not teach it as a rule, and several of our own canonical examples model the exact anti-pattern instead.
Two gaps:
No positive rule in the agent-facing guide. The scaffold's packages/cli/templates/AGENTS.md has a short "Type everything" section that says "Never reach for any or a loose as any cast", but it says nothing about unknown (the more common escape hatch an agent reaches for, because it looks safe), and it never teaches the positive patterns that make the escape hatch unnecessary: derive row types from the Drizzle schema, carry them over the .server boundary with import type, type routing files with PageProps / LayoutProps / RouteHandlerContext, and generate the route union with webjs types. The canonical skill reference .agents/skills/webjs/references/typescript.md documents each mechanism but never states the rule, so an agent skimming it has no reason to prefer the typed form.
Our own examples teach the anti-pattern. An agent copies the example far more reliably than it follows the prose. Today the examples say:
export default function RootLayout({ children }: { children: unknown }) in at least 10 places (the scaffold gallery, the docs site, the skill references, packages/cli/templates/scripts/clear-gallery.mjs), even though @webjsdev/core exports LayoutProps whose children is a TemplateResult (packages/core/src/routes.d.ts L136-139). This is the single most-copied unknown in the whole corpus and it appears in the ROOT LAYOUT, the first file a new app opens.
export async function createPost(input: unknown) in the docs site's flagship "Full-Stack Type Safety" section (website/app/docs/typescript/page.ts L86) and in /docs/server-actions (L310). The section whose whole point is "TypeScript sees the real function signature" demonstrates a signature that carries no information, and the following comment even says "TypeScript knows createPost accepts (input: unknown)".
export const validate = (input: any) in .agents/skills/webjs/references/data-and-actions.md L108 and website/app/docs/backend-only/page.ts L155.
{ id: crypto.randomUUID() as any, ... } in the optimistic-UI example, in AGENTS.md L372 and .agents/skills/webjs/references/optimistic-ui.md L32-34.
Net effect: a WebJs app built by an agent ends up with unknown-typed layouts and any-typed action inputs, which silently discards the framework's main type-safety selling point, and webjs check cannot catch it (it is a convention, not a correctness rule).
Design / approach
Docs and examples only. No framework code, no new webjs check rule (this is guidance a sensible app could legitimately do differently in a specific spot, so per the AGENTS.md conventions-vs-checks split it belongs in prose, not in the tool).
Two halves:
A. State the rule, with the positive patterns. Add a section to .agents/skills/webjs/references/typescript.md (the canonical full reference, copied verbatim into every scaffolded app) that says: at every app boundary, the type is derivable, so derive it. Cover the concrete ladder an agent should climb before reaching for an escape hatch:
DB rows: typeof posts.$inferSelect / $inferInsert from db/schema.server.ts, re-exported as a named type, carried into shipping components with import type (the existing carrier rule).
Action inputs and outputs: a named interface for the input, ActionResult<T> for the return, never input: unknown / input: any.
validate: type its argument as the action's input type (it receives the action's first argument) rather than any.
Routing files: PageProps<'/blog/[slug]'>, LayoutProps, RouteHandlerContext, plus webjs types for the route union.
Reactive props: prop<Student>(Object) / prop<Tag[]>(Array), not prop(Object) with a cast at the read site.
Optimistic temp rows: model the pending shape (Todo & { pending?: boolean }) instead of as any on the temp id.
The narrow, legitimate uses of unknown that should REMAIN: a genuinely untrusted external payload at the moment before it is narrowed (a route.tsawait req.json(), a third-party webhook body, a catch (e: unknown)), where unknown is the correct type and the very next line narrows it. The rule is "never unknown as a substitute for a type you could write", not "never unknown".
Then reference it, briefly, from the surfaces an agent reads first: the repo AGENTS.md TypeScript invariant area, .agents/skills/webjs/SKILL.md, and the scaffold's packages/cli/templates/AGENTS.md "Type everything" section (extend it to name unknown and point at the skill reference for the ladder).
B. Fix every example that models the anti-pattern, since a contradicted rule is a dead rule. That is the { children: unknown } sweep to LayoutProps, the input: unknown action signatures, the validate = (input: any) pair, and the optimistic as any.
Implementation notes (for the implementing agent)
Where to edit.
Rule text (new prose):
.agents/skills/webjs/references/typescript.md, "Full-stack type safety" section (starts L78). The new subsection goes here, after "Server actions type-check automatically" and before / around "The carrier rule" (L~118), since the carrier rule is one rung of the ladder.
.agents/skills/webjs/SKILL.md L48 routes to references/typescript.md; the summary line there should mention the rule so the router surfaces it.
AGENTS.md invariant 10 area (the TypeScript invariant) and/or the references/typescript.md row in the detail-docs table L22.
packages/cli/templates/AGENTS.md "Type everything (all templates)" section, L32-39. It already bans any; extend to unknown and point at .agents/skills/webjs/references/typescript.md.
Example fixes ({ children: unknown } to LayoutProps, import the type from @webjsdev/core):
packages/cli/templates/gallery/app/features/layout.ts L48 (also takes url, so LayoutProps covers url too, check the shape in packages/core/src/routes.d.ts before widening)
packages/cli/templates/scripts/clear-gallery.mjs L194 (this is a GENERATED string inside a script, so the fix must survive the escaping, see landmines)
website/app/docs/getting-started/page.ts L85, website/app/docs/routing/page.ts L113 + L125, website/app/docs/ssr/page.ts L268, website/app/docs/styling/page.ts L31 + L276, website/app/docs/layout.ts L155 (this last one is the docs site's REAL layout, not a doc snippet, so it is a live code change; webjs check and a boot must still pass)
.agents/skills/webjs/references/optimistic-ui.md L32-34 and AGENTS.md L372 (crypto.randomUUID() as any), which must stay in sync with each other
Landmines / gotchas.
The scaffold skill is copied, not duplicated.packages/cli/lib/create.js L640-660 copies .agents/skills/webjs/ from the repo root (or a prepack bundle produced by scripts/sync-scaffold-skill.mjs) into every generated app. So edit the repo-root canonical ONCE; do NOT hand-copy it under packages/cli/templates/. The bundle is gitignored in the monorepo.
Bun prose rewriting. That same copy runs bunifyProse over the skill's markdown for --runtime bun, EXCLUDING references/runtime.md. New prose containing runnable npm commands will be rewritten; keep the new text's commands in the canonical npm form (npx webjsdev types), never a hand-written bun form.
Touching packages/cli/templates/gallery/** is the scaffold surface, not just docs. Per the webjs-doc-sync skill, that slice belongs to webjs-scaffold-sync and carries a mandatory generate + boot + webjs check verification (the generators emit strings, so an escaping bug only shows in a freshly generated app). Run webjs create into a temp dir, boot it, and run webjs check before calling this done.
clear-gallery.mjs L194 emits source as a STRING. A ${...} or a backtick added there is evaluated by the script's own template literal unless escaped. Verify by running the script's output through a real generated app, not by reading the diff.
Prose-punctuation invariant (AGENTS.md invariant 11) applies to every markdown and doc-page line added: no em-dashes, no space-surrounded hyphen or semicolon as a pause, WebJs capitalized in prose but lowercase webjs as a code token. The .claude/hooks/block-prose-punctuation.sh hook enforces it on NEW content.
**No backticks inside html\...`(invariant 9)** applies to the docs-site.tspages, whose content lives insidehtmltemplates. The existing pages escape<as<inside
` blocks; match that convention exactly or the page 500s or renders wrong.
LayoutProps.children is typed TemplateResult (packages/core/src/routes.d.ts L136-139). If any of the layouts being converted passes something else through (a string, an array), fix the site rather than widening the type.
Invariants to respect. AGENTS.md invariants 8 (only the root layout writes the document shell, so a converted non-root layout must stay shell-free), 9, 10, 11. The conventions-vs-checks split: this is prose guidance, so do NOT add a webjs check rule for it.
Tests + doc surfaces.
Doc surfaces per webjs-doc-sync: AGENTS.md + .agents/skills/webjs/ (SKILL.md + references/typescript.md, references/data-and-actions.md, references/optimistic-ui.md, references/routing-and-pages.md), the docs site (website/app/docs/typescript, /server-actions, /backend-only, /routing, /ssr, /styling, /getting-started), and the scaffold (packages/cli/templates/AGENTS.md + gallery/**). llms.txt / llms-full.txt are live-generated, so no manual edit.
Tests: this is docs + templates, so the gates are webjs check on the website app and on a freshly generated app, plus npm test for the scaffold-template tests under packages/cli/test/ (there are existing scaffold tests that assert generated-file content; check whether any asserts the { children: unknown } string and update it). Boot the website locally to confirm /docs/typescript, /docs/routing, /docs/ssr, /docs/styling still render after the <pre> edits.
Acceptance criteria
.agents/skills/webjs/references/typescript.md states the rule (derive the type at every app boundary; unknown / any only for a genuinely untrusted payload about to be narrowed) and shows the concrete ladder: $inferSelect row types, import type across the .server boundary, a named action-input interface, ActionResult<T>, validate typed to the input, PageProps / LayoutProps / RouteHandlerContext, webjs types, prop<T>().
The rule is referenced from AGENTS.md, .agents/skills/webjs/SKILL.md, and the scaffold's packages/cli/templates/AGENTS.md "Type everything" section (which now names unknown, not just any).
No { children: unknown } remains in any layout example or live layout across the scaffold gallery, the docs site, the skill references, and clear-gallery.mjs; each uses LayoutProps.
No input: unknown / input: any remains in a server-action or validate example on the docs site or in the skill references.
The optimistic-UI example no longer uses as any for the temp id, and AGENTS.md matches references/optimistic-ui.md.
A freshly generated app (webjs create, both templates) boots and passes webjs check.
The website app passes webjs check and every edited docs page renders.
npm test passes, including any scaffold test asserting generated-file content.
Problem
WebJs sells full-stack type safety as a headline capability (
README.mdL48, the docs site/docs/typescriptand/docs/server-actions), but the guidance an app-building AI agent actually reads does not teach it as a rule, and several of our own canonical examples model the exact anti-pattern instead.Two gaps:
No positive rule in the agent-facing guide. The scaffold's
packages/cli/templates/AGENTS.mdhas a short "Type everything" section that says "Never reach foranyor a looseas anycast", but it says nothing aboutunknown(the more common escape hatch an agent reaches for, because it looks safe), and it never teaches the positive patterns that make the escape hatch unnecessary: derive row types from the Drizzle schema, carry them over the.serverboundary withimport type, type routing files withPageProps/LayoutProps/RouteHandlerContext, and generate the route union withwebjs types. The canonical skill reference.agents/skills/webjs/references/typescript.mddocuments each mechanism but never states the rule, so an agent skimming it has no reason to prefer the typed form.Our own examples teach the anti-pattern. An agent copies the example far more reliably than it follows the prose. Today the examples say:
export default function RootLayout({ children }: { children: unknown })in at least 10 places (the scaffold gallery, the docs site, the skill references,packages/cli/templates/scripts/clear-gallery.mjs), even though@webjsdev/coreexportsLayoutPropswhosechildrenis aTemplateResult(packages/core/src/routes.d.tsL136-139). This is the single most-copiedunknownin the whole corpus and it appears in the ROOT LAYOUT, the first file a new app opens.export async function createPost(input: unknown)in the docs site's flagship "Full-Stack Type Safety" section (website/app/docs/typescript/page.tsL86) and in/docs/server-actions(L310). The section whose whole point is "TypeScript sees the real function signature" demonstrates a signature that carries no information, and the following comment even says "TypeScript knows createPost accepts (input: unknown)".export const validate = (input: any)in.agents/skills/webjs/references/data-and-actions.mdL108 andwebsite/app/docs/backend-only/page.tsL155.{ id: crypto.randomUUID() as any, ... }in the optimistic-UI example, inAGENTS.mdL372 and.agents/skills/webjs/references/optimistic-ui.mdL32-34.Net effect: a WebJs app built by an agent ends up with
unknown-typed layouts andany-typed action inputs, which silently discards the framework's main type-safety selling point, andwebjs checkcannot catch it (it is a convention, not a correctness rule).Design / approach
Docs and examples only. No framework code, no new
webjs checkrule (this is guidance a sensible app could legitimately do differently in a specific spot, so per the AGENTS.md conventions-vs-checks split it belongs in prose, not in the tool).Two halves:
A. State the rule, with the positive patterns. Add a section to
.agents/skills/webjs/references/typescript.md(the canonical full reference, copied verbatim into every scaffolded app) that says: at every app boundary, the type is derivable, so derive it. Cover the concrete ladder an agent should climb before reaching for an escape hatch:typeof posts.$inferSelect/$inferInsertfromdb/schema.server.ts, re-exported as a named type, carried into shipping components withimport type(the existing carrier rule).interfacefor the input,ActionResult<T>for the return, neverinput: unknown/input: any.validate: type its argument as the action's input type (it receives the action's first argument) rather thanany.PageProps<'/blog/[slug]'>,LayoutProps,RouteHandlerContext, pluswebjs typesfor the route union.prop<Student>(Object)/prop<Tag[]>(Array), notprop(Object)with a cast at the read site.Todo & { pending?: boolean }) instead ofas anyon the temp id.unknownthat should REMAIN: a genuinely untrusted external payload at the moment before it is narrowed (aroute.tsawait req.json(), a third-party webhook body, acatch (e: unknown)), whereunknownis the correct type and the very next line narrows it. The rule is "neverunknownas a substitute for a type you could write", not "neverunknown".Then reference it, briefly, from the surfaces an agent reads first: the repo
AGENTS.mdTypeScript invariant area,.agents/skills/webjs/SKILL.md, and the scaffold'spackages/cli/templates/AGENTS.md"Type everything" section (extend it to nameunknownand point at the skill reference for the ladder).B. Fix every example that models the anti-pattern, since a contradicted rule is a dead rule. That is the
{ children: unknown }sweep toLayoutProps, theinput: unknownaction signatures, thevalidate = (input: any)pair, and the optimisticas any.Implementation notes (for the implementing agent)
Where to edit.
Rule text (new prose):
.agents/skills/webjs/references/typescript.md, "Full-stack type safety" section (starts L78). The new subsection goes here, after "Server actions type-check automatically" and before / around "The carrier rule" (L~118), since the carrier rule is one rung of the ladder..agents/skills/webjs/SKILL.mdL48 routes toreferences/typescript.md; the summary line there should mention the rule so the router surfaces it.AGENTS.mdinvariant 10 area (the TypeScript invariant) and/or thereferences/typescript.mdrow in the detail-docs table L22.packages/cli/templates/AGENTS.md"Type everything (all templates)" section, L32-39. It already bansany; extend tounknownand point at.agents/skills/webjs/references/typescript.md.Example fixes (
{ children: unknown }toLayoutProps, import the type from@webjsdev/core):packages/cli/templates/gallery/app/examples/layout.tsL7packages/cli/templates/gallery/app/features/layout.tsL48 (also takesurl, soLayoutPropscoversurltoo, check the shape inpackages/core/src/routes.d.tsbefore widening)packages/cli/templates/gallery/app/features/auth/dashboard/layout.tsL10packages/cli/templates/scripts/clear-gallery.mjsL194 (this is a GENERATED string inside a script, so the fix must survive the escaping, see landmines)website/app/docs/getting-started/page.tsL85,website/app/docs/routing/page.tsL113 + L125,website/app/docs/ssr/page.tsL268,website/app/docs/styling/page.tsL31 + L276,website/app/docs/layout.tsL155 (this last one is the docs site's REAL layout, not a doc snippet, so it is a live code change;webjs checkand a boot must still pass).agents/skills/webjs/references/routing-and-pages.mdL44Example fixes (action input / validate / optimistic):
website/app/docs/typescript/page.tsL86 + the comment at L96website/app/docs/server-actions/page.tsL310website/app/docs/backend-only/page.tsL155 (validateUser = (input: any)).agents/skills/webjs/references/data-and-actions.mdL108 (validate = (input: any)).agents/skills/webjs/references/optimistic-ui.mdL32-34 andAGENTS.mdL372 (crypto.randomUUID() as any), which must stay in sync with each otherLandmines / gotchas.
packages/cli/lib/create.jsL640-660 copies.agents/skills/webjs/from the repo root (or a prepack bundle produced byscripts/sync-scaffold-skill.mjs) into every generated app. So edit the repo-root canonical ONCE; do NOT hand-copy it underpackages/cli/templates/. The bundle is gitignored in the monorepo.bunifyProseover the skill's markdown for--runtime bun, EXCLUDINGreferences/runtime.md. New prose containing runnable npm commands will be rewritten; keep the new text's commands in the canonical npm form (npx webjsdev types), never a hand-written bun form.packages/cli/templates/gallery/**is the scaffold surface, not just docs. Per thewebjs-doc-syncskill, that slice belongs towebjs-scaffold-syncand carries a mandatorygenerate + boot + webjs checkverification (the generators emit strings, so an escaping bug only shows in a freshly generated app). Runwebjs createinto a temp dir, boot it, and runwebjs checkbefore calling this done.clear-gallery.mjsL194 emits source as a STRING. A${...}or a backtick added there is evaluated by the script's own template literal unless escaped. Verify by running the script's output through a real generated app, not by reading the diff.WebJscapitalized in prose but lowercasewebjsas a code token. The.claude/hooks/block-prose-punctuation.shhook enforces it on NEW content.html\...`(invariant 9)** applies to the docs-site.tspages, whose content lives insidehtmltemplates. The existing pages escape<as<insideLayoutProps.childrenis typedTemplateResult(packages/core/src/routes.d.tsL136-139). If any of the layouts being converted passes something else through (a string, an array), fix the site rather than widening the type.Invariants to respect. AGENTS.md invariants 8 (only the root layout writes the document shell, so a converted non-root layout must stay shell-free), 9, 10, 11. The conventions-vs-checks split: this is prose guidance, so do NOT add a
webjs checkrule for it.Tests + doc surfaces.
webjs-doc-sync:AGENTS.md+.agents/skills/webjs/(SKILL.md +references/typescript.md,references/data-and-actions.md,references/optimistic-ui.md,references/routing-and-pages.md), the docs site (website/app/docs/typescript,/server-actions,/backend-only,/routing,/ssr,/styling,/getting-started), and the scaffold (packages/cli/templates/AGENTS.md+gallery/**).llms.txt/llms-full.txtare live-generated, so no manual edit.webjs checkon the website app and on a freshly generated app, plusnpm testfor the scaffold-template tests underpackages/cli/test/(there are existing scaffold tests that assert generated-file content; check whether any asserts the{ children: unknown }string and update it). Boot the website locally to confirm/docs/typescript,/docs/routing,/docs/ssr,/docs/stylingstill render after the<pre>edits.Acceptance criteria
.agents/skills/webjs/references/typescript.mdstates the rule (derive the type at every app boundary;unknown/anyonly for a genuinely untrusted payload about to be narrowed) and shows the concrete ladder:$inferSelectrow types,import typeacross the.serverboundary, a named action-input interface,ActionResult<T>,validatetyped to the input,PageProps/LayoutProps/RouteHandlerContext,webjs types,prop<T>().AGENTS.md,.agents/skills/webjs/SKILL.md, and the scaffold'spackages/cli/templates/AGENTS.md"Type everything" section (which now namesunknown, not justany).{ children: unknown }remains in any layout example or live layout across the scaffold gallery, the docs site, the skill references, andclear-gallery.mjs; each usesLayoutProps.input: unknown/input: anyremains in a server-action orvalidateexample on the docs site or in the skill references.as anyfor the temp id, andAGENTS.mdmatchesreferences/optimistic-ui.md.webjs create, both templates) boots and passeswebjs check.webjs checkand every edited docs page renders.npm testpasses, including any scaffold test asserting generated-file content.