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.
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 static gallery (the visual-regression harness from WORK-409) transforms pages with merged.config — i.e. coreConfig plus the enabled plugins' configs — but never calls assembleThemeConfig. The site assembles core → plugins → theme so theme-level overrides reach the engine; the gallery does not, so it can only ever render base output.This is a latent gap in the regression net: any override a theme applies via mergeThemeConfig (tints, icon swaps, and — after WORK-425 — per-rune defaultElevation/defaultProminence deltas) is invisible in the gallery. The gallery shows what a plugin ships, not what a theme actually renders.Not a blocker for WORK-425. Under the agreed Option A, base surface defaults live in core + plugin configs, which the gallery already loads, so default data-elevation/data-prominence emission is gallery-verifiable today. This item only matters once a theme overrides those defaults (Lumina currently inherits the base defaults unchanged).
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.
Once cancelled / superseded exist, review the plan corpus for items that should be retired rather than left dangling. The main pool is the 13 pending work items, all sourced from SPEC-008 (unbuilt runes): stat, math, concept, exercise, glossary, objective, prerequisite, quiz, partner, job, album, artist, video. A reviewed, per-item judgment pass — not an automated flip.
Build the core Vite plugin that intercepts .md files and emits JS modules with rendered HTML, frontmatter, SEO data, and serialized tree. This is the Level 1 (static, per-file) integration — no cross-page awareness.
Implement virtual:refrakt/styles module that imports theme tokens, per-rune CSS, and package CSS. Supports tree-shaking to include only CSS for runes actually used in content.
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.
Implement the level: 'pipeline' mode that runs the full four-phase cross-page pipeline at build time, enabling breadcrumbs, nav, glossary auto-linking, and other entity-dependent features.
With WORK-110 through WORK-114 complete, convert the 5 postTransform uses identified in SPEC-033 to their declarative equivalents. Validate identical HTML output before and after each migration.
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.
Export a thin initBehaviors() wrapper from @refrakt-md/vite/behaviors that frameworks can call after navigation to re-initialize interactive rune behaviors (accordion, tabs, datatable, etc.).
Internal-only cleanup that lands the rest of WORK-323 once WORK-331 has stopped emitting the dual-emit metas. Remove the now-dead legacy read/strip path so data-rune-fields is the engine's only field-data input. No external/contract benefit beyond Tier 1 — this is for a single internal representation and a smaller engine.
Build the Astro framework adapter — the first non-SvelteKit target. Astro is MPA-first and SSG-focused, making it the simplest adapter to build and validate.
The v0.20.0 surface model shipped with per-rune reference docs (card cover, recipe cover, bg, surfaces.md) but no single page that shows the vocabulary as a system. Build a dedicated showcase gallery that demonstrates the whole surface model on one page — the "wow" surface a visitor lands on to grasp what the model can do — organised by the model's own axes.
packages/transform/src/contracts.ts independently re-derives what the identity transform emits, from config — it imports types, helpers and merge, and never the engine. So refrakt contracts --check validates the contracts file against its own derivation, not against engine output: any engine change contracts does not mirror produces confidently wrong contracts while CI stays green. Now that the axes are facets, investigate whether each facet can declare its own contract contribution and collapse the two implementations into one.This is the spec's principal open question, and it may not pan out. Time-box the investigation and record the answer either way.
packages/transform/src/validate.ts already declares IDENTITY_FIELDS = ['block', 'modifiers', 'sections', 'variants'] and enforces it against SPEC-091 variant deltas — "a delta restructures a rune, never redefines it". The theme-override path does not: mergeRuneConfig shallow-merges { ...base, ...override } with no field restrictions, so a theme can write runes: { Card: { sections: {} } } and silently disable reading on every card in a site.One position, applied to one of two merge paths. This closes the other.
Move the schema↔engine join tables out of RuneConfig and into the tag modules that own them, with config referencing the declaration rather than owning it. This is what makes WORK-534 possible: createContentModelSchema needs the gating facts at construction time, and today it has none.
packages/transform/refrakt.config.schema.json is hand-maintained and had drifted from the RefraktConfig / SiteConfig interfaces in @refrakt-md/types. Nothing guarded it, so seven discrepancies had accumulated — fields that exist in the types but are absent from the schema (and therefore missing from editor autocomplete and from the configuration docs), one field the schema advertised that the types no longer have, and two nested objects the schema actively rejected.Separately, refrakt config migrate kept its own hand-copied SITE_FIELDS array, which had drifted from the authoritative one in config-normalize.ts. The two lists describe inverse operations over the same field set — the normalizer mirrors site → top level, the migration moves top level → site — so a divergence strands fields.
The /docs vs /extend split is sound in principle — extend/index.md states it outright ("If you're here to author content... head to the Docs handbook"). But seven pages sat on the wrong side of the line, and the entity-registry concept had no author-facing home at all.
A repo script flattens refrakt.config.schema.json into a committed JSON artifact; a docs page renders it with {% data %}. The configuration pages stop carrying hand-written field tables.
Frontmatter has no machine-readable description. Add packages/content/frontmatter.schema.json, publish it on the versioned routes BUG-004 built, and make Frontmatter derive its documented members from it.
Only 13 of ~115 rune pages have an ## Attributes section. Before generating anything, work out why — the answer sets the scope of WORK-548 and nothing sensible can be estimated without it.Two possibilities, with very different consequences:
Rune pages render their attribute tables from the reference data refrakt reference --format json already produces, instead of hand-copying schemas. Extends scripts/check-rune-docs.mjs from page coverage to page content.
Implement SPEC-129: an {% include %} rune that pastes a file's AST during preprocess, so data and snippet inside it resolve. Unblocks WORK-548's shared-block criterion.
A {% data %} inside another one's body should resolve, with $row bound to the nearest enclosing query — a correlated subquery, parameterised by the outer row.
Retire the eight module-level *_WARNED sets in engine.ts, route every engine diagnostic through the facet WarningCollector, and settle the dedupe-scope question the migration deliberately deferred. Also convert the ten test files that currently spy on console.warn into assertions on returned warning data.
refrakt contracts is documented as describing "the complete HTML structure the identity transform produces for every rune". It does not. Measured across the checked-in contracts/structures.json (132 runes), not one universal axis appears:
The twelve corrections in WORK-529–WORK-531 were found by an ad-hoc script comparing each rune's declared slots (from layout, structure, autoLabel, blocks, contentWrapper) against its sections map. Nothing stops the same drift returning the next time a rune gains a body slot.Turn that comparison into a standing check.
RefraktConfig, SiteConfig, PlanConfig, XrefPattern, EntityRoute, and RouteRule live in packages/types/src/theme.ts, which is not about themes. Move them to config.ts, leave the theme-system types behind, and stop index.ts re-exporting the whole batch under one // Theme system types comment.The file was created by c3fe915 (v0.25.0), a mechanical split of one types module into fifteen files. The same commit left packages/types/src/types.ts as a zero-line file that is still empty and still unreferenced.This matters because SPEC-126 claims the docs are generated from the schema and the schema is drift-tested against the types. A reader following that chain should not land in a file whose name says it is about something else.
An inline rune that renders its body as code, so a variable can be code-styled. Backticks cannot do it: a code span is literal by definition, so the Markdoc syntax inside one is never parsed.
{% accordion schema="none" %} emits no structured data — not on the accordion, not on its items.Narrow on purpose. The general answer is SPEC-130; this is the one value that is needed now, and "none" stays valid under whatever that spec settles, so shipping it forecloses nothing.