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.
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.
With WORK-604 landed, playlist's tracks field can declare emitTag: 'track' and receive one homogeneous, document-ordered array instead of a mix of list nodes and track tags.That deletes the interleaving machinery at plugins/media/src/tags/playlist.ts:316-353 — the listCursor consuming tracksData by list length, and the defensive tail for "a shape the resolver collected but this loop did not recognise".It does not delete adoptNestedTrack. See below.
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.
WORK-628 ran @adobe/structured-data-validator over contracts/seo-baseline/. Its rich-result layer was mostly noise for refrakt. Its schema.org layer was the opposite: zero false positives. It checks that a type exists and that every property is in that type's domain. Over the baseline's history it caught the NonProfit type and the nested Offer.offers over-match. On today's baseline it caught three defects nobody had recorded (BUG-033), on rows whose comments say nothing checks them.SPEC-145 D25 keeps the ban on user schema because this check is too narrow to stand in for a reviewer. It is still a cheap net under the reviewer for first-party rows. This item adds it to CI as a maintainer-side check. Nothing ships to users.
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 staged testimonial has been approved on staging.refrakt.md. Its final form places a featured quotation (WORK-700), so it goes live after that lands, on the path WORK-702 opens.
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.
Deferred from WORK-409 (its final acceptance criterion). The harness runs end-to-end locally and in the pinned Playwright container; what's left is the CI automation, which needs a browser-backed runner to develop against — hence its own item, unassigned to a milestone until a browser CI environment is in place.
runMigrateIds sorts colliding claimants by path and keeps the first (plugins/plan/src/commands/migrate-ids.ts:165), so on a branch it can renumber the entity already on main and leave the local draft holding the ID.The safety argument in the code — that it only acts when nothing references the ID — rests on a check that reads plan files only (migrate-ids.ts:170-182). Plan IDs are referenced from four published CHANGELOGs (runes 130, transform 120, lumina 80, content 51), where a renumber is permanent and invisible.
A metaFields entry can render its value as marks: rating: { total } makes the engine emit total spans, the first value of them data-filled="true" (buildRatingValue, packages/transform/src/engine.ts). The mechanism is generic (its own doc comment says "star ratings, progress dots, etc."), but the output is not:
The composed bond, character, faction, lore and realm were built for SPEC-147. They live in packages/content/test/fixtures/composed-storytelling/runes/. Under SPEC-181 they are staged replacements for plugin runes, so they move to site/staged-runes/. They stay staged until SPEC-147's plugin-level decision (SPEC-181 D5).
SPEC-187 D2, D3, D8 and D9: site/content/_partials/rune-intro.md, included as {% include file="rune-intro.md" variables={r: "rune:<name>"} /%}, renders a section whose eyebrow is the package, headline the title, blurb the description, and whose body is a bar. The spec's sketches show the intended result for a composition, a native plugin rune, a core rune, a non-stable rune and a staged rune.
SPEC-187 D1 and D6: all 119 pages under site/content/runes/ open with the intro include. The hand-written # Title, the "This rune is part of @refrakt-md/…" hint (on 45 pages) and any opening paragraph that only restates the description are removed.
accordion's mainEntity, how-to's step and tool, and recipe's recipeIngredient and recipeInstructions are collections that still serialise as a bare object when they hold one item and an array when they hold two.Every other collection in the catalog has been declared: pricing's offers, playlist's track / hasPart, breadcrumb's and timeline's itemListElement. These five are the remainder.
SPEC-186 D4.3: content-missing ships as a warning for one release, then becomes an error, as content-unmatched did (WORK-638). WORK-694 shipped the warning, and its sweep found no instance in site/content, plan/, the SEO fixtures, the rune and plugin fixtures or the composed fixtures.
SPEC-187 D7: the committed intro data describes main, so staging.refrakt.md would call a staged rune native. On staging, a rune runes.prefer resolves to site/staged-runes/ shows a Staged badge and that file as its source.
ADR-029 records three couplings as open questions rather than asserting them. Each is answerable by reading code and running existing tooling; none needs a design. This item answers all three and writes the answers back into the ADR, so the implementing spec is built on measurements rather than expectations.It is a research item. The deliverable is three findings, not a code change.
hero becomes a section with a split, cover, the header and {% actions %}, with no rune CSS of its own (SPEC-160 D7). WORK-654 leaves a composed hero fixture; this item completes it and compares it with the plugin.
ADR-042 makes a composed rune's config its definition's: a theme cannot know which definition it faces, so an override of a composed rune's blocks or metaFields is a guess that silently resolves to nothing. Nothing enforces this yet. mergeThemeConfig and mergeRuneConfig (packages/transform/src/merge.ts) guard the identity fields on every rune, and nothing else, so a theme's runes: entry keyed to a composed rune's type name applies.v0.44.0 adds the first composed runes a theme could try to override (hero, cta, testimonial), so the guard lands with them.
A composition template has no way to arrange ordinary slots as a row, except by placing the bar rune, which is a name-resolution boundary (SPEC-145 D19). And it has no way to wrap an each slot's items in a list. SPEC-156 D2 and D3 answer both with one template tag, {% group name="…" layout="bar|ladder" %}, an engine wrapper with an arrangement.
Whether an ordinal is information is a fact about the content, so sequence is identity (SPEC-158 D2). A composition declares it in its definition, where a theme's replacement template cannot remove it (SPEC-156 D12, ADR-045). And a ladder's data-sequence lands on the ladder only, not on every ordered list inside the rune (D14).
A second site, staging, renders site/content with the project runes in site/staged-runes/ preferred over the plugin runes they replace (SPEC-181 D1). It uses only existing configuration: runes.dir and runes.prefer (__project__).
The composed hero, cta and testimonial move out of the test fixtures into site/staged-runes/. This makes the review of WORK-665, WORK-666 and WORK-668 possible: the runes' reference pages, the homepage and every other page that uses them, rendered with the compositions on the staging site.
The composed hero takes align="start | center | end", which is section's vocabulary. The plugin hero takes left | center | right. The staging site renders the same content as main, so align="left" on the homepage and on the hero reference page would be invalid there (SPEC-181 D7). The plugin hero learns the new values first, and the content moves to them.
The composed testimonial (WORK-668) takes author and role attributes and reports a **Name** — Role paragraph as content-unmatched. The plugin testimonial takes no author attribute. So WORK-668 built refrakt marketing migrate-testimonial but did not apply it: the rewritten content would not have rendered through the plugin.The staging site renders the same content as main (SPEC-181 D7). The plugin testimonial therefore learns the attributes first, and the codemod is then applied.
The content model reads structured values out of a list item (itemModel) and a heading (headingExtract), but not out of a paragraph, and what it extracts can only reach an emitted child or $each (SPEC-182). This item adds both missing pieces, reusing the list-item vocabulary.
The staged testimonial reads **Name** — Role through WORK-686's paragraph item model, and keeps author and role as the alternative form (SPEC-182 D6). The content migration and the codemod that pointed the other way are reversed before v0.44.0 is released (D7).
site/staged-runes/hero.md has been reviewed on staging.refrakt.md and shows no difference from the plugin hero (WORK-689). SPEC-181 D5 makes going live its own item. This is the first one, so it is also the first use of a plugin runeDir (SPEC-153 D2) and of the contract and baseline diffs a go-live produces.
Every rune that declares sequence: names the container holding its items — except one. Steps builds its <ol> at plugins/marketing/src/tags/steps.ts:166 and pushes it straight into children two lines later, so the element carries no data-name, gets no BEM element class, and cannot be addressed from a layout tree.
ADR-046 decides that an eyebrow is a paragraph directly before the headline, never a heading. The docs still teach the heading form. The guide's only example of it uses hero, which renders it wrongly (BUG-047), and nineteen rune pages say "a short paragraph or heading" in the same boilerplate sentence.The native page-section runes keep their grammar until each one is composed (ADR-046, decision 4), so some of them still accept the heading form. The docs stop recommending it anyway: it is not the direction, and documenting less than a rune accepts is safe where documenting more is not.
The native hero and feature host a media guest with guestFit: 'bleed', a config field. A config field cannot differ between compositions that place the same section, and hero needs bleed where a card-like composition needs clip (SPEC-160 D7).
A blocks entry renders through bar (a row) or definition-list (a label/value grid). Neither draws a byline, a lead value with secondary values beneath, which testimonial and cast each hand-roll in their CSS (SPEC-161 D6).
testimonial becomes one section with a portrait media zone, the rating and byline metablocks, and the quote slot (SPEC-161 D1). Its author and role move from a **Name** — Role paragraph to attributes, so the schema table can publish them.
A theme that configures no rune cannot promise each rune looks right. It needs to cover the vocabulary instead (ADR-043 decision 5). Lumina's coverage test (packages/lumina/test/css-coverage.test.ts) checks BEM selectors derived from rune configs; it does not check vocabulary values.
steps is a section with a numbered sequence of steps, each a section with a split (SPEC-161 D5). Its numbering needs a ladder, which WORK-676 and WORK-677 provide, and SPEC-169 D4's example is the template.
Every conversion of a plugin rune into a composition is staged until it goes live, and goes live on its own (SPEC-181 D5, D6). Writing that down is not enough to make it happen, so this item also adds a test that fails when a conversion skips staging.
At 3.25rem a marquee title cannot fit a long word on a phone: the cover example's "Documentation" overflows a 390px screen on refrakt.md and staging alike. Found reviewing staging on a phone. Below 640px of container width the two loud steps ramp smaller: marquee at 5/3 of the resting size (2.5rem) and display at 4/3 (2rem). The plugin hero and cta take the same sizes, so the two sites stay identical until they go live.
SPEC-186 D1 and D3: a field may be a group, which matches all of its required members in order or takes nothing; and a field may claim its container's last node with position: last.
SPEC-186 D5: section's header is read from a declared header group (eyebrow, headline, blurb), and sectionHeaderParts and its header-role reader are removed.
SPEC-185 D3: each quotation role carries data-quote on its outermost element, the core blockquote node marks a plain quotation, and Lumina styles roles rather than the bare element.
SPEC-185 D2, D4, D6 (the after form) and D9: pullquote renders a featured quotation as a <figure>, reads its attribution from the paragraph after the quote, takes prominence, gains the quote alias, and deprecates variant.
SPEC-185 D5: a blockquote with an attribution in the epigraph position (after a section's headline and blurb, or at the start of a page's content) is an epigraph.
SPEC-185 D6 (the inside form): — **Name**, Source as a blockquote's last paragraph is its attribution, read by a nested model with position: last, so it works in a composition as in the core node.
SPEC-185 D7: the staged testimonial places {% pullquote %} for its quote, and the pullquote owns the attribution; the testimonial keeps its rating and its Review schema, read from the pullquote.
SPEC-187 D10–D12. The rune intro's Install and Source are text links built with ref and file-ref (WORK-706): neither rune can carry an icon, the two render in different styles, and actions cannot place them, because only those runes open a drawer. This item makes an ordinary link open a drawer, gives actions a bar context, and moves the intro onto both.
ADR-044 lets a plugin rune bring its own structure CSS only for genuinely novel parts, written only in theme tokens. Two checks make the tiers checkable rather than advice. Neither exists.
ADR-047 lets a rune declare one narrative field that accepts only the editorial runes textblock and mediatext, and says the field is never published as structured data. As amended, the field is inferred from its match, and both rules are checked.
The "Content boundaries" section of site/content/extend/rune-authoring/patterns.md puts "Story, editorial, famous quotes" outside recipe. ADR-047 adds a case: a rune may declare one narrative field that takes {% textblock %} and {% mediatext %}, so a template can place the story between the rune's parts. A site's own composed rune can declare one today.
Track's config is { block, parent, defaultElevation, editHints }. Its artist, duration and date are hand-built spans, and track.css hand-writes the row and the duration's push to the end. The byline form (name above, artist, year and duration beneath) needs no new mechanism, only adoption (SPEC-156 D7).