πβRules for AI Agents
What do I paste into my repo so an agent stops writing decks that build wrong?
This page is a set of blocks meant to be pasted into your own projectβs
AGENTS.md, CLAUDE.md, or .cursor/rules β not a description of how Narro
works. A project scaffolded with npm create narro@latest already has all of
this in its AGENTS.md; this page exists for the case where you added Narro to
a repository that already existed, so nothing wrote that file for you.
Paste the two contracts below and the verification loop. Each one prevents a specific failure that a model, left to its own defaults, produces reliably.
Choose the entry point before you scaffold
The first decision, and the one most often made wrong β a deck ends up inside a Vite application it never needed, and every later question is about the project rather than the deck.
| The deck is | Use | You get |
|---|---|---|
| Prose, lists, code, tables β one file | npx @getnarro/cli new deck.md | deck.md, nothing else |
| The same, but version-pinned in a repo | npm create narro@latest x -- --template markdown-minimal | deck.md + package.json |
| Needs its own dependencies or React layouts | npm create narro@latest x -- --template markdown-app | the deck inside a Vite project |
| Live components, data, interaction | npm create narro@latest x | a React deck |
A single markdown file in an empty directory is a complete deck: the CLI brings
its own runtime, so npx @getnarro/cli dev deck.md needs no package.json, no
node_modules, and no config. Custom layouts (layouts/<name>.tsx) and custom
CSS (deck.css) both work beside a bare deck.md β reaching for a project is
about dependencies, not capability.
Why a contract rather than a link to the docs
A model that has not read Narroβs documentation falls back on the presentation tooling it has seen most: Marp, reveal.js, Slidev. Those have a different markdown dialect and a different component model, and the resulting deck builds and renders wrong rather than failing β directives appear as literal text on the slide, a heading is imported from the wrong package, a layout name that never existed silently falls back to the default. Nothing throws, so nothing tells the model to try again.
The contracts below are written to be retrieved by the next agent that is about to make one of those mistakes.
Contract 1 β the markdown dialect
## Narro markdown rules
Narro is not Marp, reveal.js, or Slidev. Their syntax does not work here and
fails silently β a directive Narro does not recognise renders as literal text
on the slide.
1. Slides are separated by a line of exactly `---`. A `---` inside a fenced
code block does not split a slide.
2. The file opens with YAML deck frontmatter; each slide may carry its own
frontmatter block immediately after its separator.
3. Attach styling with `{.class}` after an element β `# Revenue {.text-7xl}`.
These are Tailwind classes. There is no `<!-- .element: -->` and no `<style>`
block.
4. `{#id}` sets an element id; `{key=value}` sets a prop.
5. `::name::` on its own line opens a named layout slot. Every built-in layout
takes `::header::` and `::footer::`; the column layouts take one slot per
column β
<!-- generated:layout-slots-inline -->`default` takes `header`, `footer`; `cover` takes `header`, `footer`; `section` takes `header`, `footer`; `quote` takes `header`, `footer`; `end` takes `header`, `footer`; `two-column` takes `right`, `header`, `footer`; `three-column` takes `two`, `three`, `header`, `footer`; `image-right` takes `header`, `footer`; `image-full` takes `header`, `footer`<!-- /generated:layout-slots-inline -->.
Content after a slot the layout does not read renders nowhere, silently.
6. Speaker notes are `<!-- notes: β¦ -->`. Not `???`, not `Note:`.
7. `{.step}` on a list item reveals it on the next keypress β one press per
marker, in source order, then the slide advances. There is no
`<!-- .slide: data-fragment -->`.
8. Any frontmatter key beginning `x-` is yours: nothing reads it, and
`check --strict` stays quiet. Use it to say what the file is.
9. Deck frontmatter keys are exactly:
<!-- generated:deck-frontmatter -->`title`, `author`, `date`, `theme`, `template`, `aspectRatio`, `transition`, `class`, `keyboard`, `mouse`, `touch`, `routing`, `favicon`, `maxDuration`<!-- /generated:deck-frontmatter -->.
Slide frontmatter keys are exactly:
<!-- generated:slide-frontmatter -->`layout`, `class`, `id`, `transition`, `background`, `notes`<!-- /generated:slide-frontmatter -->.
An unrecognised key is ignored, not reported.
10. Set `id:` on any slide you may edit again β it keeps diffs stable and makes
the slide linkable.
11. `{ β¦ }` takes whatever Tailwind calls a class, including arbitrary values
(`text-[10px]`), opacity modifiers (`bg-white/5`), fractions, variants
(`hover:`, `md:`), half-steps (`p-1.5`) and `!` overrides. On a list item it
styles the `<li>`; on a fence, the `<pre>`.
12. Full dialect: https://getnarro.com/docs/markdown-mode.md
Contract 2 β never invent a name
This is the one that matters most, because every failure it prevents is silent.
layout: split and theme: corporate both read exactly like names Narro would
have. Neither exists.
## Narro closed sets
Every name a Narro deck can use is enumerated. If a name is not on the list, it
does not exist β do not reach for a plausible-looking alternative, and do not
assume a name from another presentation tool carries over.
- Built-in layouts (`layout:`):
<!-- generated:layouts-inline -->`default`, `cover`, `section`, `quote`, `end`, `two-column`, `three-column`, `image-right`, `image-full`<!-- /generated:layouts-inline -->
- Named slots, per layout:
<!-- generated:layout-slots-inline -->`default` takes `header`, `footer`; `cover` takes `header`, `footer`; `section` takes `header`, `footer`; `quote` takes `header`, `footer`; `end` takes `header`, `footer`; `two-column` takes `right`, `header`, `footer`; `three-column` takes `two`, `three`, `header`, `footer`; `image-right` takes `header`, `footer`; `image-full` takes `header`, `footer`<!-- /generated:layout-slots-inline -->.
`::right::` on a three-column slide is a slot nothing reads, and the content
silently disappears. The image layouts take their image from the slide's
`image:` key (falling back to `background:`), not from a slot.
- Themes (`theme:`):
<!-- generated:themes-inline -->`default`, `corporate-dark`, `minimalist-light`, `creative-gradient`, `ocean-blue`, `forest-green`, `sunset-warm`, `midnight-purple`, `tech-dark`, `startup-bold`, `academic-classic`<!-- /generated:themes-inline -->
- Colour schemes β **palettes, not a frontmatter key**; `narro themes apply <id>
--color-scheme`, or read from `@getnarro/marketplace`:
<!-- generated:color-schemes-inline -->`ocean-blue`, `sunset-warm`, `forest-mist`, `royal-purple`, `rose-gold`, `arctic-blue`, `charcoal-slate`, `emerald-night`<!-- /generated:color-schemes-inline -->
- Font presets β **not a frontmatter key**; `applyFontPreset(name)` from
`@getnarro/core`:
<!-- generated:font-presets-inline -->`system`, `inter`, `roboto`, `openSans`, `lato`, `montserrat`, `poppins`, `raleway`, `sourceSerif`, `playfairDisplay`, `merriweather`, `jetBrainsMono`<!-- /generated:font-presets-inline -->
The complete machine-readable set β the above plus every component's props,
every exported string union (transitions, fragment effects, shapes, chart
types), every CLI command β is one fetch:
https://getnarro.com/catalog.json
Offline, the same file is `@getnarro/docs/catalog.json` after
`npm i -D @getnarro/docs`, pinned to the version actually installed.
## Narro imports
Two packages. Reaching into the wrong one is the most common broken deck, and
it fails at build time rather than silently β but only after you have written
the whole file.
`@getnarro/core` β the presentation runtime plus the motion and canvas
primitives. Exactly these, and nothing else:
<!-- generated:components:core -->`AnimatePresence`, `Canvas`, `CanvasElement`, `CanvasShape`, `ErrorBoundary`, `ErrorOverlay`, `MarqueeRow`, `MasonryBackground`, `Motion`, `MotionContainer`, `MotionList`, `MotionNumber`, `MotionPresence`, `MotionSpotlight`, `MotionStep`, `MotionSteps`, `MotionText`, `MotionTransform`, `NebulaBackground`, `Notes`, `OverviewGrid`, `Presentation`, `PresenterNotes`, `Slide`, `SlideContent`, `TransformSlide`, `TrustedByMarquee`<!-- /generated:components:core -->
`@getnarro/shared-ui` β every slide content component: `Heading`, `Text`,
`List`, `Code`, `Image`, and the rest
(<!-- generated:component-count:shared-ui -->53<!-- /generated:component-count:shared-ui --> in total).
`import { Heading } from "@getnarro/core"` does not resolve. If a component is
not in the `core` list above, import it from `@getnarro/shared-ui`. Every
component's package and props: https://getnarro.com/components.json, or one at
a time at https://getnarro.com/llms/<Component>.md.
Contract 3 β verify, do not trust
Documentation tells a model what is possible. Only a checker tells it whether what it wrote is correct, and a model with a fast checker self-corrects while a model with only prose cannot.
## Narro verification loop
Run after every edit, not once at the end:
npx @getnarro/cli check deck.md
`check` parses the deck, resolves every layout, theme, slot, and directive
name, reports every class that was written but landed on no element, and exits
non-zero naming what was wrong and which names would have worked. It takes about
a second.
When you want to see what a slide says, without a browser:
npx @getnarro/cli check deck.md --render
Before you finish:
npx @getnarro/cli check deck.md --strict # the CI gate: warnings are errors
npx @getnarro/cli check deck.md --fit # does it fit the slide? needs Chromium
npx @getnarro/cli build deck.md # markdown decks
npm run build # React projects
A deck that renders is not a deck that is correct. `check` is the only thing
that catches a layout that does not exist, a slot name the layout has no slot
for, a theme that is not installed, or a class that attached to nothing β none of
which produce an error at render time. `--fit` is the only thing that catches a
slide whose content is clipped, in either mode: for a React deck, build first and
point it at `dist`.
Do not run Prettier over `deck.md`. A reflow can move an attribute block onto its
own line, which changes what it styles, silently. `narro fmt deck.md` is the safe
formatter.
Predicting a name you have not looked up
Narroβs naming is regular, so a name can usually be derived rather than guessed. State the rule, not just the list β a model that knows the rule stops producing coin-flip guesses:
| You want | The name is | Not |
|---|---|---|
| A frontmatter key | camelCase β aspectRatio, colorScheme, fontPreset | aspect-ratio, color_scheme |
| A layout, theme, or colour scheme | kebab-case β two-column, corporate-dark | twoColumn, TwoColumn |
| A layout slot | the position, not the content β ::right::, ::left:: | ::sidebar::, ::second:: |
| A component | PascalCase, singular β List, Image, Slide | Lists, SlideItem |
A Tailwind class in {β¦} | whatever Tailwind calls it | a Narro-specific alias |
Two places where the regularity breaks, and both bite:
layout means two different things. In slide frontmatter it is the
built-in layout name (two-column). On the React SlideContent component it
is a content alignment:
They are separate closed sets and neither accepts the otherβs values.
List variants are CSS marker names, not semantic ones. variant is
not "bullet" and not "number", which are the two a model reaches for first.
The rest of the AI surface
- https://getnarro.com/llms.txt β the entry point: install commands, the closed sets, and every page with the question it answers
- https://getnarro.com/llms-full.txt β the same file with every page inlined, for one-fetch ingestion
- https://getnarro.com/catalog.json β the closed sets, machine-readable
- https://getnarro.com/components.json β every componentβs props, read out of the TypeScript types
- Any page as markdown: append
.mdto its URL npm i -D @getnarro/docsβ all of the above through the npm registry, with no web access, pinned to the installed version
For an assistant with no shell, the MCP server covers
the same ground as tools. For one with a shell, documentation plus check is
the better setup β see AI integration for the split.