For Agents
Every Agent Prompt the Mandu docs ship, indexed in one place — fetch this URL and you have the whole site.
On this page
For Agents
Every Agent Prompt the Mandu docs ship, indexed in one place. AI agents can fetch this single URL instead of crawling the docs tree.
Currently aggregating 63 prompts across 63 pages.
Auto-generated. Edit
scripts/build-for-agents.tsor the source pages' frontmatter / Agent Prompt blocks to change this output.
AI Integration
mandu ai chat
Interactive REPL with streaming responses and slash commands. Works offline with the local echo provider, or hit Claude / OpenAI / Gemini / Ollama / LM-Studio.
ai-hint —
mandu ai chatis a streaming REPL. Slash commands: /help /reset /save /load /preset /system /provider /model /quit. History scrollback bounded to 100 turns. Default providerlocalworks without API keys.
Invariants:
- Default provider is
local— works offline with deterministic echo - In-memory scrollback bounded to 100 turns — older entries drop when exceeded
- History JSON schema v1 —
/save//loadare portable across sessions - Ctrl+C during streaming aborts the turn cleanly — failed turn is dropped from history
- Slash-command args are escaped:
/preset ../etcrejected by strict alphanumeric allow-list
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/chat to my project.
Summary of the page:
`mandu ai chat` is a streaming REPL. Slash commands: /help /reset /save /load /preset /system /provider /model /quit. History scrollback bounded to 100 turns. Default provider `local` works without API keys.
Required invariants — must hold after your changes:
- Default provider is `local` — works offline with deterministic echo
- In-memory scrollback bounded to 100 turns — older entries drop when exceeded
- History JSON schema v1 — `/save` / `/load` are portable across sessions
- Ctrl+C during streaming aborts the turn cleanly — failed turn is dropped from history
- Slash-command args are escaped: `/preset ../etc` rejected by strict alphanumeric allow-list
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
mandu ai eval
Non-interactive prompt evaluator — runs one prompt across one or more providers and prints JSON to stdout. Exit 0 if every provider succeeded; exit 1 if any errored. Ideal for CI diff tests.
ai-hint —
mandu ai evalis the batch / non-interactive counterpart tomandu ai chat. Accepts--providers=a,b,cto fan out,--prompt=...or stdin, streams to JSON on stdout. Use withjqor snapshot tests in CI. Exit 0 = all providers OK, exit 1 = any provider failed.
Invariants:
- Output is JSON to stdout —
jqfriendly, snapshot-friendly - Exit 0 if every provider in
--providerssucceeded; exit 1 if any returned an error - Prompts can be passed via
--promptOR piped over stdin - Each provider call honors
MANDU_AI_TIMEOUT_MS— slow ones fail the whole eval - No history — every invocation is a fresh context
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/eval to my project.
Summary of the page:
`mandu ai eval` is the batch / non-interactive counterpart to `mandu ai chat`. Accepts `--providers=a,b,c` to fan out, `--prompt=...` or stdin, streams to JSON on stdout. Use with `jq` or snapshot tests in CI. Exit 0 = all providers OK, exit 1 = any provider failed.
Required invariants — must hold after your changes:
- Output is JSON to stdout — `jq` friendly, snapshot-friendly
- Exit 0 if every provider in `--providers` succeeded; exit 1 if any returned an error
- Prompts can be passed via `--prompt` OR piped over stdin
- Each provider call honors `MANDU_AI_TIMEOUT_MS` — slow ones fail the whole eval
- No history — every invocation is a fresh context
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
AI
mandu ai is a terminal playground for streaming chat with Claude, OpenAI, Gemini, or a local Ollama/LM-Studio. Plus the prompt template system, 4 new MCP tools, loop closure framework, and auto skills generator.
ai-hint — AI category hub. Four providers: claude, openai, gemini, local (echo + Ollama/LM-Studio). Two CLI commands:
mandu ai chat(REPL) +mandu ai eval(non-interactive). Phase 14 ships: 4 new MCP tools (run.tests, deploy.preview, ai.brief, loop.close), prompt templates, loop closure framework.
Invariants:
- Four providers:
claude,openai,gemini,local(Ollama/LM-Studio compatible) - API keys NEVER logged — errors mask them as
sk-*** - Local provider echo responder works offline with no API key — deterministic, CI-safe
- History JSON schema v1 —
/saveproduces portable exports - MCP tool
mandu.loop.closeis pure — no I/O, no spawn, advisory text only
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/index to my project.
Summary of the page:
AI category hub. Four providers: claude, openai, gemini, local (echo + Ollama/LM-Studio). Two CLI commands: `mandu ai chat` (REPL) + `mandu ai eval` (non-interactive). Phase 14 ships: 4 new MCP tools (run.tests, deploy.preview, ai.brief, loop.close), prompt templates, loop closure framework.
Required invariants — must hold after your changes:
- Four providers: `claude`, `openai`, `gemini`, `local` (Ollama/LM-Studio compatible)
- API keys NEVER logged — errors mask them as `sk-***`
- Local provider echo responder works offline with no API key — deterministic, CI-safe
- History JSON schema v1 — `/save` produces portable exports
- MCP tool `mandu.loop.close` is pure — no I/O, no spawn, advisory text only
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Loop closure
Pure detector + emitter framework that recognizes stall patterns in agent output (stdout / stderr + exit code) and composes an advisory nextPrompt. No I/O. Deterministic. Safe to call on untrusted output.
ai-hint — Library:
@mandujs/skills/loop-closure→closeLoop(). MCP tool:mandu.loop.close. 10 detectors in priority order: typecheck-error > test-failure > missing-module > syntax-error > not-implemented > unhandled-rejection > incomplete-function > todo-marker > fixme-marker > stack-trace. All pure functions — neverfs/spawn/fetch/Math.random/timestamps.
Invariants:
- Every detector is a pure function:
(DetectorInput) => Evidence[] - Emitter is pure:
(Evidence[], exitCode) => LoopClosureReport closeLoop()performs NO I/O — neverfs,spawn,fetch,Math.random, or time-dependent logic- Output is deterministic — identical inputs yield identical reports
- Oversized streams are truncated to the last 1,000,000 characters (tails kept — banners live at the end)
- Detectors err on LOW false-positive — well-formed Mandu source text produces zero matches
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/loop-closure to my project.
Summary of the page:
Library: `@mandujs/skills/loop-closure` → `closeLoop()`. MCP tool: `mandu.loop.close`. 10 detectors in priority order: typecheck-error > test-failure > missing-module > syntax-error > not-implemented > unhandled-rejection > incomplete-function > todo-marker > fixme-marker > stack-trace. All pure functions — never `fs`/`spawn`/`fetch`/`Math.random`/timestamps.
Required invariants — must hold after your changes:
- Every detector is a pure function: `(DetectorInput) => Evidence[]`
- Emitter is pure: `(Evidence[], exitCode) => LoopClosureReport`
- `closeLoop()` performs NO I/O — never `fs`, `spawn`, `fetch`, `Math.random`, or time-dependent logic
- Output is deterministic — identical inputs yield identical reports
- Oversized streams are truncated to the last 1,000,000 characters (tails kept — banners live at the end)
- Detectors err on LOW false-positive — well-formed Mandu source text produces zero matches
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
MCP tools (Phase 14.3)
Four new tools added to @mandujs/mcp in Phase 14.3 — mandu.run.tests, mandu.deploy.preview, mandu.ai.brief, and mandu.loop.close. Each closes the loop between an agent's actions and its orchestrator.
ai-hint — Phase 14.3 Agent G adds 4 MCP tools: run.tests (runs mandu test, returns structured summary), deploy.preview (mandu deploy --dry-run adapter), ai.brief (project briefing for new agents), loop.close (pure stall-pattern detector → nextPrompt). All four in TOOL_MODULES in packages/mcp/src/tools/index.ts.
Invariants:
- All four tools ship in the default
fullMCP profile — appear inlist_tools mandu.loop.closeis pure (readOnlyHint: true) — no I/O, no spawnmandu.deploy.previewalways passes--dry-run— cannot trigger real deploymentsmandu.run.testschild process timeout: 10 minutesmandu.ai.briefis read-only — git log is cap'd at 10s, failure → empty recent_changes
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/mcp-tools to my project.
Summary of the page:
Phase 14.3 Agent G adds 4 MCP tools: run.tests (runs mandu test, returns structured summary), deploy.preview (mandu deploy --dry-run adapter), ai.brief (project briefing for new agents), loop.close (pure stall-pattern detector → nextPrompt). All four in TOOL_MODULES in packages/mcp/src/tools/index.ts.
Required invariants — must hold after your changes:
- All four tools ship in the default `full` MCP profile — appear in `list_tools`
- `mandu.loop.close` is pure (readOnlyHint: true) — no I/O, no spawn
- `mandu.deploy.preview` always passes `--dry-run` — cannot trigger real deployments
- `mandu.run.tests` child process timeout: 10 minutes
- `mandu.ai.brief` is read-only — git log is cap'd at 10s, failure → empty recent_changes
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Prompt templates
Mandu's prompt template system — docs/prompts/*.md files loaded via --preset or /preset, plus the provider-specific adapter architecture behind mandu ai.
ai-hint — Prompt presets live at
docs/prompts/<name>.md. Loaded bymandu ai chat --preset=<name>/mandu ai eval --preset=<name>//preset <name>slash command. Name is allow-listed to[a-zA-Z0-9_-]+— path traversal blocked. Adapter layer: each provider maps Mandu-normalized messages → provider-specific SSE format.
Invariants:
- Preset names are allow-listed to
[a-zA-Z0-9_-]+— slashes, dots, tilde rejected at parse - Preset lookup path is exactly
docs/prompts/<name>.mdrelative to the project root - Frontmatter is optional but recommended —
name,version,audience,last_verified - Adapter layer normalizes messages into a provider-agnostic shape before streaming
- System prompt is separate from user messages — never concatenated into the first user turn
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/prompts to my project.
Summary of the page:
Prompt presets live at `docs/prompts/<name>.md`. Loaded by `mandu ai chat --preset=<name>` / `mandu ai eval --preset=<name>` / `/preset <name>` slash command. Name is allow-listed to `[a-zA-Z0-9_-]+` — path traversal blocked. Adapter layer: each provider maps Mandu-normalized messages → provider-specific SSE format.
Required invariants — must hold after your changes:
- Preset names are allow-listed to `[a-zA-Z0-9_-]+` — slashes, dots, tilde rejected at parse
- Preset lookup path is exactly `docs/prompts/<name>.md` relative to the project root
- Frontmatter is optional but recommended — `name`, `version`, `audience`, `last_verified`
- Adapter layer normalizes messages into a provider-agnostic shape before streaming
- System prompt is separate from user messages — never concatenated into the first user turn
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Skills generator
@mandujs/skills auto-generates a per-project skill manifest — the list of domain-specific actions an agent can perform in this repo. Pure, deterministic, safe to run in CI.
ai-hint — Skills generator: scans project source for exported functions matching skill conventions, builds a manifest at
.mandu/skills/manifest.jsonwith { id, source: 'generated'|'static', path?, description? }. Static skills are human-written underskills/. Both surfaced viamandu.ai.briefMCP tool. Generator is pure — no network, no spawn.
Invariants:
- Static skills live under
skills/<id>/index.ts(human-written, versioned) - Generated skills live under
.mandu/skills/generated/<id>.ts(machine-emitted, regenerable) - Manifest is deterministic — identical source tree produces byte-identical
.mandu/skills/manifest.json - Generator is pure — no network, no spawn, only fs reads under project root
- Skills surface in
mandu.ai.briefMCP tool output underskills: Array<{ id, source, path? }>
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/ai/skills-generator to my project.
Summary of the page:
Skills generator: scans project source for exported functions matching skill conventions, builds a manifest at `.mandu/skills/manifest.json` with { id, source: 'generated'|'static', path?, description? }. Static skills are human-written under `skills/`. Both surfaced via `mandu.ai.brief` MCP tool. Generator is pure — no network, no spawn.
Required invariants — must hold after your changes:
- Static skills live under `skills/<id>/index.ts` (human-written, versioned)
- Generated skills live under `.mandu/skills/generated/<id>.ts` (machine-emitted, regenerable)
- Manifest is deterministic — identical source tree produces byte-identical `.mandu/skills/manifest.json`
- Generator is pure — no network, no spawn, only fs reads under project root
- Skills surface in `mandu.ai.brief` MCP tool output under `skills: Array<{ id, source, path? }>`
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Architecture
Client Rendering
"use client" pages, *.island.tsx islands, and partials — when to pick which.
ai-hint — Mandu has three client-render mechanisms: 'use client' page (whole page client-rendered), .island.tsx (partial hydration of a component inside a server page), and partial (even smaller than island, for single buttons). Prefer islands for most interactive UI.
Invariants:
*.island.tsxfiles must live next to apage.tsxand include\"use client\"- Page-level
\"use client\"opts the whole route out of prerender - Partials render server-side for initial HTML then hydrate on demand
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/client-rendering to my project.
Summary of the page:
Mandu has three client-render mechanisms: 'use client' page (whole page client-rendered), .island.tsx (partial hydration of a component inside a server page), and partial (even smaller than island, for single buttons). Prefer islands for most interactive UI.
Required invariants — must hold after your changes:
- `*.island.tsx` files must live next to a `page.tsx` and include `\"use client\"`
- Page-level `\"use client\"` opts the whole route out of prerender
- Partials render server-side for initial HTML then hydrate on demand
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Content Layer
Astro-style build-time content collections with Zod validation and digest caching.
ai-hint — Mandu Content Layer loads Markdown/JSON/YAML/API content at build time into typed collections. Define collections in content.config.ts using glob/file/api loaders + Zod schemas. Query with getCollection() and getEntry().
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/content-layer to my project.
Summary of the page:
Mandu Content Layer loads Markdown/JSON/YAML/API content at build time into typed collections. Define collections in content.config.ts using glob/file/api loaders + Zod schemas. Query with getCollection() and getEntry().
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Decision Memory (ADR)
Persistent architectural memory for agents — save, search, and enforce decisions.
ai-hint — Mandu Decision Memory stores ADRs (Architecture Decision Records) per project. Agents use searchDecisions() to avoid re-relitigating, saveDecision() to record choices, and checkConsistency() to verify implementation matches intent.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/decision-memory to my project.
Summary of the page:
Mandu Decision Memory stores ADRs (Architecture Decision Records) per project. Agents use searchDecisions() to avoid re-relitigating, saveDecision() to record choices, and checkConsistency() to verify implementation matches intent.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Accessing generated content
The official pattern for reading .mandu/generated/ artifacts from user code — runtime registry, not direct imports.
ai-hint — Direct imports from .mandu/generated/ or any /generated/ path are forbidden by the guard rule INVALID_GENERATED_IMPORT. The official access path is the runtime registry: getGenerated(key) / getManifest() / getRouteById(id) / tryGetGenerated(key) exported from @mandujs/core/runtime. The registry is seeded during server boot by registerManifestHandlers() from @mandujs/cli. User code that wants a new generated artifact extends the GeneratedRegistry interface via module augmentation.
Invariants:
- Files under app/, src/, and packages/ cannot import from any path containing /generated/
- The runtime registry is the single source of truth for generated artifacts at runtime
- getGenerated() throws a helpful error with docs link if called before manifest registration
- registerManifestHandlers() (from @mandujs/cli) is the only glue that seeds the routes manifest; third-party artifacts need their own glue
- Barrel re-export workarounds through src/shared/**/index.ts bypass the guard but are not endorsed
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/generated-access to my project.
Summary of the page:
Direct imports from .mandu/generated/ or any /generated/ path are forbidden by the guard rule INVALID_GENERATED_IMPORT. The official access path is the runtime registry: getGenerated(key) / getManifest() / getRouteById(id) / tryGetGenerated(key) exported from @mandujs/core/runtime. The registry is seeded during server boot by registerManifestHandlers() from @mandujs/cli. User code that wants a new generated artifact extends the GeneratedRegistry interface via module augmentation.
Required invariants — must hold after your changes:
- Files under app/, src/, and packages/ cannot import from any path containing /generated/
- The runtime registry is the single source of truth for generated artifacts at runtime
- getGenerated() throws a helpful error with docs link if called before manifest registration
- registerManifestHandlers() (from @mandujs/cli) is the only glue that seeds the routes manifest; third-party artifacts need their own glue
- Barrel re-export workarounds through src/shared/**/index.ts bypass the guard but are not endorsed
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Guard
The invariant layer — every rule Mandu's static analyzer enforces, what each one means, and how to fix the violation.
ai-hint — Guard is a build-time static analyzer. It enforces six invariant families: layer-violation, circular-dependency, cross-slice, deep-nesting, file-type, invalid-shared-segment. It also runs FS-routes rules (page/layout/route import whitelists) and contract rules (slot/contract exports). Severity is configurable; 'error' fails the build. Each violation carries filePath, line, column, ruleName, ruleDescription, suggestions.
Invariants:
- Any .js or .jsx file in a scanned layer is a file-type violation
- src/shared is restricted to {contracts, schema, types, utils/client, utils/server, env}; other segments are invalid-shared-segment
- Client-layer files cannot import shared/env (server-only)
- page.tsx cannot import another page.tsx (noPageToPage rule)
- Layer imports must appear in the importing layer's canImport list
- Cross-slice imports within the same layer are rejected unless via a public API
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/guard to my project.
Summary of the page:
Guard is a build-time static analyzer. It enforces six invariant families: layer-violation, circular-dependency, cross-slice, deep-nesting, file-type, invalid-shared-segment. It also runs FS-routes rules (page/layout/route import whitelists) and contract rules (slot/contract exports). Severity is configurable; 'error' fails the build. Each violation carries filePath, line, column, ruleName, ruleDescription, suggestions.
Required invariants — must hold after your changes:
- Any .js or .jsx file in a scanned layer is a file-type violation
- src/shared is restricted to {contracts, schema, types, utils/client, utils/server, env}; other segments are invalid-shared-segment
- Client-layer files cannot import shared/env (server-only)
- page.tsx cannot import another page.tsx (noPageToPage rule)
- Layer imports must appear in the importing layer's canImport list
- Cross-slice imports within the same layer are rejected unless via a public API
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Architect
The three pillars — Prerender, Island, Guard — that shape every Mandu app.
ai-hint — Architect category hub. Three pillars: Prerender (default), Island (opt-in), Guard (invariants).
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/index to my project.
Summary of the page:
Architect category hub. Three pillars: Prerender (default), Island (opt-in), Guard (invariants).
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Island
The .island.tsx contract — the single signal Mandu uses to decide what ships to the browser, and the mismatch guards that keep hydration honest.
ai-hint — Islands are the opt-in client boundary. The .island.tsx suffix and 'use client' directive together form the contract. The FS scanner picks the first island co-located with a page as that route's clientModule; otherwise a page with 'use client' is its own clientModule. A known SSR-shell pattern triggers a hydration-shell-mismatch error at scan time.
Invariants:
- Any file named *.island.tsx is a client-bundled module
- An island must start with the 'use client' directive
- For a page route, the co-located island (if any) becomes clientModule; otherwise a page with 'use client' is its own clientModule
- A page that imports an island and also renders {typeof X !== 'undefined' && null} against it is rejected at scan time as a hydration-shell-mismatch-risk
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/island to my project.
Summary of the page:
Islands are the opt-in client boundary. The .island.tsx suffix and 'use client' directive together form the contract. The FS scanner picks the first island co-located with a page as that route's clientModule; otherwise a page with 'use client' is its own clientModule. A known SSR-shell pattern triggers a hydration-shell-mismatch error at scan time.
Required invariants — must hold after your changes:
- Any file named *.island.tsx is a client-bundled module
- An island must start with the 'use client' directive
- For a page route, the co-located island (if any) becomes clientModule; otherwise a page with 'use client' is its own clientModule
- A page that imports an island and also renders {typeof X !== 'undefined' && null} against it is rejected at scan time as a hydration-shell-mismatch-risk
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Architecture Overview
The three pillars that keep Mandu apps — and the agents writing them — from drifting.
ai-hint — Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer.
Invariants:
- Every route is prerendered unless it explicitly opts into dynamic rendering
- Interactivity is opt-in via .island.tsx files; nothing ships to the client by default
- Guard rules are enforced at build time and fail the build on violation
- src/server/** never crosses the client boundary
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/overview to my project.
Summary of the page:
Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer.
Required invariants — must hold after your changes:
- Every route is prerendered unless it explicitly opts into dynamic rendering
- Interactivity is opt-in via .island.tsx files; nothing ships to the client by default
- Guard rules are enforced at build time and fail the build on violation
- src/server/** never crosses the client boundary
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Prerender
Static-by-default rendering — how Mandu turns your routes into HTML at build time, and the tiny lever that opts out.
ai-hint — Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths.
Invariants:
- A page route with no dynamic segment is always prerendered
- A dynamic page route is prerendered only for params returned by generateStaticParams()
- Output lives under .mandu/static/
/index.html (clean URLs) - generateStaticParams() must return an array; non-array returns are warned and skipped
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/prerender to my project.
Summary of the page:
Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths.
Required invariants — must hold after your changes:
- A page route with no dynamic segment is always prerendered
- A dynamic page route is prerendered only for params returned by generateStaticParams()
- Output lives under .mandu/static/<pathname>/index.html (clean URLs)
- generateStaticParams() must return an array; non-array returns are warned and skipped
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Rendering Modes
Mandu blends eight rendering strategies — here's when to use each.
ai-hint — Mandu supports 8 rendering modes. Pages default to prerender + island hydration. Use SSR for user-specific data, streaming SSR for slow pages with Suspense, 'use client' for fully interactive pages, partials for fine-grained hydration.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/rendering-modes to my project.
Summary of the page:
Mandu supports 8 rendering modes. Pages default to prerender + island hydration. Use SSR for user-specific data, streaming SSR for slow pages with Suspense, 'use client' for fully interactive pages, partials for fine-grained hydration.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Smooth Navigation
CSS View Transitions + hover-based link prefetch, auto-injected into every SSR response. Closes the perceived-latency gap against Next.js / Astro / SvelteKit without requiring a client-side SPA runtime.
ai-hint — Smooth navigation adds two things to every SSR response: (1)
<style>@view-transition{navigation:auto}</style>for supported browsers, and (2) a ~500-byte inline hover-prefetch IIFE that injects<link rel=prefetch as=document>on same-origin anchor hover. Opt out globally viatransitions: false/prefetch: falseinmandu.config.ts; per-link viadata-no-prefetch.
Invariants:
- Both
transitionsandprefetchdefault to enabled (true) - View Transitions is a CSS at-rule — zero JS runtime cost, ~70 bytes HTML overhead
- Hover prefetch is a ~500-byte inline IIFE in
<head>— CSPunsafe-inlineor a future nonce required for strict CSP - Prefetch helper only acts on same-origin anchors (
href^='/') that are not<a download>, nottarget=_blank, and not markeddata-no-prefetch - Prefetch ≠ SPA navigation — click still triggers a full document reload; use
<Link>from@mandujs/core/clientfor SPA-style routing
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/smooth-navigation to my project.
Summary of the page:
Smooth navigation adds two things to every SSR response: (1) `<style>@view-transition{navigation:auto}</style>` for supported browsers, and (2) a ~500-byte inline hover-prefetch IIFE that injects `<link rel=prefetch as=document>` on same-origin anchor hover. Opt out globally via `transitions: false` / `prefetch: false` in `mandu.config.ts`; per-link via `data-no-prefetch`.
Required invariants — must hold after your changes:
- Both `transitions` and `prefetch` default to enabled (`true`)
- View Transitions is a CSS at-rule — zero JS runtime cost, ~70 bytes HTML overhead
- Hover prefetch is a ~500-byte inline IIFE in `<head>` — CSP `unsafe-inline` or a future nonce required for strict CSP
- Prefetch helper only acts on same-origin anchors (`href^='/'`) that are not `<a download>`, not `target=_blank`, and not marked `data-no-prefetch`
- Prefetch ≠ SPA navigation — click still triggers a full document reload; use `<Link>` from `@mandujs/core/client` for SPA-style routing
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
SSR & Streaming SSR
Render on the server per request — including chunked Suspense streams.
ai-hint — Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/ssr to my project.
Summary of the page:
Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Transactions & Rollback
Atomic code changes — beginChange, commitChange, rollbackChange.
ai-hint — Mandu transactions (beginChange/commitChange/rollbackChange) wrap agent edits in atomic snapshots. Begin takes a snapshot, edits happen, commit locks it in or rollback restores. Pairs with ATE/Guard for safe LLM-driven changes.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/transaction to my project.
Summary of the page:
Mandu transactions (beginChange/commitChange/rollbackChange) wrap agent edits in atomic snapshots. Begin takes a snapshot, edits happen, commit locks it in or rollback restores. Pairs with ATE/Guard for safe LLM-driven changes.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
benchmarks
Mandu vs Hono
When the API layer is everything — Hono's router-as-library vs Mandu's full fullstack framework with built-in agent surface.
ai-hint — Hono is a tiny edge-first router; Mandu is a fullstack framework that includes routing, React UI, build tooling, runtime guard, and 100+ MCP tools. Pick Hono if you only need an API; pick Mandu if you also want UI, contracts, and agent-driven scaffolding.
I have a Hono API service and I'm evaluating moving it under Mandu (https://mandujs.com) so I can add a React UI in the same repo.
1. Pick 3 representative routes from my Hono app.
2. For each, show me the Mandu equivalent — contract file (`spec/contracts/<NAME>.contract.ts`) + handler (`app/api/<NAME>/route.ts` using `Mandu.handler(contract, ...)`).
3. Map Hono middleware to Mandu's filling chain `.use(...)` middleware.
4. Identify any Hono-specific features I lose (specific edge adapters, bundler tricks).
5. Estimate the percentage of routes that port cleanly vs need rewriting.
Mandu vs Next.js
An honest, agent-first comparison — file-system routing, type-safe APIs, runtime guard, and AI-editor workflows on Bun + React.
ai-hint — Mandu replaces Next.js for projects where AI agents drive most of the code. Same file-system route convention (
app/**/page.tsx) but Mandu adds runtime architecture guard, contract-first APIs (one zod schema → types + validation + OpenAPI), 100+ MCP tools for direct agent control, and Bun-native runtime. Use Next.js if you want maximum ecosystem; use Mandu if you want agent-safe code at scale.
I have a Next.js app and I'm evaluating Mandu (https://mandujs.com).
1. Pick a representative route (an API + a page) from my codebase.
2. Show me what the same route would look like as Mandu — `Mandu.filling()` chain or `Mandu.contract({...})` + `Mandu.handler(contract, ...)` — using the exact paths and types my Next route already uses.
3. Highlight which lines disappear (CSRF, manual zod parse, status code branching) because Mandu derives them from the contract.
4. Estimate how much of the codebase would migrate cleanly vs need rewriting (give me a percentage with reasoning).
5. List the top 3 risks of switching for my project specifically.
Mandu vs Remix
Two routing-first React frameworks compared — loaders/actions vs filling chain, web standards vs MCP-driven agents.
ai-hint — Mandu and Remix both lean on web standards and file-system routing, but Mandu adds runtime architecture guard, contract-first APIs, and 100+ MCP tools so AI editors can drive the codebase. Remix focuses on progressive enhancement and tight loader/action coupling; Mandu focuses on agent-safe scaffolding and explicit contracts.
I have a Remix v2 app and I'm evaluating Mandu (https://mandujs.com).
1. Pick one of my routes that has a loader + action + UI.
2. Show me the Mandu shape — contract file (`spec/contracts/<NAME>.contract.ts`), API route (`app/api/<NAME>/route.ts` using Mandu.handler), and UI page that consumes the typed `client(contract)` wrapper.
3. Identify which Remix-specific patterns disappear (loader return types, action `_action` discriminator, fetcher data) and which require manual replacement (progressive enhancement, useFetcher state).
4. Estimate the percentage of routes that would port cleanly vs need rewriting.
5. List the top 3 risks of switching for my project.
Build with Agents
ATE — Agent Test Engine
Extract → Generate → Run → Report → Heal. A six-stage pipeline that turns the interaction graph of your app into runnable Playwright specs and suggests diffs when they fail.
ai-hint — ATE is @mandujs/ate. Pipeline: ateExtract → ateGenerate → ateRun → ateReport → ateHeal with ateImpact for subset selection. CLI: bunx mandu test:auto. MCP: mandu.ate.* tools plus mandu.ate.auto_pipeline. Oracle levels L0-L3 gate assertion depth.
Invariants:
- Every stage writes to .mandu/ate or .mandu/reports and can be resumed by runId
- ate.heal produces diff suggestions only; it never auto-commits code
- ate.apply_heal always creates a snapshot via the change system before writing
- oracleLevel determines assertion depth: L0 smoke, L1 HTTP, L2 contract, L3 behavioral
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/ate to my project.
Summary of the page:
ATE is @mandujs/ate. Pipeline: ateExtract → ateGenerate → ateRun → ateReport → ateHeal with ateImpact for subset selection. CLI: bunx mandu test:auto. MCP: mandu.ate.* tools plus mandu.ate.auto_pipeline. Oracle levels L0-L3 gate assertion depth.
Required invariants — must hold after your changes:
- Every stage writes to .mandu/ate or .mandu/reports and can be resumed by runId
- ate.heal produces diff suggestions only; it never auto-commits code
- ate.apply_heal always creates a snapshot via the change system before writing
- oracleLevel determines assertion depth: L0 smoke, L1 HTTP, L2 contract, L3 behavioral
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Change — snapshot transactions
begin → commit or rollback. Atomic, snapshot-backed edits so agents can experiment without leaving the repo in a half-written state.
ai-hint — The Change system is in @mandujs/core/change. CLI: bunx mandu change begin|commit|rollback|status|list|prune. MCP: mandu.tx.begin|commit|rollback|status. State lives under .mandu/history with snapshots, changes.json, and a single active.json lock.
Invariants:
- Only one transaction can be active at a time per project (enforced via .mandu/history/active.json)
- Every begin takes a full snapshot of the manifest and spec/slots before any write
- rollback restores files from the captured snapshot; commit only marks the record committed
- Change IDs use YYYYMMDD-HHmmss-xxx and never collide with snapshot IDs
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/change to my project.
Summary of the page:
The Change system is in @mandujs/core/change. CLI: bunx mandu change begin|commit|rollback|status|list|prune. MCP: mandu.tx.begin|commit|rollback|status. State lives under .mandu/history with snapshots, changes.json, and a single active.json lock.
Required invariants — must hold after your changes:
- Only one transaction can be active at a time per project (enforced via .mandu/history/active.json)
- Every begin takes a full snapshot of the manifest and spec/slots before any write
- rollback restores files from the captured snapshot; commit only marks the record committed
- Change IDs use YYYYMMDD-HHmmss-xxx and never collide with snapshot IDs
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Build with Agents
Guards, MCP, and ATE — the tools that keep LLM-written code honest.
ai-hint — Build-with-agents hub. Covers MCP setup, Guard authoring, ATE agent test engine.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/index to my project.
Summary of the page:
Build-with-agents hub. Covers MCP setup, Guard authoring, ATE agent test engine.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
MCP Setup
Connect Claude Code, Cursor, and Gemini CLI to the Mandu MCP server so agents can negotiate, scaffold, and guard without ad-hoc shell commands.
ai-hint — The Mandu MCP server ships as the bin mandu-mcp from @mandujs/mcp. Client JSON config points at command bunx, args [mandu-mcp]. Tools are namespaced mandu.
. and filtered by the MANDU_MCP_PROFILE env (minimal|standard|full).
Invariants:
- The MCP server is launched as bunx mandu-mcp, never via a Python shim named mcp
- Tool names use dot-notation under the mandu prefix (for example mandu.negotiate, mandu.generate, mandu.guard.check)
- MANDU_MCP_PROFILE gates which tool categories are registered at startup
- mandu init writes .mcp.json, .claude.json, and .gemini/settings.json atomically with backups
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/mcp-setup to my project.
Summary of the page:
The Mandu MCP server ships as the bin mandu-mcp from @mandujs/mcp. Client JSON config points at command bunx, args [mandu-mcp]. Tools are namespaced mandu.<category>.<verb> and filtered by the MANDU_MCP_PROFILE env (minimal|standard|full).
Required invariants — must hold after your changes:
- The MCP server is launched as bunx mandu-mcp, never via a Python shim named mcp
- Tool names use dot-notation under the mandu prefix (for example mandu.negotiate, mandu.generate, mandu.guard.check)
- MANDU_MCP_PROFILE gates which tool categories are registered at startup
- mandu init writes .mcp.json, .claude.json, and .gemini/settings.json atomically with backups
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Architecture Negotiation
Agents propose structure first, get approval, then scaffold — no cowboy coding.
ai-hint — Mandu Negotiation is a pre-implementation dialog. Call mandu.negotiate with {intent, requirements, constraints} → framework returns a structured plan {files, layers, decisions} → if approved, mandu.negotiate.scaffold materializes it. RFC-001.
Invariants:
- Every new feature an agent adds must pass
mandu.negotiatebefore any files are written - The scaffold step respects the Guard preset (layers, naming, imports)
- A rejected plan is NEVER partially applied
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/negotiation to my project.
Summary of the page:
Mandu Negotiation is a pre-implementation dialog. Call mandu.negotiate with {intent, requirements, constraints} → framework returns a structured plan {files, layers, decisions} → if approved, mandu.negotiate.scaffold materializes it. RFC-001.
Required invariants — must hold after your changes:
- Every new feature an agent adds must pass `mandu.negotiate` before any files are written
- The scaffold step respects the Guard preset (layers, naming, imports)
- A rejected plan is NEVER partially applied
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
CLI
mandu db seed
Replay deterministic fixture data against the live database. Declarative or imperative seed files, parameter-bound SQL with quoteIdent() injection defense, idempotent upsert + tamper detection.
ai-hint —
mandu db seedrunsspec/seeds/*.seed.tsin lexicographic order. Declarative ={ resource, key, data }; imperative =async function seed(ctx) { ... }. SQL injection blocked via quoteIdent() + parameter binding. Prod deploys require MANDU_DB_SEED_PROD_CONFIRM=yes.
Invariants:
- Every seed identifier flows through
quoteIdent()— SQL injection blocked at the driver layer - Every value is parameter-bound via
Bun.SQL— never concatenated - Rows validated against the resource's Zod schema BEFORE any INSERT
- Each seed file runs inside a transaction — any failure rolls the whole file back
- Tamper detection: if a previously-applied file's bytes change, the runner refuses to replay until
--reset --env=prodrequiresMANDU_DB_SEED_PROD_CONFIRM=yesin the environment
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/cli/db-seed to my project.
Summary of the page:
`mandu db seed` runs `spec/seeds/*.seed.ts` in lexicographic order. Declarative = `{ resource, key, data }`; imperative = `async function seed(ctx) { ... }`. SQL injection blocked via quoteIdent() + parameter binding. Prod deploys require MANDU_DB_SEED_PROD_CONFIRM=yes.
Required invariants — must hold after your changes:
- Every seed identifier flows through `quoteIdent()` — SQL injection blocked at the driver layer
- Every value is parameter-bound via `Bun.SQL` — never concatenated
- Rows validated against the resource's Zod schema BEFORE any INSERT
- Each seed file runs inside a transaction — any failure rolls the whole file back
- Tamper detection: if a previously-applied file's bytes change, the runner refuses to replay until `--reset`
- `--env=prod` requires `MANDU_DB_SEED_PROD_CONFIRM=yes` in the environment
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
mandu mcp register
Automatic IDE integration — writes a Mandu MCP server entry into Claude Code, Cursor, Continue, or Aider without touching sibling configuration. Merge-safe, tolerant of JSON-C, preserves YAML preamble.
ai-hint —
mandu mcp registermergesmcpServers.manduinto ~/.claude/mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor), ~/.continue/config.json (Continue),/.aider.conf.yml (Aider). Never overwrites sibling keys. Stashes .bak. before every write. Token defaults to ${localEnv:MANDU_MCP_TOKEN} placeholder.
Invariants:
- Four IDEs: claude, cursor, continue, aider —
--ide=allcovers them all - JSON-C comments (Claude/Cursor/Continue) are tolerated on read; stripped on write with a warning
- YAML (Aider) merge preserves preamble and trailing keys verbatim
- Every modified file gets a
.bak.<unix-ms>sibling — atomic rename semantics - Tokens default to
${localEnv:MANDU_MCP_TOKEN}placeholder — never embedded inline unless explicitly requested
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/cli/mcp-register to my project.
Summary of the page:
`mandu mcp register` merges `mcpServers.mandu` into ~/.claude/mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor), ~/.continue/config.json (Continue), <cwd>/.aider.conf.yml (Aider). Never overwrites sibling keys. Stashes <config>.bak.<ms> before every write. Token defaults to ${localEnv:MANDU_MCP_TOKEN} placeholder.
Required invariants — must hold after your changes:
- Four IDEs: claude, cursor, continue, aider — `--ide=all` covers them all
- JSON-C comments (Claude/Cursor/Continue) are tolerated on read; stripped on write with a warning
- YAML (Aider) merge preserves preamble and trailing keys verbatim
- Every modified file gets a `.bak.<unix-ms>` sibling — atomic rename semantics
- Tokens default to `${localEnv:MANDU_MCP_TOKEN}` placeholder — never embedded inline unless explicitly requested
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
mandu upgrade
Self-updater for both the npm-installed @mandujs/* packages and the standalone compile-binary builds. SHA-256 verified, atomic swap, --rollback capable.
ai-hint —
mandu upgradeauto-detects package vs binary mode via process.execPath. Binary mode: downloads, verifies SHA256 from SHA256SUMS.txt, atomically swaps (rename on POSIX; rename-then-replace on Windows). Stashes the old binary in ~/.mandu/bin/previous/ so --rollback works. SLSA L2 provenance attached.
Invariants:
- Package mode (
bun run mandu): runsbun update @mandujs/* - Binary mode (
mandu.exe/mandu-bun-linux-x64): downloads + verifies + swaps - SHA-256 verified against
SHA256SUMS.txtreleased alongside each binary - Windows: rename running
.exeto.old.<pid>.exefirst (allowed while loaded), then move new binary into place - Previous binary stashed in
~/.mandu/bin/previous/—--rollbackrestores it - SLSA Build L2 provenance is attached via
actions/attest-build-provenance
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/cli/upgrade to my project.
Summary of the page:
`mandu upgrade` auto-detects package vs binary mode via process.execPath. Binary mode: downloads, verifies SHA256 from SHA256SUMS.txt, atomically swaps (rename on POSIX; rename-then-replace on Windows). Stashes the old binary in ~/.mandu/bin/previous/ so --rollback works. SLSA L2 provenance attached.
Required invariants — must hold after your changes:
- Package mode (`bun run mandu`): runs `bun update @mandujs/*`
- Binary mode (`mandu.exe` / `mandu-bun-linux-x64`): downloads + verifies + swaps
- SHA-256 verified against `SHA256SUMS.txt` released alongside each binary
- Windows: rename running `.exe` to `.old.<pid>.exe` first (allowed while loaded), then move new binary into place
- Previous binary stashed in `~/.mandu/bin/previous/` — `--rollback` restores it
- SLSA Build L2 provenance is attached via `actions/attest-build-provenance`
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Deploy
Cloudflare Pages
mandu deploy --target=cf-pages emits a wrangler.toml + a functions/_middleware.ts glue file. Artifact-only in Phase 13 — runtime compatibility via the Phase 15 Workers adapter.
ai-hint — cf-pages is the Pages+Functions variant (as distinct from
@mandujs/edge/workerswhich is the dedicated Workers adapter). Emits wrangler.toml + functions/_middleware.ts routing every request to the Worker handler. Runtime compat via Phase 15's Bun.CookieMap → LegacyCookieCodec polyfills.
Invariants:
- Distinct from
@mandujs/edge/workers— cf-pages uses Pages Functions (per-project), Workers adapter uses standalone Worker - Required secret:
CLOUDFLARE_API_TOKEN— stored in Bun.secrets - Minimum wrangler version: 3.0.0
- App bindings (KV, R2, D1) are declared in wrangler.toml — secret values go via
wrangler pages secret put - Runtime compatibility lands in Phase 15 via @mandujs/edge — some Bun APIs currently shim slowly
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/cf-pages to my project.
Summary of the page:
cf-pages is the Pages+Functions variant (as distinct from `@mandujs/edge/workers` which is the dedicated Workers adapter). Emits wrangler.toml + functions/_middleware.ts routing every request to the Worker handler. Runtime compat via Phase 15's Bun.CookieMap → LegacyCookieCodec polyfills.
Required invariants — must hold after your changes:
- Distinct from `@mandujs/edge/workers` — cf-pages uses Pages Functions (per-project), Workers adapter uses standalone Worker
- Required secret: `CLOUDFLARE_API_TOKEN` — stored in Bun.secrets
- Minimum wrangler version: 3.0.0
- App bindings (KV, R2, D1) are declared in wrangler.toml — secret values go via `wrangler pages secret put`
- Runtime compatibility lands in Phase 15 via @mandujs/edge — some Bun APIs currently shim slowly
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Docker
Emit a multi-stage Dockerfile + .dockerignore tuned for Bun + Mandu. Artifact-only — users run docker build / docker push themselves.
ai-hint —
mandu deploy --target=dockeremits a Dockerfile usingoven/bunas the build and runtime base. Multi-stage: deps → build → runtime. Runtime stage usesUSER bun(non-root) and exposes port 3333..dockerignoreexcludes.mandu/,node_modules/,coverage/.
Invariants:
mandu deploy --target=dockerNEVER invokesdocker build— users run it themselves- Runtime stage runs as
USER bun(non-root) — avoid running as root - Default port is 3333 — override with
PORTenv at runtime - Build cache is optimized via layered COPY:
package.json+bun.lockfirst, source last
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/docker to my project.
Summary of the page:
`mandu deploy --target=docker` emits a Dockerfile using `oven/bun` as the build and runtime base. Multi-stage: deps → build → runtime. Runtime stage uses `USER bun` (non-root) and exposes port 3333. `.dockerignore` excludes `.mandu/`, `node_modules/`, `coverage/`.
Required invariants — must hold after your changes:
- `mandu deploy --target=docker` NEVER invokes `docker build` — users run it themselves
- Runtime stage runs as `USER bun` (non-root) — avoid running as root
- Default port is 3333 — override with `PORT` env at runtime
- Build cache is optimized via layered COPY: `package.json` + `bun.lock` first, source last
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Docker Compose
Multi-service scaffold — Mandu app + Postgres sidecar + .env.example. Ideal for local reproducible dev environments and single-host self-hosting.
ai-hint —
mandu deploy --target=docker-composeemits Dockerfile + docker-compose.yml + .env.example. The compose file scaffolds a Postgres service by default (Phase 4c DB integration). Runtime service uses the same non-root Dockerfile as --target=docker.
Invariants:
- Postgres sidecar is scaffolded by default; disable with
--no-db - Services communicate over a private bridge network (
internalby default) .env.exampleis committed;.envis gitignored (compose reads.envautomatically)- No volumes for app code — only for database data (
postgres_data:/var/lib/postgresql/data)
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/docker-compose to my project.
Summary of the page:
`mandu deploy --target=docker-compose` emits Dockerfile + docker-compose.yml + .env.example. The compose file scaffolds a Postgres service by default (Phase 4c DB integration). Runtime service uses the same non-root Dockerfile as --target=docker.
Required invariants — must hold after your changes:
- Postgres sidecar is scaffolded by default; disable with `--no-db`
- Services communicate over a private bridge network (`internal` by default)
- `.env.example` is committed; `.env` is gitignored (compose reads `.env` automatically)
- No volumes for app code — only for database data (`postgres_data:/var/lib/postgresql/data`)
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Fly.io
mandu deploy --target=fly emits a fly.toml + Dockerfile pair. With --execute + flyctl ≥ 0.1.0 installed, Mandu invokes fly deploy on your behalf.
ai-hint — Fly adapter: uses the same non-root Dockerfile as Docker adapter, plus a fly.toml tuned for Mandu (port 3333, auto-stop, auto-start). Secret:
FLY_API_TOKEN— read from OS keychain, never passed on argv.--executerequires flyctl.
Invariants:
- Uses the same multi-stage Dockerfile as
--target=docker - Required secret:
FLY_API_TOKEN— stored in Bun.secrets / OS keychain - Minimum flyctl version: 0.1.0
internal_port = 3333,auto_stop_machines = true,auto_start_machines = trueby default- Multi-region: default primary region from flyctl login — override with
primary_region
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/fly to my project.
Summary of the page:
Fly adapter: uses the same non-root Dockerfile as Docker adapter, plus a fly.toml tuned for Mandu (port 3333, auto-stop, auto-start). Secret: `FLY_API_TOKEN` — read from OS keychain, never passed on argv. `--execute` requires flyctl.
Required invariants — must hold after your changes:
- Uses the same multi-stage Dockerfile as `--target=docker`
- Required secret: `FLY_API_TOKEN` — stored in Bun.secrets / OS keychain
- Minimum flyctl version: 0.1.0
- `internal_port = 3333`, `auto_stop_machines = true`, `auto_start_machines = true` by default
- Multi-region: default primary region from flyctl login — override with `primary_region`
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Deploy
mandu deploy turns a built Mandu project into the config files + pipeline each hosting provider expects. Seven adapters, one CLI surface, zero secrets-in-artifacts.
ai-hint —
mandu deploy --target=<adapter>runs validate → guard → build → check → prepare → (optional) deploy. Secrets are stored inBun.secrets(OS keychain), never written to artifact files. 7 adapters: docker, docker-compose, fly, vercel, railway, netlify, cf-pages.
Invariants:
--target=<name>is required — unsupported values return CLI_E200- Secrets NEVER touch artifact files — writeArtifact throws SecretLeakError (CLI_E210) if a value appears verbatim
- Secrets NEVER get logged — all formatters route through
maskSecret()→ constant\"****\" --executeis required for adapter.deploy() to run; without it, prepare-only- Adapter
check()only probes provider CLIs when--executeis passed — clean CI runners still get through--dry-run
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/index to my project.
Summary of the page:
`mandu deploy --target=<adapter>` runs validate → guard → build → check → prepare → (optional) deploy. Secrets are stored in `Bun.secrets` (OS keychain), never written to artifact files. 7 adapters: docker, docker-compose, fly, vercel, railway, netlify, cf-pages.
Required invariants — must hold after your changes:
- `--target=<name>` is required — unsupported values return CLI_E200
- Secrets NEVER touch artifact files — writeArtifact throws SecretLeakError (CLI_E210) if a value appears verbatim
- Secrets NEVER get logged — all formatters route through `maskSecret()` → constant `\"****\"`
- `--execute` is required for adapter.deploy() to run; without it, prepare-only
- Adapter `check()` only probes provider CLIs when `--execute` is passed — clean CI runners still get through `--dry-run`
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Netlify
mandu deploy --target=netlify emits a netlify.toml + a Node SSR function at netlify/functions/ssr.ts. With --execute and netlify ≥ 17.0.0 installed, Mandu triggers a deploy.
ai-hint — Netlify adapter: emits netlify.toml with a redirect to Node Functions at netlify/functions/ssr.ts. Static assets from
public/; every other request → SSR function. Edge Functions adapter deferred to Phase 15.3. Secret:NETLIFY_AUTH_TOKEN.
Invariants:
- Runtime is Node.js Functions (nodejs20.x) — not Edge Functions in Phase 13.1
- Static routes served from
public/; dynamic requests go throughnetlify/functions/ssr.ts - Required secret:
NETLIFY_AUTH_TOKEN— stored in Bun.secrets - Minimum netlify CLI version: 17.0.0
- Environment variables are set via
netlify env:set— the emitted netlify.toml never contains secret values
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/netlify to my project.
Summary of the page:
Netlify adapter: emits netlify.toml with a redirect to Node Functions at netlify/functions/ssr.ts. Static assets from `public/`; every other request → SSR function. Edge Functions adapter deferred to Phase 15.3. Secret: `NETLIFY_AUTH_TOKEN`.
Required invariants — must hold after your changes:
- Runtime is Node.js Functions (nodejs20.x) — not Edge Functions in Phase 13.1
- Static routes served from `public/`; dynamic requests go through `netlify/functions/ssr.ts`
- Required secret: `NETLIFY_AUTH_TOKEN` — stored in Bun.secrets
- Minimum netlify CLI version: 17.0.0
- Environment variables are set via `netlify env:set` — the emitted netlify.toml never contains secret values
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Railway
mandu deploy --target=railway emits a railway.json + nixpacks.toml pair tuned for Bun + Mandu. With --execute and railway ≥ 3.0.0 installed, Mandu triggers a deploy.
ai-hint — Railway adapter emits railway.json (metadata) + nixpacks.toml (build recipe). Nixpacks installs Bun, runs
bun install --frozen-lockfile, thenbun run build. Start command isbun run start. Secret:RAILWAY_TOKEN.
Invariants:
- Uses Nixpacks (Railway's default builder) — not a custom Dockerfile
- Nixpacks installs Bun during the build step — no pre-installed runtime assumed
- Required secret:
RAILWAY_TOKEN— stored in Bun.secrets - Minimum railway CLI version: 3.0.0
- App env vars are set via
railway variables set, not--set-secret
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/railway to my project.
Summary of the page:
Railway adapter emits railway.json (metadata) + nixpacks.toml (build recipe). Nixpacks installs Bun, runs `bun install --frozen-lockfile`, then `bun run build`. Start command is `bun run start`. Secret: `RAILWAY_TOKEN`.
Required invariants — must hold after your changes:
- Uses Nixpacks (Railway's default builder) — not a custom Dockerfile
- Nixpacks installs Bun during the build step — no pre-installed runtime assumed
- Required secret: `RAILWAY_TOKEN` — stored in Bun.secrets
- Minimum railway CLI version: 3.0.0
- App env vars are set via `railway variables set`, not `--set-secret`
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Vercel
mandu deploy --target=vercel emits a vercel.json + a Node SSR function at api/_mandu.ts. With --execute and vercel ≥ 28.0.0 installed, Mandu triggers a deploy.
ai-hint — Vercel adapter: emits vercel.json with a rewrite to the Node SSR function at api/_mandu.ts. Runtime: Node 20 (Vercel's Bun support is not GA yet in Phase 13.1). Static routes served from
public/; dynamic routes through the SSR function.VERCEL_TOKENis the Mandu-side secret.
Invariants:
- Runtime is Node.js 20 — Bun-specific APIs that are Node-incompatible will fail at runtime (see edge adapter for Workers path)
- Static routes served from
public/; every dynamic request rewrites to/api/_mandu - Required secret:
VERCEL_TOKEN— stored in Bun.secrets - Minimum vercel CLI version: 28.0.0
- Environment variables must be set via
vercel env add— the emitted vercel.json never contains secret values
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/vercel to my project.
Summary of the page:
Vercel adapter: emits vercel.json with a rewrite to the Node SSR function at api/_mandu.ts. Runtime: Node 20 (Vercel's Bun support is not GA yet in Phase 13.1). Static routes served from `public/`; dynamic routes through the SSR function. `VERCEL_TOKEN` is the Mandu-side secret.
Required invariants — must hold after your changes:
- Runtime is Node.js 20 — Bun-specific APIs that are Node-incompatible will fail at runtime (see edge adapter for Workers path)
- Static routes served from `public/`; every dynamic request rewrites to `/api/_mandu`
- Required secret: `VERCEL_TOKEN` — stored in Bun.secrets
- Minimum vercel CLI version: 28.0.0
- Environment variables must be set via `vercel env add` — the emitted vercel.json never contains secret values
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Edge
Cloudflare Workers
Ship a Mandu app to Cloudflare Workers via @mandujs/edge/workers. mandu build --target=workers emits a bundled Worker + wrangler.toml. Bindings (KV, R2, D1) via AsyncLocalStorage-backed accessors.
ai-hint — Cloudflare Workers adapter:
mandu build --target=workersemits.mandu/workers/worker.js+wrangler.toml. Bindings accessed viagetWorkersEnv()/getWorkersCtx()— AsyncLocalStorage (requiresnodejs_alscompat flag) with per-Request WeakMap fallback. Uncaught 500s return structured JSON withcorrelationId,runtime: 'workers'.
Invariants:
- Workers runtime is V8 isolates — 1–5ms cold start, 10ms CPU cap on free tier
mandu build --target=workersemits.mandu/workers/worker.js(bundled) +wrangler.toml(only if absent)- Bindings require
compatibility_flags = [\"nodejs_als\"]in wrangler.toml for cross-waitUntilaccess - Without nodejs_als,
getWorkersEnv()/getWorkersCtx()fall back to per-Request WeakMap (fetch-time only) - Uncaught 500s return JSON with
correlationId,runtime: 'workers', generic message in production - Stack traces /
causeare NEVER in the HTTP body — logged viaconsole.errorfor Logpush
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/edge/cloudflare-workers to my project.
Summary of the page:
Cloudflare Workers adapter: `mandu build --target=workers` emits `.mandu/workers/worker.js` + `wrangler.toml`. Bindings accessed via `getWorkersEnv()` / `getWorkersCtx()` — AsyncLocalStorage (requires `nodejs_als` compat flag) with per-Request WeakMap fallback. Uncaught 500s return structured JSON with `correlationId`, `runtime: 'workers'`.
Required invariants — must hold after your changes:
- Workers runtime is V8 isolates — 1–5ms cold start, 10ms CPU cap on free tier
- `mandu build --target=workers` emits `.mandu/workers/worker.js` (bundled) + `wrangler.toml` (only if absent)
- Bindings require `compatibility_flags = [\"nodejs_als\"]` in wrangler.toml for cross-`waitUntil` access
- Without nodejs_als, `getWorkersEnv()` / `getWorkersCtx()` fall back to per-Request WeakMap (fetch-time only)
- Uncaught 500s return JSON with `correlationId`, `runtime: 'workers'`, generic message in production
- Stack traces / `cause` are NEVER in the HTTP body — logged via `console.error` for Logpush
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Deno / Vercel Edge / Netlify Edge
Roadmap for Phase 15.2+ adapters in @mandujs/edge. Deno Deploy, Vercel Edge Functions, and Netlify Edge Functions are scaffolded stubs — file structure in place, implementations coming.
ai-hint — Deno/Vercel/Netlify Edge adapters: currently scaffolded stubs under
packages/edge/src/{deno,vercel,netlify}/. Phase 15.2 will complete Deno and Vercel Edge; Phase 15.3 Netlify Edge. API shape mirrors workers adapter: createXHandler(manifest, opts) + getXEnv()/getXCtx() accessors. Polyfill mapping identical — WebCrypto + web-standard fetch.
Invariants:
- All three adapters (deno, vercel-edge, netlify-edge) currently export stub factories — calling them returns CLI_E214
- API surface will mirror
@mandujs/edge/workers—createXHandler(manifest, opts)+getXEnv() / getXCtx()accessors - Polyfill mapping is identical to Workers — WebCrypto, web-standard fetch, no Bun APIs at runtime
- Phase 15.2: Deno Deploy + Vercel Edge complete
- Phase 15.3: Netlify Edge complete
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/edge/deno-vercel-netlify to my project.
Summary of the page:
Deno/Vercel/Netlify Edge adapters: currently scaffolded stubs under `packages/edge/src/{deno,vercel,netlify}/`. Phase 15.2 will complete Deno and Vercel Edge; Phase 15.3 Netlify Edge. API shape mirrors workers adapter: createXHandler(manifest, opts) + getXEnv()/getXCtx() accessors. Polyfill mapping identical — WebCrypto + web-standard fetch.
Required invariants — must hold after your changes:
- All three adapters (deno, vercel-edge, netlify-edge) currently export stub factories — calling them returns CLI_E214
- API surface will mirror `@mandujs/edge/workers` — `createXHandler(manifest, opts)` + `getXEnv() / getXCtx()` accessors
- Polyfill mapping is identical to Workers — WebCrypto, web-standard fetch, no Bun APIs at runtime
- Phase 15.2: Deno Deploy + Vercel Edge complete
- Phase 15.3: Netlify Edge complete
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Edge
@mandujs/edge ships edge-runtime adapters for Cloudflare Workers (Phase 15.1, shipped). Deno Deploy, Vercel Edge, and Netlify Edge adapters are scaffolded and land in Phase 15.2+.
ai-hint —
@mandujs/edgeis a separate package from core — install withbun add @mandujs/edge. Polyfill mapping: Bun.CookieMap → LegacyCookieCodec (WebCrypto); Bun.CSRF → crypto.subtle HMAC-SHA256; Bun.serve →export default { fetch }. Unsupported APIs returnBunApiUnsupportedOnEdge500 JSON. Cloudflare Workers shipped; Deno/Vercel/Netlify coming.
Invariants:
@mandujs/edgeis a separate package — not bundled with@mandujs/core- Cloudflare Workers adapter is shipped (Phase 15.1); Deno/Vercel/Netlify Edge are stubs
- Calling an unsupported Bun API inside a Worker returns a structured 500 with
BunApiUnsupportedOnEdgepayload - Wrangler is a peer dependency — install with
bun add -D wrangler - All edge adapters use Mandu's same
manifest.json+ registered handlers — zero app-code changes required
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/edge/index to my project.
Summary of the page:
`@mandujs/edge` is a separate package from core — install with `bun add @mandujs/edge`. Polyfill mapping: Bun.CookieMap → LegacyCookieCodec (WebCrypto); Bun.CSRF → crypto.subtle HMAC-SHA256; Bun.serve → `export default { fetch }`. Unsupported APIs return `BunApiUnsupportedOnEdge` 500 JSON. Cloudflare Workers shipped; Deno/Vercel/Netlify coming.
Required invariants — must hold after your changes:
- `@mandujs/edge` is a separate package — not bundled with `@mandujs/core`
- Cloudflare Workers adapter is shipped (Phase 15.1); Deno/Vercel/Netlify Edge are stubs
- Calling an unsupported Bun API inside a Worker returns a structured 500 with `BunApiUnsupportedOnEdge` payload
- Wrangler is a peer dependency — install with `bun add -D wrangler`
- All edge adapters use Mandu's same `manifest.json` + registered handlers — zero app-code changes required
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Migrate
Migrate
Move an existing Next.js, Vite, or Remix app to Mandu.
ai-hint — Migrate hub. Porting guides from Next.js, Vite, Remix.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/migrate/index to my project.
Summary of the page:
Migrate hub. Porting guides from Next.js, Vite, Remix.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Recipes
Authentication
Sessions, JWT, OAuth, and CSRF — wired into Mandu's filling chain via middleware.
ai-hint — Use
withSession()andwithCsrf()from@mandujs/core/authas.use(...)middleware on filling chains. UseloginUser(ctx, userId)to start a session andctx.get("user")to read it. Short-circuit unauthenticated requests withctx.unauthorized()inside.guard(...). Never roll your own session cookie.
Invariants:
- Session middleware (
withSession()) must be.use(...)-d before any handler that readsctx.get("user") - CSRF middleware (
withCsrf()) is required for anyPOST/PUT/PATCH/DELETEthat mutates user data - Never construct
Set-Cookieheaders directly — useloginUser(ctx, id)/logoutUser(ctx)so the framework can rotate session ids - Password verification uses argon2id via
verifyPassword(plain, hash); never bcrypt or sha256 - For OAuth, register the provider in
mandu.config.tsauth.providersand route the callback throughMandu.filling().get(...)atapp/api/auth/[provider]/callback/route.ts
Add authentication to my Mandu app:
1. For email/password login: create `app/api/login/route.ts` as `Mandu.filling().use(withSession()).use(withCsrf()).post(...)`. Verify the password with `verifyPassword(plain, hash)` from `@mandujs/core/auth`, then call `await loginUser(ctx, user.id)` and redirect.
2. For protected routes: chain `.use(withSession())` then `.guard((ctx) => { if (!ctx.get('user')) return ctx.unauthorized('Login required'); })`.
3. For OAuth: register the provider in `mandu.config.ts` under `auth.providers`, then create `app/api/auth/<provider>/callback/route.ts` that calls `handleOAuthCallback(ctx, '<provider>')` and `loginUser(ctx, user.id)`.
Required invariants:
- withSession() must be .use()-d before any handler that reads ctx.get('user')
- withCsrf() is required for any mutating method (POST/PUT/PATCH/DELETE)
- Never construct Set-Cookie directly; use loginUser() / logoutUser() so the framework rotates session ids
- Passwords use argon2id (verifyPassword), never bcrypt/sha256
After writing the route, run `bun run guard` and `bun run check`.
Build an API endpoint
Two patterns for an API route — a quick filling chain, or a type-safe contract. One prompt covers both.
ai-hint — For quick endpoints, use
Mandu.filling().get(...).post(...)chain on a default export atapp/api/<route>/route.ts. For type-safe contracts, defineMandu.contract({ request: { GET, POST }, response: { 200, 400 } })and pair it withMandu.handler(contract, { GET, POST }). Usectx.body<T>(),ctx.query,ctx.ok(),ctx.created(),ctx.unauthorized()helpers — never read the raw Request directly.
Invariants:
- Handler files live at
app/api/<route>/route.tsand export adefault(file-system routing) - Filling chain shape —
Mandu.filling().use(...).guard(...).get(...).post(...); never instantiatenew Mandu()or write a rawRequesthandler - Contract shape —
Mandu.contract({ request: { GET, POST, ... }, response: { 200, 400, ... } })with zod schemas keyed by HTTP method / status code - Use
ctx.body<T>(),ctx.query, and the response helpers (ctx.ok,ctx.created,ctx.unauthorized,ctx.redirect) — Mandu Guard rejects rawResponseconstruction in API handlers - Middleware runs before handlers — chain with
.use(withSession()), not inside the body
Add a Mandu API route at `app/api/<NAME>/route.ts` for the resource `<NAME>` (e.g. users, posts, signup).
Pick the pattern based on what I need:
- If I want it quick or just prototyping, use Pattern A (filling chain): `export default Mandu.filling().get(...).post(...)` with `ctx.body<T>()` and `ctx.ok()` / `ctx.created()` / `ctx.unauthorized()`. Add middleware via `.use(withSession())` / `.use(withCsrf())` and short-circuit auth via `.guard((ctx) => ...)`.
- If I want type-safe + OpenAPI, use Pattern B (contract): create `spec/contracts/<NAME>.contract.ts` with `export const <name>Contract = Mandu.contract({ request: { GET, POST }, response: { 200, 400 } })` using zod schemas keyed by method / status. Then in the route file: `export default Mandu.handler(<name>Contract, { GET: async (ctx) => ..., POST: async (ctx) => ... })`.
Required invariants:
- File path: app/api/<NAME>/route.ts (default export only)
- Use ctx helpers (ctx.body<T>, ctx.query, ctx.ok, ctx.created, ctx.unauthorized, ctx.redirect) — never construct a raw Response or read raw Request.
- Middleware chains with .use(...); never inline auth/CSRF logic in the handler body.
- For Pattern B, contract path is spec/contracts/<NAME>.contract.ts with a named export ending in `Contract`.
After writing the files, run `bun run guard` and `bun run check` and report any violations.
Deploy
Ship a Mandu app — Vercel, Cloudflare Pages, Fly, Railway, Netlify, Docker, or a plain VPS. One command per target.
ai-hint — Use
mandu deploy --to=<target>once per environment. Mandu generatesvercel.json,wrangler.toml,fly.toml,Dockerfileetc. on first deploy. The static build target writes todist/— never edit dist files by hand.
Invariants:
- Use
mandu deploy --to=<target>(notnpx vercel/flyctl deploydirectly) so the generated config matches the runtime expectations - Generated config files (
vercel.json,wrangler.toml,fly.toml,Dockerfile) must be committed; they are the deploy contract - The static build output lives at
dist/; the production server entry isdist/server/main.jsfor container targets - Edge targets (Cloudflare Workers, Vercel Edge) have stricter API surface — see
edge/for adapter notes before deploying
Deploy my Mandu app to <TARGET> (one of: vercel, cloudflare, fly, railway, netlify, docker, docker-compose).
Steps:
1. Run `mandu deploy --to=<TARGET>`. This generates the platform's config file (vercel.json, wrangler.toml, fly.toml, Dockerfile etc.) on first run, then triggers the deploy.
2. Commit the generated config — it is the deploy contract.
3. For edge targets (Cloudflare Workers, Vercel Edge, Deno Deploy, Netlify Edge), confirm the route's API surface is edge-safe by checking the relevant adapter page under /docs/edge.
Required invariants:
- Don't bypass `mandu deploy` with `npx vercel` / `flyctl deploy` directly; the wrappers know which build artefacts each target needs.
- Never edit files in `dist/` by hand — they're regenerated by the static build.
- The container entry is `dist/server/main.js` for docker/fly/railway.
After deploy, fetch the live URL and confirm `/`, `/docs`, and any auth flow render. Report any 5xx in the deploy log.
Internationalisation (i18n)
Add a [lang] segment, translation modules, and a locale switcher — the same pattern this docs site uses across 23 languages.
ai-hint — Wrap localised pages under
app/[lang]/and passparams.langto agetTranslation(lang)helper that returns a typed Translation object. Validatelangagainstlocalesand fall back todefaultLocale. Setdir="rtl"for ar/he/ur. Use named exportgenerateStaticParamsto prerender every locale.
Invariants:
- Localised pages live under
app/[lang]/...—params.langis the only source of truth for locale - Validate
params.langwithisValidLocale(params.lang)and fall back todefaultLocalefor unknown values - Translation modules export a typed
Translationobject; never import locale strings inline dir="rtl"must be set on the layout's root element whenisRtl(lang)is true (ar, he, ur, fa)generateStaticParamsmust return every locale so the static build emits one HTML per language
Add i18n to my Mandu app:
1. Create `src/shared/utils/client/i18n/types.ts` with a `Translation` interface, then `translations/<locale>.ts` per locale exporting a typed Translation. Add an `index.ts` that re-exports `locales` (string[]), `defaultLocale`, `getTranslation(lang)`, `isValidLocale(lang)`, `isRtl(lang)` (true for ar/he/ur/fa).
2. Move pages that need localisation under `app/[lang]/...`. In each page: validate via `isValidLocale(params.lang)`; if invalid, render a 404 fallback. Read copy as `getTranslation(params.lang)`.
3. Add `export function generateStaticParams() { return locales.map((lang) => ({ lang })); }` so the static build emits every locale.
4. In `app/[lang]/layout.tsx`, set `dir={isRtl(lang) ? 'rtl' : 'ltr'}` on the root wrapper.
Required invariants:
- params.lang is the only source of truth for locale.
- Never import a locale module directly in a page; always go through getTranslation(lang).
- generateStaticParams must enumerate every entry of `locales` so the static build emits one HTML per language.
- dir='rtl' must be applied for ar, he, ur, fa.
After writing the files, run `bun run guard`, `bun run check`, and verify a couple of locale URLs in the dev server.
Recipes
Copy-paste patterns for common tasks — auth, forms, data, deploy.
ai-hint — Recipes hub. Task-oriented copy-paste snippets.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/recipes/index to my project.
Summary of the page:
Recipes hub. Task-oriented copy-paste snippets.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Streaming responses
SSE and chunked streams for AI chat, live progress, and large payloads — without breaking the filling chain.
ai-hint — For Server-Sent Events, return
ctx.sse((emit) => { ... emit({ data }); ... })from a.get()handler. For incremental chunked text, usectx.stream(async function* () { yield "..."; }). Both keep the filling chain (.use(),.guard()) intact and live inapp/api/<route>/route.ts.
Invariants:
- SSE handlers must return
ctx.sse(emitter); nevernew Response(stream, { ... })manually - Chunked text/JSON streams use
ctx.stream(asyncGenerator)so the runtime setsTransfer-Encoding: chunkedand flushes correctly - Don't mix
.post(...)andctx.sse(...)on the same route — SSE clients always GET - When piping LLM output, await the model's stream and
emit({ data: token })per token; never accumulate before emitting - Always send a
doneevent before closing so clients can stop reconnecting
Add a streaming endpoint to my Mandu app at `app/api/<NAME>/route.ts`.
Pick the kind:
- For SSE (live LLM tokens, progress, push updates): return `ctx.sse(async (emit) => { ... emit({ data }); ... emit({ event: 'done', data: '' }); })` from a `.get()` handler.
- For chunked text/JSON (CSV export, NDJSON, large payloads): return `ctx.stream(async function* () { yield '...'; }, { contentType: '...' })`.
Required invariants:
- Stay inside the filling chain — `.use(withSession())` etc. still applies.
- Never construct a `new Response(stream, ...)` directly; the framework wraps it.
- For SSE: emit a `done` event before the generator returns so EventSource clients can close cleanly.
- For LLM streaming: await the model and `emit({ data: token })` per token; do not accumulate before emitting.
- SSE handlers must use `.get(...)`, never `.post(...)` (browsers can only EventSource GET).
After writing the route, test with `curl -N` for SSE or `curl --no-buffer` for chunked, and run `bun run guard`.
Testing
Unit, integration, and E2E — all through Bun's test runner with Mandu's ATE auto-generating contract tests for you.
ai-hint — Use
bun testfor unit + integration. Mandu's ATE (Automated Test Engineer) auto-generates contract tests viabun run mandu ate generate. E2E runs through Playwright viabun run test:e2e. Tests live next to the file (*.test.ts) for unit, intests/for integration, and intests/e2e/for browser tests.
Invariants:
- Unit tests live alongside the source as
*.test.ts; integration tests live intests/; E2E intests/e2e/ - Test runner is Bun (
bun test); nevervitestorjest - Generated contract tests live under
tests/_generated/and must NOT be hand-edited (ATE regenerates them) - For DB-backed tests, use
tests/setup-db.tsto provision an in-memory SQLite instance — never share DB state across tests - E2E tests require
bun run devrunning on port 3333 (orbun run test:e2e:serverto manage automatically)
Add tests to my Mandu project:
1. For pure functions (utils, domain logic): create `<file>.test.ts` next to the source. Use `import { describe, expect, test } from 'bun:test'`.
2. For API contracts: run `bun run mandu ate generate` and review the generated tests under `tests/_generated/`. Add the missing edge cases to a sibling `<contract>.cases.test.ts` (don't edit the generated file).
3. For E2E flows: create `tests/e2e/<flow>.spec.ts` using `@playwright/test`. Assume the dev server is on http://localhost:3333.
Required invariants:
- Use `bun:test`, NEVER `vitest` or `jest`.
- Unit tests live next to the source as `*.test.ts`; integration in `tests/`; E2E in `tests/e2e/`.
- Generated contract tests under `tests/_generated/` are read-only; ATE regenerates them on demand.
- For DB-backed tests, use `tests/setup-db.ts` so each test gets a fresh in-memory SQLite — never share state.
After writing, run `bun test` (and `bun run test:e2e:server` if E2E was added) and report any failures.
Reference
CLI Reference
Exhaustive reference for every mandu subcommand: flags, environment variables, exit codes.
ai-hint — Canonical list of all mandu CLI subcommands registered in @mandujs/cli. Use this to know which commands exist, which flags are accepted, and which exit codes to expect. Do not invent commands not listed here.
Invariants:
- The runnable command set is whatever @mandujs/cli registers in commands/registry.ts — this list reflects v0.22/v0.23
- Unknown commands print an error and exit with code 1; unknown subcommands do the same
- CLI entrypoint is the bun shebang (#!/usr/bin/env bun) — do not invoke with node
- Short flags are fixed: -h=--help, -q=--quiet, -v=--verify, -d=--diff, -y=--yes
- Subcommands that only make sense with a target (add, middleware, session, auth, collection, scaffold) exit with code 1 when called without their required subcommand
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/reference/cli to my project.
Summary of the page:
Canonical list of all mandu CLI subcommands registered in @mandujs/cli. Use this to know which commands exist, which flags are accepted, and which exit codes to expect. Do not invent commands not listed here.
Required invariants — must hold after your changes:
- The runnable command set is whatever @mandujs/cli registers in commands/registry.ts — this list reflects v0.22/v0.23
- Unknown commands print an error and exit with code 1; unknown subcommands do the same
- CLI entrypoint is the bun shebang (#!/usr/bin/env bun) — do not invoke with node
- Short flags are fixed: -h=--help, -q=--quiet, -v=--verify, -d=--diff, -y=--yes
- Subcommands that only make sense with a target (add, middleware, session, auth, collection, scaffold) exit with code 1 when called without their required subcommand
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Client Router
useRouter, useParams, Link, NavLink — the SPA navigation hooks.
ai-hint — Mandu's client router (from @mandujs/core/client) mirrors React Router: useRouter for imperative navigation, useParams for route params, Link for declarative nav, NavLink for active-state links. Use inside islands or 'use client' pages.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/reference/client-router to my project.
Summary of the page:
Mandu's client router (from @mandujs/core/client) mirrors React Router: useRouter for imperative navigation, useParams for route params, Link for declarative nav, NavLink for active-state links. Use inside islands or 'use client' pages.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
mandu.config.ts Reference
Full schema for ManduConfig — every top-level field and sub-field with types, defaults, and examples.
ai-hint — ManduConfig schema from @mandujs/core/src/config/mandu.ts. Use this to know which fields are allowed in mandu.config.ts, what types they take, and which files Mandu will load. Do not invent fields not listed here.
Invariants:
- All top-level fields on ManduConfig are optional — a config file may be empty
- Mandu searches, in order: mandu.config.ts, mandu.config.js, mandu.config.json, .mandu/guard.json
- .mandu/guard.json may contain either the full config or a guard-only object; in the guard-only case it is coerced to { guard: ... }
- When a config file exists but fails to load or parse, loadManduConfig returns {} — invalid configs do not throw
- guard.preset must be one of: mandu, fsd, clean, hexagonal, atomic, cqrs
- GuardRuleSeverity is limited to: error, warn, warning, off
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/reference/config to my project.
Summary of the page:
ManduConfig schema from @mandujs/core/src/config/mandu.ts. Use this to know which fields are allowed in mandu.config.ts, what types they take, and which files Mandu will load. Do not invent fields not listed here.
Required invariants — must hold after your changes:
- All top-level fields on ManduConfig are optional — a config file may be empty
- Mandu searches, in order: mandu.config.ts, mandu.config.js, mandu.config.json, .mandu/guard.json
- .mandu/guard.json may contain either the full config or a guard-only object; in the guard-only case it is coerced to { guard: ... }
- When a config file exists but fails to load or parse, loadManduConfig returns {} — invalid configs do not throw
- guard.preset must be one of: mandu, fsd, clean, hexagonal, atomic, cqrs
- GuardRuleSeverity is limited to: error, warn, warning, off
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Reference
Auto-generated API surface, CLI flags, and config reference.
ai-hint — Reference hub. API surface from TypeDoc, CLI command reference, mandu.config options.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/reference/index to my project.
Summary of the page:
Reference hub. API surface from TypeDoc, CLI command reference, mandu.config options.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Getting Started
Start
Install Mandu, scaffold an app, and run it locally in 5 minutes.
ai-hint — Start category hub. Entry point: quickstart. Covers install, scaffold, dev server.
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/start/index to my project.
Summary of the page:
Start category hub. Entry point: quickstart. Covers install, scaffold, dev server.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Quickstart
Ship a Mandu app in 5 minutes — install, scaffold, dev. Two install paths, one prompt your agent can run end-to-end.
ai-hint — Install via the standalone binary (curl/iwr) or
bun install -g @mandujs/cli, scaffold withmandu create my-app, thenbun run devon port 3333. Routes live underapp/aspage.tsx. Bun >= 1.3.12 is required for the npm path.
Invariants:
- Bun >= 1.3.12 is required for the npm/Bun package path; the standalone binary embeds Bun and has no prerequisite
- Routes are recognised only as
app/**/page.tsx(no.jsx, noindex.tsx) - Interactive components must end with
.island.tsxand start with"use client" - The dev server defaults to port 3333; configure via
mandu.config.ts
Bootstrap a new Mandu project. Walk me through:
1. Install Mandu — prefer `curl -fsSL https://raw.githubusercontent.com/konamgil/mandu/main/install.sh | sh` on Unix, `iwr https://raw.githubusercontent.com/konamgil/mandu/main/install.ps1 -useb | iex` on Windows. (Or `bun install -g @mandujs/cli` if Bun is already installed.)
2. `mandu create my-app` (template: `starter`).
3. `cd my-app && bun run dev`.
4. Open http://localhost:3333 and confirm the welcome page renders.
Diagnose using these invariants: routes live at app/**/page.tsx (.tsx only); .island.tsx with `use client` is required for hydration; dev port is 3333; the npm path requires Bun >= 1.3.12 (the binary embeds Bun).
Once dev is running, show me the project tree (app/, src/client, src/server, src/shared, spec/, content/, mandu.config.ts).
Testing
Coverage
mandu test --coverage turns on Bun's native line coverage and Playwright V8 coverage, then merges them into the canonical .mandu/coverage/lcov.info.
ai-hint —
--coveragemerges LCOV v2 from Bun (coverage/lcov.info) + Playwright (coverage/e2e.lcov) →.mandu/coverage/lcov.info. Thresholds configured inmandu.config.ts::test.coveragefail CI with CLI_E065 when unmet.
Invariants:
- Merged LCOV always lands at
.mandu/coverage/lcov.info - Missing input files are silently skipped — single-leg coverage still produces the merged output
- Thresholds are evaluated on the MERGED LCOV — unit + E2E hits combine before the check
- Exit code 1 on threshold failure, with CLI_E065 and
actual% < expected% - Round-trip:
parse(serialize(parse(x))) === parse(x)— merge is byte-stable for CI diffs
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/coverage to my project.
Summary of the page:
`--coverage` merges LCOV v2 from Bun (`coverage/lcov.info`) + Playwright (`coverage/e2e.lcov`) → `.mandu/coverage/lcov.info`. Thresholds configured in `mandu.config.ts::test.coverage` fail CI with CLI_E065 when unmet.
Required invariants — must hold after your changes:
- Merged LCOV always lands at `.mandu/coverage/lcov.info`
- Missing input files are silently skipped — single-leg coverage still produces the merged output
- Thresholds are evaluated on the MERGED LCOV — unit + E2E hits combine before the check
- Exit code 1 on threshold failure, with CLI_E065 and `actual% < expected%`
- Round-trip: `parse(serialize(parse(x))) === parse(x)` — merge is byte-stable for CI diffs
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
E2E Tests
mandu test --e2e wraps the ATE (Automation Test Engine) pipeline to extract the interaction graph, generate Playwright specs, and run them — with optional heal-on-failure suggestions.
ai-hint —
mandu test --e2eruns extract → generate → spawn. Specs land undertests/e2e/auto/. Requires@playwright/test >= 1.40.0as a peer dep. Use--healto emit non-destructive diff suggestions after a failure.
Invariants:
@playwright/test >= 1.40.0is a peer dependency — missing it returns CLI_E063- Playwright config must live at
tests/e2e/playwright.config.ts(override viatest.e2e.configPath) - Generated specs land under
tests/e2e/auto/<routeId>.spec.ts(regenerated each run) - Interaction graph is cached at
.mandu/interaction-graph.json --healNEVER auto-commits — it emits diffs for human review
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/e2e to my project.
Summary of the page:
`mandu test --e2e` runs extract → generate → spawn. Specs land under `tests/e2e/auto/`. Requires `@playwright/test >= 1.40.0` as a peer dep. Use `--heal` to emit non-destructive diff suggestions after a failure.
Required invariants — must hold after your changes:
- `@playwright/test >= 1.40.0` is a peer dependency — missing it returns CLI_E063
- Playwright config must live at `tests/e2e/playwright.config.ts` (override via `test.e2e.configPath`)
- Generated specs land under `tests/e2e/auto/<routeId>.spec.ts` (regenerated each run)
- Interaction graph is cached at `.mandu/interaction-graph.json`
- `--heal` NEVER auto-commits — it emits diffs for human review
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Testing
Mandu ships its own batteries-included test runner — unit, integration, E2E, coverage, watch — all driven by a single mandu test command.
ai-hint — Testing category hub.
mandu testwraps bun:test for unit + integration + E2E (ATE/Playwright) with coverage merge, watch mode, and snapshot testing. Zero external deps required for unit/integration.
Invariants:
mandu testrequires Bun >= 1.3.12 — the runner wrapsbun testdirectly- Test configuration lives under the
testblock ofmandu.config.tsand is.strict()— unknown keys throw at load - Exit code 0 = success, 1 = test failure, 2 = infra error, 4 = config error
--e2erequires@playwright/testas a peer dependency (bun add -d @playwright/test)- Merged LCOV always lands at
.mandu/coverage/lcov.info(Phase 12.3)
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/index to my project.
Summary of the page:
Testing category hub. `mandu test` wraps bun:test for unit + integration + E2E (ATE/Playwright) with coverage merge, watch mode, and snapshot testing. Zero external deps required for unit/integration.
Required invariants — must hold after your changes:
- `mandu test` requires Bun >= 1.3.12 — the runner wraps `bun test` directly
- Test configuration lives under the `test` block of `mandu.config.ts` and is `.strict()` — unknown keys throw at load
- Exit code 0 = success, 1 = test failure, 2 = infra error, 4 = config error
- `--e2e` requires `@playwright/test` as a peer dependency (`bun add -d @playwright/test`)
- Merged LCOV always lands at `.mandu/coverage/lcov.info` (Phase 12.3)
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Integration Tests
Exercise the full HTTP stack with createTestServer + createTestSession + createTestDb — from @mandujs/core/testing. Each fixture implements Symbol.asyncDispose so await using handles teardown.
ai-hint — Integration tests boot a real
Bun.servelistener on an ephemeral port. UsecreateTestSession()to bypass login. UsecreateTestDb({schema, seed})for an isolated sqlite instance. Every async fixture supportsawait using— no manual.close()needed.
Invariants:
- Default include glob:
tests/integration/**/*.test.ts createTestServer(manifest, opts)binds to port 0 (ephemeral) — tests run in parallel safely- Every async fixture implements
Symbol.asyncDispose—await usingcleans up in reverse order - The default
dbUrlissqlite::memory:— per-test isolation unless you point at a shared URL - Integration test timeout defaults to 60_000 ms (override via
test.integration.timeout)
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/integration to my project.
Summary of the page:
Integration tests boot a real `Bun.serve` listener on an ephemeral port. Use `createTestSession()` to bypass login. Use `createTestDb({schema, seed})` for an isolated sqlite instance. Every async fixture supports `await using` — no manual `.close()` needed.
Required invariants — must hold after your changes:
- Default include glob: `tests/integration/**/*.test.ts`
- `createTestServer(manifest, opts)` binds to port 0 (ephemeral) — tests run in parallel safely
- Every async fixture implements `Symbol.asyncDispose` — `await using` cleans up in reverse order
- The default `dbUrl` is `sqlite::memory:` — per-test isolation unless you point at a shared URL
- Integration test timeout defaults to 60_000 ms (override via `test.integration.timeout`)
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Snapshot testing
Use bun:test's toMatchSnapshot() helper for golden-file tests, plus --update-snapshots to regenerate after intentional UI or contract changes.
ai-hint — Snapshot testing uses
expect(x).toMatchSnapshot()from bun:test. Snapshots live in__snapshots__/<file>.snapnext to the test. Run--update-snapshotsafter an intentional change. Keep snapshots deterministic — strip timestamps/IDs first.
Invariants:
- Snapshot files live at
__snapshots__/<test-file>.snapnext to the test --update-snapshotsregenerates snapshots in place — always review the diff before committingtoMatchSnapshot()is provided bybun:test; no separate dep is required- Snapshots must be deterministic — serialize timestamps/IDs through a helper before the assertion
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/snapshot to my project.
Summary of the page:
Snapshot testing uses `expect(x).toMatchSnapshot()` from bun:test. Snapshots live in `__snapshots__/<file>.snap` next to the test. Run `--update-snapshots` after an intentional change. Keep snapshots deterministic — strip timestamps/IDs first.
Required invariants — must hold after your changes:
- Snapshot files live at `__snapshots__/<test-file>.snap` next to the test
- `--update-snapshots` regenerates snapshots in place — always review the diff before committing
- `toMatchSnapshot()` is provided by `bun:test`; no separate dep is required
- Snapshots must be deterministic — serialize timestamps/IDs through a helper before the assertion
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Unit Tests
Run fast, pure-function tests against fillings and handlers without booting a server — testFilling, createTestRequest, createTestContext via @mandujs/core/testing.
ai-hint —
mandu test unitwrapsbun:testand picks up**/*.test.ts+**/*.test.tsxby default. UsetestFilling(fn, opts)to invoke a filling without a server. Every async fixture implementsSymbol.asyncDisposeforawait using.
Invariants:
- Default include glob:
**/*.test.ts,**/*.test.tsx; exclude:node_modules/**,.mandu/** testFillingis a pure function call — no HTTP stack, no Bun.serve- Unit test timeout defaults to 30_000 ms (override via
test.unit.timeout) - Mocks (
mockMail,mockStorage) implementSymbol.disposesousingclears them on scope exit
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/unit to my project.
Summary of the page:
`mandu test unit` wraps `bun:test` and picks up `**/*.test.ts` + `**/*.test.tsx` by default. Use `testFilling(fn, opts)` to invoke a filling without a server. Every async fixture implements `Symbol.asyncDispose` for `await using`.
Required invariants — must hold after your changes:
- Default include glob: `**/*.test.ts`, `**/*.test.tsx`; exclude: `node_modules/**`, `.mandu/**`
- `testFilling` is a pure function call — no HTTP stack, no Bun.serve
- Unit test timeout defaults to 30_000 ms (override via `test.unit.timeout`)
- Mocks (`mockMail`, `mockStorage`) implement `Symbol.dispose` so `using` clears them on scope exit
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Watch mode
mandu test --watch keeps a persistent chokidar watcher over app/, src/, and packages/ and re-runs only the tests affected by each change — a ~500ms red-green-refactor cycle.
ai-hint —
--watchuses a 200ms debounced chokidar watcher. Change → compute affected tests (direct match OR substring import scan) →bun test <narrowed>. Concurrency guarded: no pile-ups. For transitive-impact precision usemandu test:watchinstead.
Invariants:
- Watch dirs:
app/,src/,packages/— ignoresnode_modules/,.git/,.mandu/,dist/ - Debounce window: 200ms — bursts of saves coalesce into a single re-run
- Concurrency guarded — re-runs already in flight absorb new changes into a follow-up batch
- Affected-test heuristic: direct file match OR substring import scan (NOT full dep-graph)
--e2eis incompatible with--watch— usemandu test:watchfor E2E watch
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/watch to my project.
Summary of the page:
`--watch` uses a 200ms debounced chokidar watcher. Change → compute affected tests (direct match OR substring import scan) → `bun test <narrowed>`. Concurrency guarded: no pile-ups. For transitive-impact precision use `mandu test:watch` instead.
Required invariants — must hold after your changes:
- Watch dirs: `app/`, `src/`, `packages/` — ignores `node_modules/`, `.git/`, `.mandu/`, `dist/`
- Debounce window: 200ms — bursts of saves coalesce into a single re-run
- Concurrency guarded — re-runs already in flight absorb new changes into a follow-up batch
- Affected-test heuristic: direct file match OR substring import scan (NOT full dep-graph)
- `--e2e` is incompatible with `--watch` — use `mandu test:watch` for E2E watch
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
For Agents
This page aggregates every Agent Prompt from the Mandu docs. Agents can fetch this single URL instead of crawling individual docs pages. Auto-generated by scripts/build-for-agents.ts.