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 (), the symmetric counterpart to the one-directional lifecycle checks already in place. Second, remove the bespoke plan build / plan serve render stack () that v0.16.0 deprecated but kept alive — with the 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 validate now 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) — a draft spec with started work.
  • spec-status-ahead (warning) — an implemented/shipped spec with a non-terminal linked work item.
  • released-in-without-shipped (warning) — released-in set on a non-shipped spec (mirror of shipped-without-release).
  • stale-blocked (warning) — a blocked work item whose ## Blocked by targets are all achieving-terminal.
  • milestone-complete-with-open-work (warning) — a complete milestone with a non-terminal member (replaces complete-milestone-open-item, now keyed on terminal rather than done/fixed so retired members don't falsely flag).
  • plan status's suggestImplemented hint and validate's spec-status-lag share one predicate, so the suggestion and the warning can never diverge. --strict promotes the new warnings to errors.
  • Remove the bespoke plan build / plan serve render stack (SPEC-120).
  • Breaking: the refrakt plan build and refrakt plan serve commands are gone, along with their private static-site generator (the three-family render-pipeline router, planLayout shell, 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

SPEC-119implemented

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.

planvalidationlifecycletraceabilitydx

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 38 draft specs whose linked work was 100% done — shipped features still labelled "not yet agreed to build" — plus 32 accepted specs that were never advanced to implemented/shipped. None of this was caught by plan 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 blocked status after their ## Blocked by targets finish (silently excluding them from plan next), and milestones sit active long after every assigned item is done.

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

TypePredicateSeverityPriority
spec-status-lagSpec is draft/review/accepted, has ≥1 linked work item, and all linked work is achieving-terminalwarningv1
spec-started-in-draftSpec is draft and ≥1 linked work item has started (in-progress/achieving-terminal)infov1
spec-status-aheadSpec is implemented/shipped but ≥1 linked work item is non-terminalwarningv1
retired-spec-live-workSpec is superseded/deprecated but ≥1 linked work item is non-terminalwarningv2
terminal-spec-no-workSpec is implemented/shipped with zero linked work itemsinfov2

Attribute / status consistency

TypePredicateSeverityPriority
released-in-without-shippedreleased-in is set but status ≠ shipped (mirror of the existing shipped-without-release)warningv1

Work status vs. dependency evidence

TypePredicateSeverityPriority
stale-blockedWork status=blocked but all ## Blocked by targets are achieving-terminalwarningv1
false-readyWork is ready/in-progress but ≥1 ## Blocked by target is non-terminalinfov2

Milestone completion drift

TypePredicateSeverityPriority
milestone-complete-with-open-workMilestone status=complete but ≥1 assigned item is non-terminalwarningv1
milestone-done-not-completeMilestone active/planning, ≥1 assigned item, and all assigned items are achieving-terminalinfov2

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 validate emits spec-status-lag (warning) for a non-terminal spec whose linked work is entirely achieving-terminal.
  • plan validate emits spec-started-in-draft (info) for a draft spec with started work.
  • plan validate emits spec-status-ahead (warning) for an implemented/shipped spec with a non-terminal linked work item.
  • plan validate emits released-in-without-shipped (warning) when released-in is set on a non-shipped spec.
  • plan validate emits stale-blocked (warning) for a blocked work item whose ## Blocked by targets are all achieving-terminal.
  • plan validate emits milestone-complete-with-open-work (warning) for a complete milestone with a non-terminal member.
  • All drift predicates use the isTerminal/isAchieving helpers from SPEC-117; no status-set is re-declared in validate.ts.
  • No lifecycle warning fires for an entity with zero linked evidence.
  • cancelled/superseded work does not trigger any drift-toward-done check.
  • Each new issue type has a fixture-backed unit test (positive and negative case).
  • --strict promotes the new warnings to errors, consistent with existing behaviour.
  • The suggestImplemented logic in plan status and spec-status-lag in plan validate share 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 --fix autofixer — a plausible follow-up, but a separate spec.

Open questions

  • Should the v1 warnings ship off by default (opt-in via --lifecycle or 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-draft vs. legitimate exploratory work: is draft + started work always wrong, or a valid "spike first, formalise later" pattern? If common, demote to opt-in.

Retire the bespoke plan render stack

SPEC-120implemented

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.

planclideprecationcleanup

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 build and plan serve commands 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, the planLayout shell, 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 / init are unaffected — only rendering is retired.
  • The plan runes and pipeline hooks stay. The register / aggregate hooks and entity registration feed entityRoutes / collection; they are retained intact.
  • No changes to the plan site scaffold itself — that shipped under SPEC-071.

Acceptance Criteria

  • plan build / plan serve are 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 serve as 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 build and npm test pass

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 — collection rune, the dashboards / listings replacement

Plan site scaffolding

SPEC-071shipped

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.

planscaffoldingentity-routescollectiondogfooddeprecation

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 → entityRoutes rules (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 docs layout 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 docsLayout preset + a plan _layout.md with 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 featureReplacement
One page per spec/work/bug/decision/milestoneentityRoutes rule per type, body {% expand $item.id /%}
Dashboard (index.html): progress, recent activity, milestones, blockers, decisionsAuthored 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 pagesexpand-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 reloadAdapter dev server (refrakt dev); plan-entity edits need a rebuild in v1 (see Resolved Decisions)
theme.css + behaviors.js emissionStandard adapter asset pipeline
Sidebar nav + plan layout shelldocsLayout 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 the docsLayout preset 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.json with @refrakt-md/plan + the entityRoutes block above.
  • A plan/ seed (one example spec + work item + decision + milestone) so refrakt dev shows a populated site immediately.
  • A plan-site/ content dir with _layout.md + the dashboard pages above.
  • The adapter scaffold for the chosen --target (so create-refrakt --type plan --target sveltekit is 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

  1. Same release that ships entityRoutes + collection + the plan-site scaffold: mark plan build and plan serve deprecated. On invocation they print a deprecation notice pointing at create-refrakt --type plan (for new projects) and the entityRoutes config (for existing ones), then still run.
  2. The authoring CLI (create/next/update/validate/status/migrate/next-id/history) is unaffected.
  3. A later release removes build/serve and 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 SitePage shape as any refrakt site; entity-detail pages carry source.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 sets sourceFile + an extract (embeddability); no rune changes. build/serve commands gain deprecation notices, scheduled for removal.
  • @refrakt-md/create-refrakt — new plan project type: config + plan/ seed + plan-site/ content dir + adapter scaffold. New template dir(s) under packages/create-refrakt/.
  • refrakt root — new sites.plan entry + a plan-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 entityRoutes block 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/backlog listings (no bespoke per-status route generation)
  • create-refrakt --type plan [--target <adapter>] scaffolds a complete, runnable plan site (config + seed plan/ + plan-site/ dashboards + layout)
  • refrakt's root refrakt.config.json declares a plan site; refrakt build produces a browsable plan site from refrakt's own plan/
  • The generated plan site is feature-comparable to plan serve output (entity pages, dashboards, listings, search, sidebar nav) — gaps documented
  • plan build and plan serve print deprecation notices pointing at the site approach; the authoring CLI is unchanged
  • Docs cover: configuring a plan site, the entityRoutes rules, dashboard authoring with collection, and the migration from plan build/serve

Out of Scope

  • Removing plan build/serve in 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 $item contract
  • SPEC-064 — plan plugin registration (the registry this site builds on)
  • plugins/plan/src/commands/build.ts + serve.ts — the bespoke SSG being retired
  • plugins/plan/src/pipeline.ts — register/aggregate hooks; where sourceFile+extract get added
  • packages/create-refrakt/ — scaffold mechanics; where the plan type lands