Promote refrakt.config.json from a site-only file into the unified root config defined in ADR-010. Add type-level support for plugins, plan, site, and sites, normalize the three valid input shapes (flat / singular site / plural sites) into a canonical internal form, and publish a JSON Schema so editors can autocomplete and validate the file.
Replace the lazy runPlugin import-on-demand pattern in packages/cli/src/bin.ts with a first-class discoverPlugins() helper that returns the full set of installed plugins. The helper has four consumers in v0.11.0 — CLI dispatch, refrakt --help, refrakt plugins list, and the new MCP server — and centralizing the logic eliminates three different implementations of the same scan.
Add three optional fields to the CliPluginCommand interface so plugin commands can declare structured input/output schemas and an MCP-friendly handler that bypasses argv parsing. Existing plugins keep working unchanged — the fields are purely additive.
Switch the CLI's plugin dispatch in packages/cli/src/bin.ts from blind import('@refrakt-md/<namespace>/cli-plugin') to a discoverPlugins() lookup. This produces a friendlier "did you mean?" error when a namespace is misspelled (since we know the full set of installed plugins) and centralizes plugin loading on the new helper.
Update all five framework adapter packages — @refrakt-md/sveltekit, @refrakt-md/astro, @refrakt-md/nuxt, @refrakt-md/next, @refrakt-md/eleventy — to accept a site?: string option. Each adapter resolves its target site via the shared normalizer from WORK-159, then reads contentDir, theme, target, packages, routeRules, icons, backgrounds from the resolved site entry instead of the top-level config. Single-site projects and the legacy flat shape continue to work without changes; multi-site repos pick a target per adapter.
Wire the @refrakt-md/plan CLI commands with the new schema fields from WORK-161 so the MCP server exposes them as cleanly typed tools. Each command gets a JSON Schema for its inputs and an mcpHandler that bypasses argv parsing and accepts a structured object directly.
Wire @refrakt-md/plan to the unified refrakt.config.json. plan init scaffolds the config (creating it or extending an existing one with a plan section), and plan serve / plan build / other plan commands read plan.dir from the loaded config instead of relying solely on the --dir flag and defaults. The plan section currently exposes only dir; specs path remains derived from dir (a child folder), and surfacing it as a config field can come later if real-world projects demand it.
Create the @refrakt-md/mcp package: stdio MCP server entry point, auto-detection of plan and site contexts from the unified config and the filesystem, and the initial set of core CLI-mirroring tools (refrakt.inspect, refrakt.contracts, refrakt.validate, refrakt.reference, refrakt.package_validate).
Wire plugin-contributed tools (notably the @refrakt-md/plan commands) into the MCP server via discoverPlugins(), and implement the read-only resources (refrakt://detect, refrakt://reference, refrakt://contracts, refrakt://rune/<name>, refrakt://plan/*) so agents that prefer pull semantics have first-class URIs.
The collection rune core — a sentinel-emitting schema plus a postProcess resolver that queries the registry (filter/sort/group/limit) and renders the built-in layouts, with the pinned $item bound-variable contract and the per-layout body interpretation.
The built-in adapter (in @refrakt-md/content) that turns site.entityRoutes into contributed pages: selects entities by type + filter, substitutes placeholders, renders an inline render string or a render-template partial per entity with $item bound, and back-fills sourceUrl.
Make plan entities embeddable so {% expand $item.id /%} renders their bodies in entityRoutes detail pages. The plan register hook currently sets sourceUrl only; add sourceFile and an extract that returns the entity rune's transformed body. This is the prerequisite the SPEC-071 dogfood depends on.
Wire refrakt's own plan/ as a sites.plan site and author its plan-site/ content, proving that entityRoutes + collection replace the bespoke plan serve. This is the milestone's proof-of-practice.
A shared Playwright harness that photographs the generated gallery + layout fixtures and diffs them against a baseline. This closes the AI iteration loop and — critically — gives the v0.23.0 skeleton/skin extraction its "diff must be empty" proof. No screenshot testing exists in the repo today; this is greenfield.Baselines are ephemeral, not committed. Fixtures and theme CSS churn constantly, so committed golden PNGs would mean endless binary diffs and a re-baseline commit on nearly every PR — noise that trains everyone to ignore the check, plus repo bloat. Instead the baseline is whatever you compare against in the moment (__screenshots__/ is gitignored): capture-then- compare locally, or compare-against-base in CI.
The wholesale pass the WORK-410 spike sized: re-bucket Lumina's CSS into the two @layers per the spike's cut-line rule, so structure ships in @refrakt-md/skeleton and aesthetics stay in Lumina's skin. Mechanical and low-risk per declaration, but large (~114 files / ~6,058 declarations; the spike estimates ≈40% skeleton / 55% skin / 5% content).
Surface the installed plugin set in two places: the top-level refrakt --help output (so users discover available namespaces without reading docs) and a dedicated refrakt plugins list command (canonical machine-readable output for tooling, including MCP clients debugging their setup).
Site-scoped commands (inspect, contracts, validate, scaffold-css, package validate) need to know which site they are operating on when the project declares multiple. Add a --site <name> flag that selects an entry from the normalized sites map; for single-site projects the flag is optional and resolves to the lone entry.
Move site/refrakt.config.json to a unified refrakt.config.json at the repo root, declaring plugins, the plan section, and sites.main (or whatever name we settle on) for the existing site. Validates the design against our own project and gives us a multi-site-ready structure for any future additions (separate plan dashboard site, blog, etc.).
create-refrakt currently writes a flat-shape site/refrakt.config.json for site projects and (via plan init) no config at all for plan projects. Update both paths to produce the new unified root-shape config — site projects get a sites.main section, plan projects get a plan section, and any combined scaffolds (future) declare both.
Document the unified refrakt.config.json at site/content/docs/configuration/. Cover the three valid shapes (flat / singular site / plural sites), each section (plugins, plan, site/sites), the multi-site workflow, and the migration story for existing flat-shape configs.
Document the new @refrakt-md/mcp server (registration, tool reference, resource reference) at site/content/docs/mcp/, update the existing plugin authoring docs to cover the new cli-plugin schema fields (inputSchema, outputSchema, mcpHandler), and add a brief pointer in the root CLAUDE.md so AI agents know the MCP server exists when registered.
The rich-table path: a table collection's body uses the sections content model (heading = column separator + label, body = per-cell markdoc template with $item), with collection owning the <table>/<thead> and row alignment.
Add a plan project type to create-refrakt that scaffolds a complete, runnable plan site: config (plugin + entityRoutes), a seed plan/, and a plan-site/ content dir (layout + dashboards), for a chosen adapter target.
Close the loop on the use case that motivated SPEC-107: the same content rune reading as a contained card and as a full-width hero, by composing the axes — no rune fork.
Add a lint pass to refrakt package validate that checks a package's cli-plugin export for structural issues — missing namespace, missing descriptions, malformed inputSchema, namespace conflicts with already-installed plugins. Catches problems at package-publish time rather than at runtime when the MCP server tries to advertise the broken tool.
Add a small migration command that rewrites a flat-shape refrakt.config.json into the nested form (site.* and explicit plugins). Optional for users — the flat shape stays valid indefinitely — but useful for projects adopting the unified config or moving to multi-site.
The first core plain-presentational card rune — ordinary attributes, no $item/registry knowledge — shipped as the reference implementation for collection body templates. Proves the "cards are plain runes; the template wires entity fields into attributes" model.
Mark plan build and plan serve deprecated, pointing users at the site approach, once refrakt's own plan site proves the replacement. Keep the authoring CLI. Removing the bespoke render stack is a later release, out of scope here.
Implement the canonical field:value selector grammar from SPEC-070 as a single shared module in @refrakt-md/runes, consumed by collection, entityRoutes (SPEC-069), and backlog. The existing plugins/plan/src/filter.ts folds into it, gaining glob/regex, url resolution, and case-consistency while preserving backlog behavior.
Implement the deferBody mechanism SPEC-070's prototype validated: a catalog flag plus a pre-transform pass in the content loader that captures a rune's pristine body as a source string and empties it, so postProcess can re-parse and transform it per entity with a bound variable. Capture must happen before the page transform — by schema-transform time Markdoc has already resolved the body's $item interpolations to undefined.
Add the Plugin.contributePages hook and a contribution phase in the content loader (after file-page registration, before aggregation) that collects ContributedPage[] and runs them through the normal pipeline — the underlying primitive both the entityRoutes adapter and third-party plugins build on.
refrakt edit cannot launch in a multi-site project: EditOptions is just port/contentDir/devServer/noOpen, and resolveSite() throws when multiple sites are declared, telling the user to pass --content-dir — which bypasses config resolution entirely, losing plugins, tints, icons, and route rules. Refrakt's own repo (sites main + plan) hits this. Add a --site flag and an in-editor site switcher so the editor works first-class against multi-site configs.
In block mode, registry-resolved runes — collection, relationships, aggregate, xref, expand, and data-bound sandbox — render as unresolved sentinel shells with no explanation. They resolve in core postProcess against coreData.registry, which the client-side block renderer never runs; RUNTIME_ONLY_TYPES in block-renderer.ts only placeholders nav*. The server already holds the registry and resolvers (the full-page iframe preview uses them), so render these blocks server-side; where that's not possible, show an honest placeholder instead of an empty shell.
The tier-1 foundation of SPEC-103: a preprocess-time data rune that reads a sandboxed external file and emits a Markdoc table AST node, plus the CSV/TSV/JSON/NDJSON adapters and the shared projection/typing core. SQLite and any remote source are explicitly out of scope (later work items). Proves the headline claim — chart and datatable consume the emitted <table> with no edits.
Turn elevation into the universal chrome/depth axis from SPEC-107: an ordered semantic ladder emitted as data-elevation, superseding both §8's surface enum and today's shadow-only none/sm/md/lg scale. The foundation the rest of the milestone builds on.
Add the header-emphasis axis from SPEC-107: prominence scales a rune's page-section header (eyebrow / title / blurb / rhythm), selecting its typographic register. Unlike elevation it is not universal — it is available only to runes that carry the page-section header model.
The gate for SPEC-104: teach bg to host a live sandbox as a bare, presentational, full-bleed backdrop. Covers §1–§4 — the constrained body, the engine relocation that mirrors bg-video, the bare-surface guardrail, and boot-frame layering. Everything downstream (the preset, the showcase) builds on the data-bg-guest contract this establishes.
SPEC-104 §5: make a reusable sandbox backdrop applicable by name (bg="midnight-waves") like any other preset, by adding a sandbox descriptor to BgPresetDefinition — resolved at transform time (not the identity engine, which has no file access), expanding into the WORK-428 data-bg-guest body.
The gate for SPEC-105 (§1–§3): a shared, closed reveal modifier plus an orthogonal stagger modifier on section-level runes, emitting the generic data-* hooks and the per-child index the motion CSS and behaviour read. Pure intent → attributes; no look here.
SPEC-105 §4: a single dimensions/motion.css + motion tokens that render each reveal character — covering all section runes from one stylesheet, with the transform-composition and global-hook rules that keep it from becoming per-rune work.
SPEC-105 §5–§6: the timing trigger and the no-JS/SSR safety. A tiny IntersectionObserver behaviour flips data-in-view; enhancement gating guarantees the static page is always complete. JS = when, CSS = how.
The prerequisite the WORK-410 spike surfaced: a clean skeleton/skin split needs spacing to be tokenized, exactly as the type split needed WORK-404/WORK-405. Skeleton references spacing tokens by name; skin owns their values — but today Lumina hardcodes magnitudes (0.5rem, 0.375rem, 0.8125em, …), so the skin layer can't retune them without restating structure. The gate for the re-bucketing.
Stand up the packaging + cascade-layer infrastructure the WORK-410 spike settled, so skeleton and skin can ship and version separately. The structural home the re-bucketing fills.
Register a small set of author-facing markdoc functions — currency, date, number, join — in @refrakt-md/runes as the shared value-formatting layer, usable anywhere markdoc transforms run: collection cells, body templates, and entityRoutes render strings. Keeps formatting out of fields and out of any bespoke projection DSL.
Generalize SPEC-066's sourceFile + extract into the embed() contract: an entity is embeddable if it has embed() or (sourceFile + extract). expand and the entityRoutes adapter accept either, so external plugins can make entities embeddable without a source file on disk.
Implements SPEC-095: an explicit group-order attribute so authors can sequence groups over a non-enum field (e.g. category, plugin, edge kind), which today render in arbitrary insertion order.
editHints drive click-to-edit in the block editor; every rune without them is one where clicking a section silently does nothing. Coverage today is 93/130 runes (71.5%): business, design, docs, places, and storytelling are at 100%, but core is at 58% (35/60), plan at 45% (5/11), media at 60% (3/5), learning at 67% (2/3), and marketing at 80% (12/15). Close the gaps using the established SPEC-009 playbook (visible elements as refs/data-name, hints declared in the rune's config).
The engine grew a rich soft-lint system (posture demotion, escape-hatch lints, nesting validation, entity-id collisions) but the editor actively swallows it: runPreviewPostProcess in packages/editor/src/preview.ts collects PipelineWarnings into an array and drops it, and hook errors hit a bare catch {} ("degrade silently"). Authors only discover problems at build time. Pipe warnings through to the UI as a validation rail.This is the warning channel that SPEC-098's inline lints build on.
Execute ADR-019: move bento + bento-cell from @refrakt-md/marketing to core (@refrakt-md/runes + @refrakt-md/lumina) as Layout primitives, with a back-compat re-export shim from marketing. Not a blocker for WORK-350; target a future minor.
SPEC-104 §6 + docs: document the bg guest body, the bare-surface guardrail, the sandbox preset (with the refrakt.config.json example), and the boot-frame layering; ship the music-blog backdrop pattern as a working showcase exercising the layout-cascade + SPEC-006 audio-bridge composition.
The "content, not CSS" finding from the WORK-410 spike (SPEC-094 §8's icon-from-config): rune CSS embeds glyph shapes as data:image/svg+xml mask-images. Move them to the theme icon registry so a theme swaps glyph sets without touching CSS.
Add the variants field to RuneConfig — modifier-keyed config deltas — and validate at config load that every variant axis is a declared modifier. This is the foundational type + validation layer for engine config variants.
Resolve each variant axis per instance and merge matching deltas over the base config before layout assembly, reusing the existing mergeThemeConfig machinery.
Migrate card/bento-cell to the SPEC-081 flat-slot + base-layout model (prerequisite for the cover variant), migrate feature's grid/stack conditional out of its transform into a media-position variant, and document the variants primitive.
Add RuneConfig.frameTarget routing, the host-owned clip contract, the oversize opt-out, a shared frame CSS layer, and reconcile with bento's existing media vars.
Add the tint-tracking inset surface: a --rf-surface-inset-shift mix-amount token and a use-site color-mix recipe applied to media wells and chart/diagram.
hero grammatically accepts media-position="cover" but has never been styled or tested for it (SPEC-101 §1–§3, §6). The engine side (scrim, foreground flip, posture demotion) already fires for hero — and hero emits the anatomy cover.css keys on — so this item is the hero-side delta: config variant parity with card, a band-appropriate height authority, padding rerouting, and an overlay legibility pass.
cover.css stretches only img/video to fill the media well; any other guest — a sandbox above all — sits at its own height. CSS alone can't fix the sandbox case: the custom element (packages/behaviors/src/elements/sandbox.ts) sets an inline iframe height (fixed px, or live auto-resize via rf-sandbox-resize postMessage when data-height="auto"), which fights a host-owned height. This item makes any guest fill a cover well, with sandbox height="fill" as the mechanism for the live case (SPEC-101 §4). Benefits every cover host (card, bento-cell, hero alike).
Extend the SPEC-048 TokenContract with a real typographic system. Today the contract carries only font.sans / font.mono — no scale, leading, weight, or tracking — so typographic identity (the primary differentiator for editorial/magazine themes) is not themeable. This is the gate for the SPEC-094 Tier 1 foundations.
Replace Lumina's hardcoded typography with the WORK-404 tokens. Today there are ~351 hardcoded font-size declarations against 5 tokenized — so an editorial theme wanting a different scale must override hundreds of declarations across 90 files. Tokenizing here is also what shrinks the WORK-410 skeleton/skin spike's classification surface.
A CLI command that emits a deterministic static rune gallery: every rune across its variant matrix, on one page, rendered through the HTML adapter. This is the AI-iteration surface and the deterministic subject the harness (WORK-409) photographs.
The gating spike for the v0.23.0 wholesale extraction. The skeleton/skin cut is a per-declaration design call (structure and aesthetics are braided inside single rules), so settle it empirically on a small slice before committing to the public contract every future theme depends on.
The foundation of SPEC-102: pin down the fixture format and unify the four fragmented stores into one source the CLI, gallery, editor, and reference all read. The WORK-407 gallery exposed the cost of the status quo — getFixture reads only cli/lib/fixtures.ts (13 runes), ignores RUNE_EXAMPLES (24), and 25 runes fall back to a naive stub that renders wrong (<p> inside an inline badge span).
With the format + loader in place (WORK-411), author a good fixture for every rune so the gallery, editor, and inspect render the whole catalogue correctly. The WORK-407 gallery surfaced ~25 runes with no real fixture (rendering the broken stub) and several child runes shown standalone.
Implement the author-defined levels ladder for bento's heading-sugar path, per ADR-013. One attribute replaces the need for the removed sizing/span mode: a user who wants the old uniform-width grid writes levels="6,5,4,3,2,1".Queued deliberately — the design is settled in ADR-013; left to marinate before implementation.
The deferred half of WORK-296. plan-progress now lowers to an aggregate composition whose per-status badges render neutral — the badge rune keys colour off data-meta-sentiment, and aggregate does not project a per-group sentiment onto $item, so the body has nothing to feed it. This is SPEC-076's listed future extension — "per-group sentiment projected onto $item, derived from the rune's domain ordering when the group field has a sentiment-mapped enum." The status→sentiment maps already exist in the plan config (plugins/plan/src/config.ts metaFields.status.sentimentMap per type: work done→positive, bug fixed→positive, blocked→negative, …) but are invisible to the generic resolver.
Re-express showcase as frameTarget: 'self' consuming frame, with deprecated aliases for its old attributes (breakout bleed retained), and document the surface model.
Split the conflated overlay string into a structured flat-wash overlay and a structured scrim legibility facet, with a deprecation path for the raw-string passthrough.
A generic, content-agnostic section rune: the shared page-section header (eyebrow → headline → blurb → image) followed by an arbitrary body. It exists so a preamble-less grid primitive like bento can be introduced with a title and intro — the role the feature "runes that work together" block plays on the site index today. Unblocks WORK-350 (the bento capstone), which replaces that feature section with a section + bento.
The showcase scene for the animated hero background (SPEC-101 §7): the brand metaphor, literally. A slowly rotating prism; a thin stream of markdown glyph particles (#, *, >, {%) enters one face as a faint ishi-tinted beam (stone grey — undifferentiated input); fanned spectrum streams leave the other in the Niwaki syntax roles — wakaba (keyword green), sakura (function pink), matsu (pine — link/constant), momiji (string peach, punchy string-expression orange as an accent). Markdown in, structured meaning out, in the exact colours this site renders syntax in.
The capstone of SPEC-101: document hero-cover and ship the animated prism background as a live, authored example. There is currently no docs example of media-position="cover" on hero at all.
Long-standing leak in the density dimension, surfaced by the SPEC-101 hero-cover docs: sections.css scaled titles with a descendant selector ([data-density="compact"] [data-section="title"] { font-size: 1.25rem }), which punches through nested densities — a full-density hero inside a compact preview renders its headline at 1.25rem, wildly unrepresentative of the real rune. The existing "nested density scoping" resets in density.css covered descriptions and meta ranks but never the title, and a value-based reset can't work for titles (each rune sets its own size).
Make tokens.ts the single source for Lumina's token CSS. Today tokens/base.css and tokens/dark.css are hand-authored mirrors of luminaTokens, kept in lockstep by token-config-coverage.test.ts — the file header even notes "a future build script that regenerates this file." Wire generateThemeStylesheet into the build so the mirror and its coverage test retire and drift becomes impossible.
Extend the gallery generator (WORK-407) to a second subject class: layout fixtures — representative sample pages rendered through each LayoutConfig so page chrome (header, nav, sidebar, TOC, mobile panel, footer) is visually covered. Chrome carries as much of a theme's identity as runes, and the skeleton/skin extraction touches styles/layouts/*, so this is what lets the harness guarantee hold for chrome, not just content blocks.
ai/src/modes/write.ts hardcodes an "Example structure" in its prompt. Replace it with the unified fixtures — drawing each rune's rich/canonical fixture (with its notes) as in-context few-shot exemplars. No training pipeline; retrieval into the prompt only.
Guard the corpus: parse the frontmatter fields (the format established in WORK-411 strips the block; this work consumes it), validate them against the schema, support <rune>.<scenario>.md scenarios, and ensure every fixture parses + transforms. plugin-validate reports role coverage instead of only warning "no fixture".
The WORK-407 gallery renders the static pre-enhancement HTML, so custom-element / lifecycle runes render empty: diagram → <rf-diagram>, chart → <rf-chart>, nav → <rf-nav>, sandbox → <rf-sandbox>, map → ~nothing. Progressive-enhancement runes (accordion, details, tabs, …) render their no-JS fallback (all-expanded) rather than their real state. Including the behaviors client makes the lifecycle runes draw and the PE runes show their enhanced (representative) state.
The foundation for SPEC-106: recognise custom URL schemes in a Markdown image's src during the transform and resolve them to a renderable, before the generic <img> fallback. Custom schemes (placeholder:, icon:) already survive markdown-it's validateLink, so  arrives as an image node with src intact.
The inline icon shorthand discussed when the icon rune landed:  resolves to the named icon's inline SVG — the same source the {% icon %} rune uses.
 resolves to a generated, theme-tinted inline SVG placeholder sized to the named shape — deterministic, offline, screenshot-stable.
With the placeholder: resolver in place, swap the SPEC-102 fixtures' base64-PNG image blobs for clean  references, and confirm the image-consuming runes render the resolved SVG.
Promote BgPresetDefinition.style to a documented, intentional last-resort escape hatch and confirm the project-config backgrounds home with merge-over-theme semantics.
A cover-backdrop sandbox is inert (SPEC-090 posture demotion) and above the fold, so the WORK-381 activation affordances contradict it: visible is a no-op there and click's poster + "Run" control is a dead end on a pointer-events: none surface. Eager is the background mode; a non-eager sandbox under cover should warn at build time (SPEC-101 §5).
Document the fixture format, frontmatter fields, the role (coverage-vs-exemplar) split, and the <rune>.<scenario>.md convention under the plugin/theme authoring section.