Tooling
AGENTS.md
<!-- Generated by kerf agents-md from kerf@0.1.0. Do not edit: change kerf.config.ts and regenerate. -->
This is a kerf app. It is not React: components run once, state is a box, and React hooks don't exist. Run kerf check --json after every change; each diagnostic gives code, location, why and fix.
Start with .kerf/map.md (kerf map): what this app has and what to call.
Rules
import { derived } from '@kerf-js/core'export function Cart({ items, taxRate }: { items: { id: string; name: string; price: number }[] taxRate: number}) { const subtotal = derived(() => items.reduce((s, i) => s + i.price, 0), ) const total = derived(() => subtotal.value * (1 + taxRate)) return ( <section> <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> <p>{total.value.toFixed(2)}</p> </section> )}- State is a box. Read and write
.value, including in JSX (W015). - Compute from other values with
derived, notwatchor the component body (W001). - JSX is live. Each
{expr}and attribute re-evaluates when its inputs change. - Props are current. Destructure one level (W006). Read in
derived,watch, JSX or handlers.const x = props.yis a live read-only alias;let x = props.yandprops.y =write locally; onlybind:y={box}writes out. - The body runs once. No state-based early returns (W040) or
letcounters (W041). Branch in JSX. - Pass a box only to give write access (
value={coupon}, child prop typedState<string | null>), or so list rows can read one entry:cart.get(p.id)re-runs one row,cart[p.id]orcart.value[p.id]re-runs them all (W017), in the list and in the row component alike. - Lists and conditionals:
.mapwithkey(W010),&&, ternaries. Keep items stable: replace only the changed item (p.id === id ? { ...p, price } : p), never({ ...p })every item (W014). - Copy a prop into state with
state.linked(() => prop), notstate(prop)(W005).untrack(() => …)is a snapshot that never updates: never render it (W018). - Side effects go in
watch(() => source, value => cleanup?); DOM work inonMount.flush()applies pending DOM updates now. - Async data is
resource(signal => fetch(…))(.value .pending .error .refresh()), neverderived(async …)(W012). Loading and errors:<Boundary pending={…} error={(e, retry) => …}>.
Leave props untyped: kerf docs props.
API in kerf@0.1.0
@kerf-js/core:Boundary,batch,context,defineState,derived,flush,onCleanup,onMount,resource,state,untrack,watch,state.linked@kerf-js/core/dom:render,hydrate.@kerf-js/core/ssr:renderToString,renderToStringAsync. Never import the rest.@kerf-js/core/browser:online,visible,lifecycle,media,on,interval. Create them in setup (W051); server-renderedmedianeedsssr(W054). More:kerf docs browser; hover and press as state:kerf docs ui-state.@kerf-js/core/surface: canvas and WebGL, never raw<canvas>:kerf docs surface.@kerf-js/core/test:assertNoLeaks,clock,flush,memoryStorage,net,page,render,scope,screen,tabs,touches,user, andexpect(fn).toTouchAtMost(n, { runs }). Query by role/label/text;useractions andclock.advanceend inflush(), so never sleep orwaitFor.
Diagnostics
kerf docs <code> explains one. Also flagged: an early return in a .map callback (W013), raw addEventListener, setInterval or sockets with no cleanup (W050), and a watch that folds a stream (W052) or copies a live value into state (W056).
Test checks (kerf check on test files): T001–T009, T013. No assertion, tautologies, sleeps, container/selectors/test ids, mocking kerf, snapshots, conditional expect, .only/.skip. To allow one: // kerf-allow T00x: <reason>.
Before a change is done, see it work (kerf browser snapshot|act|assert, or __kerf__.agent in your own browser) and run kerf test --gate: new tests must fail on the old code and kill a mutant (T010, T011, T020). kerf build audits its pages at phone and desktop width (L-codes) and fails on an error. More: kerf docs verify.
Before publishing, set build.audit.indexing (public or private) and audit the deployed URL: kerf audit --url <url> --indexing public. Never drop a private noindex for a score.
Full stack
@kerf-js/start app: file routes in src/routes (_layout.tsx, [id].tsx, load, meta), server functions in src/server/** or *.server.ts. Import your own code as #/server/todos (src/server/todos); generated files are kerf:slot/<id> and kerf:tokens/<name>.
- Reads:
query({ tags: ['todos'], auth: 'user', run: ({ ctx }) => ctx.db.select().from(todos) }). In a componenttodos().value ?? []; inloadand handlersawait todos.fetch(). - Writes:
mutation({ input, auth: 'user', invalidates: ['todos'], optimistic, run }). - Content an owner edits (menu, settings, pages): read with
cache: { life: 'hours', warm: true }; edits go to a draft (preview.start(id)shows the owner), publishing applies it, slow follow-ups go inafter().kerf docs start§ Publishing. - Database: Drizzle;
awaitevery statement. Schema change:kerf db generate(Q015). Inputs:schemas.todos.insert.pick({ title: true }). - Auth:
ctx.useris the user ornull;auth: 'user'refuses anyone else (A002),{ role: 'admin' }or{ can: (ctx) => … }the wrong user (A006). With auth on, mutations need a signed-in user unlessauth: 'public'(sign-in, sign-up, sign-out). Guard pages withredirect('/login')in a layout'sload;runstill checks who owns a row. Sessions live in the database; pages cache them 5 minutes in a signed cookie (session.cookieCache). JWTs are API-only (api: { functions }). - Never copy query results into
state, importdbfrom a component (Q012),fetch/_kerf/*, or poll (todos.live(); rooms:channel). - Navigation: chrome shared by sections (sidebar, tabs) goes in the one layout above them, following
currentPath(): a per-section_layout.tsxis replaced on each move. Route changes show a skeleton from the page's markup;export const skeleton = Named(plain markup, class and data-* only) overrides it. - Every page sets
metaas you build it, fromdataandparams:kerf docs meta.
Styling
Run kerf style find <what it is> first, then reuse a group or add an axis value; never class lists. New: const card = group('card', { base: { pad: 'group' }, axes: { status: { late: { tone: 'critical' } } } }) from @kerf-js/core/style, <li {...card({ status })}>. Tokens only. D001–D007, D011, D012, D020–D025, D030–D041, W060: kerf docs style, kerf docs design; kerf fix applies the safe fixes.
Commands
| Command | Does | Side effects |
|---|---|---|
kerf check --json | Compile every component in check mode and report W- and P-diagnostics with fixes | none |
kerf docs <topic> | Print a docs topic or a diagnostic code (kerf docs W014) | none |
kerf add | Install a plugin and wire it into the config (--dry-run shows the changes) | fs:kerf.config.*, fs:package.json, network:install |
kerf dev | Dev server | network:listen |
kerf build | Production build and contract | fs:.output/** |
kerf db | generate: new migrations | fs:drizzle/, fs:migrations/, fs:prisma/** |
kerf test | The test gate on the diff (--gate) | fs:.kerf/gate |
kerf browser | Drive the running app in any open browser by role/label/text (kerf browser help) | network:dev-server, fs:.kerf/browser |
kerf run | Run a project task by name | task |
kerf run content | Generate docs, search indexes, llms.txt and numbers from the repo | fs |
kerf tasks --json lists the rest.
Plugins
Fonts (@kerf-js/text)
- Declare fonts once in
text({ fonts })in kerf.config.ts:{ body: { family, src, fallback } }. Never hand-write@font-face(X020). - The build subsets each face by
unicode-range, adds a metric-matched"<Family> Fallback"face and preloadsbodyanddisplay.@kerf-js/core/viteloads them: no import.kerf docs fonts. - A build trims faces on its own: custom axes the source never varies are pinned, and uncommon latin characters load on demand. An axis set at runtime needs declaring in
axes;trim: falseturns it all off. text.measureis lazy: it costs nothing until something measures, a canvass.text(),measure(),lines()orfit()from @kerf-js/core/text, or a veneer list giventext(measure: 'off'removes it).- Font stacks name the fallback second:
font-family: "Inter", "Inter Fallback", sans-serif.font-displayisswaporoptional, neverblock(X021).
veneer (UI kit)
Import components from @kerf-js/veneer. Layout: Stack Row Grid Split Center Cover Frame Spacer Inset (gap: hair tight related group section page). No raw values (D001–D007); intent props only: tone (neutral accent positive caution critical info), emphasis (solid soft outline ghost frame link), size (sm md lg). The look comes from dials: data-expression|theme|density|motion|flair on any element (scopes nest). Fields take label (required), hint, error. App-wide: veneer({ seed: { accent }, dials, radius, icons, fonts, tokens }) (icons pins the icon tier); D010 fails a pair that can't pass. Theming: colors names colours (tone="brand"), system retunes scales, parts restyles an element per state (parts.button.hover.bg; kerf docs theme lists parts), themes are named looks switched by data-palette or setPalette. Own CSS: never a bare [data-part]; hang it from its [data-scope]. A tall sticky header sets --veneer-scroll-offset.
Icons
Import icons from @kerf-js/veneer-icons: CheckIcon is hollow, the default, CheckIconSolid the other form (<Icon name="check" solid />); <Icons.Check /> and veneer's <Icon name="check" /> build to the same bytes. Tiers are a prop, tier, or the page's data-expression. Decorative by default; label gives one a name. An unknown literal name is I001 at the build and in kerf check. A name only known at runtime is legal and loads the pack lazily (I002 reports it), or single files with icons({ source }) and kerf icons publish <dir>. trash, bell, check, refresh and copy animate with action. kerf docs icons has the rest.
More
kerf docs <topic> or kerf mcp: rules, props, lists, async, testing, browser, verify, start, style, surface, config, diagnostics, commands.