← Back to list

rp1 on EmDash, Part 1: mapping a codebase that AI agents built

rp1 Build Series · EmDash · Part 1: Onboarding

Mahesh Shivamallappa in rp1 Journal · 2026-04-12 11:14 · 10 claps · 6.4 min read
#ai-engineering #em-dash #software-development #claude-code #agentic-workflow
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents

Photo by Solen Feyissa on Unsplash

Photo by Solen Feyissa on Unsplash

rp1 on EmDash, Part 1: mapping a codebase that AI agents built

rp1 Build Series · EmDash · Part 1: Onboarding

*← Series overview · Part 2: Build a Plugin →*

Cloudflare’s EmDash was built primarily by AI coding agents over two months. The result ships — and ships well. But when you sit down to contribute, you immediately hit a question that doesn’t have an obvious answer from the README: how does a plugin actually run, and where does each piece of it live?

This is exactly the question rp1 is built to answer before you write a single line. Two commands do the work: /knowledge-build reads the entire repository and produces a persistent knowledge graph, and /project-birds-eye-view synthesises that into a full project overview with diagrams, workflow sequences, a development guide, and an honest gaps section.

This part walks through both commands on EmDash and surfaces three findings that will save you a debugging session the first time you write a plugin.

The payoff shows up in Part 2, where that knowledge graph prevents four mistakes that a raw AI coding session makes on the exact same task.

Prerequisites

Completed the series setup:

# EmDash
git clone https://github.com/emdash-cms/emdash.git && cd emdash
pnpm install && pnpm build
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev          # admin at localhost:4321/_emdash/admin

# rp1 — macOS / Linux
brew install rp1-run/tap/rp1
# or: curl -fsSL https://rp1.run/install.sh | sh
rp1 install      # wires rp1 into Claude Code
rp1 init         # run from the emdash/ root — creates .rp1/ and updates CLAUDE.md
rp1 verify       # confirms everything is wired up correctly

All rp1 commands run from the emdash/ root directory inside Claude Code.

Step 1 — Build the knowledge graph

/knowledge-build

Run this once. It reads the entire repository, extracts architecture, conventions, and anti-patterns, and persists everything to .rp1/context/. Every subsequent rp1 command in this series loads it automatically.

EmDash is a pnpm monorepo with two published packages — emdash (core) and @emdash-cms/admin (React SPA) — plus demo sites, templates, a docs site, and an e2e fixture. The knowledge build analyses all of them.

What the output looks like:

  Knowledge Base Generated Successfully

  - Repository type: monorepo (18 projects)
  - Files analyzed: 1,571 (TypeScript, Astro, MDX)
  - Files written under ~/.rp1/context/:
    - index.md (orchestrator-generated)
    - concept_map.md (228 lines)
    - architecture.md (206 lines, Mermaid diagram included)
    - interaction-model.md (267 lines)
    - modules.md (330 lines)
    - patterns.md (141 lines — within 150-line budget)
    - state.json + meta.json

  Agents load the KB automatically — no manual /knowledge-load needed. This workflow does not register an Arcade run.

Six knowledge files, each covering a different lens on the codebase:

  • architecture.md — system layers, component paths, integrations, the full architecture Mermaid diagram. Where the plugin execution model, sandbox tiers, and deployment adapters live.
  • concept_map.md — 23 domain entities (Collection, ContentItem, Revision, Plugin, PluginContext, MCP Server, etc.), terminology glossary (ec_*, ULID, ApiResult, PluginCapability), and entity relationships.
  • interaction-model.md — admin SPA flows, authentication sequences, visual editing, content preview, CLI commands.
  • modules.md — both packages documented with responsibilities, key source files, and the cross-module dependency map.
  • patterns.md — 15 coding patterns: API envelope, error handling, input validation, authorization, CSRF, SQL safety, pagination, route conventions, migrations, index discipline, plugin definition, testing, admin UI error handling, import conventions, and environment gating.
  • index.md — quick reference index across all files.

Step 2 — Generate the bird’s-eye view

/project-birds-eye-view

This reads the knowledge base and produces a comprehensive project overview saved to .rp1/docs/project-overview.md. On EmDash it generates an 11-diagram document covering executive summary, system context, architecture layers, package breakdown, data model, five key workflows, the full API surface, a development guide, integration map, and an assumptions/gaps section.

If you have rp1 Arcade: open the file directly in the Artifact Viewer — it renders all 11 Mermaid diagrams as interactive visuals natively.

The three diagrams below are the most relevant for plugin contributors. Each is paired with the finding it illustrates.

Three findings from the knowledge graph

These are the things that will trip you up if you don’t know them — none are in the README.

Finding 1: Two completely separate execution tiers.

From architecture.md, Plugin System section:

“Two plugin modes: Trusted (plugins: [], native format) — full host access; can provide React admin UI (adminEntry) and Portable Text components (componentsEntry). Sandboxed (sandboxed: [], standard format) — runs in V8 isolates via SandboxRunner; metadata contributions validated against allowlists."

The distinction isn’t cosmetic. A trusted plugin runs in the same process as the Astro server — it can access the database directly, provide React components for the admin UI, and use the full PluginContext API including ctx.content, ctx.media, and ctx.users. A sandboxed plugin runs in a V8 isolate with serialized inputs and outputs at the RPC boundary, declared capabilities enforced, and resource limits (CPU, memory, wall-clock time, subrequests) applied.

The hook naming convention is where plugin authors most commonly go wrong. From concept_map.md, PluginHooksManager:

“Hook names: plugin:install/activate/deactivate/uninstall, content:beforeSave/afterSave/beforeDelete/afterDelete, media:beforeUpload/afterUpload, cron, email:beforeSend/deliver/afterSend, comment:beforeCreate/moderate/afterCreate/afterModerate, page:metadata/fragments."

content:afterSave fires on every collection — posts, pages, and media uploads alike. If you're building a plugin that should only react to a specific content type, you need to filter by collection in the hook body. Miss this and your plugin runs on operations it was never meant to touch.

Here’s where the Plugin System sits in the full architecture — invoked by the Handler layer as a side-effect of content operations, not as part of the request chain:

Architecture Overview

Architecture Overview

The PluginManager sits inside EmDashRuntime alongside the Handler Layer — plugins are invoked by handlers at lifecycle points (content:beforeSave, content:afterSave, etc.), not wired into the middleware chain above.

Finding 2 — The platform adapter boundary is enforced at build time via virtual modules

From architecture.md, Virtual Module Build Bridge section:

“Vite virtual modules (virtual:emdash/*) connect build-time config to runtime code without bundling Node-only code into Workers."

EmDash selects its database adapter, storage backend, plugin sandbox implementation, and auth provider by generating Vite virtual modules at build time — not by conditional imports at runtime. This is what makes the same emdash package work correctly on both Node.js and Cloudflare Workers: the correct adapter is wired in before the bundle is produced.

The practical consequence for plugin authors: you can’t import a platform-specific adapter directly. The DB instance you get via ctx.content or via EmDashRuntime is already the correct adapter for the deployment target. Writing a plugin that reaches for a specific driver (better-sqlite3, @libsql/client, the D1 binding) will break on every other deployment target.

The package relationships make the boundary clear:

Package Relationship

Package Relationship

The database adapters — SQLite, libSQL, PostgreSQL, D1 — live inside packages/core/src/storage/ and packages/core/src/db/adapters.ts, selected via the virtual:emdash/dialect virtual module. There is no separate platform package that wraps core. The demo sites (demos/cloudflare/, demos/simple/, demos/postgres/) show how the same core package deploys to different targets purely through configuration.

Finding 3 — Sandboxing only enforces on Cloudflare deployments

From modules.md, Plugin Sandbox section:

“packages/core/src/plugins/sandbox/ — Plugin sandboxing interface; NoopSandboxRunner for non-Worker environments."

The NoopSandboxRunner is the Node.js implementation — it runs all sandboxed plugins in-process with no isolation, no resource limits, and no capability enforcement. The real enforcement only activates via the Cloudflare Worker Loader sandbox runner, which requires a paid Cloudflare plan for Dynamic Workers.

A contributor who tests locally with demos/simple/ sees all plugin hooks fire and all capability checks pass regardless of what the plugin declares in its manifest. The gap is completely invisible until you deploy to Cloudflare.

This is also what Section 12 flags as a risk to verify: “plugin V8 isolate cold-start latency on Cloudflare Workers” — you can’t measure cold-start locally because there’s no isolate locally.

The content authoring and plugin installation flows converge on the same Handler Layer — the path looks identical whether you’re local or on Cloudflare:

User Flows

User Flows

Plugin hooks fire during the ManualSave and Publish steps — content:beforeSave, content:afterSave, content:beforeDelete, content:afterDelete. On Node.js, the NoopSandboxRunner runs every sandboxed plugin in-process with no isolation. The flow completes identically whether you're local or on Cloudflare, which is exactly why missing capability declarations slip through local testing undetected.

What comes next

The knowledge graph from this part — the two execution modes, the virtual module platform boundary, and the sandboxing gap — is loaded automatically by every rp1 command in Parts 2 and 3. In Part 2, it prevents four specific mistakes when building a reading-time plugin. A raw AI coding session hits all four. rp1 hits none.

If this is the first time you’re seeing rp1: **rp1.run/getting-started**

Appendix — rp1 knowledge graph files

These are the actual files rp1 generated on the EmDash repository. All findings and diagrams in this article are drawn from them.

*rp1.run/getting-started · AI Engineer Melbourne, June 3–4 2026*


메타데이터
post_id
7fa605ba1ef3
slug
rp1-on-emdash-part-1-mapping-a-codebase-that-ai-agents-built-7fa605ba1ef3
url
https://blog.rp1.run/rp1-on-emdash-part-1-mapping-a-codebase-that-ai-agents-built-7fa605ba1ef3
canonical_url
https://blog.rp1.run/rp1-on-emdash-part-1-mapping-a-codebase-that-ai-agents-built-7fa605ba1ef3
author_url
https://medium.com/@inbox4mahesh
status
ok
fetched_at
2026-06-15 20:49:13