Releases: ux287/routekit-shell
Release list
v0.29.4
Marketing site: add Open Graph + Twitter Card link-preview metadata and a 1200x630 social share card (public/og-image.png), so sharing routekit.dev renders a branded preview (title, description, image) instead of a bare imageless fallback. og:image uses an absolute URL (relative paths are silently dropped by scrapers). Preview copy adopts the "project operating system" framing; the on-page title/description keep their current framing pending a separate positioning pass.
v0.29.3
Marketing site naming pass: product naming now follows the brand rules — "Routekit Shell" in the hero, header, and branding; "Routekit" in headings and callouts; "rks" reserved for body copy. Updates the Proof headline and the services callout accordingly.
First release cut with a corrected commit identity. Prior releases were authored by a stale repo-local git identity, which caused the production deploy to be blocked and left routekit.dev serving a pre-0.29.0 build; this release should restore automatic deployment of the accumulated marketing surface (UX287 logo, enlarged header lockup, self-hosted Overpass Black wordmark, Plausible analytics, Model economics pane, and the UX287 deep-dive links).
v0.29.2
Presentation decks: adds a model-economics beat to the AAR (building-a-product-knowledge-graph), product/design, and how-rks-works decks — covering how work starts on a small model, retries before escalating, escalates by resuming rather than restarting, and caches prompt/tools/turns, with spend and a waste ratio reported per unit of work. Each beat is audience-tailored and bounded to what the shipped code does.
Also carries the marketing-site surface from 0.29.1 (UX287 logo, enlarged header lockup, self-hosted Overpass Black wordmark, Plausible analytics, and the Model economics pane) to a deployable state: 0.29.1's production deploy was blocked because commit authorship resolved to a stale repo-local git identity rather than the project owner. The identity is corrected; this release is the first authored correctly.
v0.29.1
Marketing site (routekit.dev): Plausible privacy-friendly analytics (self-hosted script tag, no npm dependency); the real UX287 highway-marker logo in the header sub-brand, replacing the fabricated shield (now removed from Hero and Footer); self-hosted Overpass Black display face for the Routekit Shell wordmark (no third-party font CDN, keeping visitor IPs off Google); an enlarged header lockup; a new full-viewport "Model economics" pane covering how rks routes work to a small model first and escalates only when the work earns it; and the deep-dive cards now link to the UX287 blog. No framework changes — marketing surface is publish-excluded and does not ship to the public repo.
v0.29.0
Marketing & presentation surface: routekit.dev landing SPA (packages/marketing-site, dark-mode + ux287 blue) and four presentation decks (what-is-rks, how-rks-works, rks-for-product-and-design, building-a-product-knowledge-graph). Backlog: logged build-path/off-rail hardening items (refine-noop escalation, sync-hooks off-rail wedge, on-rail non-core builds). Marketing content is publish-excluded and does not ship to the public repo.
v0.28.0
Greenfield builds work end-to-end, unattended.
Two fixes that together let the planner build a fresh app from create_file directives without a human rescuing it — the last two blockers a clean-machine UAT hit.
The dependency-add escape hatch is no longer fatal to execute
The planner is grounded on package.json and its own prompt tells it: if it needs an undeclared package, emit an explicit dependency-add step (an npm install run_command, or a package.json edit) in the same plan. The quality gate honours that. And then exec rolled the plan back for it.
Both of exec's scope guards — the final one and the per-step one inside runApplyTool — built their expected-file set from step.target || step.path, and a run_command step has NEITHER. So the package.json and package-lock.json an install writes landed in unexpectedFiles → scope violation / exec.diverged → the whole plan rolled back. A planner that obeyed its instructions was punished for it, and a greenfield build burned two identical retries on turn one.
There is now ONE exported exemption rule, consumed by BOTH guards (three copies of the set-difference existed and two disagreed — the per-step guard silently never exempted what the final guard did). It exempts the manifest/lockfile writes ONLY when the plan genuinely declares a dependency-add step — keyed on the plan step, never the filename, so a plan cannot smuggle a package.json rewrite past the guard by touching the file.
The mirror-image bug is fixed too: a plan that "installs" a package package.json ALREADY declares is now flagged (redundant_dependency_add) rather than left to rewrite the manifest and trip the guard.
And the advice stopped lying. On an import-not-declared failure the gate returned a hardcoded [decompose, use_search_replace] — neither of which can declare a dependency — while the check had already computed the right remedy (ground_imports) and thrown it away. Suggestions are now derived from the failing check and travel through to the Build Governor, which reads them.
The React SPA template no longer ships a landmine
The app.web.react.spa scaffold shipped no App.test.tsx, so the planner wrote the route test freehand: screen.getByText(/about/i). That throws "Found multiple elements" the instant a nav <Link to="/about">About</Link> coexists with the page <h1>About</h1> — which the About page's own copy tells you to create. The first story built on the stack that added a page hit it.
The scaffold now ships a correct, nav-collision-safe App.test.tsx that scopes route-name assertions to a unique element (getByRole("heading", { name })) and carries the rule as guidance — so it is both the fix and the exemplar the planner had nothing to copy from. And @testing-library/user-event is now in the scaffold's devDependencies (the standard companion to @testing-library/react, which every interaction test needs).
v0.27.3
Three P0s: the Build Governor can now recover on its own.
A clean-machine greenfield UAT completed only because a human intervened twice. These are the two places it got stuck, plus the on-ramp that misled it.
exec no longer strands a story in executing
A story is at phase executing on ENTRY to exec — the plan writer makes that hop, exec only gates on it — so EVERY exit left it there unless something put it back, and the reset existed but was wired to 2 of ~12 exits. A scope violation, a quality gate, a divergence, or any thrown error stranded the story. rks_plan refuses that phase, so the re-plan the error message told you to run was IMPOSSIBLE: a recoverable failure became terminal and the story had to be deleted and recreated.
The fix is a THREE-REGION recovery, and the regions are not interchangeable — two of them destroy work if you pick the wrong one:
- FINALIZED (the commit landed) → phase reset only. NEVER rollback(): it would
git branch -Dthe branch holding the commit, destroying a green build because a JSON write failed. - POST-MUTATION (a branch, backup or guardrails session exists) → rollback(), then reset. A bare reset would leak all three.
- PRE-MUTATION (nothing mutated) → bare reset only. NEVER rollback(): its cleanup step is unguarded, so with no branch it degenerates to WIPING THE TREE — and on the dirty-tree precondition failure, that tree is dirty with the USER'S OWN uncommitted work.
Plus return-value inspection, because a try/catch does not intercept return and most of exec's failures are returns. Exactly two carve-outs — needs_approval and incomplete_previous_story — because both tell you to retry, and the retry re-enters exec through the same gate.
refine_apply no longer reports success after changing nothing
It returned ok:true AND requiredNext:rks_plan after a byte-identical no-op — telling the Build Governor "success, now go re-plan". The Governor re-planned an unchanged story, got the identical failure, refined again, and was told "success" again. Forever. The retry could never converge because the feedback never changed the input.
The dedup was merely how it got there: add_test_exemplar checked for a bare "### Test Exemplar:" header, so ANY prior exemplar suppressed every future injection — on BOTH sides of the loop. The suggester never emitted the correction, so fixing only the apply half would have been useless. It is now effect-aware, matching the two siblings in the same file that already worked that way.
A genuine no-op now returns status:"refine_noop", names what was skipped, and does NOT tell anyone to re-plan. The chain moves to a new escalated state which permits neither rks_plan, rks_refine, rks_refine_apply nor rks_exec — every one of them is a way back into the loop — and the Build Governor aborts and hands back to a human. A manual:true request and a decompose that declines are NOT no-ops: a false escalation kills a healthy build as surely as a missed one loops.
The clean-machine on-ramp stops lying
- The detached-HEAD advice shipped in 0.27.2 printed
git checkout -B staging --track origin/staging, which FAILS on a public-mirror clone — the mirror publishes only main and tags, so there is no origin/staging to track. The fix that detected the problem prescribed a cure that fails in the environment it was most likely to run in. It is now mirror-aware. - rksVersion reported the version on DISK while the checks ran from the code loaded in MEMORY at server start. After a
git checkout <newtag>without a restart those are different builds, and preflight confidently named the wrong one — it announced 0.27.2 while a check that only exists in 0.27.2 was absent from its own list. It now reports the LOADED version, and a new server_freshness check fails when the two diverge and tells you to restart the MCP server. Telemetry events carry the loaded version, so a failure can finally be attributed to a build. - .tmp-replan-* scratch dirs are gitignored. rks was tripping its own clean-tree guard on its own garbage, and auto-ship swept three of them into a commit.
v0.27.2
Shell self-sync skill wipe + the health oracle that certified it healthy.
THE BUG: syncProject copies skills by rm'ing each destination skill and then copying the source over it. When projectRoot === shellRoot they are THE SAME DIRECTORY — the rm destroys the source, the copy finds nothing, and the whole thing exits 0 reporting "Synced 0 file(s)". Every skill, gone, silently. A clean-machine UAT lost all 17 distributable skills this way, and rks_preflight reported 7/7 green throughout, because not one of its checks looked at .claude/skills. rks could not route a single thing — no /build, no /ship, no /research — and the health check said it was fine.
REACHABLE FROM FOUR CALLERS, because setup.mjs registers the SHELL in its own registry and nothing told them a shell is not one of its own children: project sync --all, project upgrade --all, routekit doctor, and bootstrap.
-
THE PRIMITIVE: syncProject and bootstrap now refuse a self-targeted sync, compared by filesystem IDENTITY (device + inode) — not by string, which is defeated by a trailing slash, a
..segment, a symlink, and a case-variant on a case-insensitive volume; and not by realpath, which still cannot see two different real paths sharing an inode. The guard fires on identity, NEVER on containment: a legitimate child nested inside the shell directory syncs normally. A second guard sits at the point of destruction, because a child's skills dir can be a symlink back to the shell's and the entry guard would never see it. -
THE CALLERS:
sync --all,upgrade --allanddoctorskip the shell's own registry record. doctor skips at the top of the loop — repinMcpServer and migrateConfig run there too, and migrateConfig writes the shell's own project.json. -
THE HEALTH ORACLE: rks_preflight gains a core_skills check, manifest-driven. It also stops reporting a DETACHED HEAD as healthy —
git rev-parse --abbrev-ref HEADSUCCEEDS when detached and returns the literal string "HEAD", so the old!!currentBranchfallback was truthy. That is the state README's own "pin to a tag for stability" advice puts you in. -
THE MANIFEST: .routekit/skills-manifest.json is now the one source of truth for the skill inventory, replacing two separately-hardcoded SKILLS_EXCLUDE sets. A test holds it to the filesystem in both directions. It ships to the public mirror — without that, the new check would silently no-op on a mirror clone, which is exactly the machine the wipe was found on.
-
SILENCE IS A BUG: a missing skills source is now LOUD (a shell with no skills to give cannot bootstrap or repair a child); missing OPTIONAL sources (hooks/prompts/agents) stay tolerated. setup.mjs gains a health gate that names a detached HEAD and restores missing skills FROM GIT — never from syncProject, which is the thing that deletes them.
Also: setup's dirty-tree branch-landing skip is no longer a neutral aside — on the UAT box the tree was dirty BECAUSE the skills were deleted, so the one signal that could have surfaced the problem was suppressed by it.
v0.27.1
Planner dropped-step diagnostics: fix five defects in the v0.27.0 note-step guard.
-
TASK-MODE COVERAGE HOLE: v0.27.0 replaced the note-step guard with
hasTargetCoverageGap, which derives fromenhancedEditableTargetsand is therefore ALWAYS false in task mode. A task-mode plan shipped silently minus every step that was dropped, with no indication anything was discarded. The guard is now one pure exported decision (evaluatePlanGuard) that computestaskModeandhasTargetCoverageGapitself and applies a MODE GATE internally: story mode is governed by coverage, task mode by dropped work. Both guard sites are pure delegations. -
DROPPED-STEP SET: new exported
computeDroppedSteps— an identity-keyed set difference ({raw steps the LLM authored as executable} MINUS {classified.executable}). It never gates onrejectionReasons, which is a SUPERSET of the drops (the advisory "missing paired test/doc" loop pushes descriptors for steps that were KEPT, into the same array object, mutating it in place). Identity keying is required: a label-keyed difference computes {P} \ {P} = ∅ when a kept step and a dropped step share a path, losing the drop entirely. -
UNNAMEABLE REJECTIONS: the "No executable actions generated" description rendered rejections via
r.path, so a pathless step became the literal string "null: ". It now renders byr.label, which always falls back path → title → . -
SILENCED DIAGNOSTIC: steps hard-rejected as
_invalidvanished from the retry prompt, so the planner asked the LLM to fix something it never named. Dropped steps are now named — with action, path, title and reason — in both the retry guidance and therefinement_requiredpayload. -
DEBT: the exhausted-return payload's
noteStepsis nownoteLabels(the apologetic comment and the contorted inline expression are gone), and the source-text grep in planner-note-step-degeneracy.test.mjs that pinned that expression in place is de-mirrored into behavioral assertions against the real exported payload builder.
LLM-authored note steps are never treated as lost work — commentary must not cost a build.
v0.26.0
Greenfield first-code generation: the planner can now author file bodies from scratch.
FIXED — greenfield plans no longer die in an unwinnable retry loop.
On a fresh child with an empty RAG index, a story whose targets are all op: create never
produced code. The JSON-embedded planner emitted PROSE NARRATION in a step's content field
("Create a Calculator component that handles the four operations.") instead of an actual file
body, and two failures compounded:
-
PREDICATE DIVERGENCE. The LLM-side executability check accepted any non-empty
content
string and called the planexecutable; the server-side create-coverage gate was stricter
and called the same target uncovered. The plan was simultaneously too complete to retry
usefully and too empty to execute, so it round-tripped asoutput_invalid/has_note_steps
until retries exhausted. No refinement could escape it — the planner cannot emit a whole file
body through the JSON channel, so re-asking just re-narrates. -
NO SYNTHESIS PATH FOR SOURCE FILES. Test files had an escape hatch (a focused raw-code LLM
call); source files had none. Greenfield is exactly the case where every file is a source
file being born.
CHANGES
-
NEW
isSynthesizedBody(content, filePath)inserver/plan-quality.mjs— ONE shared predicate
answering "is this a real file body?" for BOTH the LLM-side and server-side gates, so they can
no longer diverge. Rejects empty bodies, placeholder/comment-ONLY bodies, and prose narration in
code files. Deliberately conservative: it fail-safes to "this is a real body" when unsure,
because a false negative is catastrophic (a real component downgraded to a note, or a covered
target declared unauthorable → hard unrefinable failure) while a false positive is merely the
status quo ante. -
NEW
enrichCreateFileContentinllm/planner.mjs— synthesizes real file bodies via a focused
raw-code LLM call for anycreate_filestep the JSON planner narrated instead of authoring,
running UPSTREAM of every gate (critic, retry, coverage). Explicitly NOT gated ontestExemplar
the way the test-content enricher is: a fresh child has no exemplar, and the fresh child is
precisely the case this rescues. When synthesis itself cannot author the file, the step is left
untouched so the coverage gate fails LOUD and unrefinable — rather than swapping in another
unauthorable body and re-entering the loop. -
The placeholder rule is now a WHOLE-BODY test, not an unanchored substring match. A real
200-line component containing one incidental// TODO:is no longer downgraded to a note.
LLM-authored code carries TODOs routinely; that body is real and now stays executable. -
De-fragilized the witness tests: replaced source-grep pins (which assert nothing about behavior
and break on every refactor) with behavioral assertions against the real predicate, removed a
duplicated local mirror of the coverage rule, and killed asrc.slice(idx, idx + 2200)fixed
window that empties silently when the code above it grows — making every assertion below it
vacuously pass.