v0.30.0
Released August 27, 2026 · All releases
Plan lifecycle validation & render-stack removal. Two threads of plan-tooling hardening that finish work the previous milestones deliberately left open. First, teach plan validate to catch lifecycle drift — statuses that contradict the terminal evidence around them (Lifecycle-drift validation for the spec–work–milestone graph), the symmetric counterpart to the one-directional lifecycle checks already in place. Second, remove the bespoke plan build / plan serve render stack (Retire the bespoke plan render stack) that v0.16.0 deprecated but kept alive — with the Plan site scaffolding plan-site transition shipped, a plan site is now an ordinary refrakt site, so the parallel SSG can go.
Changes
- Add lifecycle-drift validation to
plan validate(SPEC-119). plan validatenow flags entities whose status contradicts the terminal evidence around them — the symmetric counterpart to the existing one-directional lifecycle checks. New v1 checks:spec-status-lag(warning) — a pre-implemented spec (draft/review/accepted) whose linked work is entirely achieving-terminal.spec-started-in-draft(info) — adraftspec with started work.spec-status-ahead(warning) — animplemented/shippedspec with a non-terminal linked work item.released-in-without-shipped(warning) —released-inset on a non-shippedspec (mirror ofshipped-without-release).stale-blocked(warning) — ablockedwork item whose## Blocked bytargets are all achieving-terminal.milestone-complete-with-open-work(warning) — acompletemilestone with a non-terminal member (replacescomplete-milestone-open-item, now keyed on terminal rather than done/fixed so retired members don't falsely flag).plan status'ssuggestImplementedhint and validate'sspec-status-lagshare one predicate, so the suggestion and the warning can never diverge.--strictpromotes the new warnings to errors.- Remove the bespoke
plan build/plan serverender stack (SPEC-120). - Breaking: the
refrakt plan buildandrefrakt plan servecommands are gone, along with their private static-site generator (the three-family render-pipeline router,planLayoutshell, the port-3000 dev server + file watcher + SSE reload, the behavior bundler, and the pagefind invocation). Invoking either command now yields an unknown-command error. - A plan directory is an ordinary refrakt site since SPEC-071: scaffold a deployable plan site with
create-refrakt --type plan(or `refrakt plan init
Lifecycle-drift validation for the spec–work–milestone graph
Lifecycle-drift validation for the spec–work–milestone graph
Teach plan validate to flag entities whose status lags behind (or runs ahead of) the terminal evidence around them — a spec still draft after all its work shipped, a work item still blocked after its blockers finished, a milestone still active after everything in it is done. Today these contradictions are invisible to tooling and only surface through periodic manual audits.
Problem
SPEC-049 gave specs the terminal states implemented and shipped and gave plan status a suggestImplemented hint. But that hint lives only in the reporting tool — a surface you run when you choose to. Nothing fails a check when reality and status diverge, so drift accumulates silently:
- A manual audit of
plan/found 38draftspecs whose linked work was 100%done— shipped features still labelled "not yet agreed to build" — plus 32acceptedspecs that were never advanced toimplemented/shipped. None of this was caught byplan validate; it took a hand-rolled cross-reference of work statuses to find. - The drift is bidirectional and not spec-only. Work items keep a stale
blockedstatus after their## Blocked bytargets finish (silently excluding them fromplan next), and milestones sitactivelong after every assigned item isdone.
plan validate already polices lifecycle consistency in one direction — shipped-without-release, superseded-without-target, resolution-not-done, done-without-resolution all assert "your status claims X, so the evidence X requires must be present." This spec adds the symmetric check: "the terminal evidence has outrun (or contradicts) the status."
Design Principles
Validate flags contradictions; status suggests opportunities. This is the dividing line that keeps validate low-noise. A spec that could advance is an opportunity — it stays a plan status nudge. A spec whose status is demonstrably at odds with terminal evidence is a contradiction — that becomes a validate warning. Validate fires on lies, not on to-dos.
A deliberate, scoped revision of SPEC-049's stance. SPEC-049 chose "manual transitions over coupled automation" and "carrot before stick" for its v1, on purpose. This spec does not overturn that — it does not auto-flip statuses, and it does not add a "must have pr" nag. It only makes the contradiction subset visible in the CI-gating tool now that the vocabulary has bedded in. Suggestions stay carrots; contradictions become warnings.
Key on achieving-terminal, not all-terminal. A spec whose linked work is entirely cancelled/superseded must not be nudged toward implemented — it should probably be retired itself. Drift-toward-done checks key on ACHIEVING_STATUSES (done/fixed); drift-toward-retired checks key on the non-achieving terminals. Reuse the isTerminal/isAchieving helpers introduced by SPEC-117; do not hand-roll status sets.
Cap assertions at implemented; never infer shipped. Deriving released-in reliably needs changelog + release heuristics a validator cannot prove. Validate may assert "this should be at least implemented"; choosing shipped and its version stays a human step (as SPEC-049 intended).
No warning without linked evidence. Every drift check requires ≥1 linked entity (work item, dependency, or milestone member). Specs with no linked work produce no lifecycle warning — absence of evidence is not a contradiction.
Scope — the drift checks
Linkage is the existing source="SPEC-xxx" edge for spec↔work, the SPEC-114 directed ## Blocked by edges for work↔work, and the milestone attribute for milestone membership. achieving = done/fixed; terminal = achieving ∪ cancelled/superseded/wontfix/duplicate; retired = the non-achieving terminals.
Spec status vs. work evidence
| Type | Predicate | Severity | Priority |
|---|---|---|---|
spec-status-lag | Spec is draft/review/accepted, has ≥1 linked work item, and all linked work is achieving-terminal | warning | v1 |
spec-started-in-draft | Spec is draft and ≥1 linked work item has started (in-progress/achieving-terminal) | info | v1 |
spec-status-ahead | Spec is implemented/shipped but ≥1 linked work item is non-terminal | warning | v1 |
retired-spec-live-work | Spec is superseded/deprecated but ≥1 linked work item is non-terminal | warning | v2 |
terminal-spec-no-work | Spec is implemented/shipped with zero linked work items | info | v2 |
Attribute / status consistency
| Type | Predicate | Severity | Priority |
|---|---|---|---|
released-in-without-shipped | released-in is set but status ≠ shipped (mirror of the existing shipped-without-release) | warning | v1 |
Work status vs. dependency evidence
| Type | Predicate | Severity | Priority |
|---|---|---|---|
stale-blocked | Work status=blocked but all ## Blocked by targets are achieving-terminal | warning | v1 |
false-ready | Work is ready/in-progress but ≥1 ## Blocked by target is non-terminal | info | v2 |
Milestone completion drift
| Type | Predicate | Severity | Priority |
|---|---|---|---|
milestone-complete-with-open-work | Milestone status=complete but ≥1 assigned item is non-terminal | warning | v1 |
milestone-done-not-complete | Milestone active/planning, ≥1 assigned item, and all assigned items are achieving-terminal | info | v2 |
The v1 set is chosen for high signal and low false-positive rate; the v2 set is more opinionated (a retired spec may keep open work on purpose; a milestone may stay active for non-work reasons) and can follow once the v1 warnings are proven non-noisy.
Acceptance Criteria
plan validateemitsspec-status-lag(warning) for a non-terminal spec whose linked work is entirely achieving-terminal.plan validateemitsspec-started-in-draft(info) for adraftspec with started work.plan validateemitsspec-status-ahead(warning) for animplemented/shippedspec with a non-terminal linked work item.plan validateemitsreleased-in-without-shipped(warning) whenreleased-inis set on a non-shippedspec.plan validateemitsstale-blocked(warning) for ablockedwork item whose## Blocked bytargets are all achieving-terminal.plan validateemitsmilestone-complete-with-open-work(warning) for acompletemilestone with a non-terminal member.- All drift predicates use the
isTerminal/isAchievinghelpers from SPEC-117; no status-set is re-declared invalidate.ts. - No lifecycle warning fires for an entity with zero linked evidence.
cancelled/supersededwork does not trigger any drift-toward-done check.- Each new issue
typehas a fixture-backed unit test (positive and negative case). --strictpromotes the new warnings to errors, consistent with existing behaviour.- The
suggestImplementedlogic inplan statusandspec-status-laginplan validateshare one predicate (no divergent second implementation).
Out of scope
- Auto-flipping statuses. Validate reports drift; it never mutates. (Preserves SPEC-049's manual-transition principle.)
- Inferring
shipped/released-in. Left to the human release step. pr-absence nagging. SPEC-049 deliberately declined this; unchanged here.- A dedicated
plan doctor --fixautofixer — a plausible follow-up, but a separate spec.
Open questions
- Should the v1 warnings ship off by default (opt-in via
--lifecycleor config) for one release to gauge real-world noise before they gate CI, or straight to on? Recommendation: on, but land alongside a one-time migration pass so the repo starts clean. spec-started-in-draftvs. legitimate exploratory work: isdraft+ started work always wrong, or a valid "spike first, formalise later" pattern? If common, demote to opt-in.
Retire the bespoke plan render stack
Retire the bespoke plan render stack
Remove the plan build / plan serve commands and the private static-site generator behind them. This is the deferred removal phase that SPEC-071 anticipated but deliberately scoped out of its own (now shipped) deliverable: SPEC-071 rebuilt the plan site as an ordinary refrakt site (entityRoutes + collection, scaffolded by create-refrakt --type plan) and deprecated the bespoke commands in v0.16.0, but kept them alive so existing users weren't broken. With the site approach proven and shipped, the parallel render stack can now go.
Problem
The plan plugin still ships a miniature SSG that predates the cross-page pipeline: plan build and plan serve call a private runPipeline() that bypasses loadContent, builds its own three-family router (entity / per-status / grouped pages), wraps output in a private planLayout shell, runs its own pagefind index, and — for serve — stands up an HTTP server on port 3000 with a file watcher and SSE live-reload. Everything it does by hand is now a first-class framework capability (SPEC-069 routes, SPEC-070 collections, the standard adapter dev server and asset pipeline). Keeping it means maintaining two SSGs.
WORK-273 shipped the deprecation notices (v0.16.0). This spec covers the removal those notices promised.
Scope
- Remove the
plan buildandplan servecommands from the plan CLI (registration +--help). - Delete the bespoke render stack: the command modules,
render-pipeline.ts(three-family router +resolveThemeCss),bundle-behaviors.ts, the private HTTP dev server + file watcher + SSE reload, theplanLayoutshell, and the pagefind invocation. - Drop now-orphaned dependencies (e.g. pagefind) from
plugins/plan/package.json. - Move the docs fully to the site approach and record the removal as a breaking change (changeset).
Non-Goals
- The authoring / management CLI stays.
create/next/update/validate/status/migrate/next-id/history/initare unaffected — only rendering is retired. - The plan runes and pipeline hooks stay. The
register/aggregatehooks and entity registration feedentityRoutes/collection; they are retained intact. - No changes to the plan site scaffold itself — that shipped under SPEC-071.
Acceptance Criteria
plan build/plan serveare gone from the CLI; invoking them yields an unknown-command error- The bespoke render stack (commands,
render-pipeline.ts,bundle-behaviors.ts, private dev server,planLayout, pagefind) is deleted - Orphaned dependencies are removed from
plugins/plan/package.json - The authoring CLI, plan runes, and register/aggregate hooks are unchanged and green
- Docs no longer present
plan build/plan serveas the way to view plan content and point at the site approach - A changeset records the removal as a breaking change to
@refrakt-md/plan npm run buildandnpm testpass
References
- SPEC-071 — plan site scaffolding (shipped); § Deprecation of plan build / plan serve step 3 anticipated this removal and scoped it out
- WORK-273 — the deprecation phase (v0.16.0) that deferred removal to a later release
- SPEC-069 —
entityRoutes/ contributed routes, the per-entity render replacement - SPEC-070 —
collectionrune, the dashboards / listings replacement
Plan site scaffolding
Plan site scaffolding
Turn a project's plan/ directory into an ordinary refrakt site — built by the standard content pipeline, using entityRoutes (SPEC-069) for per-entity detail pages and collection (SPEC-070) for dashboards and listings — and retire the bespoke plan build / plan serve commands. Ship it as a create-refrakt scaffold, and prove it by converting refrakt's own plan/ into a site.
This spec is the concrete realization of SPEC-069's Replacing plan-serve section. It is deliberately a proof-of-practice: if entityRoutes + collection can fully replace the hand-rolled plan SSG, the two specs are validated against a real, demanding consumer, and we delete code rather than add it.
- Released in
- v0.16.0
Problem
The plan plugin ships its own miniature static-site generator. plan build (plugins/plan/src/commands/build.ts) and plan serve (plugins/plan/src/commands/serve.ts) call a bespoke runPipeline() that bypasses loadContent, builds its own three-family router (entity pages, per-status filter pages, grouped "view" pages), wraps output in a private planLayout HTML shell, runs its own pagefind index, and — for serve — stands up a separate HTTP server on port 3000 with a file watcher and SSE live-reload.
This predates the cross-page pipeline. Everything it does by hand is now a first-class capability:
- Per-entity pages →
entityRoutesrules (SPEC-069), one route per spec/work/bug/decision/milestone, body{% expand $item.id /%}. - Status / grouped listings & dashboards →
collection/backlog(SPEC-070) on authored dashboard pages. - Search → the standard site search (the
docslayout already wires a search behavior); contributed pages are in the search index per SPEC-069. - Dev server + reload → the chosen adapter's dev server (
refrakt dev). - Layout → the
docsLayoutpreset + a plan_layout.mdwith sidebar nav.
So the plan plugin carries a parallel rendering stack that the framework now supersedes. Keeping it means maintaining two SSGs; replacing it means the plan site becomes "just a refrakt site," and the bespoke commands can go.
The plan plugin already does the hard part: its register hook (plugins/plan/src/pipeline.ts) indexes every plan entity with a rich data payload (status, priority, severity, title, source, tags, milestone, checkbox progress) and sets sourceUrl. The aggregation runes (plan-progress, plan-activity, plan-history, decision-log, backlog) already exist. What's missing is the routing and the embeddability — both of which SPEC-069/070 now provide.
Design Principles
Reuse the registry, don't re-scan. The plan plugin already scans plan/ and registers entities. The plan site is built on that registry — entity files are registry sources, not content pages. This is the model that proves SPEC-069: routes are generated from registered entities, exactly as they would be for an external (Jira/Notion) source.
entityRoutes for detail, collection for listings. One page per entity is entityRoutes' job (1:1, {% expand %}). Many entities on a page (dashboards, by-status, by-milestone) is collection's job (N:1). The plan site uses both, which is precisely why it's a good joint proof.
Keep the authoring CLI; retire only the renderer. plan create / next / update / validate / status / migrate / next-id / history are authoring and management tools — they stay. Only the rendering commands (plan build, plan serve) are deprecated, because rendering is now the standard site's job.
The scaffold is the product. Most users won't hand-write the entityRoutes config and dashboard pages. create-refrakt ships a plan-site scaffold with a working config, layout, dashboards, and seed content — the same artifact refrakt uses for its own plan site.
What the replacement must match
Inventory of the current commands' user-facing surface, and where each lands:
plan build/serve feature | Replacement |
|---|---|
| One page per spec/work/bug/decision/milestone | entityRoutes rule per type, body {% expand $item.id /%} |
Dashboard (index.html): progress, recent activity, milestones, blockers, decisions | Authored index.md using plan-progress, plan-activity, backlog, decision-log runes |
Per-status filter pages (/work/ready/, /bug/confirmed/) | One grouped/filterable collection/backlog view per type (see Resolved Decisions), not discrete per-status URLs |
| Grouped "view" pages (by tag / assignee / milestone) | collection group="…" on authored pages |
| Relationships + history tabs on entity pages | expand-embedded entity body already carries these; plan-history rune for the timeline |
| Full-text search (pagefind) | Standard site search (docs layout) |
| Dev server + file watcher + SSE reload | Adapter dev server (refrakt dev); plan-entity edits need a rebuild in v1 (see Resolved Decisions) |
theme.css + behaviors.js emission | Standard adapter asset pipeline |
| Sidebar nav + plan layout shell | docsLayout preset + plan _layout.md |
Authoring Surface
The plan-site config
A plan site is an ordinary SiteConfig with @refrakt-md/plan in plugins and an entityRoutes block:
{
"contentDir": "./plan-site",
"theme": "lumina",
"plugins": ["@refrakt-md/plan"],
"entityRoutes": [
{ "type": "spec", "url": "/specs/{id}/", "title": "{title}", "render": "{% expand $item.id /%}" },
{ "type": "work", "url": "/work/{id}/", "title": "{id} — {title}", "render": "{% expand $item.id /%}" },
{ "type": "bug", "url": "/bugs/{id}/", "title": "{id} — {title}", "render": "{% expand $item.id /%}" },
{ "type": "decision", "url": "/decisions/{id}/", "title": "{title}", "render": "{% expand $item.id /%}" },
{ "type": "milestone", "url": "/milestones/{name}/","title": "{name}", "render": "{% expand $item.name /%}" }
]
}
contentDir holds only the layout and the authored dashboard pages — not the entity files (those live in plan/ and are reached through the registry). The route rules generate the detail pages; the dashboards list them.
Dashboard pages (the collection/backlog side)
The scaffold's contentDir ships authored pages, e.g.:
index.md— overview:{% plan-progress /%},{% plan-activity /%}, a "ready work"{% backlog filter="status:ready" sort="priority" /%},{% decision-log /%}.work.md—{% collection type="work" group="status" sort="priority" /%}(replaces per-status filter pages with one grouped, filterable view).milestones.md—{% collection type="milestone" %}plus per-milestone work via{% collection type="work" filter="milestone:{id}" /%}inside a milestone route's template (the SPEC-069 grouping pattern)._layout.md—{% layout %}using thedocsLayoutpreset with a plan sidebar ({% region %}for nav listing the entity-type sections).
The create-refrakt scaffold
create-refrakt selects output by --type (today: the adapter projects). Add a plan project type (orthogonal to the adapter --target) that scaffolds:
refrakt.config.jsonwith@refrakt-md/plan+ theentityRoutesblock above.- A
plan/seed (one example spec + work item + decision + milestone) sorefrakt devshows a populated site immediately. - A
plan-site/content dir with_layout.md+ the dashboard pages above. - The adapter scaffold for the chosen
--target(socreate-refrakt --type plan --target sveltekitis a complete, runnable plan site).
Dependency versions derive from create-refrakt's own package.json at runtime, as the other templates do.
Prerequisite — plan entity embeddability
{% expand $item.id /%} requires each plan entity to be embeddable. SPEC-069's embeddability contract is either embed() or sourceFile + extract. The plan plugin's register hook currently sets sourceUrl but not sourceFile/extract (the "WORK-251" gap). This spec includes filling it: the register/scan sets sourceFile (the entity's source .md path) and an extract that returns the entity rune's transformed body, so {% expand %} renders the spec/work/bug/etc. inline. Without this, the entity routes produce empty pages.
refrakt's own plan site (the dogfood)
Add a second site to refrakt's root refrakt.config.json:
{
"sites": {
"main": { /* unchanged */ },
"plan": {
"contentDir": "./plan-site",
"theme": "lumina",
"plugins": ["@refrakt-md/plan"],
"entityRoutes": [ /* the rules above */ ]
}
}
}
refrakt's existing plan/ entity files are unchanged (they remain the registry sources). A new plan-site/ dir holds the layout + dashboards. Building the plan site renders refrakt's real specs/work/decisions/milestones through the standard pipeline. When this site builds and matches what plan serve produced, the proof is complete.
The two sites build independently (per-site pipeline). The plan site can be served at its own base path or as a separate output, decoupled from the docs site.
Deprecation of plan build / plan serve
- Same release that ships entityRoutes + collection + the plan-site scaffold: mark
plan buildandplan servedeprecated. On invocation they print a deprecation notice pointing atcreate-refrakt --type plan(for new projects) and theentityRoutesconfig (for existing ones), then still run. - The authoring CLI (
create/next/update/validate/status/migrate/next-id/history) is unaffected. - A later release removes
build/serveand the bespoke render stack (render-pipeline.ts's router,planLayout, the private dev server, the pagefind invocation). The plan plugin keeps its runes, register/aggregate hooks, and authoring CLI.
Output Contract
- The plan site produces the same
SitePageshape as any refrakt site; entity-detail pages carrysource.type === "contributed"(SPEC-069), dashboards are file-backed. - Per-entity routes resolve via
{% ref %}and appear in the sitemap, search index, and nav (SPEC-069 guarantees). - The plan plugin's aggregation runes render inside authored dashboard pages exactly as they do today.
Engine / Package Changes
@refrakt-md/plan— register hook setssourceFile+ anextract(embeddability); no rune changes.build/servecommands gain deprecation notices, scheduled for removal.@refrakt-md/create-refrakt— newplanproject type: config +plan/seed +plan-site/content dir + adapter scaffold. New template dir(s) underpackages/create-refrakt/.- refrakt root — new
sites.planentry + aplan-site/content dir committed to the repo. - No new engine primitives — this spec consumes SPEC-069 (entityRoutes, contributePages, embeddability) and SPEC-070 (collection, field-match grammar,
$item, formatter functions) as-built. If it needs a primitive neither provides, that's a finding to feed back into those specs.
Acceptance Criteria
- Plan entities are embeddable: the register hook sets
sourceFile+extract, and{% expand $item.id /%}renders a spec/work/bug/decision/milestone body inline - An
entityRoutesblock generates one detail page per plan entity type at the templated URLs; pages resolve via{% ref %}, appear in sitemap + search - Authored dashboard pages reproduce the current dashboard (progress, activity, ready-work backlog, decisions) using existing plan runes +
collection - Per-status and grouped views are expressed as
collection/backloglistings (no bespoke per-status route generation) create-refrakt --type plan [--target <adapter>]scaffolds a complete, runnable plan site (config + seedplan/+plan-site/dashboards + layout)- refrakt's root
refrakt.config.jsondeclares aplansite;refrakt buildproduces a browsable plan site from refrakt's ownplan/ - The generated plan site is feature-comparable to
plan serveoutput (entity pages, dashboards, listings, search, sidebar nav) — gaps documented plan buildandplan serveprint deprecation notices pointing at the site approach; the authoring CLI is unchanged- Docs cover: configuring a plan site, the
entityRoutesrules, dashboard authoring withcollection, and the migration fromplan build/serve
Out of Scope
- Removing
plan build/servein this release. Deprecate now, remove later, so existing users aren't broken. - Changing the plan authoring CLI or rune schemas. Only embeddability is added.
- A general external-source dogfood (GitHub issues, typedoc). Valuable separate proofs of SPEC-069's external path, but the plan site is the in-repo one.
- Multi-theme plan sites. The scaffold ships
lumina; theming is the standard site concern.
Resolved Decisions
Dev-server HMR for plan-entity edits — accept rebuild-on-edit in v1. plan serve watches plan/*.md and live-reloads. Standard adapter HMR watches the content dir, but plan entity files are registry sources outside it, and SPEC-069 scoped contributed-page HMR out. So editing plan/work/WORK-1.md requires a full rebuild to refresh its generated route. Decision: accept rebuild-on-edit for plan entities in dev for v1 and document it as a known difference from plan serve; a plan-plugin watcher that invalidates affected routes is a possible later enhancement, not v1.
Per-status views — one grouped/filterable collection, not discrete per-status pages. plan build emits discrete /work/ready/ URLs. Decision: v1 ships one grouped/filterable view per type ({% collection type="work" group="status" sort="priority" /%}) rather than a page per status value. Discrete status URLs would need the deferred groupBy primitive (SPEC-069 Out of Scope) or a handful of authored pages; if they prove necessary, promote status to a registered entity or author those pages — not a v1 concern.
refrakt's plan site is its own site, not a sub-path of the docs site. Decision: a dedicated sites.plan entry with its own base path and independent build, surfaced from the docs site via a link. Clean separation, independent deploy cadence, and it keeps the dogfood from entangling the main site's build.
Search — rely on the standard site search. Decision: the plan site uses the standard site search rather than a plan-specific pagefind UI; SPEC-069 guarantees entityRoutes-generated pages are in the search index. The acceptance criteria verify generated detail pages appear in search; if the standard search proves an inadequate replacement for the plan-specific UI during implementation, that's a finding to feed back (not a reason to keep the bespoke index).
References
- SPEC-069 — plugin-contributed routes;
entityRoutes, embeddability, the Replacing plan-serve migration this spec realizes - SPEC-070 — collection rune; dashboards, listings, the field-match grammar and
$itemcontract - SPEC-064 — plan plugin registration (the registry this site builds on)
plugins/plan/src/commands/build.ts+serve.ts— the bespoke SSG being retiredplugins/plan/src/pipeline.ts— register/aggregate hooks; wheresourceFile+extractget addedpackages/create-refrakt/— scaffold mechanics; where theplantype lands