Changelog
What each release of bb and the plugin changed. It's built from their changelogs, so it updates with every release.
bb and the plugin are released together and always share a version number.
bb
0.23.0
Minor Changes
-
ccfda0a:
bb mcp, a stdio MCP server the plugin now bundles in its.mcp.json, pinned to the plugin’s version. It serves four read-only tools,show_plan(Gate 1),show_space_plan(Gate 2),show_push_planandshow_review, each linking aui://backbase/<view>MCP Apps widget:bb report’s view drawn in the chat, with its images inline, in an app that shows MCP Apps (Claude Desktop, Claude on the web). Each returns the output contract’s short card as text, which is all a terminal shows, with the report page’s path. The widgets load nothing from the network and post nothing that writes; approvals stay a typed yes in the chat.--rootsets the folder whose projects the tools may read (the plugin passes Claude Code’s project folder), and bb mcp refuses the disk’s root or the home folder; an empty or unexpanded--rootmeans the folder it runs in. Each widget’s CSP lets only its own script run, by hash.backbase-output’s templates call the moment’s tool; when the app draws the widget, the message leaves out only the line the widget draws, and keeps that nothing is written yet, what can be lost, closing the Settings story, the decisions, the approval sentence and the report link. The space view says to close the Settings story when the write saves it. -
eb9f5b6:
bb report [plan|space|push|review|all] [--open]draws the run as a local HTML page (MAR-5017): a picture of what it’s going to do, beside the agent’s short answer.- Four views, in plain words.
- The plan (Gate 1): each section beside its Figma shot, how it gets built (“built from existing blocks”, “a new component built from Backpack’s React”, “from the site settings”), how sure the plan is and how close Backpack gets. Then every decision, numbered, each with its recommended answer: off-scale values (96px → xxxl, 128px, requested from GCS), the design’s colours that aren’t the theme’s, what Backpack can’t do, and the open questions.
- The space (Gate 2): the components bb creates or updates and those left out with their reason, the theme’s colours as swatches (the space’s today beside the design’s), the custom colours, the Settings story’s draft and the field plugins.
- The push: each section new, changed, removed or the same since the last push or pull, beside its Figma shot, the images with thumbnails, and the target: always a draft, never published.
- The review: each section’s review image, Figma beside the page, its height and the measure’s largest deltas in words, and what refine kept or reverted there.
- It reads only. It draws from the files the run already has in
.backbase/, and never writes to Storyblok or Figma.bb report(orall) writes.backbase/report.htmlwith every view whose files are there, and says which aren’t drawn yet and why;bb report <view>writes.backbase/reports/<view>.html. With--json,data.writtenlists the page, anddata.viewsanddata.summarysay what each view shows. - One self-contained page. Inline CSS, no script, nothing fetched: its Content-Security-Policy loads only the page’s own images. They’re linked as sibling files, each opening whole, or put in the page with
--inline: as 480px thumbnails (a review’s side-by-side image 960px), each once however many views show it, and at most 4 MB in all, with a warning for what’s left out. On ILS, 1.2 MB. Light and dark, readable at phone width.--openopens it in the default browser. - Plain words, from one glossary.
packages/core/src/glossary.pure.tsholds the output contract’s table (thebackbase-outputskill’s, MAR-5016), for the views and the contract’s templates alike: a section by its name without the Figma path, a colour by its look (“lavender, accent 3 background”) with the Figma variable in a tooltip, and “theme colour” and “option list” for bb’s own words. - It reads only what’s there, and only the project’s. A review index or a measure that isn’t one makes the review unreadable, naming the field.
--slugtakes only a story’s slug, and a path that leads out of the project (../, an absolute path, a link) is refused. - The skills link it.
backbase-output’s layer 3 runsbb reportat each moment and links.backbase/report.html, and each template’s last line does too: “The full plan, with a picture per section:.backbase/report.html.” The detail the page doesn’t draw stays in.backbase/reports/<moment>.md. The output eval reads the glossary, the hidden words and the section and colour names from core’sglossary.pure.ts, and checks the skill’s table says the same. - Built once, in core. The view builders (
planView,spaceView,pushView,reviewView) and their HTML are pure, for E-3’s MCP widgets to show the same views. bb createignores.backbase/report.htmland.backbase/reports/, and so does the analyse flow’s.gitignorestep.
- Four views, in plain words.
-
19bf028: Every skill now talks to the person the same way, through one output contract, the new
backbase-outputskill (MAR-5016):- Three layers. First, a line or two on where things are and whether anything needs the person. Then a decision card: only the choices they have to make, numbered, each with the recommended answer, so “yes” or “2: no” is a whole reply. Last, the full detail, which goes to
.backbase/reports/<moment>.mdand is linked, and “show section 6” gets just that part. The reports aregate-1.md,gate-2.md,push.md,review.mdanddone.md. - Plain words. A glossary replaces Backbase’s own terms: Wrap is “a new component built from Backpack’s React”, slot “a placeholder until it’s built”, Compose “built from existing blocks”, preset “a saved layout”. Stamps, envelopes, fingerprints,
plan.json, block trees and design-fit codes aren’t shown unless the person asks. Approval sentences and drafts to approve are still quoted word for word. - A word budget: about 150 words a message, and one line per stage at most while work runs. Errors and refusals say what happened, what it means and the one thing to do.
- Five templates, for Gate 1, Gate 2, the push plan, refine and review results, and done.
backbase-analyze,backbase-new,backbase-sync-space,backbase-compose,backbase-edit,backbase-refine,backbase-componentandbackbase-upstreamfill them instead of their own “tell the person” paragraphs. Every gate, approval and rule stays as it was. - Gate 1’s card keeps MAR-4845’s rule. A yes takes each recommended answer the card names, and nothing else. A question with no recommended answer still has to be answered before the plan is confirmed. The card lists those questions first, a section it couldn’t place and the ones that change what gets built, and the report numbers the rest, so the person answers them by number (“7: live”). Decisions beat the budget: those questions go on the card whatever it costs. What another blok would fit is its own recommendation on the card, to switch, and never a request to GCS. A question with a blok’s name in it goes on the card in plain words. On the ILS frame, Gate 1 goes from a 1,257-word table to a card of 150 words, plus the 8 questions only the person can answer.
bb createadds.backbase/reports/to a new project’s.gitignore.- A new eval,
output, replays the ILS run’s five messages through the templates with the fixtures’ data. It checks that each fits the budget, shows no hidden word, and keeps every decision the old output asked for, among them 96px → 128px requested and the theme colours at the space step. On ILS, a plain yes to Gate 1 doesn’t confirm the plan while a question with no recommended answer is open.
- Three layers. First, a line or two on where things are and whether anything needs the person. Then a decision card: only the choices they have to make, numbered, each with the recommended answer, so “yes” or “2: no” is a whole reply. Last, the full detail, which goes to
0.22.0
Minor Changes
-
ca1ae72: The EF Chaperone preset moves to Chaperone 0.12.0’s native rules, so baselines count per component (MAR-5009):
- Chaperone 0.12.0 is the minimum.
bb doctor --writeandbb createpin it, raising an olderchaperoneVersionand keeping a newer one. A project still pinned below it fails every schema underschema-name-matches-folder, since an older Chaperone ignores the preset’s case transform:bb doctor’sproject-chaperonenow names the old pin and that symptom, andnpx @ef-global/backbase doctor --writefixes it. schema-is-registeredis nativeregistered-in-map. It readssrc/components/component-list.tsand the project’scomponent-map.exceptions.json: a listed schema passes, an unlisted one fails.- A missing
component-map.exceptions.jsonis now an error. Chaperone reports “Cannot check registration”, which no baseline can record.bb doctorreports the missing file underproject-chaperone, andbb doctor --writeandbb createcreate it as{}, never overwriting an existing one. (bb check components --registrationstill treats a missing file as{}.) schema-name-matches-folderis a nativefile-pairingwith apascaltransform.<kebab>.sb.tsmust sit insrc/components/<Pascal>/, with exactly that case, soFancard/fan-card.sb.tsfails on macOS as it does on Linux CI.- The three command rules list one result per component.
wrapper-visibility-keeps-display,schema-is-placeableandflex-child-has-design-fieldrunbb check components --visibility,bb check placeableandbb check flex-designwith--jsonand"output": "results". With--json, those checks’ envelope addsresults: one{ file, message, fingerprint }per violation, at the schema or wrapper, with fingerprints such asschema-is-placeable:fan-stack. Baselining one unplaceable component now leaves a second, new one failing. A check that can’t run, or any error without a file, lists nothing and fails, which Chaperone reports as a rule-level error, never a baselined violation. chaperone/bb.mjsstill says “@ef-global/backbase isn’t installed” for each command rule when bb’s CLI is missing.
- Chaperone 0.12.0 is the minimum.
-
41a9646:
bb createruns the machine checks it needs,bb doctor’s own, and stops early with doctor’s fix text instead of failing part-way through. A new Check the machine step (preflight) after the folder check runsnodeandgit.packages-tokenruns right before the install, when the project’s.npmrcreadsGH_PACKAGES_TOKEN: a token GitHub Packages refuses stops the run, and a timeout or another failure is a warning, so the install still runs.githubnever stops a clone that would work: it runs only after an HTTPS clone fails, to add its fix.data.checkslists every check it ran, and a check that warns is indata.warnings.runMachineCheckstakesonly, andpackages-tokengives its cause infacts.cause. -
d380b83:
/backbase:editcan upload a new image, through the same guarded path as compose’s images (MAR-4838):- Edit offers “upload this image”. Before, the person had to upload the image in Storyblok’s asset manager and type its filename back. Now they give the agent a local file (in the project or outside it, such as
~/Downloads), a Figma node (its image fill, downloaded with their Figma MCP) or an image URL. The agent shows them one image plan with each image, where it came from, its slot and the alt text it proposes, and one yes covers the upload. Then it sets each image in its slot with an ops envelope. Nothing is uploaded without that yes, and nothing is published. bb assets add <file or URL> --story <story file> --slot <uid>.<field>is new.- Before it reads anything, it checks that the slot is an image (
asset) field in the blok’s schema (--catalog, or the project’s own). So an image is never uploaded for a fieldbb story ops applywould refuse it in. - It sniffs the bytes (PNG, JPEG, WebP or GIF, never SVG), reads the image’s size, copies it into
.backbase/edits/<slug>/images/and adds it to.backbase/edits/<slug>/images.json. It uploads nothing. --originrecords where the image came from, such as its source and licence, and--freshstarts the manifest over for a new edit.
- Before it reads anything, it checks that the slot is an image (
- A URL is downloaded from a public HTTPS host only. A URL may come from text an agent read, so bb treats it as untrusted:
- It refuses HTTP,
localhost, and loopback, private, link-local, unique local and other non-public addresses, IPv4 addresses inside IPv6 ones included. - It resolves a name itself and connects only to an address it checked.
- It follows at most 5 redirects, each checked before it’s requested.
- It streams the body and stops at 25 MB, whatever
Content-Lengthsays.
- It refuses HTTP,
- The upload is compose’s upload. The manifest is an images file
bb assets plantakes, with the story in place of the frame. The images go intoEdits / <story name>, each named after its source and its hash.bb assets upload --approved <fingerprint>uploads them; the guard’s prompt now names both kinds of folder. An image’s key is the one.backbase/assets.jsonalready has it under, or else its SHA-1, the key compose gives a Figma image. So an image the project uploaded before, from compose or an earlier edit, is reused, never uploaded again. The image plan shows where each upload came from. - The push plan lists the new images. For an update,
bb story push --planlists each image the push puts in the story that the story in Storyblok doesn’t have (data.assets): its file, section, blok, field and alt text, and whether.backbase/assets.jsonrecords it. It refuses a new image without alt text (alt-missing). - New projects ignore
.backbase/edits/*/images/, andbb assets addadds that line to.gitignorein existing ones.
- Edit offers “upload this image”. Before, the person had to upload the image in Storyblok’s asset manager and type its filename back. Now they give the agent a local file (in the project or outside it, such as
Patch Changes
- a53a805: Run external commands through a shared launcher so doctor and create find Windows .cmd shims and .exe programs. Escape arguments for local and global forwarding shims, preserve npm registry auth environment keys while merging PATH overrides, and stop the whole process tree on timeout.
0.21.0
Minor Changes
-
cd5fd44: Gate 2 saves the Settings story’s theme as a draft, so the theme shows right after the sync (MAR-5005, ADR 0031):
- The theme shows locally and in Storyblok’s preview straight after Gate 2. The site renders the Settings story’s
theme.selected, which thebackpack-themeplugin rebuilt from the datasources only when a person opened Settings and saved.bb space syncnow saves it after the theme’s datasource writes, as a draft, never published. bb rebuilds the field exactly as the plugin does, ported from ef-global/backpack-sb-plugins-react at 63b70fc (plugin 1.1.3) and pinned against the plugin’s own code and a recorded field, so the next time someone opens Settings, the plugin has nothing to change. - It’s a Gate 2 plan item.
bb space plan --sectionsreads the Settings story the site reads (settings, or<locale>/settingsper locale folder, asbb doctorfinds it) and lists its id and slug and each token oftheme.selectedthat changes, mode, before and after. It plans the story only whenselectedwould change, because of the theme’s writes or because it already disagrees with the datasources. The fingerprint covers the story, when its draft was saved, andselectedbefore and after, and the Gate 2 hook’s ask names the story. - The sync writes it safely. It reads the story’s draft again and skips it (
theme-plan-again) when it was saved since the plan, or when the datasources no longer give the plannedselected, and keeps its theme when a theme write failed. When sb-mig’s plan writescolors-theme-darkorcolors-theme, which only the field is rebuilt from, the sync still writes the theme’s colours and skips the Settings story alone. It changes only thethemefield (the model andselectedthe plugin saves) and keeps every other edit in the draft..backbase/space-sync.jsonrecords theselectedand the model it replaced, aspreviousandpreviousModel, and bb dropsbb story serve’s cached copies of the story so the local server fetches the new draft. A space with no Settings story, or one without athemefield, gets a note, not an error. Storyblok has no conditional write, so the last save wins: Gate 2 asks the person to close the Settings story before the sync and reopen it after, as a Settings tab opened before it holds the old theme. /backbase:syncand/backbase:composeend with “the theme is saved as a draft in Settings; publish it when you’re happy” instead of “open Settings, save and publish it”, which stays for a space whose Settings story wasn’t saved.
- The theme shows locally and in Storyblok’s preview straight after Gate 2. The site renders the Settings story’s
0.20.0
Minor Changes
-
1f98bef: Two gaps from the ILS 0.18 run (MAR-4989):
- An icon icons/bold doesn’t have becomes a design request. The frame’s “Sign in with Google” button had no icon to map to. EF’s icons/bold set is Phosphor without its brand icons, EF’s logos are only EF’s own, and bb has no icons of its own, so the Google “G” can’t be added to bb.
bb plan requestsnow drafts one request per icon that a section bb builds takes from Backpack’s Figma icons and icons/bold doesn’t have (Icon: google), with every section that uses it, what holds it and the closest icons/bold names.bb request plandrafts it as an issue for ef-global/backpack, and/backbase:upstream requestsfiles it after a person’s yes. A global section, such as a footer’s social links, lives in site settings and bb never composes it, so its icons ask for nothing. An icon counts as in icons/bold under its kebab name, its name run together, or either with-logo, as Phosphor names brands (LinkedInislinkedin-logo), so a request goes away once icons/bold adds the icon. bb checks the icons against the cached icon manifest, and fetches it once from EF’s CDN when there’s no cache, asbb story materializedoes. Without a sections.json or a manifest, a warning says the icons weren’t checked. - New projects get a dev server for Claude’s preview pane.
bb createwrites.claude/launch.jsonwith the boilerplate’s dev server: itsdevscript, run with the package manager its lockfile names (npm run dev), on the port the script gives (3000,-p3000too), with"url": "https://localhost:3000"because the script has--experimental-https. A port the environment gives (-p $PORT) can’t be told, so bb writes no entry for it. A new doctor check,project-launch, reports it missing in an existing project, andbb doctor --write(or--fix) adds it beside the configurations there. A configuration with the same name or port counts as the dev server already. A launch.json that isn’t JSON, such as one with comments, is left as it is, with a warning from doctor and frombb create. Running either twice changes nothing.
- An icon icons/bold doesn’t have becomes a design request. The frame’s “Sign in with Google” button had no icon to map to. EF’s icons/bold set is Phosphor without its brand icons, EF’s logos are only EF’s own, and bb has no icons of its own, so the Google “G” can’t be added to bb.
0.19.0
Minor Changes
-
9c90a25: Make Chaperone work in a real project: bb is a pinned dev dependency.
bb doctor --write(and--fix) now pins@ef-global/backbaseto the running version indevDependenciesand installs it with the package manager the project’s lockfile names, never lowering a newer pin, nor raising a release pin to a prerelease. The install finishes before the other checks start, since it rewritesnode_modules, and it runs through a shell on Windows, where npm isnpm.cmd.bb createdoes the same for a new project, and also writes the Chaperone preset and.prettierignore.bb doctor’sproject-chaperonecheck fails when the preset doesn’t resolve or bb’s CLI isn’t installed, namingbb doctor --writeas the fix, and warns about a version skew between the installed bb and the running one.The preset’s five command rules run bb through a shim beside the preset, so a project without bb’s CLI gets “@ef-global/backbase isn’t installed in this project: run npx @ef-global/backbase doctor –write” instead of a module-not-found stack.
/backbase:componentand/backbase:newno longer leave the agent to run those checks by hand.A new
project-prettierignorecheck, whichbb doctor --writefixes, adds.next,.turboandnode_modulesto.prettierignore, counting**/node_modulesand the like as already there. A correction to 0.18.0’s note: the preset’s excludes cover Chaperone’s own file scans only. Its Prettier runner callsprettier --check ., which reads.prettierignorealone, so.next/cachewas still linted until now. -
8aef90e: Backbase D-5 (MAR-4988): fixes from the ILS run, where refine kept none of its tries because the measurements misled it.
bb page measurepairs each text field only with a text layer inside its blok’s own match: a card’s title and body never pair with the other card’s text, or the nearest one with the same words. A field’s words come first, then its role (heading, body, label), then the order, pairing as many fields as it can. A text no layer there fits isunpaired, with the reason, and gets no deltas. Two cards whose copy was partly edited still pair with their own frames. Each measured blok lists itstextsand how each paired.bb page measurereads sticky bars at their resting place, with sticky turned off while it reads, and never measures two bars as one box, a wrapper around them included: each section lists itsbars, top and bottom apart, each with its own height.- /backbase:refine’s budget covers the page: every section with a delta beyond a token step gets one try before any gets a second, and the rest of the budget goes to the most px off. The budget is 5 tries per section and 15 in all (
--tries,--budget;--sectionsstill works for this release, deprecated, as a budget of that many sections’ tries), and the report lists any section left untried, and why. Core’splanRefine,nextSectionandrefineBudgetFromhold the rule. bb page reviewcaptures the last section at the bottom of a long page on its first attempt: the capture stops at the page’s end rather than asking for rows the page can’t scroll to, and reports the height it captured, and a section whose box runs past the end settles in view once more first. A failed capture gets one more attempt after settling again, as a fallback that’s reported (attempts,retried, a warning).
-
6f8c5a1: Off-scale design values use the nearest step, are flagged, and become a request to GCS (MAR-4985). A spacing, padding, gap or radius the frame has that no step is, such as the ILS frame’s 96px section padding between
xxl(64) andxxxl(128), is written as the nearest step and never silently: the design fit records it (offScale), Gate 1’s plan lists it once per value (96px → xxxl (128), +32px, requested),bb story spacing plannames each one inoffScale, only for a frame px no step of the scale is, andbb plan requestsdrafts one request per capability and value (Padding: 96px), with every section as evidence. A padding or margin tie at Gate 1 now goes where the spacing plan sends it, to the step Backpack’s presets use more, so the plan says what compose writes; a gap field’s value and a radius still go to the smaller. Design fits stored before this go stale, since their facts now carryoffScale(ILS run 2: 8 stale fits become 11; run 3: 7 become 11): runbb plan validate <plan> --sections <file> --write-design-fitagain, thenbb plan requests. Newbb request plan --issues <gh issue list JSON>drafts each request as an issue foref-global/backpack, or as a comment on an open issue that has it already, by its marker or by its title, from the list of every open issue;bb request record <key> <url>records a filed link, whichbb plan requestskeeps; replacing another link needs--force. ADR 0030: the agent files a design request after the person’s yes./backbase:upstream requestsfiles them, each only after the person’s yes. -
e2d6085: Gate 2 writes the theme even when a component fails, and a component can be left out (MAR-4984).
bb space syncwrites the theme’s overrides and custom colours whether or not a component failed: the theme depends on the colour datasources, not on the components. It’s skipped only when sb-mig’s plan writes a datasource the theme was read from. Each failed component is its owncomponent-failederror, next to the theme’s result, with--excludeas the way round it.bb space plan --exclude "<name>=<reason>"(repeatable) leaves a component out. The plan lists it with the reason, the fingerprint covers it, andbb space sync --exclude <name>syncs exactly what the plan held, naming the other components to sb-mig instead of--all. It fails (component-not-synced) when sb-mig’s report lacks a name it was given, andspace-sync.jsonrecords the components left out with their reasons.bb space planand a newbb doctorcheck,space-field-plugins, read the account’s and organisation’s field plugins (GET /v1/field_types,GET /v1/org_field_types, paged byTotal), and name a component field whose plugin is missing (missingFieldPlugins) only on positive evidence: a listing that assigns it to other spaces and not this one. Anything else is an unverified note, never an--excludesuggestion.- When Storyblok’s 422 says the space lacks a field plugin, the
component-failederror names it,space-sync.jsonlists it (fieldPluginFailures), and/backbase:syncoffers to install it or leave the component out. The hint now parses Storyblok’s own wording. - When the space has no
custom-colors, the theme plan creates it (additive) instead of dropping the frame’s custom colours. /backbase:syncoffers the exclusion when a component fails or lacks a plugin, and it and/backbase:composeend with the person’s step after theme writes: open Settings, check the colours, save and publish.
-
5769871:
bb component newno longer turns a Wrap’s grid or flex root into a block when its visibility is Show. The wrapper passes Backpack’sgetVisibilityClassesthe root’s own display asoriginalDisplayProperty: read from Backpack’s styles for a Wrap, on the element that gets the wrapper’s className (gridfor FanStack),flexfor a Create. When bb can’t tell, it puts the visibility on a wrapper that isdisplay: contentswhen shown. The wrapper names the classes the helper builds at runtime, so Tailwind builds them.bb check components --visibility, and the preset’s newwrapper-visibility-keeps-displayrule, flag a local wrapper that callsgetVisibilityClasses(or the deprecateduseVisibility) withoutoriginalDisplayPropertyon a root that isn’t a block, naming the file and the display to pass./backbase:componentoffers that fix for existing wrappers, and makes it only after the person’s yes.
0.18.0
Minor Changes
-
9c04b9a: Ship an EF project Chaperone preset for local component layout, wrappers, registration, placement, Backpack source boundaries and flex-child design fields. Use native Chaperone contracts with narrow commands for folder/schema comparison, map exceptions, effective placement and installed Backpack design options. Doctor repairs missing preset inheritance and minimum version pins. Check the real stock boilerplate and document why command-rule baselines must not be used for adoption.
Keep separate schema-is-registered and schema-name-matches-folder rule ids, with registration and names filters on bb check components.
Keep the preset compatible with doctor-installed Backbase slots, including unformatted schemas, and verify doctor then FanStack generation together.
-
301af6e: Add /backbase:component to build approved Wrap and Create slots, check the project, sync components only at Gate 2, replace slots through ops, measure and push one approved draft. Badge slot sections as expected review gaps, validate slot placements, and map filled section roots normally.
Exclude generated Next.js files, node_modules and the Turbo cache from the project Chaperone preset’s file scans.
-
67aa646: Render Wrap and Create gaps as local backbase-slot bloks in drafts. Install the template and placements, list slots in validation and push plans, and add story slots and undoable story slot conversion with plan evidence. Exclude reserved geometry and scores from measure and compare, and stop refine after two rounds of unexplained structural deltas for the person to decide on a component.
Format installed slot files with the project toolchain, preserve formatted installations with advisory drift checks, and limit placements to section containers and slide tracks. Match innermost sibling design instances and keep refine evidence compact.
0.17.0
Minor Changes
-
e2a8977:
/backbase:upstreamtakes a Wrap or a Create a project built to Backpack (C-6, ADR 0029): an issue inef-global/backpack, then a draft pull request in Backpack’s conventions, each after the person’s yes, never merged and nothing published.bb component upstream-plan <folder> --backpack <clone>plans it against a fresh clone of Backpack, reading Backpack’s layout from it (a sibling component with a Storyblok wrapper, Accordion when it’s there): where each file goes (FanStackSB.tsx→src/components/FanStack/storyblok/FanStackSB.tsx), thesb-names (fan-stack.sb.ts→sb-fan-stack.sb.ts, the blok’snameandcomponentfan-stack→sb-fan-stack, display nameFan Stack), the@ef-global/backpack/<X>imports rewritten to Backpack’s own (relative inside the component, its@/src/*alias elsewhere), what Backpack expects that the project lacks (presets, schema and wrapper tests, the preview image, the layout schema and breakpoints, the story’sparameters.siteBuilder, the exactexportsandtypesVersionsentries, the files that list every blok, its generated component list with the command that regenerates it, and the whitelist of each parent the project places it in; a Create’s index, styles and test), the imports it can’t carry over (Storyblok’s ownStoryblokComponentandstoryblokEditableamong them, which Backpack’s ESLint forbids), warnings for a parent or a local blok Backpack lacks, the branch, the base (the clone’sorigin/HEAD), the commit in Backpack’s convention, and the story migration note. It refuses a name Backpack already has.--writewrites the moved files into the clone, only one in the temp folder, and only on the plan’s branch.- The
backbase-upstreamskill and/backbase:upstream: pick the component, plan it, draft the issue with ADR 0025’s fields, file it on the person’s yes, build and check the branch in the clone, then push it and open a draft pull request on a second yes. - The Bash guard blocks merging a pull request in ef-global/backpack, or marking one ready, with gh:
gh pr merge,gh pr ready, the merge API and GraphQL’s merge and ready mutations, named with--repo, a pull request’s URL orGH_REPO, or run in a clone whose remote is Backpack.
-
d8bcd78:
bb component new <name>writes a local component the Site Builder way (MAR-4122), as a Wrap or a Create.--wraps FanStackis a Wrap: Backpack’s React draws it and only Storyblok lacks it. bb writessrc/components/FanStack/FanStackSB.tsx, which imports@ef-global/backpack/FanStackand maps the blok’s fields to its props, andfan-stack.sb.ts, and noFanStack.tsx. The fields are the export’s own props, read from Backpack’s type declarations:childrenbecomes theitemsbloks field, which the wrapper renders withStoryblokComponent. An optional boolean with no boolean default, like FanStack’sactive(unset: fan out on scroll), becomes an Automatic / On / Off option, Automatic by default, which the wrapper passes asundefined: a plain boolean field would be bornfalseand keep the stack shut.--variant prop=valuesets a prop in the wrapper.--from Slider,SegmentedControls,Buttonis a Create: it writesTripBuilder.tsxtoo, built from those primitives, each imported from its own Backpack export.--fields spec.jsongives the fields as Storyblok fields by name, each with an optionalprop.Both copy the design field (
backpack-breakpoints-v2) from Backpack’s own section and flex children, never by hand: the options most of them share, from the first that has them all, which the schema’s header anddata.designFromname. Both also add'<name>': <Name>SBtocomponent-list.tswith an edit that leaves the rest of the file alone, and put the name under each--inparent (sb-sectionby default) inbackbase.placements.json. bb makes sure the project has the one resolver that applies that file when sb-mig syncs:src/storyblok/backbase-placements.ts, spread intobackbase.sb.resolvers.ts, or into the project’s own resolvers file, because sb-mig reads only the first. Then it runs the project’scomponents:discover, and the project’s own prettier over every file bb wrote andall-components.ts, which discover writes with double quotes: the boilerplate’snpm run buildpasses straight after. Running it again changes neither the placements nor the resolver; the helpers are inplacements.ts, forbb adopt. It refuses a name that starts withsb-or ends in-sb, one Backpack has, and one the project has.--dry-runprints the files and writes nothing. The output passes the project Chaperone preset’s rules for local components (MAR-4112), and type-checks in the boilerplate.bb cataloglists the new component with its parents. The Backpack 4.11.0 snapshot inevals/fixturesnow has each public entry’sindex.d.ts, andevals/fixtures/boilerplateis the boilerplate’s master, trimmed, for the tests. -
3cfe109: Replace Promote with local Wrap plans, require evidence for Wrap and Create, and represent missing components as slots. Read legacy Promote plans with a rename warning. Until slots render, scaffold their composable surroundings and report each omitted slot.
Keep real legacy Promote plans usable with migration warnings for missing evidence and slots. New Wrap/Create plans require slots; library-instance evidence is Wrap-only and checked against section data. Omitted slots no longer suppress required-field filling in scaffolded stories.
Keep legacy migration provenance internal, preserve Promote on design-fit write-back, and match library-instance evidence to the wrapped export. Report when an omitted slot’s required field uses Backpack’s example.
-
2f82c67:
bb page reviewputs every section of the rendered page beside its Figma shot, one image a section, for the agent to look at section by section, worst first.- Each section is found by its own
data-blok-uid, never by the frame’s coordinates, brought into view, and captured only once it has settled there (M-22’s settle, per section): images decoded, a lazy carousel built, nothing moving. It’s captured one viewport at a time, never by a full-page screenshot, whose capture beyond the viewport fires a resize that re-lays out a carousel, and with what’s fixed or sticky outside it hidden. A section that doesn’t settle, has no element or moves while it’s captured isnot-captured, with its reason: never a half-loaded crop. - Each image has the Figma shot on the left and the page on the right, both at the frame’s width, under a header with the section’s name, its height frame → page, the delta and the measure’s “N off”. Every run is a numbered folder,
.backbase/review/<slug>/<n>/, withindex.json, worst first by height delta, each section with itspreviousimage.--section <key>captures only the sections an envelope touched. No scores. - compose (before the first push), refine (every pass) and edit (the sections it changed, on the local render before the push, with
bb story serve; on the draft after it only when the person declines the serve) make the review required: the agent opens each image, writes what differs in eight fixed categories, and after each envelope marks each item fixed, a design gap or content. The measure stays the referee for px; a visible improvement overrides a measuredgrewonly written down. The reports list each item and how it ended. - What’s sticky inside a section is pinned where the flow puts it while it’s captured, so a section taller than the viewport shows its sticky bar once, not once a slice. Every call to the page during a capture is bounded: a page that stops answering fails with
page-not-settled. - The whole review has 3 minutes, or
--review-timeout <ms>: a section it doesn’t get to in time isnot-captured, for another run with--section. bb page review --check-notes <review.md>checks the agent’s notes without opening a page: a heading for each captured section, and each item- [<category>] <what's off, and where> — <outcome>. The skills run it before their report.- Like measure and compare, it hides
[data-backbase-ignore]and checks withbb story servewhere the page’s story came from (served-from-storyblok,served-other-file,not-served,serve-unreachable; with--draft,served-from-local), and reviews nothing when it’s the wrong one. --sectionon the page commands takes a section’s review key too.
- Each section is found by its own
-
6ea2251:
bb page measureandbb page comparenever read a page that hasn’t finished rendering (MAR-4910). On 0.16.0 they waited five seconds for fonts, images and the network, then captured anyway with a warning, and never checked that the layout had stopped moving: React hydration, carousels and sticky bars that set themselves up after load,next/imageplaceholders, CSS background images and fonts that reflow text could all be measured half-built, and refine then “fixed” spacing that was right.Now they wait for the document’s load, the fonts, every image (a
next/imageuntil it’s loaded with its blur placeholder gone), and the CSS background images on the measured sections, preloaded and decoded, then for the page’s height, each measured section’s box and the offset boxes of its bloks and what holds them (so a carousel’s track sizing itself counts, and a progress bar animating outside any blok doesn’t) to hold still, within 0.5px for 500ms, with no request loading, starting or ending in that window. bb tracks the page’s requests itself, since Playwright’snetworkidlenever waits again once it has fired; event streams and fetches open 5 seconds or more don’t count, and a warning names them; WebSockets never do. All of it has 20 seconds, or--settle-timeout <ms>, and every look at the page is bounded by it. A page that doesn’t settle fails withpage-not-settled, naming each section still moving, the images not decoded and the requests still loading, and writes no report; so does a page that reloads while it settles, during a look, between two, or before bb has read it, or whose main thread stops answering.--allow-unsettledcaptures it anyway, markedsettled: false:bb page measure --againstjudges nothing from such a measure, nor against one, and with--iterationthe capture stays out ofhistory.json, the verdicts andlatest/.measure.jsonandcompare.jsonrecordsettledandsettle: { ms, stable, waited }.bb catalog render-semantics --measure, which reads computed style rather than boxes, waits the same way and reads a page that doesn’t settle anyway, with a warning./backbase:compose,/backbase:refineand/backbase:edittreatpage-not-settledas a page that isn’t ready: they try once more after a short wait, then tell the person, and never pass--allow-unsettled. -
37a99d8: One name everywhere (MAR-4914): a story file’s name,
<slug>.json, is itsstory.slug, and once pushed, its slug in Storyblok.bb story validate,serve,push,page measureandpage comparerefuse a story whose names differ withslug-mismatch, naming all three and the fix. New:bb story slug <file> <new-slug>is the one way a slug changes: it setsstory.slug, renames the file and its report, push record, ops journal, snapshots and design log, and drops the stamp; for a pushed story, the nextbb story pushtakes the slug to Storyblok, shown in its plan as a slug change. After a hand-edit ofstory.slug, the records still under the file’s name are found and moved too, so a pushed story moves rather than being created twice.bb story pullrefuses a story renamed locally and not yet pushed (story-renamed-locally). Push reads the slug in the space before it creates or moves a story, and refuses one a different story has (slug-taken, naming it).bb story serveserves each story at its slug, refuses to start onslug-mismatchorserve-conflict, putsstory._backbase = { source: "local", file, servedAt }on what it answers from a file (nothing on Storyblok’s answers), and keeps the last answers per slug atGET /_backbase/served?slug=…. Measure and compare check the page came from the--storyfile (served-from-storyblok,served-other-file,not-served,serve-unreachable; with--draft,served-from-local), and hide[data-backbase-ignore]before they capture. Compose, refine and edit measure at the story’s own slug, and handleslug-takenwithbb story slugafter asking.
0.16.0
Minor Changes
-
6c252ba: bb brings its own browser for
bb page measureandbb page compare(MAR-4848), so measuring a page changes neither the project nor Playwright’s shared cache. bb depends onplaywright-core1.64.1 itself, and keeps that copy’s Chromium in a cache of its own,<user cache>/backbase/ms-playwright. The project’s own Playwright, in its ownnode_modules, still comes first when its headless Chromium is installed; without it, bb uses its own and the report says so in one line. A Playwright onPATHno longer counts. An explicitPLAYWRIGHT_BROWSERS_PATHstill wins over bb’s cache, as on backpack-automations.On a first run they exit 1 with
browser-missing, naming the folder, the size and the command:bb page browser installinstalls Chromium’s headless shell only (about 120 MB) with bb’s ownplaywright-core, into bb’s cache, or an explicitPLAYWRIGHT_BROWSERS_PATH, where it removes nothing. It refuses Playwright’s shared cache, where an install deletes the browsers it thinks unused, which other projects still use.bb doctorgains apage-browsercheck: it passes when the Playwright bb would use has its headless Chromium installed, and warns, with the command, when it hasn’t./backbase:composeand/backbase:refineask once before running the install, and no longer tell the person to add Playwright to the project. -
bb5504e:
bb page measureleads with the section heights.data.heightssays how many sections are within ±2px of the frame’s height, and how many more are within a token step (4px), wheredoneis: “1 of 7 within ±2px; 3 more within a token step”.measure.txtand the printed summary start with them. A new list,data.sizesBeyondStep, holds the section heights more than a step (4px) off, then the bloks’ widths and heights more than their step off. It sits besidedata.beyondStep, which stays gaps and padding. A single-blok section’s height is listed once, as the section’s. The judgement’s text gives a section’s height when it’s a step off./backbase:compose’s measure before the push,/backbase:refine’s baseline and/backbase:edit’s measure of the draft now report the section heights first. (MAR-4840) -
cc27705: Full bleed comes from the frame, and Gate 1’s open questions are never answered by default (MAR-4845). Design fit reads full bleed from the frame’s geometry: in a section at the frame’s left edge and as wide as the frame, an image, a media item or a background of its own colour that is as wide as the frame, under layers just as wide, runs edge to edge. Its blok, or the one it sits in on the section’s grid, goes on the grid’s bleed lines from the render table (
[0,6]atsandm,[0,16]atlin Backpack 4.11.0), and ansb-imagetakesviewport_size 100vw. It’s written at the frame’s breakpoint (its section’sBreakpointvariant, or else its width against Backpack’smdandlg) and below: a mobile frame’s bleed never reaches desktop. Since Backpack renders positions mobile-first, the breakpoint above the frame’s is written as it rendered before, the content’s edges andviewport_size’s default.bb plan validate --write-design-fitstores what to write in the plan entry’s newdesignFit.bleed, only when something bleeds, so other stored fits stay current;bb plan showlists it under Full bleed, and/backbase:composewrites it as it is. On reference frame 1, MEDIA / Image full width’s photo is now planned at the bleed lines instead of being asked about. A plan question can now be{ "question", "recommended", "answer" }(a plain string still parses, as an open question with no recommended answer).bb plan showmarks each questionRecommended: …orAnswered: …, counts only the open ones in the table, and ends with a Confirmation that names each recommended answer (Confirm, with: the image bleeds);--jsongivesdata.openQuestionsanddata.confirmation. At Gate 1,/backbase:analyzelists each open question as its own item and offers that confirmation, never a bare “Confirm plan”. A confirmation that doesn’t answer a question leaves it open, and the agent asks again before it writes the plan: it never records a default as the person’s answer. -
f1c3d15:
bb page measure --draft <full slug>andbb page compare --draft <full slug>open the pushed draft through the boilerplate’s preview route,/api/preview/preview?secret=…&slug=…, without the person pasting a secret (MAR-4846). bb builds the URL itself: the secret isSTORYBLOK_PREVIEW_SECRETfrom the shell, else the project’s.env.local, else its.env, and the origin ishttps://localhost:3000, or--url <origin>. The secret, as it is and as the URL encodes it, is masked as***indata.url(new on both commands), inmeasure.json,compare.json,index.htmland the history, in the output and in errors, such as Playwright’s when the dev server isn’t up. When it isn’t set, theno-preview-secreterror says which variable bb looked for and in which files. With--draft, measure writes to.backbase/measure/<story filename>/draft/and compare to.backbase/compare/<story filename>/draft/, so the draft’s never replace the local story’s. core exportsdraftUrl, pure.After the push,
/backbase:refinemeasures the draft itself withbb page measure --draft <data.fullSlug>instead of handing the person a command with a secret to fill in, and/backbase:editdoes the same for a story composed in the project./backbase:composeoffersbb page compare --draftfor the pushed draft. -
65ab854: The knowledge pack knows the section grid and flex alignment (MAR-4844).
<section_grid>says what aposition’s numbers are at each breakpoint: each line by name, the content’s edges and full bleed. In Backpack 4.11.0 that’s lines 0–6 atsandm(full bleed[0,6]) and 0–16 atl(full bleed[0,16]). A number past the last line ismain-start/main-end, the content’s edge, with no error, so[0,16]atmbleeds on the left but ends at the content’s right edge.<flex_alignment>covers each flex box that’s a column by default: which field moves its children vertically and which horizontally, from the classes each renders. In ansb-flex-group,alignisjustify-contentand centres a column vertically when the children hug (item_flexshrink).justifyrenders nothing whilecross_axis_stretchholdsalign-itemsat stretch. In a row the properties swap, and so do a flex group’s fields, soalignstays the vertical one wheredirectionis written at that breakpoint. Both come from the render table, so a new Backpack’s table brings its own.bb catalog render-semanticsnow reads the grid from the installed Backpack’s section helper (mapLayout), and recordsjustify-*anditems-*classes. The table bb ships for 4.11.0 is regenerated with both. A project’s own table written before this (it has no grid) is a warning inbb catalog, to write it again: it takes the grid bb ships for its version, and the pack says alignment is unknown for it rather than leave<flex_alignment>out.bb story design get’s legend says a field with no line changes no spacing, size or layout.bb catalog’srenderSemanticshas thegrid, andalignment: falsefor such a table. The ILS-scope diet bound moves to 62,500 characters./backbase:composestep 2b and/backbase:editpoint to both parts before apositionor an alignment is written. -
1426e44: A saved catalog or knowledge pack is never used against a different installed Backpack (MAR-4851).
bb catalogrecords what it compiled them from: the installed Backpack’s version, the bb version, and a hash of every file the build read. That’s Backpack’spackage.json, its type declarations and every file its schemas and datasources load (their presets, plugin schemas and shared fields);storyblok.configand its imports; the project’s own schemas and datasources with every file they import and the packages they require;component-list.ts,component-map.exceptions.json,backbase.placements.json,.backbase/render-semantics.json; and the space-presets file when it’s used. Backpack’s files are keyed undernode_modules/@ef-global/backpack/however it’s installed, so a linked or patched Backpack at the same version is caught. Line endings don’t count. The catalog has it asfingerprint, with each file’s hash. The knowledge pack has it on its header line:Backpack 4.11.0 · … · bb 0.15.0 · schemas <hash>.Every command that takes
--catalogcompares the saved catalog with the project as installed, re-hashing exactly the files it recorded and the schemas every catalog starts from. A catalog saved for another Backpack, or before one of the project’s schemas changed, fails withcatalog-stale. The message names both versions, or the files that changed, and gives the command that rebuilds it, with--space-presetswhen the file exists. A catalog saved by another bb, or saved before this change, is still used when its Backpack matches.bb catalog --json --out <file>writes the catalog’s envelope to a file, through a temporary file it renames over, so nothing reads half a catalog.--if-stale, with--out, rebuilds the catalog or the knowledge pack only when its fingerprint differs, and says whether it did (data.rebuilt,data.why). What it prints is small: itsdata.fingerprintleaves out each file’s hash, which the file keeps. For the pack it compares the fingerprint only, not--rootor--sections. A catalog saved with--spaceis rebuilt for that space. compose, edit and refine run both rebuilds before they use either file, and tell you in one line when they rebuilt one. analyze writes them with--out.bb doctorhas acatalogcheck: it warns about a stale.backbase/catalog.json, with the command that rebuilds it, and when it can’t compare it, such as with astoryblok.configthat won’t load.
Patch Changes
-
c70fba9:
bb story design getwith a project render table from before bb read alignment classes (MAR-4844) now says so: arenderNotethat whatalignorjustifyrenders is unknown for that table, to write it again withbb catalog render-semantics, andalignment: falsein its data. Its legend then leaves alignment out of what a field with no → line doesn’t change, instead of claiming it. -
a18b902:
bb story fillplaces a preset frame’s copy (MAR-4847). On the reference frame it filled 4 of 31 placeholders and left 27ambiguous, because a preset’s repeated cards share their layers’ names and a repeated blok with no anchor never filled by order. Now:- By instance order. A run of repeated bloks with no anchor takes the frame’s run of repeated instances in order: the n-th card takes the layers whose node ids start with the n-th instance (
I4115:5215;4001:2564;…). A run is one component’s instances, those whose layers share ids, so two buttons beside three cards are two runs. It’s taken only when it’s the frame’s only run of instances there, at any depth, with exactly as many instances, and every anchored sibling sits where its words are. With two runs, neither is taken, whatever their sizes, and only the roles can pair the cards. With one run, the node ids are enough, so this fills by count even without text styles: that’s deliberate. - By role. Each text is a heading, a body or a label. A placeholder gets its role from its blok and where it sits: an
sb-textbefore a headline is an eyebrow, and one in a list item may be either. A layer gets its role from its text style’s whole name in the design read (Header 4,Title 3,Body/Regular,UI Label; a name that says two roles says none), which core’s newcopyStylespairs with the copy. Placeholders fill only when exactly one in-order pairing gives each a layer of its role, and an unknown role fits any. With one layer for each placeholder that’s the order, as before, unless a role contradicts it (an eyebrow doesn’t take aHeader 4title); with more, it’s the one pairing the roles leave, as with a hero’s title and text beside its button’s label. - Repeats without instances (plain frames, such as 6-surfaces) fill by order only by role, and only from layers with a text style.
It still never invents copy, and reports
ambiguousonly when two texts really compete or a role contradicts the order. Onevals/fixtures/ref-1, scaffolded again from its plan, it fills all 31, with the words the run’s agent set by hand. The ILS run 2 fixtures are unchanged. - By instance order. A run of repeated bloks with no anchor takes the frame’s run of repeated instances in order: the n-th card takes the layers whose node ids start with the n-th instance (
-
a47173c:
bb story fillwrites the frame’s copy into empty text fields too, not only placeholders (MAR-4854). On the reference frame, the hero button’s label is an empty field in the scaffolded story, so fill skipped it and the button shipped without a label until the agent set it by hand.- Empty fields. After the placeholders, fill takes the layers they left and writes them into the empty text a blok shows (empty, left out, or
null), in core’s newSHOWN_TEXTtable: a button’s label (sb-text-button’stext), a link’s text (sb-link-text, now a body or label role), a tag’slabel, and a headline’s, a text’s or a body text’scontent. It’s a list because the schema doesn’t say what’s shown: some required text fields aren’t copy, and a button’s label is optional. It works the same way as for placeholders (by node id, by instance order, then by order, with the placeholders it filled as anchors too), but only by role: a layer with no text style, one of another role, or one anambiguousplaceholder could be takes none. - Optional. An empty field may stay empty: each layer goes to at most one field, and they fill when exactly one pairing places the most layers (core’s new
pairSome). An empty subtitle the frame has noBodyfor doesn’t keep the button beside it from its label; two empty buttons and one label layer fill neither. - Never overwrite. A field with words in it is never written, a text field nobody sees (an aria label, an embed’s URL) stays empty, and it never invents copy.
- Reported apart. They’re under
fill.fieldsFilledin the report and on their own lines,Filled empty …, so a person can tell them from placeholders. Those still empty are underfill.fieldsLeft, asStill empty …, with a reason (frame-has-none,ambiguous,no-layer-of-its-roleorno-copy) and their candidates.
On
evals/fixtures/ref-1, scaffolded again from its plan, the hero button gets “Label”, and nothing else that has a value changes. The ILS run 2 fixtures are unchanged: no empty field fills there, and 6 are listed asframe-has-none. - Empty fields. After the placeholders, fill takes the layers they left and writes them into the empty text a blok shows (empty, left out, or
-
fe504b5: The design read now keeps the auto-layout inside library instances, and
bb story spacing planpads a card’s footer from it (MAR-4852). On reference frame 1, eachsb-card-primitive-footerneededpt m. The plan couldn’t find it, becausebackbase-read-figma’s design script left out everything inside a library instance except text, images and instances.- The design script now keeps the geometry of every layer it reads inside a library instance: its size, its auto-layout (
layoutMode,itemSpacing, the padding, the alignment, wrap and the sizing modes), and how it sizes in its parent. It still leaves out the layer’s fills, strokes, radii, effects and typography, and its paging and caps are the same.bb figma sections designkeeps the same and nothing more, whatever the read sends. - The spacing plan pairs a blok inside a library instance that has no layer of its own and no layer inside it, such as a card’s footer, with the layer the library named for it (
Footer). It does that only when exactly one layer no blok has is named that way, and then pads the blok as that layer does:frame padding top 24 → pt m; no own layer: in Content card, a library instance, sb-card-primitive-footer is Footer, by its name. A layer is one blok’s: when two bloks are named for the same layer (two footers, oneFooter), neither pairs with it. A blok with no layer named for it, or one it shares, is skipped, and the skip says why. - A blok inside a library instance whose layer may not be its own, such as a card body paired with the text frame inside the library’s Body, is now looked through to the layers of the bloks inside it. That stops the plan from padding the card around a body that already pads itself. A local layer is still the blok’s own: a card narrower than its repeats in a bento row can be just that.
A read made before this change still has no auto-layout inside library instances, so its card footers are skipped as before. Read the frame again to plan them.
- The design script now keeps the geometry of every layer it reads inside a library instance: its size, its auto-layout (
-
1a24a9c:
bb page measurereads a list’s row padding as the gap between its rows (MAR-4843). Where the render table says a blok spaces its children by padding each one on both sides (sb-list’sitem_spacingpads every row above and below), that padding is measured as the gap, as Figma draws it: the gap between two rows is the first one’s bottom padding, the borders between them and the next one’s top padding, and each row’s size is its content box. The list’s size leaves out what its first and last rows pad at its edges, and a gap next to the list counts that padding: a text’s 24px margin and the first row’s 24px padding are the frame’s 48. Those deltas carry a note saying so. No more of a row’s padding is taken for the gap than the field pads a row. Where the list ends its parent, its last row’s padding stays in the parent’s size, and that size’s note says how much. On ref-1’s COMPOSITION, the list’s gaps go from 0 to 48 against the frame’s 49, its rows from 100–101 to 52–53 against 48, and the gap under the intro from 24 to 48, so none of them is beyond a token step any more. Measure now reads each element’s border widths too. A catalog saved before it had a render table (--catalog old.json) reads the one bb ships for its Backpack, asbb story design getdoes. Where no table has a row for a blok whose children’s gaps are measured, the report warns and names it. -
046b818: Hardening
--draftonbb page measureandbb page compare, from #95’s review (MAR-4846):- A
STORYBLOK_PREVIEW_SECRETshorter than 4 characters is refused asno-preview-secret, with why: masking a secret likeenwould mask it inside ordinary words. - The secret is masked in three forms, longest first: as it is, as the query encodes it (
a+b%28c%29) and asencodeURIComponentdoes (a%20b(c)). A form inside another no longer leaves a tail (abc%maskedsecret=***25). - bb checks where the preview route led, from the last response of goto’s redirects, and measures or compares only the draft; refine could read anything else as drift. A 401 or 403 from the route itself is
preview-refused, naming where bb read the secret: at the route’s path, trailing slash aside (the boilerplate’strailingSlash: true308s to it first), or with no redirect before it. A page that isn’t/<full slug>, such as a login a redirect led to, ispreview-elsewhere, whatever it answered. A 401 or 403 at the draft’s path, after the route accepted the secret (a gated route, basic-auth middleware, a protected deployment), ispreview-blocked, neverpreview-refused. core exportspreviewLanding, pure. draftUrl’s comment says the precedence is bb’s (shell,.env.local,.env), and that.env.development(.local)isn’t read.
The skills tell the person what
preview-refused,preview-elsewhere,preview-blockedand a too-short secret mean, and say the draft wasn’t measured. - A
-
1931c59: The plugin’s Bash guard reads the saved plan from the folder bb runs in, as the shell will resolve it, not always from the session’s folder (MAR-4839). bb reads the space, the remote record and
.backbase/stories/push-plan.jsonfrom its cwd, so the guard follows a plaincd <dir> &&chain at the very start of the command whenbb story pushcomes right after it.cd site && bb story push .backbase/stories/<slug>.json --approved <fingerprint>, run from the folder above the project, is no longer blocked when the fingerprint matches. Anything it can’t follow for sure falls back to the session’s folder, which blocks when the plan isn’t there: acdwith~,$, a backtick,-or a redirect; one followed by;,||or a subshell; apushd; anything else before the push; or a relativecdinto a folder that isn’t there, or whileCDPATHis set. Approved writes in one command that run in different folders are blocked.bb space syncandbb assets uploadfind their saved plans the same way. Every block names the plan file it read. -
6027993: A section’s only root blok (its
sb-section, or a reused preset’s root) now always pairs with the section’s own Figma layer, even when its children split the texts the way an inner frame’s do.bb story scaffoldandbb story fillrecord it that way.bb story spacing planandbb page measureuse the section’s layer even over an older report that recorded an inner frame. In the ref-1 run, COMPOSITION’s section had been paired with its 960px content row, so the spacing plan proposedpb 0and the measure reported a width of 960 andpadding right 0 → 32. Now the section measures 1024 wide with 32 padding, and the plan leaves itspbalone. -
94c4bcb:
bb story spacing plannow plans the padding and gaps beside a blok with no Figma layer (MAR-4842). An icon has no text, so the map often can’t pair it, and the plan used to skip the padding above it and the gap below it. On reference frame 1 that left each surface 24px short at the top. A child with no layer now stands for its parent frame’s layer in the same place (the first, the last, or the one beside its sibling’s) when:- no blok has that layer;
- the page and the frame hold as many children, and the layer is at the child’s place among them;
- the child’s text is the layer’s: none, for an icon, since a text the map didn’t pair with a layer is no match for it;
- the child renders that layer’s size, where the render table knows it.
The padding and the gap are then the frame’s, and each note says so:
frame padding top 24 → pt m; from the parent frame's padding, no own layer: sb-icon stands for Icon. The plan doesn’t use a stand-in when the child has a margin at that edge, or when the gap depends on what its parent holds (a list item’s column gap, which only a custom marker turns on); it skips it and says why. A card body standing for its frame says it in the same words.The bloks inside a library instance that have no layer of their own, such as a card’s footer, are skipped once for each instance. The skip names them: the design read keeps none of the auto-layout inside a library instance, so there’s nothing to read them from.
-
4297a6b:
bb story spacing plan: a blok with no text no longer stands in for a text layer (MAR-4842 follow-up). A text layer has no size in the read, so where the section’s copy had no entry for it, an icon passed both the text check and the size check. It could then stand in for aLabeland the plan would read padding from it. A blok without text now never stands in for a layer that is or holds a text. A blok with text still stands in only when the section’s copy matches its words. When a text in the layer has no copy entry, as in ILS run 2’s read, the skip says that, instead of saying the blok’s own text doesn’t match. -
9486fc1: The plugin’s Storyblok MCP write guard now covers every Storyblok MCP tool and server name (MAR-4858). Two calls used to get past it in a Backbase project:
- A tool it didn’t know. It checked only
execute_readonly,execute_mutating,execute_destructive,upload_assetandupload_asset_finish, somcp__storyblok__execute_batchpassed. The hook now runs before every tool of a server whose name hasstoryblokin it. Tools named as reads pass:searchanddescribe, and on another server named for Storyblok, such as a docs server,get_…,list_…,search…,describe…andread…. Any other tool there is blocked, in a project and when the guard can’t tell which project the call is for, since the guard doesn’t recognise it as the content MCP’s and can’t tell what it writes. - A renamed server. A Storyblok MCP added under another name, such as
sb_cms, was never checked. Itsexecute_tools are now checked whatever the server is named, since only Storyblok’s MCP has them, and so are itsupload_assetandupload_asset_finishwhen given aspace_id, as Storyblok’s are. Another CMS’supload_asset, such as Contentful’s, passes.
An unknown tool on a server not named for Storyblok, such as
mcp__sb_cms__execute_batch, still passes. The hook sees one tool name at a time, so it can’t tell a renamed Storyblok server from a database’s MCP.execute_readonlyof an operation not named as a read (get…,list…,retrieve…and the like) now gets the same in-project checks as anexecute_mutatingwrite, since Storyblok’s MCP doesn’t document refusing a write sent throughexecute_readonly. When the guard can’t tell which project a call is for, only reads pass:execute_readonlyof an operation named for publishing, releases, scheduling or imports is blocked there too. - A tool it didn’t know. It checked only
-
ceb8be6: The plugin’s Storyblok MCP write guard no longer switches off when the session sits above its project (MAR-4853). It used to let every call through when it found no
.backbase/from the session’s folder up, so a session inbackbase-testswith the project inbackbase-tests/sitebuilder-presets-framewasn’t guarded. When the walks up from the session’s folder and from the folder Claude Code was opened in find nothing, it now looks in the folders right under those two, following symlinks. With exactly one project there, it checks the call against it as before. With several, it blocks every call but a read, lists them and names the folders it searched. With projects one level deeper still (work/clients/{a,b}), it blocks the same way and says where they are (projects found under work/clients/: a, b). With no.backbase/anywhere, it isn’t a Backbase context, and every call passes, as before.
0.15.0
Minor Changes
-
4aec35d: The frame’s palette through Site Builder’s theme configurator (MAR-4769, D-5). Site Builder is meant to be re-themed per space, so Backbase reproduces a frame’s colours by configuring the space’s theme instead of approximating them blok by blok.
- core exports
themeOverrides(sections, datasources), pure. For each semantic colour variable the frame paints with (fills and strokes, with their opacity), it compares the frame’s colour with what the space paints today, thecolors-theme-lighttoken’suservalue when it has one, else its default, through everyvar(…). Where they differ it plans theuservalue:set,change(it replaces an override) orreset(an empty value, when the default is the frame’s colour). The value isvar(--color-<primitive>)when the frame’s colour is exactly acolorsentry’s, else the colour itself (#E0D7F9): near misses never snap to a primitive. A translucent paint is listed, not planned, as D-1 counts it approximate. Each colour of its own is offered as acustom-colorsentry named after the frame and the variable, unless one has that colour. Only the light theme: the frame’s read has no dark-mode colours, so Backpack’s dark defaults stay. On the ILS frame it sets the four✱ System/Surface/Brand/Accent/…tokens and finds the other seven already agree. bb space plan --sections <sections.json>adds that theme part to the Gate 2 plan, read from the space’s colour datasources (the theme in itsuserdimension). It shows each token, what it is today and what it becomes, and the custom colours to add. The fingerprint covers it, with theuservalue each write replaces. Eachuservalue someone set that the plan replaces is named in the approval sentence and the hook’s ask, old value and new, and the plan doesn’t say “No destructive changes” while there are any. A value or name read from the space is quoted on one line, cut to 60 characters, so it can’t split the approval sentence. A plan whose theme writes nothing keeps sb-mig’s fingerprint, and the plan prints thebb space synccommand with the same--sections.bb space sync --approved <fingerprint> --sections <sections.json>writes the theme part after both of sb-mig’s syncs succeed, through the guarded Management API client: thecustom-colorsentries (additive), then eachuservalue with theuserdimension’s id, reading the entries again first. Default values are never changed. It skips the theme (theme-plan-again) when sb-mig’s plan wrote one of the datasources it was planned from, and skips a token whose entry was renamed or whoseuservalue changed since the plan. Only#RRGGBB,rgba(…),var(--…)or a reset’s empty value are ever sent.theme-write-failednames each write that failed, and.backbase/space-sync.jsonrecords them, with each token’spreviousvalue. The settings story isn’t written: a person opens Settings, where the plugin rebuilds the theme from the datasources, then saves and publishes it.- The Gate 2 hook names the theme colours and custom colours a sync writes.
- D-1’s
designFit: a colour the frame paints through a semantic variable the theme can express is nowexact, withvia: "theme"and the override intheme: { token, value }, instead ofapproximate. A translucent paint, or a primitive the frame binds directly, stays approximate. So D-2’sdesignFitinplan.json,bb plan showandbb plan validatecount it as fitting: on the ILS plan, the hero has 1 approximation instead of 3. - The sync skill passes
--sectionswhen the project has a Gate 1 plan, shows the theme part at Gate 2, and tells the person to open, save and publish the Settings story after a theme write.
- core exports
-
6a265e6:
/backbase:refineis a measured audit now (MAR-4816). It measures every section withbb page measure, runsbb story spacing planfor each section first (its envelope is a try), writes one ops envelope for each section’s largest deltas, applies it withbb story ops apply, and measures the page again locally, before any push: it keeps what moved each targeted delta toward the frame, undoes the rest withbb story ops undo --entry last, and re-applies a partial try’s good half as the next envelope. Then it fixes up section by section, the most px off first, until every delta and the section’s height are within one token step, 3 tries in a row move nothing, or the section’s 5 tries are spent.fitis only a regression guard: a drop of more than 0.05 (--fit-drop) asks for a look at the images, and never reverts a change that matched the frame. The report gives each section’s px off before and after, the envelopes kept and undone, and what’s left with a reason.--thresholdis gone.bb page measure --against <measure.json> --target <id>…judges a try: keep, partial or revert, per section and for the envelope, from the targeted deltas and whether any other grew by more than its own token step. A step is the interval, at the frame’s value toward the page’s, of the scale that sets the delta: the spacing scale for a gap or padding, Backpack’s type ramp at the frame’s width for a font size or line height (bb ships 4.11.0’s), and a blok’s own sizing field for a width or height, else 4px. A delta is within up to its step and half a pixel, M-4’s rule, which compose’sbeyondSteplist uses too. A section isdoneonly when its height is within a step too; with every delta inside a step and the height not, it’sheightUnexplained. The fit guard reads each section’s fit from page compare’s view of every section at the kept iteration. It needs the catalog’s spacing scale. Without--against,data.judgementsays what’s still a token step off. Each delta has anid,<blok uid>/<what>, the same in every measure, and each section atotal, its px off together.--iteration latestnow records the iteration’s number (it wasnullbehind compare’slatestsymlink) and keeps the measure initerations/<n>/too. -
266307a: The skills write ops (MAR-4820):
/backbase:compose,/backbase:editand/backbase:refinechange a story only throughbb story ops apply, and every change is one journal entry thatbb story ops undotakes back. A new skill,backbase-ops, teaches them how: read the targets withbb story targets, write one envelope (each op, with an example that a test applies to ILS run 3’s story), repair arepairableanswer in at most 4 rounds, acceptunchanged, never edit the story JSON and never pass a uid that wasn’t offered. Compose’s copy, options, icons, images and design values are envelopes afterbb story scaffold, alongsidebb story filland beforebb story materialize. Those three deterministic steps stay as they are. Edit’s structure changes (a FAQ item, a moved section, a wrap or unwrap, a duplicated card, a preset) are envelopes too. Each refine try is one envelope, undone withbb story ops undo --entry last, and the budget counts envelopes. Refine keeps, keeps part of, or undoes a try by whatbb page measuresays about the deltas it set out to close, never by its fit, which only makes the agent look at the images when it falls. A partial keep is an undo, then a second envelope of the ops that closed their deltas, which counts as a try.bb story design setandrevertare thin wrappers now.setchecks its assignments as before, with the same errors, then applies them as one envelope (updateDesign, orsetPropfor an option field) through ops apply’s own code, writing exactly what it wrote before; a call that changes nothing isunchanged, and nothing is written.revertisbb story ops undofor one entry, so it undoes an envelope of several ops in one step and refuses (design-revert-stale) when the story changed since. Their flags and their output stay compatible, but both write the ops journal,.backbase/stories/<slug>.ops.jsonl, not<slug>.changes.jsonl, which stays as history:bb page comparereads it beside the journal, so a story refined before keeps its changes. Each journal entry records the design values it changed (design).bb story design loglists them from the journal, andbb page comparematches its entries to sections by the uids each one touched.bb story ops undotakes--note.bb story ops applyanddesign setread.backbase/datasources.jsononly for an envelope that may set a colour, a target it made itself included. The cage now lets a blok keep a field its schema dropped, when the story already had that field there, as a story pulled from a space can, and aduplicateNodecopy keep its source’s, so adding a FAQ item to such a story works. A test reads bb’s sources and fails on any file that writes a story outside ops or a recorded generator (scaffold, fill, materialize, pull). -
659ab36: Add
bb story spacing plan <story> [--section <name|uid>] [--out <file>](MAR-4815): the frame’s auto-layout, turned into the design values that render it, before anything renders. For each blok with a Figma layer (the mapbb page measurecompares through) it reads what each value renders, in px, from the render table, and chooses the field, the blok and the token: a container’sgaporitem_spacingwhen the table says it spaces the children (item_spacingpads each row above and below, so 16px between rows isxs), else each child’smb; padding on the blok that renders it, a card body’s default 24 included;item_flexFill or Hug in a row, anddirection: rowwhere the frame is a row; anitem_basisfraction only within a pixel of a fixed width. Values land at the frame’s breakpoint only, so what the story has for phones stays. Each px rounds to the nearest token with the error said, a tie to the token Backpack’s own presets use more, listed asties. It writes an ops envelope forbb story ops apply, oneupdateDesignper blok with a note per change (frame gap 16 → item_spacing xs, rows 16 apart), to.backbase/spacing/<story>.ops.json, and lists what no field can express as design-fit gaps, never a guess. Planning again after the apply changes nothing. The render table now records how each element lays its children out (display,flex-direction), so a blok with no direction field (a button, a list item) is laid out as its table says.bb page measurenow lists, asbeyondStep, the gaps and padding more than one token step off the frame; a delta of exactly one step, to half a pixel, is within./backbase:composeruns the spacing plan and applies it after materialize, and, with the person’s yes, measures the page once before the push: when something is more than a step off, it lists it, and the person chooses to refine now or push as is. Without a dev server it says plainly that the page wasn’t measured.
0.14.0
Minor Changes
-
ccfd940: Add the render table (MAR-4813): what each Backpack design value renders as on the page, in CSS and px, on which element, and what each blok renders with its schema defaults.
bb catalog render-semanticsreads it from the project’s own Backpack by rendering every blok and value with its wrappers on the server, and writes.backbase/render-semantics.json;--storywrites the probes as one story forbb story serve, and--measure --url <page>checks the table against the page’s computed style with Playwright. bb ships the table for Backpack 4.11.0.bb catalogshows each blok’s row asrenderSemantics,bb story design getsays under each subfield what each value renders (item_spacing: s → item li padding 16px top/bottom (32px between items)), and the knowledge pack has a short<field_spacing>section with ILS run 3’s traps and what most bloks share, which moves its diet bound to 61k for the ILS scope. A project’s table for another Backpack is a warning, not read, and a Backpack with no table says so inbb catalog, the pack andbb story design get./backbase:composeand/backbase:refineget “How fields turn into spacing”, generated from the table. -
669975c: The ops engine, ported from new-blok (MAR-4818): Backbase’s single editing interface to be, pure, in
packages/core/src/ops/, with no command yet (bb story targetsandbb story opscome in O-2). An edit is one envelope,{ summary, ops, warnings }, of ten typed ops:setProp,updateDesign,insertChild,removeNode,reorderChildren,moveNode,wrapChildren,duplicateNode,unwrapChildrenandinsertPreset. The targets an envelope may address come from the catalog: each blok’s props, the bloks each of its fields takes, and the design subfields its placement admits. An op on a uid that wasn’t offered is refused, and the refusal names at most 20 legal targets. The ops apply all or nothing on a copy, then pass the cage (no invented uids, every blok known and placed where its parent takes it, no new missing required value) and a structural diff. The result isapplied,unchanged(the envelope changed nothing),repairable(with the reasons) orterminal.updateDesignstores exactly whatbb story design setstores, through the same code (setStoryDesignValues), andnullclears a breakpoint asgap@s=does. New bloks get their_uids from the caller, as scaffold’s do, and are born whole through the birth codec, which now leaves the bloks already in the story as they are, such as the cards a new group wraps. AduplicateNodecopy equals its source but for its new_uids.insertPresettakes the catalog’s presets in code and refuses a stale one. A blok that changes parent (a move, a wrap or an unwrap) loses the design subfields its new parent doesn’t render. -
7ee6e7e:
bb page measure --url <page> --story <file> [--section <name|uid>]: exact pixel deltas per blok, Figma layer against rendered element (MAR-4812). Chromium opens the page asbb page comparedoes, light and at the frame’s width, scrolls it through for lazy images and waits for fonts and images, then reads everydata-blok-uidelement’s box, its computed padding, margin, gaps, display, flex direction and grow, alignment, and its first text’s font size and line height. Each blok is mapped to the Figma layer it was built from: a blok holding a frame text word for word is that text layer, and a blok holding other bloks is the layer holding exactly the same texts, with nested wrappers paired in order. Each layer’s box comes from the design read’s auto-layout, never guessed: where a size or position is missing, the geometry says why. It reads the map the story’s report recorded first, and infers only the bloks it doesn’t have, so a copy edit doesn’t lose a blok’s layer. For each mapped blok it compares the gaps between consecutive children’s boxes, along the page’s own flow (a pair that wraps, starts a grid’s next row, or stacks at a breakpoint is listed inskipped, never reported as a gap), the padding to the nearest children’s element boxes, width and height (hug, fill or fixed), alignment in a centred or end-aligned layout, and type, as{ blok, path, figmaNode, what, frame, page, delta }, largest first per section, with a note on an inline element’s deltas. Adisplay: contentselement’s children count as its parent’s. Where a layer hugs and the page’s element fills its parent, it compares the element’s content, and the stretch is a sizing finding. A child that overflows its parent is an overflow finding, and that parent’s padding is left out. Findings and unmapped bloks are listed after the deltas, so a section’s top deltas are what a field can fix. A section split across several top-level bloks maps each to one of the section’s own children, and wrappers pair outermost first around a row. A layer that fills its parent takes its size from the parent, not from a repeated instance’s stale read. A slide off a carousel’s track isn’t an overflow. A section’s top and bottom padding are one row, comparisons within a pixel are listed apart, and alignment needs the frame to say how the child sizes. Each section has its height, frame against page, in a table of its own. A frame exactly a Backpack breakpoint wide gets a warning. It writes.backbase/measure/<story>/measure.jsonandmeasure.txt, a short table per section. A section that fails (hidden, like a sticky bar before you scroll) is reported and skipped; the command fails only when nothing could be measured.--iteration <n|latest>adds the measure tobb page compare’s iteration, whose report then shows the section heights and each section’s measure table under its images; it refuses an iteration of another url, width or story.bb story scaffoldandbb story fillrecord the map in the story’s report, asbloks:{ "<_uid>": { figmaNode, section, path } }, never in the story’s content. Core’sextendBlokMapadds the bloks an op mints to it. -
a3fc31e: Handle asset finalization envelopes, bare assets and empty responses, with credential-free verbose shape diagnostics. Resolve STORYBLOK_SPACE_ID from the shell and project .env after explicit configuration. Keep page comparisons useful after section failures, await lazy images, make filtered captures repeatable and retain every section’s latest capture with shared image storage. Crop sections from one full-page screenshot per pass to preserve carousel layout. Bound asset waits with warnings, preserve animation end states, and isolate space lookup to the command environment. Explain allowed value shapes in story design get.
-
40a73e7:
bb story targetsandbb story ops apply / undo / log: an agent edits a local story with one envelope of typed ops.targetslists the bloks it may address, with their props (a rich text as its mark-up, and what mark-up can’t say), child fields and design options, filtered by section, text (--find), component or colour (--color) and packed into pages of 60k characters.applyapplies an envelope all or nothing against those targets and answersapplied,unchanged,repairable(with up to 30 legal targets for a uid it wasn’t offered) orrefused; it writes the story only when it’s applied, drops its validation stamp and journals the change in.backbase/stories/<slug>.ops.jsonl, with a byte-for-byte snapshot forundo, which refuses when the story changed since unless--force.loglists the journal.Ops set colours from an entry’s name or a hex, matched to the nearest entry by CIEDE2000 within ΔE00 10, or beyond it with
approximate: true, each approximation journalled; a stored selection must name the space’s own entries. Rich text takes a small mark-up (**bold**,*italic*,[link](url)to http(s), a path, an anchor ormailto:), refused when it doesn’t parse or would drop what it can’t say. Design fit now picks the nearest colour by CIEDE2000 too, not by RGB distance, which picked greys for pastels, and writes none past the same ΔE00 10.
0.13.0
Minor Changes
-
262f7e9:
/backbase:refine [slug]: the visual loop, and one push at the end (MAR-4803). The newbackbase-refineskill renders the local story withbb story serve(after the person’s yes to.env.local), compares every section withbb page compare --iteration auto, and works on the worst sections first. For each one it reads the Figma and rendered images, reads the section’s values withbb story design get, makes at most 3 changes withbb story design set, each with its note, and compares the section again. It keeps the changes when the fit beats the section’s kept fit by more than 0.005. Otherwise it reverts them withbb story design revertand captures the section again. A section stops at a fit of 0.9, after 3 tries in a row without an improvement, or after 5 tries; the loop refines at most 3 sections. The person can set each of these. Refine never edits the story JSON directly, never changes content or structure (that’s/backbase:edit, after its push), and skips Gate 1’s design-fit gaps. It stops before any change when someone changed the story in Storyblok since the last pull or push, and on any stop it puts.env.localback and stops what it started. It shows the compare report andbb story design log, then stamps the story and pushes it once, after the person approves the push plan.bb story design revert <file> --entry <n|last>undoes one loggedbb story design setcall exactly. Each value goes back as it was, and a value that had none is deleted again rather than left as an empty override. Where that call lifted a design field into its envelope, the field goes back to what it was, so the story is byte for byte what it was before. It refuses when a later edit changed a value the entry wrote, and logs the revert as its own entry withrevertOf. To make this possible,set’s log entries now recordunsetfor a value that had none,liftedfor a field the lift changed, andsubfields, each design subfield touched, whole, before and after. Becausesetwrites the lean shape, a revert puts the subfield back whole: anullor""over a default stays, and what wasn’t there goes.setreturns its entry’sindex, andbb story design lognumbers its entries.The evals add a scripted dry run of the loop over ILS run 2’s story, cut to the four sections it ranks, with bb’s design commands run in process and a fake compare. It shows the loop keeping improvements, reverting regressions exactly and stopping on budget, and
pnpm evalruns it. -
ad178ce:
bb story materializewrites lean design envelopes:plugin,versionandfields, with only the subfields that have a value at some breakpoint (their schema defaults included) and only the breakpoints that have one. The editor’s empty subfields and the plugin’s own_uid,titleanddescriptionare left out, andfieldsis always kept,{}when nothing is set. Backpack 4.11.0 reads an envelope’s subfields with values and nothing else, so the page renders the same; the defaults stay because an envelope present switches the schema’s defaults off.bb story design setwrites the edited blok’s design the same way, adding a left-out subfield when it sets one, and a cleared value ("") still stays where it clears a default, so materialize doesn’t put the default back. The editor’s own eager envelope, as a pulled story has it, still passesbb story validate: materialize trims it (data.counts.trimmed), and with--sectionsdoesn’t list a section indata.outsidefor that alone.bb story diff, and the section hashes a pull or a push records, compare design envelopes trimmed the same way, so an editor writing the empty subfields back isn’t a change; a cleared value still is. The ILS run-2 story goes from 394,026 to 236,830 characters.bb story validatereports the story’s size: its content as compact JSON in characters (data.size.characters), and its five heaviest sections (data.size.heaviest). The compose skill puts the size in its report. Update the site reference and getting-started flow.
0.12.0
Minor Changes
-
6f9bafc:
bb story pull <id|full path>reads a story from the project’s Storyblok space through the Management API into.backbase/stories/<slug>.json, with<slug>.remote.jsonrecording the space, the story, itsupdated_atand the hashes of its content and each section. It refuses folders, never writes to the space, and never replaces a local file with unpushed changes without--force.bb story diff <story.json>compares the file with the live story section by section (a section is a top-level blok inbody, by itssb-sectionname or_uid, plus:pagefor the page’s own fields, and:orderfor the sections’ order, so a reorder is one change of its own), in two directions: what a push would change, and what someone changed in Storyblok since the pull, each with the changed bloks and fields.bb story push --plannow guards an update: when the story’supdated_atmoved since the pull or push (or its content’s hash did, for a record with noupdated_at), and the push would overwrite a section someone changed there, or undo their reorder, the plan refuses and lists them.--overwrite-sections <keys>overwrites them, and the plan, its fingerprint, the approval sentence and the plugin’s ask name each one. A push records the same hashes as a pull, of the content as Storyblok’s answer has it.bb story materialize --sections <keys>completes only those sections and leaves every other as it is, listing indata.outsidethe ones completing would have changed.The plugin adds
/backbase:editand itsbackbase-editskill: pull, change only the sections asked for, materialize only those (and others only with the person’s yes), validate and stamp, diff, plan the push, and push after the person’s yes, never publishing./backbase:composepoints to it for changes. -
540f0b0:
bb story serve: the local story as a fake Storyblok Delivery API on 127.0.0.1 (port 4100, or--port), so the dev server renders.backbase/stories/<slug>.jsonon every reload, read fresh each time, with nothing pushed. Every blok gets an_editable, so the page hasdata-blok-uidon each section forbb page compare; the story id is the file’s own, else the onebb story pushrecorded, else a stable one bb makes up. The stories it doesn’t have, such assettings, are fetched once as drafts from the real Delivery API with the project’s public delivery token and cached in.backbase/cache/serve/(--offlineserves only the cache). Lists answer empty, GraphQL answers no paths, anything butGETis refused, and a request for any otherHostthan127.0.0.1:<port>orlocalhost:<port>gets a 403, against DNS rebinding. bb never fetches from a server on this machine, so never from another bb. A story file it can’t read or parse gets one line on stderr.--print-envprints the lines that point the dev server at it,--write-envwrites them to.env.local(never.env), keeping the old values in a commented block, and--restore-envputs them back where they were; all three warn when the shell or.env.development.localwould override them./backbase:composeoffers the comparison throughbb story serve, asking before it writes.env.local, or through the boilerplate’s preview mode, instead of a localhost URL that answers 404 for a draft. -
25b0886: Match downloaded Figma images by hash or name, preserve PNG/JPEG/WebP/GIF formats, and upload approved asset plans through the guarded Management API with resumable S3 transfers, finalization retries and atomic per-image recording. SVG is excluded pending sanitisation. Block missing or mismatched write approvals, including commands with global flags. Report unsupported downloads and image-plan/cache gitignore additions in command notes.
-
504ebb1: Real copy, not placeholders (MAR-4793).
sections.jsonkeeps every text line of a section incopy, in layer order with its Figma node id, none cut and repeats kept. It’s optional within v1, so older files stay valid, andbb figma sections designwrites it from the design read.- The design read pages through a large part: whatever a response can’t hold is a page in its
more,backbase-read-figmareads each one withPAGEuntil none is left, andbb figma sections designputs the pages back together. It names the pages still to read, andtruncatednow means only what no page can read, which the skill tells the person about. bb’s own caps on a section’s design rise to 600 layers, 16 deep, 48 children per layer. - New
bb story fill: fills a story’s copy placeholders from each section’scopy, by node id in repeated bloks and by order elsewhere (never by order in a repeated blok with no anchor). Where the frame shows no text it drops only the bloks in one table, tags, eyebrows before a headline, and the card headers they empty, and records what it filled, dropped, left empty and left, with why, underfillin the scaffold’s report. On ILS run 2’s story it leaves 3 placeholders of 35.
-
c63313a: Catch known sticky nav wrapper crashes, export VIDEO fills as checked PNG posters and count unresolved gallery images consistently, defer Figma authentication to the frame read, and compare colour channels with 8-bit alpha tolerance while grouping palette requests by theme collection.
Keep R-7 original image downloads and approved uploads while matching hashless VIDEO posters by node id, checking poster PNG bytes at plan and upload, and reporting them as posters of a video.
-
5512430: Add
bb story design get,setandlogfor targeted, catalog-validated design and variant edits. Each set records its before/after values and optional note in a per-story JSONL change log, and drops validation stamps so the story must be validated again before pushing.Prefer exact option-field names over design aliases, serialize concurrent edits and append receipts with rollback on write failures. Export the story slug and change-log path helpers for the visual loop.
Use the shared core design-log contract and slug/path helpers for both story edits and section comparisons.
-
3c9163c: Add repeatable section filters and immutable numbered page comparison iterations, with an atomic latest symlink, score history, fit verdicts, timestamp-matched L-2 change-log entries and Figma/current/previous offline reports. Keep pixel-ratio-v1 and add deterministic cumulative edge-profile and combined fit scores for tuning spacing. Document the local-story route for avoiding preview overlays.
0.11.0
Minor Changes
-
6bec561:
bb story push <story.json> --plan, then--approved <fingerprint>: bb writes a stamped story to the project’s Storyblok space itself, through the Management API, as a draft, never published (ADR 0028). The plan names the space, the story, create or update, its parent, size and sections, and whether an update’s story is published, with a fingerprint and an approval sentence, as Gate 2 does. It refuses content that changed sincebb story validate --stamp, a stamp for another space, a folder, a path another story has, and a story bb didn’t push unless--storynames it. It retries Storyblok’s 429s and 5xx, never sends a create again blind (it looks for the story at its path first, and adopts it when the create landed but its answer was lost), gives a write 120 seconds, and records what it pushed in.backbase/stories/<slug>.remote.json, so the next push updates that story. ILS run 2’s 394k-character story, which the MCP couldn’t send, goes in one request.The plugin’s Bash guard blocks
bb story pushwithout--approved, with a fingerprint that isn’t the saved plan’s, or with--approvedpassed more than once (andbb space synctoo), and asks the person before the approved push./backbase:composechecks the push plan before it uploads an image, and pushes the story with bb after the person approves the plan, instead of creating it through the MCP. -
4125146: Promote sections get their stand-ins composed (MAR-4792, R-4). In ILS run 2, Gate 1 said each of the three Promotes had a stand-in in the plan, and
bb story scaffoldthen skipped all three, so Campus, Why choose EF? and Testimonials were missing from the story.bb story scaffoldcomposes a Promote’sstructure, its stand-in, exactly like a Compose. The report lists it incomposed, withstandIn: { promote, reason }(the Backpack export it asks for, and why), and the command prints it as5. Package/Campus (stand-in for promote: Chip). A Promote with no stand-in is still skipped, with the reasonPromote with no stand-in: this section will be left out of the page until Backpack has FanStack in Storyblok.Skip reasons now start with a capital.bb plan validatewarns about a Promote with no stand-in (promote-left-out), with or without a catalog. It doesn’t fail the plan.bb plan showends its Gate 1 view with a Promote list: each Promote is eithercomposed from its stand-in until Backpack has Chip in Storyblok, orthis section will be left out of the page until Backpack has FanStack in Storyblok.bb page comparereads the scaffold’s report,<slug>.report.jsonbeside the story or--report <file>, and maps exactly the sections in itscomposedlist, so a story from bb 0.10 (no stand-ins) or one whose scaffold skipped a stand-in maps as it was built. Without a report, the plan decides, Promote stand-ins included. core exportsparseScaffoldedSections, andmapCompareSectionstakes the composed section ids.- Skills:
backbase-classifyalways gives a Promote the Compose that stands in when the catalog can approximate the section, and says what happens without one.backbase-analyzeshows the Promote list at Gate 1 in those words, never just “has a stand-in”.backbase-composereadsstandInand reports a stand-in ascomposed (stand-in for promote: Card). - core exports
isOnPage,promoteRequest,promoteLeftOut,warnPromotesLeftOutand thePromotetype.
-
8e362ad: Detect stale presets that reference removed Backpack bloks. Reject their use in plans and story preflight, warn about their writes in Gate 2 approval, and draft one Backpack maintenance request listing their files and content paths. Guide classification and composition to current atomic bloks.
0.10.0
Minor Changes
-
84f5dbe: Design fit at Gate 1 (MAR-4766, D-2):
plan.jsoncarries each section’s design fit, andbb plan showand the analyse flow present it.- The contract: a plan entry gains an optional
designFit, D-1’s matcher output trimmed: the section’sfit(fits,approximateorgap), how many facts aregapsand how manyapproximations, at most 10 of the facts (gaps first, then what another blok could express, then the other approximations; the same fact on several layers once, with itscount; exact facts left out), and the matcher’snotes. A colour fact keeps its choice,colour: { variable, frame, theme, written, writtenValue }, with the hex the written entry paints. A section whose design read has no layout tree isn’t judged. It’s optional, so every plan written before it stays valid.schemas/plan.v1.jsonis regenerated. bb plan validateworks out each entry’s fit when Backpack is installed and the plan and its sections read: for every entry with a structure whose section has a design read, against the colour datasources in--datasourcesor.backbase/datasources.json, or the catalog’s in code when there are none.--write-design-fitstores the fits in the plan once it passes, and drops one from an entry with no structure. When the plan fails, nothing is stored, and adesign-fit-not-storederror says so. Without it, a missing fit is adesign-fit-missingwarning and one that no longer matches the structure adesign-fit-stalewarning. Gaps never fail the plan.data.designFitlists each entry’s fit and status.bb plan showadds a Design fit column when the plan stores any (fits,≈ N,gap: stroke, overlay · ≈ N, or—when nothing was judged), marked*when the matcher couldn’t judge all of a section. Under the table, per section: each gap in plain words (Price card: 2px gradient stroke: No subfield of sb-surface draws a gradient border.), what another blok would fit, a count of the other approximations, and Not judged, the matcher’s notes.--approximationslists the other approximations, a colour as#e0d7f9 → Color/Red/Lightest (100) (#fff5f6).data.designFithas each section’s counts, remedies and notes.- Skills:
backbase-classifystores the fits after choosing the structure, and where another blok or preset would avoid an approximation or a gap, chooses it and says so insignals.backbase-analyze’s Gate 1 shows the design-fit summary, asks per section only about the gaps and what another blok would fit, then asks once whether to accept the remaining approximations, and mentions what wasn’t judged. Gaps are information for the person and for D-3’s Backpack requests, never a blocker. - core exports
planDesignFit,checkDesignFit,withDesignFit,designFitWarnings,describeFitandrenderFitCell.
- The contract: a plan entry gains an optional
-
3bdd3d1: Add
bb plan requests [plan]to draft design gaps for Backpack after Gate 1. It writes.backbase/requests/design-gaps.md, grouping gaps by blok and missing capability with Figma evidence and the sections that need them. Palette mismatches are separate drafts for design and Backpack together, including when a picker can match the frame exactly.Design fit stores optional, uncapped
requestFactsso the ten display facts never hide requests, with the theme’s hex only on request evidence. Older stored fits remain valid and get a migration hint until refreshed; sections without a stored fit are reported as not judged. Requests group by missing capability, including one request for off-scale typography styles on a blok. The analyse skill offers the drafts after plan confirmation, alongside Promote requests kept inplan.json, and commits a refreshed plan again; a person files requests, never the agent. Regeneration preserves other request files. Update the CLI README, site reference and getting-started flow.
0.9.0
Minor Changes
-
15881d9: Materialize bare kebab icon names into Backpack asset-selector SVG selections, with a cached icons/bold manifest and offline mode. Validate icon formats, warn with closest names for unknown picks, and teach compose to use the design’s icons.
-
be38c95: The design-fit matcher (MAR-4765, D-1): which of a section’s Figma design facts its planned bloks’ Site Builder design options can express.
- core exports
designFit(section, plannedStructure, catalog, datasources), pure and deterministic. It walks the section’sdesignfromsections.json(P-3a) beside the planned blok tree fromplan.json, pairs each Figma layer with a planned blok by order and kind (an auto-layout frame withsb-section,sb-flex-group,sb-surfaceorsb-content-group, a text with a text blok, an image with an image blok, an instance with the blok its name says), and judges each fact against that blok’s design subfields and options in the catalog (P-2).datasourcesare the colour datasources by slug, asbb space datasourcesreads them. - Each fact is
{ path, figma, blok, subfield?, value?, kind, aspect, note }.exact: the option exists and the value maps one to one (a spacing to a step of the scale, a colour to the picker entry the frame paints, a text style to a variant, a variant property to an option).approximate: the nearest value, andvaluesays what it rounds to (another hex, a gap between two steps, a shadow preset, a translucent paint, a mobile text style in a desktop frame).gap: no option on the blok, the bloks it sits in, or any container blok can express it (a gradient or image fill, a border with no border option, an inner shadow or a blur, an overlay, a text style outside the type scale, overlapping children). - What the plan is missing, rather than Backpack, is
approximate, never a gap: a painted layer no planned blok pairs with, and what the plan’s container can’t do but another container can, withremedy: "plan"and thealternativeblok and subfield (sb-content-grouphas no radius,sb-surfacehasdesign.border_radius). An overlay or a gradient is judged against the blok and the bloks it sits in: a card image’s overlay is itssb-card-media’s. - Colours are P-5’s choice (
chooseFrameColourovercolorPickerEntries, socustom-colorscome first): the entry whose value is the frame’s hex isexact, the theme’s entry for another hexapproximate, and a colour the theme can’t name goes to the nearest entry by hex. A colour fact carries the choice as data,colour: { variable, frame, theme, written }. - A section’s
fitis its worst fact’s,fits,approximateorgap, andnotessay what wasn’t judged: a design read the caps cut, the planned bloks no layer paired with, or a section with no design read. spacingScale(catalog)is Backpack’s spacing scale, read from the steps the catalog’sgapoptions label.colorPickerEntriestakes any{ name, value }entries, the catalog’s from code too.- On the ILS frame: the hero’s tags are
approximate(✱ System/Surface/Brand/Accent/3paints#e0d7f9, and the theme’sColor/Red/Lightest (100)is#fff5f6), What’s included’s green and blue tags areexactand its dotted divider is agap, Other Packages’ card overlays are the card media’s, and the price card’s gradient frame is agaponsb-surface. - Nothing calls it yet: the plan and the skills read it from D-2.
- core exports
-
cfdcd2a: The design knowledge pack, its colour map, and value bars for colour names (MAR-4739, ported from new-blok’s knowledge compiler and value bars, MAR-2176 and MAR-2830):
bb catalog --knowledge [--root page] [--out .backbase/knowledge.md] [--sections <sections.json>]compiles the catalog into the compact text an agent reads to choose bloks and write their Site Builder design values: one line per blok the root can hold, with its fields and option values, its design subfields (the values each breakpoint takes, a size’sunits:, a number’smin:/max:, anddefaults:for the breakpoints the schema writes at birth), its placements, itsparents:, and a preset manifest. Shared option sets, sets of bloks, design subfields and placements are said once ($o,$c,$d,$p). An option set shows the editor’s label where it says what the value doesn’t (secondary=Outlined,xxs=4px). A blok whose bloks field takes any nestable blok is in every blok’s parents, marked*. A colour picker lists its datasource’s real entry names, never the slug, and an icon field isasset-selector(icons), never the plugin’s settings.- With
--sections, the pack maps each colour variable the frame uses (design.tokens.colorinsections.json, from P-3a’s design read) through Backpack’s theme datasources to thecolorsname a picker stores:✱ System/Surface/Brand/Accent/3iscolor-system-surface-brand-accent-3, whichcolors-theme-lightpoints atColor/Red/Lightest (100). A token whose colourcolorshas no entry for says so, with its CSS variable, and a token the frame paints another colour than the theme gives it ends≠ frame #hex: in the ILS frame, all four brand accents. - Scoped to
page, the ILS project’s pack is 57,100 characters, and 58,700 with the real frame’s 14-variable colour map, under a 60,000-character diet with a bound per section. Without--outit prints the pack;--jsongives it asdata.text, with each section’s size, the colour map anddiagnosticsfor a maintainer. A root the catalog doesn’t have is anunknown-rooterror naming the root bloks it does. - The catalog reads the datasources in code, Backpack’s
lib/storyblok/datasources/*.sb.datasource.cjsand the project’s own*.sb.datasource.ts, intodatasources: [{ slug, name, source, entries: [{ name, value }] }], the project’s own over Backpack’s. Each design subfield also keeps a size’sunits, a number’sboundsand the editor’slabels, and an unrestricted bloks field is markedunrestricted. bb story validatewarns,design-unknown-token, about a colour name that isn’t in code’s datasource for its picker, or a hardcoded value where a name goes, naming the datasource’s closest names. It never fails a story: invented option values staydesign-invalid-valueerrors. A catalog saved without datasources checks no colours.- core exports
figmaVariableToThemeToken,resolveThemeColours,bridgeFigmaColours,frameColourVariablesandframeColourHexes. bb createadds.backbase/knowledge.mdto the project’s.gitignore.
-
1f5ffc5: Core’s
chooseFrameColourpicks thecolorsentry to write for a frame’s colour variable: the entry whose value is the frame’s hex (case aside), else the theme’s primitive, as an approximation{ variable, frame, written }, else nothing, and why. The knowledge pack’s<colour_tokens>lines say it:| write <name>for the frame’s entry where the theme’s differs,| ≈ approximationwhere no entry is the frame’s hex. In the ILS frame, the Success and Info tags are written exactly, and the hero’s✱ System/Surface/Brand/Accent/3(#e0d7f9) is approximated byColor/Red/Lightest (100)(MAR-4741). -
d781430: House style in the knowledge pack: how Backpack’s own presets are built, as rules with their counts (MAR-4742, ported from new-blok’s house style, MAR-2501):
- The pack ends with
<house_style>, the rules Backpack 4.11.0’s 90 distinct presets in code hold to, each with its count. Atitle3headline isas:"h4"(9/9). A design value is written atsand cascades up: 767/816, but 413/459 without the values that are their schema default, so it’s taught at the threshold, until it’s confirmed. One hero rule is markedCURATED, ruled by hand in new-blok: media beside the words centres both halves withitem_align, a background hero bottoms its words, white text over its photo always comes with thedarktheme, the headline staysheader2, andminimum_heightis viewport percent. - A rule is taught only when the presets hold to it at least 90% of the time, over at least five bloks, and its value isn’t the field’s schema default. new-blok’s other rules, counted on EF’s preset study space, aren’t carried over: Backpack’s presets in code have one section, so section padding, gaps, flips, card rows and header blocks have nothing to stand on yet.
bb catalog --knowledgestays under its 60,000-character diet with the house style: 59,700 for the ILS project with its colour map.bb story validatewarns,house-style, once per rule, where a story differs from the house style, with the count beside it. It never fails a story.- The compose skill follows the frame’s
designwhere it states a layout, mapped to the nearest token, and the house style where the frame is silent or ambiguous, and its report says which, per section. White text over a hero’s photo always comes with the dark theme, whatever the frame shows. - core exports
HOUSE_RULES,HOUSE_EXEMPLARS,renderHouseStyle,renderHouseExemplarsanddescribeHouseDeviations. The exemplars, Backpack’s presets distilled to their shape, are generated and checked, and wait for room in the pack.
- The pack ends with
-
4dcc876:
sections.jsoncarries each section’s design intent (MAR-4733):- The sections contract gains an optional
designper section, within v1: it’s additive, so everysections.jsonwritten before it stays valid. It haslayout(the auto-layout tree: mode, gap, padding, alignment, wrap and sizing, fills and strokes with the variable each colour is bound to, radii, effects, opacity, text with its typography, and the variables and styles bound to each layer),tokens(the variables used, by kind: spacing, colour, radius, typography and other, and the styles, with the token id a style’s description names),instances(each component with every distinct set of variant and boolean properties its instances use),icons(instances from “EF Backpack Icons”, by name, with the component holding each and the instance-swap property that chose it),images(every visible image fill: node id, image hash, size, scale mode, layer name, holder and section) andtruncated.bb figma sections validatechecks it, and counts the sections that have one.schemas/sections.v1.jsonis regenerated. bb figma sections design <sections.json> [--raw <file>]reads the design script’s output (figma-design.raw.jsonnext tosections.json, or--raw), normalizes it offline and writes each read section’sdesignintosections.json, keeping every other field. A section whose read failed keeps its fields, loses itsdesign, and gets a note saying why. A section the read doesn’t cover is left as it was. Running it again gives the same file.- The normalizer is ported from new-blok’s Figma distiller (
figma-distill.pure.ts, at 4cc09b9), with the same caps: 260 layers and 12 levels per section, 24 children per layer, 180 characters per text. It drops vectors, hidden layers, Figma’s own layer names and a library instance’s chrome. It’s tolerant: unknown fields are ignored, and missing ones are null. bb createadds.backbase/figma/*/figma-design.raw.jsonto.gitignore.- The plugin’s
backbase-read-figmaskill has a Design step: a read-onlyuse_figmascript inreferences/design-script.md, one part per call, saved with the libraries resolved by key, thenbb figma sections design. The script keeps each part’s response within a 24,000-character budget, checked before each entry is added: the tree gets everything the part’s images, instances and icons don’t need and at least 60%, and those give way when both don’t fit. It counts instances once per component and set of properties (top-level first, internal components last), counts icons once per component and chain of holders, sends a repeated sibling instance as{ sameAs, text }(bb rebuilds it with its own words), returns each component and set of properties once, uses short keys for variables, styles, components and instances, hex colours, and no defaults, and inside a library instance keeps only components, words and text styles. A layer that can’t be read comes back as an error and the rest of the part is still read; aROOTthat doesn’t resolve says so. The node’s fill is its top visible paint with an opacity above 0, and every visible paint’s variable counts. A grid’s gaps count as spacing. A test runs the script against a fake Plugin API that records every write, and fails on any mutating call in its source.backbase-classifyreadsdesign.instancesanddesign.tokensas evidence for presets and variants. - The outline and parts scripts’
cleantests character codes instead of a regular expression: the MCP turns an escaped U+2028 into a real line break before the code runs, which left the regular expression unterminated. A test checks that no script has a\uescape and that each still parses once escapes are turned into characters.
- The sections contract gains an optional
-
1f5ffc5:
bb story materializewrites the value formats the design fields’ pickers store (MAR-4741):- Colours: a colour picker’s value, in a design or as a field of its own, may be the name of an entry in the space’s
colors(orcustom-colors) datasource, and becomes the picker’s{ "selected": { "light", "dark" } }with the space’s entry. The entries come from.backbase/datasources.json: without it a name is an error (color-not-materialized), never left bare. A bare string is never stored: a name the space doesn’t have, a hex or a theme token is acolor-not-materializederror that names the closest entries. A selection already stored with names the space doesn’t have warns (color-unknown). A standalone picker’s value names its plugin,backpack-color-picker, and an entry read without a dimension is stored withdimension_value: null. - Sizes and bounds: a
backpack-sizewritten as a bare number gets the unit Backpack reads it in, vh, made explicit (size-without-unit), and"600px"is stored as its number and unit (size-not-materialized). More than 100 of%,vhorvw, and anumbersubfield past its schema’smin_valueormax_value(sb-hero’sminimum_height, 0 to 100), are clamped, with anumber-clampedwarning. The catalog keeps each size’sunitsand each number’sbounds, so acatalog.jsonsaved before this is refused. - Links: a link written as an
https:,http:ortel:URL, a path or an anchor becomes the url link the editor stores,mailto:the email link, and one with keys missing gets them. A placeholder,#backbase-placeholderincluded, stays the empty link, never a URL. Anything else,javascript:included, ismultilink-malformed. - Sections: the sticky nav’s
anchor_linksandhidden_in_sectionstake a list of sections, by name, label or anchor, and get the section selector’s{ "selectedSections", "plugin", "version" }, by eachsb-section’s name. A name no section has warns (section-unknown).
bb space datasourcesreads the entries of Backpack’s colour datasources from the project’s space, read-only, into.backbase/datasources.json.bb story materializeandbb story validateread it, or the file--datasourcesnames, and refuse one from another space.bb createignores it.Core has
themeColorEntryName, which names the theme datasource entry a Figma colour variable is:✱ System/Text/Defaultiscolor-system-text-default. - Colours: a colour picker’s value, in a design or as a field of its own, may be the name of an entry in the space’s
0.8.0
Minor Changes
-
c259a3d: Every blok is born whole: a story’s content as the Storyblok editor writes it (MAR-4732, ported from new-blok’s birth codec and design envelope, MAR-2183):
bb story materialize <file>(new) completes a story file and writes it back, offline, against the project’s catalog or--catalog. Every absent field gets its birth value: the schema’sdefault_value, else[]forbloks,optionsandmultiasset,{ "type": "doc", "content": [] }for rich text,{}for custom,""for text, textarea, option and number,falsefor a boolean, and no key for tabs and sections. A value that’s there is never touched. Everydesignbecomes the backpack-breakpoints-v2 envelope,{ plugin, version: "2.19.1", fields: { <subfield>: { type, field_type?, values: { s?, m?, l? } } } }, with every subfield the blok’s actual placement admits, each with its schema default under the values written. A design in the simplified dialect,{ "gap": { "s": "xl" }, "direction": { "s": "column", "l": "row" } }, is lifted into it, on its own or written beside an envelope (the written values win), and subfields the placement doesn’t admit (positionoutside ansb-section,item_flexoutside a flex group) are stripped. A subfield the field doesn’t have, a key beside the envelope that’s no subfield, a value outside its options, storedvaluesthat aren’t per breakpoint, or a single value for a subfield that stores an object (colours, sizes, spacing, layout, alignment with offsets, transitions) is an error, and then nothing is written. A number for a numeric-string option is written as the string. It reports what it filled, lifted, materialized, stripped and skipped, with why, and it’s idempotent: a second run writes nothing.- The catalog keeps each design field’s subfields parsed, under the field’s
design: each one’stype,fieldType,options(per breakpoint where they’re declared that way),datasource,defaultValueandplacements, with door B’s placements for a blok that declares none. Every field keeps itsdefaultValue, and every custom field itsfieldType. Acatalog.jsonsaved by an older bb is refused: save it again withbb catalog --json. bb story scaffoldcompletes what it emits the same way, so the story it writes validates, and drops the design subfields a preset holds that its schema no longer has. Each section’s top-levelsb-sectiongets its id inname, from the last part of the planned section’s Figma name in kebab case and unique in the story (whats-included,campus,whats-included-2): the sticky nav’sanchor_linksandhidden_in_sectionslist sections by it. The report adds each section’sanchor,{ id, label }, what itdroppedfrom presets, andcompletion.bb story validatereportsmissing-field,design-not-materialized,design-placement,design-unknown-subfieldanddesign-invalid-value, all errors, so--stamprefuses while any are left. The first three say to runbb story materialize. A design field in another breakpoints dialect is adesign-dialectwarning.
-
f602dda: Export Figma image layers, plan and record assets by hash, and include guarded uploads in compose’s single write approval. Reuse confirmed assets on reruns, set full CDN asset envelopes on page slots, and reject image URLs outside the project’s Storyblok space.
-
4f91a9f: Make doctor report covered token checks as passing and unseen Figma connections as neutral skips, omitting Figma under –write. Add advisory checks for the project’s translation strategy and settings stories at the root or under each configured locale folder.
-
ca46121: Add optional
bb page compareto capture rendered story sections at the Figma frame width, pair them with saved shots, and write deterministic pixel scores and an offline side-by-side report. Playwright is loaded from the project or an existing PATH install and is never installed implicitly. Support frame-only screenshots by cropping section bounds, retain both available sides of partial comparisons, and match saved story ids before falling back to page order. Offer comparison as an optional next step after compose. -
cd4d249: List field-plugin requirements before space sync and explain missing-plugin failures per component. Add plan-scoped compose preflight, one-line destructive approval, project .env guidance, and default-locale folder placement.
0.7.0
Minor Changes
-
46b736b: The approved plan becomes a draft story (MAR-4697):
bb story scaffold [plan]turns.backbase/plan.json, the frame’ssections.json(.backbase/figma/<node-id>/sections.json, or--sections) and the catalog (the project’s, or--catalog) into.backbase/stories/<slug>.json,{ "story": { "name", "slug", "content" } }, offline. The content is the project’s page blok (page, or--root) with one entry per Reuse and Compose section, in page order, built from the bloks the plan names, with fresh_uids. A blok whose plan names a preset in code starts from the preset’s content, with the plan’s children in the fields they go in.- What the plan doesn’t say is filled so the story passes
bb story validate: a required text field, and a preset’s copy, is a placeholder that starts[Backbase placeholder; every asset field is an empty asset with its section’s name asalt; a preset’s links are emptied and a required link is#backbase-placeholder; a required bloks field or option the plan leaves empty gets what Backpack’s presets put in it (leaving out bloks the catalog doesn’t have), or else the only blok it takes, or the first option. Fields a preset holds that its blok’s schema no longer has are dropped. - It writes
.backbase/stories/<slug>.report.json: the sections composed, with their text from the design; the sections skipped (Create, Promote, Global, Unsure) and why; the images to upload; every placeholder, with its path; each preset, and whether it’s applied (a preset from the space isn’t: its content stays in the space, and its blok stays in the story, empty); andtodo, the counts of placeholders, images and sections with a space preset to apply in the editor. - It deliberately fills no text from
sections.json: the plan doesn’t map the design’s text to fields, so the compose skill’s fill step does it, from each section’s text in the report and the screenshots. - The name is the frame’s, or
--name; the slug comes from it, or--slug. It never writes over a story it scaffolded before unless--force. A plan that doesn’t fit the catalog, or a root that isn’t one, fails with the plan’s paths. bb story validatereports aplaceholderwarning for each placeholder the scaffold left (text starting[Backbase placeholder, or a link to#backbase-placeholder), and counts them indata.counts.placeholders.bb story validate --stamp: once a story passes with no errors, and no placeholders unless--allow-placeholdersis passed, it records the sha256 of its content, as canonical JSON, in.backbase/stories/validated.jsonas{ "<hash>": { file, at, spaceId } }, wherespaceIdis the project’s configured space (spaceIdinstoryblok.config, thenspace.idin.backbase/config.json; never a flag, so a stamp can’t name another space). Without a space it doesn’t stamp, and fails.--jsonaddsdata.stamp. A new stamp drops the stamps for any other space, which are stale.
-
b81e722: Gate 2: sync the project’s components and datasources to its Storyblok space as a write plan a person approves (MAR-4695):
bb space planchecks whatbb doctor --writechecks, then that the project’s own sb-mig (node_modules/.bin/sb-mig) is6.5.0-beta.25or later, and runssb-mig sync datasources --all --dry-run --json, thensb-mig sync components --all --presets --dry-run --json, with nothing on stdin and the space bb checked asSTORYBLOK_SPACE_ID. It writes.backbase/space-plan.json(the space’s id and name, the sb-mig version, both plans, a summary and a timestamp) and prints the counts per kind, every destructive change on its own line (field-removed,likely-rename,type-changed,whitelist-narrowed), and the plan’s fingerprint: a hash of the space and the planned changes that stays the same while neither changes.bb space sync --approved <fingerprint>plans again and refuses, writing nothing, when the fingerprint isn’t the approved one (plan-changed), or when the plan has destructive changes and--allow-destructivewasn’t passed (destructive-not-allowed). Then it runs the two syncs, datasources first, and writes sb-mig’s reports to.backbase/space-sync.json. It fails, listing what failed, when either sync reportsok: falseor exits non-zero; when the datasources fail, the components don’t run.- Presets: sb-mig plans them by name, so it overwrites every preset the space has on each sync, and writes the presets of components whose schema is unchanged. The summary lists them (
summary.presets:createandoverwrite, by component), the render shows them, the fingerprint covers every component’s presets, anddata.written.presetssays what the sync wrote. plan-changedno longer prints the new plan’s fingerprint: a new plan needs the person’s approval first.- What bb writes to
.backbase/space-plan.jsonand.backbase/space-sync.jsonis masked like its output: the env’s secrets, the token from.envandstoryblok.config’soauthToken. - bb evaluates
storyblok.config(inbb doctor,bb catalogand the space commands) with the project’s.envloaded first, as sb-mig does, never over the shell’s variables. AspaceId: process.env.Xset only in.envnow gives bb the space sb-mig writes to. - bb never passes
--ssotto sb-mig: no flag asks for it, and the arguments are built in one place that throws if they’d hold it. bb createadds.backbase/space-plan.jsonand.backbase/space-sync.jsonto.gitignore.bb doctor --json: thespace-accesscheck’s result hasfacts: { id, name }, the space it read.- The Storyblok token belongs in the project’s
.env.storyblok-token’s fix leads with it, and gives a shell export as the alternative, saying it overrides.env.project-storyblok-tokenwarns when the shell and the project’s.envboth setSTORYBLOK_OAUTH_TOKENto different values: the shell’s silently wins for sb-mig and bb. It compares them without showing either. Under--writethis warning doesn’t block, sincespace-accesschecks the token sb-mig will use.
0.6.0
Minor Changes
-
26295b3:
bb doctorchecks the project and its Storyblok space too, not only the machine. In a Backpack project it checks that Backpack 4 is installed (project-backpack), that sb-mig can findSTORYBLOK_OAUTH_TOKENin the environment or the project’s.env(project-storyblok-token), and that git ignores.env(project-env-ignored). It checks that the project has a space id (space-id), that the token can read that space, with one read-only Management API request to the API sb-mig uses (space-access), and that the agent has the Storyblok MCP (storyblok-mcp, which only warns). Outside a project, the project and space checks skip.space-accesssends the token only over https to Storyblok’s own Management API hosts (Canada’s included), and never sends a token with spaces or line breaks in it.bb doctor --writeis the check before a write to Storyblok: the token, project and space checks must pass, and a warning or a skip counts as a failure. Its exit code follows only whether a write is ready: other failures are listed, and don’t fail it.bb doctor --space <id>takes the space id for one run. Core exportsreadyToWrite, so the skills can ask the same question of a doctor report. The token is never printed: not in the text, the JSON or the errors. -
96c6cf9: Check a story before it’s written (MAR-4691):
bb story validate <file>checks a story’s content against the project’s catalog, offline, before anything writes it through the Storyblok MCP. The file is the content ({ component, _uid, ... }), an object with it undercontent, or a whole story understory.content.- It builds the catalog the way
bb catalogdoes, or takes--catalog <file>, such as the.backbase/catalog.jsonsaved at analyse time (bb’s envelope or a bare catalog). - Each problem has a path to the key to fix (
content.body[2].content[0].component, orcontent["my key"]), a code, a severity and a message. The errors, which fail it with exit 1:unknown-component(with the closest names),not-root,root-only,child-not-allowed,min-items,max-items,missing-required,invalid-option,wrong-typeanduid(missing, not a string, or repeated). The warnings, which don’t:unchecked-option, for options from a datasource or the space’s stories, which the catalog can’t see, andunchecked-children, for a bloks field restricted by tag or by a component group the catalog can’t resolve. unknown-fieldwarns about a key the schema doesn’t have, and fails when the key holds bloks, such as a misspelledcontents, since they’d never render; the bloks in it are still checked._uid,componentand_editableare always allowed, and a translation such astitle__i18n__deis checked as its field, never as required.min-itemsonly warns about an empty bloks field that isn’t required.--jsongivesdata: { file, catalogFile, backpackVersion, root, problems, counts: { bloks, errors, warnings } }, with every problem and itsseverity, and the errors inerrors.- A number field takes a number or a decimal number as a string (
"12","0.5"), since Storyblok saves one as a string; a datetime takes"YYYY-MM-DD HH:mm", with or without seconds. A field that’s null or""counts as unset, as Storyblok saves an untouched one. - The new
story.v1contract describes the file. Its JSON Schema is the plugin’sschemas/story.v1.json. bb catalogrecords where an option field’s options come from when they aren’t its own:source(such asinternalorinternal_stories) anddatasource, the datasource’s slug.bb catalogmakes a schema withoutis_nestablenestable unless it’s a root, as Storyblok does; it used to make it not nestable.bb catalogresolves a bloks field restricted by component group (restrict_type: "groups") to the nestable bloks whosecomponent_group_nameorcomponent_group_uuidis in itscomponent_group_whitelist. When a group matches none, or the field restricts by tag, itsallowedChildrenisnullwithrestrictedTo: "groups"or"tags", instead of[], which rejected every child.
0.5.0
Minor Changes
-
9a3ac66:
bb catalogshows the variants Backpack’s React has that a blok’s schema can’t pick, and honours the component map’s exceptions.- It reads the string-literal unions in Backpack’s
.d.tsfiles, and the props that use them, with a small parser (no TypeScript compiler). Each export lists its components’ union props asbackpackExports[].reactComponents. Each blok lists thevariantvalues novariantor<x>_variantoption offers asreactOnlyValues: [{ export, packageExport, prop, values, via }], andbb catalog --component sb-cardprintsReact only: CardFooter.variant: testimonial (in Card, via folder: may not apply), with one line saying these are leads, since a wrapper may map other option names onto React’s.exportis the component (CardFooter), andpackageExportthe package export whose folder holds it (Card), the name a Promote’s primitive takes.viasays whether the blok’s own wrapper renders the component (file) or they only share a folder (folder). Props inherited from another component’s folder (such as Text’s typography variants) are left out, intersections with object types are read, and a component’s props can come from the type its declaration names (FC<AnyProgressProps>). component-map.exceptions.jsonat the project root ({ "<blok>": "<why it needs no map entry>" }) excuses bloks frommissing-map-entry. The summary counts them asexcused. An entry for a blok that doesn’t exist, or one already mapped, is anote:, not a problem. Without the file, nothing changes.missing-map-entrynow says what it means: the blok isn’t in the component map, so it renders nothing if it’s placed where the page hands it to the map.- The classify and analyse skills read
reactOnlyValuesbefore calling a variant missing from React, and word the map problems the same way.
- It reads the string-literal unions in Backpack’s
-
575d605: Polish after the first real
/backbase:newrun (MAR-4684).- Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except
\nand\tinto a space in everything they return, object keys included, and cut text lines at 160 code points with…, so an emoji is never split. The parts script reads 3 parts per call, and a batch that overflows is halved. - Classifying: a self-check before
plan.jsoncompares every Compose with the Reuse rule and the worked examples (two roots that are each one blok doing its own job are a Reuse). A question that could change the bucket caps confidence atmedium, one that decides whether Backpack can do it at all makes itunsure, and content questions don’t count.reactOnlyValuesis a lead, not proof: the agent reads the Storyblok wrapper’s source before calling a section a Promote, and avia: "folder"link counts for less. A confirmed one is a Promote of the entry’spackageExport, with the variant named inreason. A variant in neithervariantsnorreactOnlyValuesisunsure, or a Create for the person to decide. - Gate 1: the analyse flow commits exactly
sections.jsonandplan.jsonasPlan "<frame name>" at Gate 1, unsigned, with every git command run in the project folder (git -C) and the paths given togit commitso nothing else in the tree is touched. It checks the ignore rules before it stages anything, never forces an ignored path, and unstages only its own paths when the commit fails. It skips the commit when git has no identity, taken fromgit configor theGIT_AUTHOR_*andGIT_COMMITTER_*variables asbb createdoes, and says so.missing-map-entryis information, mentioned only for bloks in the plan, and never as bloks that must be registered before they render. bb createadds.backbase/catalog.json,.backbase/figma/*/shots/and.backbase/figma/*/sections.draft.jsonto.gitignore, next to.backbase/*.log.bb doctor: nobbon PATH is a pass forbb-binary, since the plugin runs bb through npx. A Babashkabbis still a warning.
- Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except
-
54366b1: Reuse the Storyblok space’s presets before composing (MAR-4427):
- A new
space-presets.v1contract,{ version: 1, spaceId, readAt, presets: [{ component, name, id, image, description }] }, for the space’s presets with their preview images and descriptions, never their content. Its JSON Schema is the plugin’sschemas/space-presets.v1.json. plan.v1structure nodes take an optionalpreset, the name of the blok’s preset they reuse. Existing plans stay valid.bb catalogandbb plan validatefind the project’s Storyblok space id in--space <id>, thenspaceIdinstoryblok.config, thenspace.idin the.backbase/config.jsonthatbb create --spacewrites. bb never reads.env.data.spacein the catalog has itsidand itssource:"flag","config","backbase-config"or"none".bb createalso ignores.backbase/space-presets.jsonand.backbase/space-presets/(its preview images), so Gate 1 never commits them: likecatalog.json, they’re a snapshot of something outside the repo, read again on every run.bb catalog --space-presets <file>adds the space’s presets to the catalog: each component lists itsspacePresets, with their images, next to itspresetsfrom code.data.space.presetssays which space they were read from and when.data.summary.spacePresetshas two counts:read(every preset in the file) andmatched(those whose blok is in the catalog). A blok the space has presets for and the catalog doesn’t have is anunknown-preset-componentproblem. Presets read from another space than the project’s fail the command.bb plan validate --space-presets <file>checks everypresetin a structure against its blok’s presets, in code and in the space. One it doesn’t have is anunknown-preseterror with the closest names; without--space-presets, the error says the space’s presets weren’t checked. A blok with a preset gets no warnings about its own fields, which the preset fills.bb plan showshows a preset after its blok:sb-section [SHOWCASE / 3-cards].- bb never calls Storyblok: the agent reads the presets and bb reads the file.
- A new
0.4.0
Minor Changes
-
c63c9d2:
bb create <dir> [--space <id>] [--ref <ref>] [--transport auto|ssh|https]makes a new site from the boilerplate,ef-global/storyblok-nextjs-backpack-boilerplate. It checks that the folder is new or empty, resolves the ref to a commit SHA, and clones it shallowly: over SSH first, never prompting, then over HTTPS through your git credential helper. It names the package after the folder, writes.envfromSTORYBLOK_OAUTH_TOKEN(or warns when it isn’t set) and installs with the lockfile’s package manager, logging to.backbase/install.log. When the project’s.npmrcreadsGH_PACKAGES_TOKENand it isn’t set, it fails before installing. Then it records the boilerplate commit and the sandbox space in.backbase/config.json, adds a Backbase section toAGENTS.mdand makes the first commit,Start from storyblok-nextjs-backpack-boilerplate@<short sha>.The first failing step stops the run with its fix and exit 1. It never takes a token as a flag, never prints one and writes nothing to Storyblok.
--jsonreports every step’s status, detail and duration.git never prompts in
bb create: no terminal prompt, no askpass dialog and no Git Credential Manager window. The first commit is unsigned, socommit.gpgsigncan’t wait on a passphrase. It needs a complete identity, for the author and the committer, fromgit configor theGIT_AUTHOR_*andGIT_COMMITTER_*env vars.data.leftBehindsays when a failed run left a half-made project in the folder. -
133c653:
bb doctorchecks that a machine is ready for Backbase:- Node 20 or later, and git.
- A
bbon the person’s own PATH that’s Backbase’s and not Babashka’s. The bin folders npx and npm exec put first don’t count. - GitHub sign-in, through
ghorGH_TOKEN/GITHUB_TOKEN. - SSH to GitHub.
- A
GH_PACKAGES_TOKENthat GitHub Packages accepts. - A Figma connection for Claude Code: a Figma plugin enabled in its settings, or a connected Figma server in
claude mcp list. STORYBLOK_OAUTH_TOKEN.
It shows every check in a table, with the exact steps under each one that fails or warns, and exits 1 only when a check fails. SSH, Figma and the Storyblok token only warn: a claude.ai connector can’t be seen from the command line.
--jsoncarries every check’s result.--fixapplies the automatic fixes; none of these checks has one yet, and it says so. It names token variables and never prints their values, nor anything from the settings it reads.Checks run side by side as far as what they need allows, and the results keep their order. A check whose need failed is skipped and says which check it was waiting on. A command that times out is killed with everything it started.
bb createand the skills can run the machine checks through the same function, which never prints. -
487393a: bb has a droid: a head riding on a rolling ball. While a command makes a person wait at a terminal (more than 400 ms), it rolls to and fro on stderr, with a little physics: the ball speeds up, cruises and brakes, spinning like a wheel (clockwise rolling right, one turn per circumference), while the head rides on a loose spring: it’s thrown back as the ball pulls away and trails behind while it rolls, then flies forward when it brakes and wobbles to and fro. Dust kicks up at speed and it leaves skid marks when it brakes. At each end it looks around, then rolls back.
bb helpin a terminal shows it beside the title, andbb droidrolls it on demand.It never appears for agents: not with
--json, not when stderr isn’t a terminal, not in CI, not on a dumb terminal, and not whenBB_NO_DROIDis set. It always clears its frames and gives the cursor back, Ctrl-C included.
Patch Changes
- 3bf1c32: bb’s droid throws its head back much harder when it speeds up, then bobs as it reaches top speed, while the swings after a stop settle a little sooner.
0.3.0
Minor Changes
-
af8159c: Polish the analyse flow after the first end-to-end rehearsal (MAR-4426):
structurein plan v1 takes one root blok or an array of roots, for a section that needs two top-level bloks. The checks,bb plan showandplan.v1.jsonfollow.bb plan showshows each structure two levels deep.--fullshows the whole tree, and--questionslists each section’s questions under the table instead of in a cell. Its JSON addsdata.sectionQuestions.bb plan validateruns the catalog checks even when the plan fails its schema, on every structure, Create and Promote that parses, so one run reports every error.bb plan validatewarns, indata.warningsand the text, when a structure leaves a required bloks field empty or places fewer bloks than a field’s minimum. Warnings don’t fail the plan; a minimum was an error before, and only maximums are now.bb plan show’s table has a Confidence column.bb catalog --componentshows each bloks field’s minimum and maximum next torequired.bb cataloglists each blok’svariants: option fields that select a variant, including the ones insidedesign, such asdesign.variantonsb-button.bb catalogreports a whitelisted child that no schema defines as anunknown-allowed-childproblem and no longer offers it as a placement.backpackExports[].schemaNameslists only the schemas in the folder of the export’s own Storyblok wrapper, so Card-family exports no longer list all 26 Card schemas.sections.v1.jsondocumentscomponent.libraryas"local","remote"or a library name, and null only when unknown.nested[]in sections v1 takes an optionaldescription, the nested component’s first non-empty description from Figma.
Patch Changes
- f208420: Report one no-component-map catalog problem with the searched paths when a project has no component map file. Keep per-component missing-map-entry problems when a map exists.
0.2.0
Minor Changes
- fba1566: The bb skeleton: commands are plain objects in a registry, and every command prints one JSON envelope with
--json, exits 0 ok, 1 failed or 2 on a usage error, and masks secret env values out of everything it prints. Commands declare their arguments, and a missing required one is a usage error;ctx.addSecretmasks secrets that don’t come from the env. Addsbb help [command...],bb version,bb check pure-tests,bb check commandsandbb check skills. An unknown command now exits 2, not 1. - 9c4bdaa: Add the sections and plan contracts (v1) and their commands:
bb figma sections validate <file>,bb plan validate <plan> --sections <file>andbb plan show <plan>. The plan checks cover the sections file and, throughCatalogView, the catalog: bloks, children and their counts, Create primitives (not*SBwrappers) and names. The JSON Schemas are inplugins/backbase/schemas/, for skills to point agents at. - 0f3d889: Add bb catalog with an installed-project loader, component metadata, effective placements, presets and Backpack React exports. Plan validation now uses the real project catalog when Backpack is installed, including placement, primitive, collision and child-count checks.
0.1.0
Minor Changes
- 1d9131b: First release:
bb --versionandbb --help, installable from GitHub Packages.
The plugin
From plugins/backbase/CHANGELOG.md
0.23.0
Minor Changes
-
ccfda0a:
bb mcp, a stdio MCP server the plugin now bundles in its.mcp.json, pinned to the plugin’s version. It serves four read-only tools,show_plan(Gate 1),show_space_plan(Gate 2),show_push_planandshow_review, each linking aui://backbase/<view>MCP Apps widget:bb report’s view drawn in the chat, with its images inline, in an app that shows MCP Apps (Claude Desktop, Claude on the web). Each returns the output contract’s short card as text, which is all a terminal shows, with the report page’s path. The widgets load nothing from the network and post nothing that writes; approvals stay a typed yes in the chat.--rootsets the folder whose projects the tools may read (the plugin passes Claude Code’s project folder), and bb mcp refuses the disk’s root or the home folder; an empty or unexpanded--rootmeans the folder it runs in. Each widget’s CSP lets only its own script run, by hash.backbase-output’s templates call the moment’s tool; when the app draws the widget, the message leaves out only the line the widget draws, and keeps that nothing is written yet, what can be lost, closing the Settings story, the decisions, the approval sentence and the report link. The space view says to close the Settings story when the write saves it. -
eb9f5b6:
bb report [plan|space|push|review|all] [--open]draws the run as a local HTML page (MAR-5017): a picture of what it’s going to do, beside the agent’s short answer.- Four views, in plain words.
- The plan (Gate 1): each section beside its Figma shot, how it gets built (“built from existing blocks”, “a new component built from Backpack’s React”, “from the site settings”), how sure the plan is and how close Backpack gets. Then every decision, numbered, each with its recommended answer: off-scale values (96px → xxxl, 128px, requested from GCS), the design’s colours that aren’t the theme’s, what Backpack can’t do, and the open questions.
- The space (Gate 2): the components bb creates or updates and those left out with their reason, the theme’s colours as swatches (the space’s today beside the design’s), the custom colours, the Settings story’s draft and the field plugins.
- The push: each section new, changed, removed or the same since the last push or pull, beside its Figma shot, the images with thumbnails, and the target: always a draft, never published.
- The review: each section’s review image, Figma beside the page, its height and the measure’s largest deltas in words, and what refine kept or reverted there.
- It reads only. It draws from the files the run already has in
.backbase/, and never writes to Storyblok or Figma.bb report(orall) writes.backbase/report.htmlwith every view whose files are there, and says which aren’t drawn yet and why;bb report <view>writes.backbase/reports/<view>.html. With--json,data.writtenlists the page, anddata.viewsanddata.summarysay what each view shows. - One self-contained page. Inline CSS, no script, nothing fetched: its Content-Security-Policy loads only the page’s own images. They’re linked as sibling files, each opening whole, or put in the page with
--inline: as 480px thumbnails (a review’s side-by-side image 960px), each once however many views show it, and at most 4 MB in all, with a warning for what’s left out. On ILS, 1.2 MB. Light and dark, readable at phone width.--openopens it in the default browser. - Plain words, from one glossary.
packages/core/src/glossary.pure.tsholds the output contract’s table (thebackbase-outputskill’s, MAR-5016), for the views and the contract’s templates alike: a section by its name without the Figma path, a colour by its look (“lavender, accent 3 background”) with the Figma variable in a tooltip, and “theme colour” and “option list” for bb’s own words. - It reads only what’s there, and only the project’s. A review index or a measure that isn’t one makes the review unreadable, naming the field.
--slugtakes only a story’s slug, and a path that leads out of the project (../, an absolute path, a link) is refused. - The skills link it.
backbase-output’s layer 3 runsbb reportat each moment and links.backbase/report.html, and each template’s last line does too: “The full plan, with a picture per section:.backbase/report.html.” The detail the page doesn’t draw stays in.backbase/reports/<moment>.md. The output eval reads the glossary, the hidden words and the section and colour names from core’sglossary.pure.ts, and checks the skill’s table says the same. - Built once, in core. The view builders (
planView,spaceView,pushView,reviewView) and their HTML are pure, for E-3’s MCP widgets to show the same views. bb createignores.backbase/report.htmland.backbase/reports/, and so does the analyse flow’s.gitignorestep.
- Four views, in plain words.
-
19bf028: Every skill now talks to the person the same way, through one output contract, the new
backbase-outputskill (MAR-5016):- Three layers. First, a line or two on where things are and whether anything needs the person. Then a decision card: only the choices they have to make, numbered, each with the recommended answer, so “yes” or “2: no” is a whole reply. Last, the full detail, which goes to
.backbase/reports/<moment>.mdand is linked, and “show section 6” gets just that part. The reports aregate-1.md,gate-2.md,push.md,review.mdanddone.md. - Plain words. A glossary replaces Backbase’s own terms: Wrap is “a new component built from Backpack’s React”, slot “a placeholder until it’s built”, Compose “built from existing blocks”, preset “a saved layout”. Stamps, envelopes, fingerprints,
plan.json, block trees and design-fit codes aren’t shown unless the person asks. Approval sentences and drafts to approve are still quoted word for word. - A word budget: about 150 words a message, and one line per stage at most while work runs. Errors and refusals say what happened, what it means and the one thing to do.
- Five templates, for Gate 1, Gate 2, the push plan, refine and review results, and done.
backbase-analyze,backbase-new,backbase-sync-space,backbase-compose,backbase-edit,backbase-refine,backbase-componentandbackbase-upstreamfill them instead of their own “tell the person” paragraphs. Every gate, approval and rule stays as it was. - Gate 1’s card keeps MAR-4845’s rule. A yes takes each recommended answer the card names, and nothing else. A question with no recommended answer still has to be answered before the plan is confirmed. The card lists those questions first, a section it couldn’t place and the ones that change what gets built, and the report numbers the rest, so the person answers them by number (“7: live”). Decisions beat the budget: those questions go on the card whatever it costs. What another blok would fit is its own recommendation on the card, to switch, and never a request to GCS. A question with a blok’s name in it goes on the card in plain words. On the ILS frame, Gate 1 goes from a 1,257-word table to a card of 150 words, plus the 8 questions only the person can answer.
bb createadds.backbase/reports/to a new project’s.gitignore.- A new eval,
output, replays the ILS run’s five messages through the templates with the fixtures’ data. It checks that each fits the budget, shows no hidden word, and keeps every decision the old output asked for, among them 96px → 128px requested and the theme colours at the space step. On ILS, a plain yes to Gate 1 doesn’t confirm the plan while a question with no recommended answer is open.
- Three layers. First, a line or two on where things are and whether anything needs the person. Then a decision card: only the choices they have to make, numbered, each with the recommended answer, so “yes” or “2: no” is a whole reply. Last, the full detail, which goes to
0.22.0
Minor Changes
-
ca1ae72: The EF Chaperone preset moves to Chaperone 0.12.0’s native rules, so baselines count per component (MAR-5009):
- Chaperone 0.12.0 is the minimum.
bb doctor --writeandbb createpin it, raising an olderchaperoneVersionand keeping a newer one. A project still pinned below it fails every schema underschema-name-matches-folder, since an older Chaperone ignores the preset’s case transform:bb doctor’sproject-chaperonenow names the old pin and that symptom, andnpx @ef-global/backbase doctor --writefixes it. schema-is-registeredis nativeregistered-in-map. It readssrc/components/component-list.tsand the project’scomponent-map.exceptions.json: a listed schema passes, an unlisted one fails.- A missing
component-map.exceptions.jsonis now an error. Chaperone reports “Cannot check registration”, which no baseline can record.bb doctorreports the missing file underproject-chaperone, andbb doctor --writeandbb createcreate it as{}, never overwriting an existing one. (bb check components --registrationstill treats a missing file as{}.) schema-name-matches-folderis a nativefile-pairingwith apascaltransform.<kebab>.sb.tsmust sit insrc/components/<Pascal>/, with exactly that case, soFancard/fan-card.sb.tsfails on macOS as it does on Linux CI.- The three command rules list one result per component.
wrapper-visibility-keeps-display,schema-is-placeableandflex-child-has-design-fieldrunbb check components --visibility,bb check placeableandbb check flex-designwith--jsonand"output": "results". With--json, those checks’ envelope addsresults: one{ file, message, fingerprint }per violation, at the schema or wrapper, with fingerprints such asschema-is-placeable:fan-stack. Baselining one unplaceable component now leaves a second, new one failing. A check that can’t run, or any error without a file, lists nothing and fails, which Chaperone reports as a rule-level error, never a baselined violation. chaperone/bb.mjsstill says “@ef-global/backbase isn’t installed” for each command rule when bb’s CLI is missing.
- Chaperone 0.12.0 is the minimum.
-
41a9646:
bb createruns the machine checks it needs,bb doctor’s own, and stops early with doctor’s fix text instead of failing part-way through. A new Check the machine step (preflight) after the folder check runsnodeandgit.packages-tokenruns right before the install, when the project’s.npmrcreadsGH_PACKAGES_TOKEN: a token GitHub Packages refuses stops the run, and a timeout or another failure is a warning, so the install still runs.githubnever stops a clone that would work: it runs only after an HTTPS clone fails, to add its fix.data.checkslists every check it ran, and a check that warns is indata.warnings.runMachineCheckstakesonly, andpackages-tokengives its cause infacts.cause. -
d380b83:
/backbase:editcan upload a new image, through the same guarded path as compose’s images (MAR-4838):- Edit offers “upload this image”. Before, the person had to upload the image in Storyblok’s asset manager and type its filename back. Now they give the agent a local file (in the project or outside it, such as
~/Downloads), a Figma node (its image fill, downloaded with their Figma MCP) or an image URL. The agent shows them one image plan with each image, where it came from, its slot and the alt text it proposes, and one yes covers the upload. Then it sets each image in its slot with an ops envelope. Nothing is uploaded without that yes, and nothing is published. bb assets add <file or URL> --story <story file> --slot <uid>.<field>is new.- Before it reads anything, it checks that the slot is an image (
asset) field in the blok’s schema (--catalog, or the project’s own). So an image is never uploaded for a fieldbb story ops applywould refuse it in. - It sniffs the bytes (PNG, JPEG, WebP or GIF, never SVG), reads the image’s size, copies it into
.backbase/edits/<slug>/images/and adds it to.backbase/edits/<slug>/images.json. It uploads nothing. --originrecords where the image came from, such as its source and licence, and--freshstarts the manifest over for a new edit.
- Before it reads anything, it checks that the slot is an image (
- A URL is downloaded from a public HTTPS host only. A URL may come from text an agent read, so bb treats it as untrusted:
- It refuses HTTP,
localhost, and loopback, private, link-local, unique local and other non-public addresses, IPv4 addresses inside IPv6 ones included. - It resolves a name itself and connects only to an address it checked.
- It follows at most 5 redirects, each checked before it’s requested.
- It streams the body and stops at 25 MB, whatever
Content-Lengthsays.
- It refuses HTTP,
- The upload is compose’s upload. The manifest is an images file
bb assets plantakes, with the story in place of the frame. The images go intoEdits / <story name>, each named after its source and its hash.bb assets upload --approved <fingerprint>uploads them; the guard’s prompt now names both kinds of folder. An image’s key is the one.backbase/assets.jsonalready has it under, or else its SHA-1, the key compose gives a Figma image. So an image the project uploaded before, from compose or an earlier edit, is reused, never uploaded again. The image plan shows where each upload came from. - The push plan lists the new images. For an update,
bb story push --planlists each image the push puts in the story that the story in Storyblok doesn’t have (data.assets): its file, section, blok, field and alt text, and whether.backbase/assets.jsonrecords it. It refuses a new image without alt text (alt-missing). - New projects ignore
.backbase/edits/*/images/, andbb assets addadds that line to.gitignorein existing ones.
- Edit offers “upload this image”. Before, the person had to upload the image in Storyblok’s asset manager and type its filename back. Now they give the agent a local file (in the project or outside it, such as
0.21.0
Minor Changes
-
cd5fd44: Gate 2 saves the Settings story’s theme as a draft, so the theme shows right after the sync (MAR-5005, ADR 0031):
- The theme shows locally and in Storyblok’s preview straight after Gate 2. The site renders the Settings story’s
theme.selected, which thebackpack-themeplugin rebuilt from the datasources only when a person opened Settings and saved.bb space syncnow saves it after the theme’s datasource writes, as a draft, never published. bb rebuilds the field exactly as the plugin does, ported from ef-global/backpack-sb-plugins-react at 63b70fc (plugin 1.1.3) and pinned against the plugin’s own code and a recorded field, so the next time someone opens Settings, the plugin has nothing to change. - It’s a Gate 2 plan item.
bb space plan --sectionsreads the Settings story the site reads (settings, or<locale>/settingsper locale folder, asbb doctorfinds it) and lists its id and slug and each token oftheme.selectedthat changes, mode, before and after. It plans the story only whenselectedwould change, because of the theme’s writes or because it already disagrees with the datasources. The fingerprint covers the story, when its draft was saved, andselectedbefore and after, and the Gate 2 hook’s ask names the story. - The sync writes it safely. It reads the story’s draft again and skips it (
theme-plan-again) when it was saved since the plan, or when the datasources no longer give the plannedselected, and keeps its theme when a theme write failed. When sb-mig’s plan writescolors-theme-darkorcolors-theme, which only the field is rebuilt from, the sync still writes the theme’s colours and skips the Settings story alone. It changes only thethemefield (the model andselectedthe plugin saves) and keeps every other edit in the draft..backbase/space-sync.jsonrecords theselectedand the model it replaced, aspreviousandpreviousModel, and bb dropsbb story serve’s cached copies of the story so the local server fetches the new draft. A space with no Settings story, or one without athemefield, gets a note, not an error. Storyblok has no conditional write, so the last save wins: Gate 2 asks the person to close the Settings story before the sync and reopen it after, as a Settings tab opened before it holds the old theme. /backbase:syncand/backbase:composeend with “the theme is saved as a draft in Settings; publish it when you’re happy” instead of “open Settings, save and publish it”, which stays for a space whose Settings story wasn’t saved.
- The theme shows locally and in Storyblok’s preview straight after Gate 2. The site renders the Settings story’s
0.20.0
Minor Changes
-
1f98bef: Two gaps from the ILS 0.18 run (MAR-4989):
- An icon icons/bold doesn’t have becomes a design request. The frame’s “Sign in with Google” button had no icon to map to. EF’s icons/bold set is Phosphor without its brand icons, EF’s logos are only EF’s own, and bb has no icons of its own, so the Google “G” can’t be added to bb.
bb plan requestsnow drafts one request per icon that a section bb builds takes from Backpack’s Figma icons and icons/bold doesn’t have (Icon: google), with every section that uses it, what holds it and the closest icons/bold names.bb request plandrafts it as an issue for ef-global/backpack, and/backbase:upstream requestsfiles it after a person’s yes. A global section, such as a footer’s social links, lives in site settings and bb never composes it, so its icons ask for nothing. An icon counts as in icons/bold under its kebab name, its name run together, or either with-logo, as Phosphor names brands (LinkedInislinkedin-logo), so a request goes away once icons/bold adds the icon. bb checks the icons against the cached icon manifest, and fetches it once from EF’s CDN when there’s no cache, asbb story materializedoes. Without a sections.json or a manifest, a warning says the icons weren’t checked. - New projects get a dev server for Claude’s preview pane.
bb createwrites.claude/launch.jsonwith the boilerplate’s dev server: itsdevscript, run with the package manager its lockfile names (npm run dev), on the port the script gives (3000,-p3000too), with"url": "https://localhost:3000"because the script has--experimental-https. A port the environment gives (-p $PORT) can’t be told, so bb writes no entry for it. A new doctor check,project-launch, reports it missing in an existing project, andbb doctor --write(or--fix) adds it beside the configurations there. A configuration with the same name or port counts as the dev server already. A launch.json that isn’t JSON, such as one with comments, is left as it is, with a warning from doctor and frombb create. Running either twice changes nothing.
- An icon icons/bold doesn’t have becomes a design request. The frame’s “Sign in with Google” button had no icon to map to. EF’s icons/bold set is Phosphor without its brand icons, EF’s logos are only EF’s own, and bb has no icons of its own, so the Google “G” can’t be added to bb.
0.19.0
Minor Changes
-
9c90a25: Make Chaperone work in a real project: bb is a pinned dev dependency.
bb doctor --write(and--fix) now pins@ef-global/backbaseto the running version indevDependenciesand installs it with the package manager the project’s lockfile names, never lowering a newer pin, nor raising a release pin to a prerelease. The install finishes before the other checks start, since it rewritesnode_modules, and it runs through a shell on Windows, where npm isnpm.cmd.bb createdoes the same for a new project, and also writes the Chaperone preset and.prettierignore.bb doctor’sproject-chaperonecheck fails when the preset doesn’t resolve or bb’s CLI isn’t installed, namingbb doctor --writeas the fix, and warns about a version skew between the installed bb and the running one.The preset’s five command rules run bb through a shim beside the preset, so a project without bb’s CLI gets “@ef-global/backbase isn’t installed in this project: run npx @ef-global/backbase doctor –write” instead of a module-not-found stack.
/backbase:componentand/backbase:newno longer leave the agent to run those checks by hand.A new
project-prettierignorecheck, whichbb doctor --writefixes, adds.next,.turboandnode_modulesto.prettierignore, counting**/node_modulesand the like as already there. A correction to 0.18.0’s note: the preset’s excludes cover Chaperone’s own file scans only. Its Prettier runner callsprettier --check ., which reads.prettierignorealone, so.next/cachewas still linted until now. -
8aef90e: Backbase D-5 (MAR-4988): fixes from the ILS run, where refine kept none of its tries because the measurements misled it.
bb page measurepairs each text field only with a text layer inside its blok’s own match: a card’s title and body never pair with the other card’s text, or the nearest one with the same words. A field’s words come first, then its role (heading, body, label), then the order, pairing as many fields as it can. A text no layer there fits isunpaired, with the reason, and gets no deltas. Two cards whose copy was partly edited still pair with their own frames. Each measured blok lists itstextsand how each paired.bb page measurereads sticky bars at their resting place, with sticky turned off while it reads, and never measures two bars as one box, a wrapper around them included: each section lists itsbars, top and bottom apart, each with its own height.- /backbase:refine’s budget covers the page: every section with a delta beyond a token step gets one try before any gets a second, and the rest of the budget goes to the most px off. The budget is 5 tries per section and 15 in all (
--tries,--budget;--sectionsstill works for this release, deprecated, as a budget of that many sections’ tries), and the report lists any section left untried, and why. Core’splanRefine,nextSectionandrefineBudgetFromhold the rule. bb page reviewcaptures the last section at the bottom of a long page on its first attempt: the capture stops at the page’s end rather than asking for rows the page can’t scroll to, and reports the height it captured, and a section whose box runs past the end settles in view once more first. A failed capture gets one more attempt after settling again, as a fallback that’s reported (attempts,retried, a warning).
-
6f8c5a1: Off-scale design values use the nearest step, are flagged, and become a request to GCS (MAR-4985). A spacing, padding, gap or radius the frame has that no step is, such as the ILS frame’s 96px section padding between
xxl(64) andxxxl(128), is written as the nearest step and never silently: the design fit records it (offScale), Gate 1’s plan lists it once per value (96px → xxxl (128), +32px, requested),bb story spacing plannames each one inoffScale, only for a frame px no step of the scale is, andbb plan requestsdrafts one request per capability and value (Padding: 96px), with every section as evidence. A padding or margin tie at Gate 1 now goes where the spacing plan sends it, to the step Backpack’s presets use more, so the plan says what compose writes; a gap field’s value and a radius still go to the smaller. Design fits stored before this go stale, since their facts now carryoffScale(ILS run 2: 8 stale fits become 11; run 3: 7 become 11): runbb plan validate <plan> --sections <file> --write-design-fitagain, thenbb plan requests. Newbb request plan --issues <gh issue list JSON>drafts each request as an issue foref-global/backpack, or as a comment on an open issue that has it already, by its marker or by its title, from the list of every open issue;bb request record <key> <url>records a filed link, whichbb plan requestskeeps; replacing another link needs--force. ADR 0030: the agent files a design request after the person’s yes./backbase:upstream requestsfiles them, each only after the person’s yes. -
e2d6085: Gate 2 writes the theme even when a component fails, and a component can be left out (MAR-4984).
bb space syncwrites the theme’s overrides and custom colours whether or not a component failed: the theme depends on the colour datasources, not on the components. It’s skipped only when sb-mig’s plan writes a datasource the theme was read from. Each failed component is its owncomponent-failederror, next to the theme’s result, with--excludeas the way round it.bb space plan --exclude "<name>=<reason>"(repeatable) leaves a component out. The plan lists it with the reason, the fingerprint covers it, andbb space sync --exclude <name>syncs exactly what the plan held, naming the other components to sb-mig instead of--all. It fails (component-not-synced) when sb-mig’s report lacks a name it was given, andspace-sync.jsonrecords the components left out with their reasons.bb space planand a newbb doctorcheck,space-field-plugins, read the account’s and organisation’s field plugins (GET /v1/field_types,GET /v1/org_field_types, paged byTotal), and name a component field whose plugin is missing (missingFieldPlugins) only on positive evidence: a listing that assigns it to other spaces and not this one. Anything else is an unverified note, never an--excludesuggestion.- When Storyblok’s 422 says the space lacks a field plugin, the
component-failederror names it,space-sync.jsonlists it (fieldPluginFailures), and/backbase:syncoffers to install it or leave the component out. The hint now parses Storyblok’s own wording. - When the space has no
custom-colors, the theme plan creates it (additive) instead of dropping the frame’s custom colours. /backbase:syncoffers the exclusion when a component fails or lacks a plugin, and it and/backbase:composeend with the person’s step after theme writes: open Settings, check the colours, save and publish.
-
5769871:
bb component newno longer turns a Wrap’s grid or flex root into a block when its visibility is Show. The wrapper passes Backpack’sgetVisibilityClassesthe root’s own display asoriginalDisplayProperty: read from Backpack’s styles for a Wrap, on the element that gets the wrapper’s className (gridfor FanStack),flexfor a Create. When bb can’t tell, it puts the visibility on a wrapper that isdisplay: contentswhen shown. The wrapper names the classes the helper builds at runtime, so Tailwind builds them.bb check components --visibility, and the preset’s newwrapper-visibility-keeps-displayrule, flag a local wrapper that callsgetVisibilityClasses(or the deprecateduseVisibility) withoutoriginalDisplayPropertyon a root that isn’t a block, naming the file and the display to pass./backbase:componentoffers that fix for existing wrappers, and makes it only after the person’s yes.
0.18.0
Minor Changes
-
301af6e: Add /backbase:component to build approved Wrap and Create slots, check the project, sync components only at Gate 2, replace slots through ops, measure and push one approved draft. Badge slot sections as expected review gaps, validate slot placements, and map filled section roots normally.
Exclude generated Next.js files, node_modules and the Turbo cache from the project Chaperone preset’s file scans.
-
67aa646: Render Wrap and Create gaps as local backbase-slot bloks in drafts. Install the template and placements, list slots in validation and push plans, and add story slots and undoable story slot conversion with plan evidence. Exclude reserved geometry and scores from measure and compare, and stop refine after two rounds of unexplained structural deltas for the person to decide on a component.
Format installed slot files with the project toolchain, preserve formatted installations with advisory drift checks, and limit placements to section containers and slide tracks. Match innermost sibling design instances and keep refine evidence compact.
0.17.0
Minor Changes
-
e2a8977:
/backbase:upstreamtakes a Wrap or a Create a project built to Backpack (C-6, ADR 0029): an issue inef-global/backpack, then a draft pull request in Backpack’s conventions, each after the person’s yes, never merged and nothing published.bb component upstream-plan <folder> --backpack <clone>plans it against a fresh clone of Backpack, reading Backpack’s layout from it (a sibling component with a Storyblok wrapper, Accordion when it’s there): where each file goes (FanStackSB.tsx→src/components/FanStack/storyblok/FanStackSB.tsx), thesb-names (fan-stack.sb.ts→sb-fan-stack.sb.ts, the blok’snameandcomponentfan-stack→sb-fan-stack, display nameFan Stack), the@ef-global/backpack/<X>imports rewritten to Backpack’s own (relative inside the component, its@/src/*alias elsewhere), what Backpack expects that the project lacks (presets, schema and wrapper tests, the preview image, the layout schema and breakpoints, the story’sparameters.siteBuilder, the exactexportsandtypesVersionsentries, the files that list every blok, its generated component list with the command that regenerates it, and the whitelist of each parent the project places it in; a Create’s index, styles and test), the imports it can’t carry over (Storyblok’s ownStoryblokComponentandstoryblokEditableamong them, which Backpack’s ESLint forbids), warnings for a parent or a local blok Backpack lacks, the branch, the base (the clone’sorigin/HEAD), the commit in Backpack’s convention, and the story migration note. It refuses a name Backpack already has.--writewrites the moved files into the clone, only one in the temp folder, and only on the plan’s branch.- The
backbase-upstreamskill and/backbase:upstream: pick the component, plan it, draft the issue with ADR 0025’s fields, file it on the person’s yes, build and check the branch in the clone, then push it and open a draft pull request on a second yes. - The Bash guard blocks merging a pull request in ef-global/backpack, or marking one ready, with gh:
gh pr merge,gh pr ready, the merge API and GraphQL’s merge and ready mutations, named with--repo, a pull request’s URL orGH_REPO, or run in a clone whose remote is Backpack.
-
3cfe109: Replace Promote with local Wrap plans, require evidence for Wrap and Create, and represent missing components as slots. Read legacy Promote plans with a rename warning. Until slots render, scaffold their composable surroundings and report each omitted slot.
Keep real legacy Promote plans usable with migration warnings for missing evidence and slots. New Wrap/Create plans require slots; library-instance evidence is Wrap-only and checked against section data. Omitted slots no longer suppress required-field filling in scaffolded stories.
Keep legacy migration provenance internal, preserve Promote on design-fit write-back, and match library-instance evidence to the wrapped export. Report when an omitted slot’s required field uses Backpack’s example.
-
2f82c67:
bb page reviewputs every section of the rendered page beside its Figma shot, one image a section, for the agent to look at section by section, worst first.- Each section is found by its own
data-blok-uid, never by the frame’s coordinates, brought into view, and captured only once it has settled there (M-22’s settle, per section): images decoded, a lazy carousel built, nothing moving. It’s captured one viewport at a time, never by a full-page screenshot, whose capture beyond the viewport fires a resize that re-lays out a carousel, and with what’s fixed or sticky outside it hidden. A section that doesn’t settle, has no element or moves while it’s captured isnot-captured, with its reason: never a half-loaded crop. - Each image has the Figma shot on the left and the page on the right, both at the frame’s width, under a header with the section’s name, its height frame → page, the delta and the measure’s “N off”. Every run is a numbered folder,
.backbase/review/<slug>/<n>/, withindex.json, worst first by height delta, each section with itspreviousimage.--section <key>captures only the sections an envelope touched. No scores. - compose (before the first push), refine (every pass) and edit (the sections it changed, on the local render before the push, with
bb story serve; on the draft after it only when the person declines the serve) make the review required: the agent opens each image, writes what differs in eight fixed categories, and after each envelope marks each item fixed, a design gap or content. The measure stays the referee for px; a visible improvement overrides a measuredgrewonly written down. The reports list each item and how it ended. - What’s sticky inside a section is pinned where the flow puts it while it’s captured, so a section taller than the viewport shows its sticky bar once, not once a slice. Every call to the page during a capture is bounded: a page that stops answering fails with
page-not-settled. - The whole review has 3 minutes, or
--review-timeout <ms>: a section it doesn’t get to in time isnot-captured, for another run with--section. bb page review --check-notes <review.md>checks the agent’s notes without opening a page: a heading for each captured section, and each item- [<category>] <what's off, and where> — <outcome>. The skills run it before their report.- Like measure and compare, it hides
[data-backbase-ignore]and checks withbb story servewhere the page’s story came from (served-from-storyblok,served-other-file,not-served,serve-unreachable; with--draft,served-from-local), and reviews nothing when it’s the wrong one. --sectionon the page commands takes a section’s review key too.
- Each section is found by its own
-
6ea2251:
bb page measureandbb page comparenever read a page that hasn’t finished rendering (MAR-4910). On 0.16.0 they waited five seconds for fonts, images and the network, then captured anyway with a warning, and never checked that the layout had stopped moving: React hydration, carousels and sticky bars that set themselves up after load,next/imageplaceholders, CSS background images and fonts that reflow text could all be measured half-built, and refine then “fixed” spacing that was right.Now they wait for the document’s load, the fonts, every image (a
next/imageuntil it’s loaded with its blur placeholder gone), and the CSS background images on the measured sections, preloaded and decoded, then for the page’s height, each measured section’s box and the offset boxes of its bloks and what holds them (so a carousel’s track sizing itself counts, and a progress bar animating outside any blok doesn’t) to hold still, within 0.5px for 500ms, with no request loading, starting or ending in that window. bb tracks the page’s requests itself, since Playwright’snetworkidlenever waits again once it has fired; event streams and fetches open 5 seconds or more don’t count, and a warning names them; WebSockets never do. All of it has 20 seconds, or--settle-timeout <ms>, and every look at the page is bounded by it. A page that doesn’t settle fails withpage-not-settled, naming each section still moving, the images not decoded and the requests still loading, and writes no report; so does a page that reloads while it settles, during a look, between two, or before bb has read it, or whose main thread stops answering.--allow-unsettledcaptures it anyway, markedsettled: false:bb page measure --againstjudges nothing from such a measure, nor against one, and with--iterationthe capture stays out ofhistory.json, the verdicts andlatest/.measure.jsonandcompare.jsonrecordsettledandsettle: { ms, stable, waited }.bb catalog render-semantics --measure, which reads computed style rather than boxes, waits the same way and reads a page that doesn’t settle anyway, with a warning./backbase:compose,/backbase:refineand/backbase:edittreatpage-not-settledas a page that isn’t ready: they try once more after a short wait, then tell the person, and never pass--allow-unsettled. -
37a99d8: One name everywhere (MAR-4914): a story file’s name,
<slug>.json, is itsstory.slug, and once pushed, its slug in Storyblok.bb story validate,serve,push,page measureandpage comparerefuse a story whose names differ withslug-mismatch, naming all three and the fix. New:bb story slug <file> <new-slug>is the one way a slug changes: it setsstory.slug, renames the file and its report, push record, ops journal, snapshots and design log, and drops the stamp; for a pushed story, the nextbb story pushtakes the slug to Storyblok, shown in its plan as a slug change. After a hand-edit ofstory.slug, the records still under the file’s name are found and moved too, so a pushed story moves rather than being created twice.bb story pullrefuses a story renamed locally and not yet pushed (story-renamed-locally). Push reads the slug in the space before it creates or moves a story, and refuses one a different story has (slug-taken, naming it).bb story serveserves each story at its slug, refuses to start onslug-mismatchorserve-conflict, putsstory._backbase = { source: "local", file, servedAt }on what it answers from a file (nothing on Storyblok’s answers), and keeps the last answers per slug atGET /_backbase/served?slug=…. Measure and compare check the page came from the--storyfile (served-from-storyblok,served-other-file,not-served,serve-unreachable; with--draft,served-from-local), and hide[data-backbase-ignore]before they capture. Compose, refine and edit measure at the story’s own slug, and handleslug-takenwithbb story slugafter asking.
0.16.0
Minor Changes
-
6c252ba: bb brings its own browser for
bb page measureandbb page compare(MAR-4848), so measuring a page changes neither the project nor Playwright’s shared cache. bb depends onplaywright-core1.64.1 itself, and keeps that copy’s Chromium in a cache of its own,<user cache>/backbase/ms-playwright. The project’s own Playwright, in its ownnode_modules, still comes first when its headless Chromium is installed; without it, bb uses its own and the report says so in one line. A Playwright onPATHno longer counts. An explicitPLAYWRIGHT_BROWSERS_PATHstill wins over bb’s cache, as on backpack-automations.On a first run they exit 1 with
browser-missing, naming the folder, the size and the command:bb page browser installinstalls Chromium’s headless shell only (about 120 MB) with bb’s ownplaywright-core, into bb’s cache, or an explicitPLAYWRIGHT_BROWSERS_PATH, where it removes nothing. It refuses Playwright’s shared cache, where an install deletes the browsers it thinks unused, which other projects still use.bb doctorgains apage-browsercheck: it passes when the Playwright bb would use has its headless Chromium installed, and warns, with the command, when it hasn’t./backbase:composeand/backbase:refineask once before running the install, and no longer tell the person to add Playwright to the project. -
bb5504e:
bb page measureleads with the section heights.data.heightssays how many sections are within ±2px of the frame’s height, and how many more are within a token step (4px), wheredoneis: “1 of 7 within ±2px; 3 more within a token step”.measure.txtand the printed summary start with them. A new list,data.sizesBeyondStep, holds the section heights more than a step (4px) off, then the bloks’ widths and heights more than their step off. It sits besidedata.beyondStep, which stays gaps and padding. A single-blok section’s height is listed once, as the section’s. The judgement’s text gives a section’s height when it’s a step off./backbase:compose’s measure before the push,/backbase:refine’s baseline and/backbase:edit’s measure of the draft now report the section heights first. (MAR-4840) -
cc27705: Full bleed comes from the frame, and Gate 1’s open questions are never answered by default (MAR-4845). Design fit reads full bleed from the frame’s geometry: in a section at the frame’s left edge and as wide as the frame, an image, a media item or a background of its own colour that is as wide as the frame, under layers just as wide, runs edge to edge. Its blok, or the one it sits in on the section’s grid, goes on the grid’s bleed lines from the render table (
[0,6]atsandm,[0,16]atlin Backpack 4.11.0), and ansb-imagetakesviewport_size 100vw. It’s written at the frame’s breakpoint (its section’sBreakpointvariant, or else its width against Backpack’smdandlg) and below: a mobile frame’s bleed never reaches desktop. Since Backpack renders positions mobile-first, the breakpoint above the frame’s is written as it rendered before, the content’s edges andviewport_size’s default.bb plan validate --write-design-fitstores what to write in the plan entry’s newdesignFit.bleed, only when something bleeds, so other stored fits stay current;bb plan showlists it under Full bleed, and/backbase:composewrites it as it is. On reference frame 1, MEDIA / Image full width’s photo is now planned at the bleed lines instead of being asked about. A plan question can now be{ "question", "recommended", "answer" }(a plain string still parses, as an open question with no recommended answer).bb plan showmarks each questionRecommended: …orAnswered: …, counts only the open ones in the table, and ends with a Confirmation that names each recommended answer (Confirm, with: the image bleeds);--jsongivesdata.openQuestionsanddata.confirmation. At Gate 1,/backbase:analyzelists each open question as its own item and offers that confirmation, never a bare “Confirm plan”. A confirmation that doesn’t answer a question leaves it open, and the agent asks again before it writes the plan: it never records a default as the person’s answer. -
f1c3d15:
bb page measure --draft <full slug>andbb page compare --draft <full slug>open the pushed draft through the boilerplate’s preview route,/api/preview/preview?secret=…&slug=…, without the person pasting a secret (MAR-4846). bb builds the URL itself: the secret isSTORYBLOK_PREVIEW_SECRETfrom the shell, else the project’s.env.local, else its.env, and the origin ishttps://localhost:3000, or--url <origin>. The secret, as it is and as the URL encodes it, is masked as***indata.url(new on both commands), inmeasure.json,compare.json,index.htmland the history, in the output and in errors, such as Playwright’s when the dev server isn’t up. When it isn’t set, theno-preview-secreterror says which variable bb looked for and in which files. With--draft, measure writes to.backbase/measure/<story filename>/draft/and compare to.backbase/compare/<story filename>/draft/, so the draft’s never replace the local story’s. core exportsdraftUrl, pure.After the push,
/backbase:refinemeasures the draft itself withbb page measure --draft <data.fullSlug>instead of handing the person a command with a secret to fill in, and/backbase:editdoes the same for a story composed in the project./backbase:composeoffersbb page compare --draftfor the pushed draft. -
65ab854: The knowledge pack knows the section grid and flex alignment (MAR-4844).
<section_grid>says what aposition’s numbers are at each breakpoint: each line by name, the content’s edges and full bleed. In Backpack 4.11.0 that’s lines 0–6 atsandm(full bleed[0,6]) and 0–16 atl(full bleed[0,16]). A number past the last line ismain-start/main-end, the content’s edge, with no error, so[0,16]atmbleeds on the left but ends at the content’s right edge.<flex_alignment>covers each flex box that’s a column by default: which field moves its children vertically and which horizontally, from the classes each renders. In ansb-flex-group,alignisjustify-contentand centres a column vertically when the children hug (item_flexshrink).justifyrenders nothing whilecross_axis_stretchholdsalign-itemsat stretch. In a row the properties swap, and so do a flex group’s fields, soalignstays the vertical one wheredirectionis written at that breakpoint. Both come from the render table, so a new Backpack’s table brings its own.bb catalog render-semanticsnow reads the grid from the installed Backpack’s section helper (mapLayout), and recordsjustify-*anditems-*classes. The table bb ships for 4.11.0 is regenerated with both. A project’s own table written before this (it has no grid) is a warning inbb catalog, to write it again: it takes the grid bb ships for its version, and the pack says alignment is unknown for it rather than leave<flex_alignment>out.bb story design get’s legend says a field with no line changes no spacing, size or layout.bb catalog’srenderSemanticshas thegrid, andalignment: falsefor such a table. The ILS-scope diet bound moves to 62,500 characters./backbase:composestep 2b and/backbase:editpoint to both parts before apositionor an alignment is written. -
1426e44: A saved catalog or knowledge pack is never used against a different installed Backpack (MAR-4851).
bb catalogrecords what it compiled them from: the installed Backpack’s version, the bb version, and a hash of every file the build read. That’s Backpack’spackage.json, its type declarations and every file its schemas and datasources load (their presets, plugin schemas and shared fields);storyblok.configand its imports; the project’s own schemas and datasources with every file they import and the packages they require;component-list.ts,component-map.exceptions.json,backbase.placements.json,.backbase/render-semantics.json; and the space-presets file when it’s used. Backpack’s files are keyed undernode_modules/@ef-global/backpack/however it’s installed, so a linked or patched Backpack at the same version is caught. Line endings don’t count. The catalog has it asfingerprint, with each file’s hash. The knowledge pack has it on its header line:Backpack 4.11.0 · … · bb 0.15.0 · schemas <hash>.Every command that takes
--catalogcompares the saved catalog with the project as installed, re-hashing exactly the files it recorded and the schemas every catalog starts from. A catalog saved for another Backpack, or before one of the project’s schemas changed, fails withcatalog-stale. The message names both versions, or the files that changed, and gives the command that rebuilds it, with--space-presetswhen the file exists. A catalog saved by another bb, or saved before this change, is still used when its Backpack matches.bb catalog --json --out <file>writes the catalog’s envelope to a file, through a temporary file it renames over, so nothing reads half a catalog.--if-stale, with--out, rebuilds the catalog or the knowledge pack only when its fingerprint differs, and says whether it did (data.rebuilt,data.why). What it prints is small: itsdata.fingerprintleaves out each file’s hash, which the file keeps. For the pack it compares the fingerprint only, not--rootor--sections. A catalog saved with--spaceis rebuilt for that space. compose, edit and refine run both rebuilds before they use either file, and tell you in one line when they rebuilt one. analyze writes them with--out.bb doctorhas acatalogcheck: it warns about a stale.backbase/catalog.json, with the command that rebuilds it, and when it can’t compare it, such as with astoryblok.configthat won’t load.
Patch Changes
-
c70fba9:
bb story design getwith a project render table from before bb read alignment classes (MAR-4844) now says so: arenderNotethat whatalignorjustifyrenders is unknown for that table, to write it again withbb catalog render-semantics, andalignment: falsein its data. Its legend then leaves alignment out of what a field with no → line doesn’t change, instead of claiming it. -
a18b902:
bb story fillplaces a preset frame’s copy (MAR-4847). On the reference frame it filled 4 of 31 placeholders and left 27ambiguous, because a preset’s repeated cards share their layers’ names and a repeated blok with no anchor never filled by order. Now:- By instance order. A run of repeated bloks with no anchor takes the frame’s run of repeated instances in order: the n-th card takes the layers whose node ids start with the n-th instance (
I4115:5215;4001:2564;…). A run is one component’s instances, those whose layers share ids, so two buttons beside three cards are two runs. It’s taken only when it’s the frame’s only run of instances there, at any depth, with exactly as many instances, and every anchored sibling sits where its words are. With two runs, neither is taken, whatever their sizes, and only the roles can pair the cards. With one run, the node ids are enough, so this fills by count even without text styles: that’s deliberate. - By role. Each text is a heading, a body or a label. A placeholder gets its role from its blok and where it sits: an
sb-textbefore a headline is an eyebrow, and one in a list item may be either. A layer gets its role from its text style’s whole name in the design read (Header 4,Title 3,Body/Regular,UI Label; a name that says two roles says none), which core’s newcopyStylespairs with the copy. Placeholders fill only when exactly one in-order pairing gives each a layer of its role, and an unknown role fits any. With one layer for each placeholder that’s the order, as before, unless a role contradicts it (an eyebrow doesn’t take aHeader 4title); with more, it’s the one pairing the roles leave, as with a hero’s title and text beside its button’s label. - Repeats without instances (plain frames, such as 6-surfaces) fill by order only by role, and only from layers with a text style.
It still never invents copy, and reports
ambiguousonly when two texts really compete or a role contradicts the order. Onevals/fixtures/ref-1, scaffolded again from its plan, it fills all 31, with the words the run’s agent set by hand. The ILS run 2 fixtures are unchanged. - By instance order. A run of repeated bloks with no anchor takes the frame’s run of repeated instances in order: the n-th card takes the layers whose node ids start with the n-th instance (
-
a47173c:
bb story fillwrites the frame’s copy into empty text fields too, not only placeholders (MAR-4854). On the reference frame, the hero button’s label is an empty field in the scaffolded story, so fill skipped it and the button shipped without a label until the agent set it by hand.- Empty fields. After the placeholders, fill takes the layers they left and writes them into the empty text a blok shows (empty, left out, or
null), in core’s newSHOWN_TEXTtable: a button’s label (sb-text-button’stext), a link’s text (sb-link-text, now a body or label role), a tag’slabel, and a headline’s, a text’s or a body text’scontent. It’s a list because the schema doesn’t say what’s shown: some required text fields aren’t copy, and a button’s label is optional. It works the same way as for placeholders (by node id, by instance order, then by order, with the placeholders it filled as anchors too), but only by role: a layer with no text style, one of another role, or one anambiguousplaceholder could be takes none. - Optional. An empty field may stay empty: each layer goes to at most one field, and they fill when exactly one pairing places the most layers (core’s new
pairSome). An empty subtitle the frame has noBodyfor doesn’t keep the button beside it from its label; two empty buttons and one label layer fill neither. - Never overwrite. A field with words in it is never written, a text field nobody sees (an aria label, an embed’s URL) stays empty, and it never invents copy.
- Reported apart. They’re under
fill.fieldsFilledin the report and on their own lines,Filled empty …, so a person can tell them from placeholders. Those still empty are underfill.fieldsLeft, asStill empty …, with a reason (frame-has-none,ambiguous,no-layer-of-its-roleorno-copy) and their candidates.
On
evals/fixtures/ref-1, scaffolded again from its plan, the hero button gets “Label”, and nothing else that has a value changes. The ILS run 2 fixtures are unchanged: no empty field fills there, and 6 are listed asframe-has-none. - Empty fields. After the placeholders, fill takes the layers they left and writes them into the empty text a blok shows (empty, left out, or
-
fe504b5: The design read now keeps the auto-layout inside library instances, and
bb story spacing planpads a card’s footer from it (MAR-4852). On reference frame 1, eachsb-card-primitive-footerneededpt m. The plan couldn’t find it, becausebackbase-read-figma’s design script left out everything inside a library instance except text, images and instances.- The design script now keeps the geometry of every layer it reads inside a library instance: its size, its auto-layout (
layoutMode,itemSpacing, the padding, the alignment, wrap and the sizing modes), and how it sizes in its parent. It still leaves out the layer’s fills, strokes, radii, effects and typography, and its paging and caps are the same.bb figma sections designkeeps the same and nothing more, whatever the read sends. - The spacing plan pairs a blok inside a library instance that has no layer of its own and no layer inside it, such as a card’s footer, with the layer the library named for it (
Footer). It does that only when exactly one layer no blok has is named that way, and then pads the blok as that layer does:frame padding top 24 → pt m; no own layer: in Content card, a library instance, sb-card-primitive-footer is Footer, by its name. A layer is one blok’s: when two bloks are named for the same layer (two footers, oneFooter), neither pairs with it. A blok with no layer named for it, or one it shares, is skipped, and the skip says why. - A blok inside a library instance whose layer may not be its own, such as a card body paired with the text frame inside the library’s Body, is now looked through to the layers of the bloks inside it. That stops the plan from padding the card around a body that already pads itself. A local layer is still the blok’s own: a card narrower than its repeats in a bento row can be just that.
A read made before this change still has no auto-layout inside library instances, so its card footers are skipped as before. Read the frame again to plan them.
- The design script now keeps the geometry of every layer it reads inside a library instance: its size, its auto-layout (
-
1a24a9c:
bb page measurereads a list’s row padding as the gap between its rows (MAR-4843). Where the render table says a blok spaces its children by padding each one on both sides (sb-list’sitem_spacingpads every row above and below), that padding is measured as the gap, as Figma draws it: the gap between two rows is the first one’s bottom padding, the borders between them and the next one’s top padding, and each row’s size is its content box. The list’s size leaves out what its first and last rows pad at its edges, and a gap next to the list counts that padding: a text’s 24px margin and the first row’s 24px padding are the frame’s 48. Those deltas carry a note saying so. No more of a row’s padding is taken for the gap than the field pads a row. Where the list ends its parent, its last row’s padding stays in the parent’s size, and that size’s note says how much. On ref-1’s COMPOSITION, the list’s gaps go from 0 to 48 against the frame’s 49, its rows from 100–101 to 52–53 against 48, and the gap under the intro from 24 to 48, so none of them is beyond a token step any more. Measure now reads each element’s border widths too. A catalog saved before it had a render table (--catalog old.json) reads the one bb ships for its Backpack, asbb story design getdoes. Where no table has a row for a blok whose children’s gaps are measured, the report warns and names it. -
046b818: Hardening
--draftonbb page measureandbb page compare, from #95’s review (MAR-4846):- A
STORYBLOK_PREVIEW_SECRETshorter than 4 characters is refused asno-preview-secret, with why: masking a secret likeenwould mask it inside ordinary words. - The secret is masked in three forms, longest first: as it is, as the query encodes it (
a+b%28c%29) and asencodeURIComponentdoes (a%20b(c)). A form inside another no longer leaves a tail (abc%maskedsecret=***25). - bb checks where the preview route led, from the last response of goto’s redirects, and measures or compares only the draft; refine could read anything else as drift. A 401 or 403 from the route itself is
preview-refused, naming where bb read the secret: at the route’s path, trailing slash aside (the boilerplate’strailingSlash: true308s to it first), or with no redirect before it. A page that isn’t/<full slug>, such as a login a redirect led to, ispreview-elsewhere, whatever it answered. A 401 or 403 at the draft’s path, after the route accepted the secret (a gated route, basic-auth middleware, a protected deployment), ispreview-blocked, neverpreview-refused. core exportspreviewLanding, pure. draftUrl’s comment says the precedence is bb’s (shell,.env.local,.env), and that.env.development(.local)isn’t read.
The skills tell the person what
preview-refused,preview-elsewhere,preview-blockedand a too-short secret mean, and say the draft wasn’t measured. - A
-
1931c59: The plugin’s Bash guard reads the saved plan from the folder bb runs in, as the shell will resolve it, not always from the session’s folder (MAR-4839). bb reads the space, the remote record and
.backbase/stories/push-plan.jsonfrom its cwd, so the guard follows a plaincd <dir> &&chain at the very start of the command whenbb story pushcomes right after it.cd site && bb story push .backbase/stories/<slug>.json --approved <fingerprint>, run from the folder above the project, is no longer blocked when the fingerprint matches. Anything it can’t follow for sure falls back to the session’s folder, which blocks when the plan isn’t there: acdwith~,$, a backtick,-or a redirect; one followed by;,||or a subshell; apushd; anything else before the push; or a relativecdinto a folder that isn’t there, or whileCDPATHis set. Approved writes in one command that run in different folders are blocked.bb space syncandbb assets uploadfind their saved plans the same way. Every block names the plan file it read. -
94c4bcb:
bb story spacing plannow plans the padding and gaps beside a blok with no Figma layer (MAR-4842). An icon has no text, so the map often can’t pair it, and the plan used to skip the padding above it and the gap below it. On reference frame 1 that left each surface 24px short at the top. A child with no layer now stands for its parent frame’s layer in the same place (the first, the last, or the one beside its sibling’s) when:- no blok has that layer;
- the page and the frame hold as many children, and the layer is at the child’s place among them;
- the child’s text is the layer’s: none, for an icon, since a text the map didn’t pair with a layer is no match for it;
- the child renders that layer’s size, where the render table knows it.
The padding and the gap are then the frame’s, and each note says so:
frame padding top 24 → pt m; from the parent frame's padding, no own layer: sb-icon stands for Icon. The plan doesn’t use a stand-in when the child has a margin at that edge, or when the gap depends on what its parent holds (a list item’s column gap, which only a custom marker turns on); it skips it and says why. A card body standing for its frame says it in the same words.The bloks inside a library instance that have no layer of their own, such as a card’s footer, are skipped once for each instance. The skip names them: the design read keeps none of the auto-layout inside a library instance, so there’s nothing to read them from.
-
4297a6b:
bb story spacing plan: a blok with no text no longer stands in for a text layer (MAR-4842 follow-up). A text layer has no size in the read, so where the section’s copy had no entry for it, an icon passed both the text check and the size check. It could then stand in for aLabeland the plan would read padding from it. A blok without text now never stands in for a layer that is or holds a text. A blok with text still stands in only when the section’s copy matches its words. When a text in the layer has no copy entry, as in ILS run 2’s read, the skip says that, instead of saying the blok’s own text doesn’t match. -
9486fc1: The plugin’s Storyblok MCP write guard now covers every Storyblok MCP tool and server name (MAR-4858). Two calls used to get past it in a Backbase project:
- A tool it didn’t know. It checked only
execute_readonly,execute_mutating,execute_destructive,upload_assetandupload_asset_finish, somcp__storyblok__execute_batchpassed. The hook now runs before every tool of a server whose name hasstoryblokin it. Tools named as reads pass:searchanddescribe, and on another server named for Storyblok, such as a docs server,get_…,list_…,search…,describe…andread…. Any other tool there is blocked, in a project and when the guard can’t tell which project the call is for, since the guard doesn’t recognise it as the content MCP’s and can’t tell what it writes. - A renamed server. A Storyblok MCP added under another name, such as
sb_cms, was never checked. Itsexecute_tools are now checked whatever the server is named, since only Storyblok’s MCP has them, and so are itsupload_assetandupload_asset_finishwhen given aspace_id, as Storyblok’s are. Another CMS’supload_asset, such as Contentful’s, passes.
An unknown tool on a server not named for Storyblok, such as
mcp__sb_cms__execute_batch, still passes. The hook sees one tool name at a time, so it can’t tell a renamed Storyblok server from a database’s MCP.execute_readonlyof an operation not named as a read (get…,list…,retrieve…and the like) now gets the same in-project checks as anexecute_mutatingwrite, since Storyblok’s MCP doesn’t document refusing a write sent throughexecute_readonly. When the guard can’t tell which project a call is for, only reads pass:execute_readonlyof an operation named for publishing, releases, scheduling or imports is blocked there too. - A tool it didn’t know. It checked only
-
ceb8be6: The plugin’s Storyblok MCP write guard no longer switches off when the session sits above its project (MAR-4853). It used to let every call through when it found no
.backbase/from the session’s folder up, so a session inbackbase-testswith the project inbackbase-tests/sitebuilder-presets-framewasn’t guarded. When the walks up from the session’s folder and from the folder Claude Code was opened in find nothing, it now looks in the folders right under those two, following symlinks. With exactly one project there, it checks the call against it as before. With several, it blocks every call but a read, lists them and names the folders it searched. With projects one level deeper still (work/clients/{a,b}), it blocks the same way and says where they are (projects found under work/clients/: a, b). With no.backbase/anywhere, it isn’t a Backbase context, and every call passes, as before.
0.15.0
Minor Changes
-
4aec35d: The frame’s palette through Site Builder’s theme configurator (MAR-4769, D-5). Site Builder is meant to be re-themed per space, so Backbase reproduces a frame’s colours by configuring the space’s theme instead of approximating them blok by blok.
- core exports
themeOverrides(sections, datasources), pure. For each semantic colour variable the frame paints with (fills and strokes, with their opacity), it compares the frame’s colour with what the space paints today, thecolors-theme-lighttoken’suservalue when it has one, else its default, through everyvar(…). Where they differ it plans theuservalue:set,change(it replaces an override) orreset(an empty value, when the default is the frame’s colour). The value isvar(--color-<primitive>)when the frame’s colour is exactly acolorsentry’s, else the colour itself (#E0D7F9): near misses never snap to a primitive. A translucent paint is listed, not planned, as D-1 counts it approximate. Each colour of its own is offered as acustom-colorsentry named after the frame and the variable, unless one has that colour. Only the light theme: the frame’s read has no dark-mode colours, so Backpack’s dark defaults stay. On the ILS frame it sets the four✱ System/Surface/Brand/Accent/…tokens and finds the other seven already agree. bb space plan --sections <sections.json>adds that theme part to the Gate 2 plan, read from the space’s colour datasources (the theme in itsuserdimension). It shows each token, what it is today and what it becomes, and the custom colours to add. The fingerprint covers it, with theuservalue each write replaces. Eachuservalue someone set that the plan replaces is named in the approval sentence and the hook’s ask, old value and new, and the plan doesn’t say “No destructive changes” while there are any. A value or name read from the space is quoted on one line, cut to 60 characters, so it can’t split the approval sentence. A plan whose theme writes nothing keeps sb-mig’s fingerprint, and the plan prints thebb space synccommand with the same--sections.bb space sync --approved <fingerprint> --sections <sections.json>writes the theme part after both of sb-mig’s syncs succeed, through the guarded Management API client: thecustom-colorsentries (additive), then eachuservalue with theuserdimension’s id, reading the entries again first. Default values are never changed. It skips the theme (theme-plan-again) when sb-mig’s plan wrote one of the datasources it was planned from, and skips a token whose entry was renamed or whoseuservalue changed since the plan. Only#RRGGBB,rgba(…),var(--…)or a reset’s empty value are ever sent.theme-write-failednames each write that failed, and.backbase/space-sync.jsonrecords them, with each token’spreviousvalue. The settings story isn’t written: a person opens Settings, where the plugin rebuilds the theme from the datasources, then saves and publishes it.- The Gate 2 hook names the theme colours and custom colours a sync writes.
- D-1’s
designFit: a colour the frame paints through a semantic variable the theme can express is nowexact, withvia: "theme"and the override intheme: { token, value }, instead ofapproximate. A translucent paint, or a primitive the frame binds directly, stays approximate. So D-2’sdesignFitinplan.json,bb plan showandbb plan validatecount it as fitting: on the ILS plan, the hero has 1 approximation instead of 3. - The sync skill passes
--sectionswhen the project has a Gate 1 plan, shows the theme part at Gate 2, and tells the person to open, save and publish the Settings story after a theme write.
- core exports
-
6a265e6:
/backbase:refineis a measured audit now (MAR-4816). It measures every section withbb page measure, runsbb story spacing planfor each section first (its envelope is a try), writes one ops envelope for each section’s largest deltas, applies it withbb story ops apply, and measures the page again locally, before any push: it keeps what moved each targeted delta toward the frame, undoes the rest withbb story ops undo --entry last, and re-applies a partial try’s good half as the next envelope. Then it fixes up section by section, the most px off first, until every delta and the section’s height are within one token step, 3 tries in a row move nothing, or the section’s 5 tries are spent.fitis only a regression guard: a drop of more than 0.05 (--fit-drop) asks for a look at the images, and never reverts a change that matched the frame. The report gives each section’s px off before and after, the envelopes kept and undone, and what’s left with a reason.--thresholdis gone.bb page measure --against <measure.json> --target <id>…judges a try: keep, partial or revert, per section and for the envelope, from the targeted deltas and whether any other grew by more than its own token step. A step is the interval, at the frame’s value toward the page’s, of the scale that sets the delta: the spacing scale for a gap or padding, Backpack’s type ramp at the frame’s width for a font size or line height (bb ships 4.11.0’s), and a blok’s own sizing field for a width or height, else 4px. A delta is within up to its step and half a pixel, M-4’s rule, which compose’sbeyondSteplist uses too. A section isdoneonly when its height is within a step too; with every delta inside a step and the height not, it’sheightUnexplained. The fit guard reads each section’s fit from page compare’s view of every section at the kept iteration. It needs the catalog’s spacing scale. Without--against,data.judgementsays what’s still a token step off. Each delta has anid,<blok uid>/<what>, the same in every measure, and each section atotal, its px off together.--iteration latestnow records the iteration’s number (it wasnullbehind compare’slatestsymlink) and keeps the measure initerations/<n>/too. -
266307a: The skills write ops (MAR-4820):
/backbase:compose,/backbase:editand/backbase:refinechange a story only throughbb story ops apply, and every change is one journal entry thatbb story ops undotakes back. A new skill,backbase-ops, teaches them how: read the targets withbb story targets, write one envelope (each op, with an example that a test applies to ILS run 3’s story), repair arepairableanswer in at most 4 rounds, acceptunchanged, never edit the story JSON and never pass a uid that wasn’t offered. Compose’s copy, options, icons, images and design values are envelopes afterbb story scaffold, alongsidebb story filland beforebb story materialize. Those three deterministic steps stay as they are. Edit’s structure changes (a FAQ item, a moved section, a wrap or unwrap, a duplicated card, a preset) are envelopes too. Each refine try is one envelope, undone withbb story ops undo --entry last, and the budget counts envelopes. Refine keeps, keeps part of, or undoes a try by whatbb page measuresays about the deltas it set out to close, never by its fit, which only makes the agent look at the images when it falls. A partial keep is an undo, then a second envelope of the ops that closed their deltas, which counts as a try.bb story design setandrevertare thin wrappers now.setchecks its assignments as before, with the same errors, then applies them as one envelope (updateDesign, orsetPropfor an option field) through ops apply’s own code, writing exactly what it wrote before; a call that changes nothing isunchanged, and nothing is written.revertisbb story ops undofor one entry, so it undoes an envelope of several ops in one step and refuses (design-revert-stale) when the story changed since. Their flags and their output stay compatible, but both write the ops journal,.backbase/stories/<slug>.ops.jsonl, not<slug>.changes.jsonl, which stays as history:bb page comparereads it beside the journal, so a story refined before keeps its changes. Each journal entry records the design values it changed (design).bb story design loglists them from the journal, andbb page comparematches its entries to sections by the uids each one touched.bb story ops undotakes--note.bb story ops applyanddesign setread.backbase/datasources.jsononly for an envelope that may set a colour, a target it made itself included. The cage now lets a blok keep a field its schema dropped, when the story already had that field there, as a story pulled from a space can, and aduplicateNodecopy keep its source’s, so adding a FAQ item to such a story works. A test reads bb’s sources and fails on any file that writes a story outside ops or a recorded generator (scaffold, fill, materialize, pull). -
659ab36: Add
bb story spacing plan <story> [--section <name|uid>] [--out <file>](MAR-4815): the frame’s auto-layout, turned into the design values that render it, before anything renders. For each blok with a Figma layer (the mapbb page measurecompares through) it reads what each value renders, in px, from the render table, and chooses the field, the blok and the token: a container’sgaporitem_spacingwhen the table says it spaces the children (item_spacingpads each row above and below, so 16px between rows isxs), else each child’smb; padding on the blok that renders it, a card body’s default 24 included;item_flexFill or Hug in a row, anddirection: rowwhere the frame is a row; anitem_basisfraction only within a pixel of a fixed width. Values land at the frame’s breakpoint only, so what the story has for phones stays. Each px rounds to the nearest token with the error said, a tie to the token Backpack’s own presets use more, listed asties. It writes an ops envelope forbb story ops apply, oneupdateDesignper blok with a note per change (frame gap 16 → item_spacing xs, rows 16 apart), to.backbase/spacing/<story>.ops.json, and lists what no field can express as design-fit gaps, never a guess. Planning again after the apply changes nothing. The render table now records how each element lays its children out (display,flex-direction), so a blok with no direction field (a button, a list item) is laid out as its table says.bb page measurenow lists, asbeyondStep, the gaps and padding more than one token step off the frame; a delta of exactly one step, to half a pixel, is within./backbase:composeruns the spacing plan and applies it after materialize, and, with the person’s yes, measures the page once before the push: when something is more than a step off, it lists it, and the person chooses to refine now or push as is. Without a dev server it says plainly that the page wasn’t measured.
0.14.0
Minor Changes
- ccfd940: Add the render table (MAR-4813): what each Backpack design value renders as on the page, in CSS and px, on which element, and what each blok renders with its schema defaults.
bb catalog render-semanticsreads it from the project’s own Backpack by rendering every blok and value with its wrappers on the server, and writes.backbase/render-semantics.json;--storywrites the probes as one story forbb story serve, and--measure --url <page>checks the table against the page’s computed style with Playwright. bb ships the table for Backpack 4.11.0.bb catalogshows each blok’s row asrenderSemantics,bb story design getsays under each subfield what each value renders (item_spacing: s → item li padding 16px top/bottom (32px between items)), and the knowledge pack has a short<field_spacing>section with ILS run 3’s traps and what most bloks share, which moves its diet bound to 61k for the ILS scope. A project’s table for another Backpack is a warning, not read, and a Backpack with no table says so inbb catalog, the pack andbb story design get./backbase:composeand/backbase:refineget “How fields turn into spacing”, generated from the table.
0.13.0
Minor Changes
-
262f7e9:
/backbase:refine [slug]: the visual loop, and one push at the end (MAR-4803). The newbackbase-refineskill renders the local story withbb story serve(after the person’s yes to.env.local), compares every section withbb page compare --iteration auto, and works on the worst sections first. For each one it reads the Figma and rendered images, reads the section’s values withbb story design get, makes at most 3 changes withbb story design set, each with its note, and compares the section again. It keeps the changes when the fit beats the section’s kept fit by more than 0.005. Otherwise it reverts them withbb story design revertand captures the section again. A section stops at a fit of 0.9, after 3 tries in a row without an improvement, or after 5 tries; the loop refines at most 3 sections. The person can set each of these. Refine never edits the story JSON directly, never changes content or structure (that’s/backbase:edit, after its push), and skips Gate 1’s design-fit gaps. It stops before any change when someone changed the story in Storyblok since the last pull or push, and on any stop it puts.env.localback and stops what it started. It shows the compare report andbb story design log, then stamps the story and pushes it once, after the person approves the push plan.bb story design revert <file> --entry <n|last>undoes one loggedbb story design setcall exactly. Each value goes back as it was, and a value that had none is deleted again rather than left as an empty override. Where that call lifted a design field into its envelope, the field goes back to what it was, so the story is byte for byte what it was before. It refuses when a later edit changed a value the entry wrote, and logs the revert as its own entry withrevertOf. To make this possible,set’s log entries now recordunsetfor a value that had none,liftedfor a field the lift changed, andsubfields, each design subfield touched, whole, before and after. Becausesetwrites the lean shape, a revert puts the subfield back whole: anullor""over a default stays, and what wasn’t there goes.setreturns its entry’sindex, andbb story design lognumbers its entries.The evals add a scripted dry run of the loop over ILS run 2’s story, cut to the four sections it ranks, with bb’s design commands run in process and a fake compare. It shows the loop keeping improvements, reverting regressions exactly and stopping on budget, and
pnpm evalruns it.
0.12.0
Minor Changes
-
6f9bafc:
bb story pull <id|full path>reads a story from the project’s Storyblok space through the Management API into.backbase/stories/<slug>.json, with<slug>.remote.jsonrecording the space, the story, itsupdated_atand the hashes of its content and each section. It refuses folders, never writes to the space, and never replaces a local file with unpushed changes without--force.bb story diff <story.json>compares the file with the live story section by section (a section is a top-level blok inbody, by itssb-sectionname or_uid, plus:pagefor the page’s own fields, and:orderfor the sections’ order, so a reorder is one change of its own), in two directions: what a push would change, and what someone changed in Storyblok since the pull, each with the changed bloks and fields.bb story push --plannow guards an update: when the story’supdated_atmoved since the pull or push (or its content’s hash did, for a record with noupdated_at), and the push would overwrite a section someone changed there, or undo their reorder, the plan refuses and lists them.--overwrite-sections <keys>overwrites them, and the plan, its fingerprint, the approval sentence and the plugin’s ask name each one. A push records the same hashes as a pull, of the content as Storyblok’s answer has it.bb story materialize --sections <keys>completes only those sections and leaves every other as it is, listing indata.outsidethe ones completing would have changed.The plugin adds
/backbase:editand itsbackbase-editskill: pull, change only the sections asked for, materialize only those (and others only with the person’s yes), validate and stamp, diff, plan the push, and push after the person’s yes, never publishing./backbase:composepoints to it for changes. -
540f0b0:
bb story serve: the local story as a fake Storyblok Delivery API on 127.0.0.1 (port 4100, or--port), so the dev server renders.backbase/stories/<slug>.jsonon every reload, read fresh each time, with nothing pushed. Every blok gets an_editable, so the page hasdata-blok-uidon each section forbb page compare; the story id is the file’s own, else the onebb story pushrecorded, else a stable one bb makes up. The stories it doesn’t have, such assettings, are fetched once as drafts from the real Delivery API with the project’s public delivery token and cached in.backbase/cache/serve/(--offlineserves only the cache). Lists answer empty, GraphQL answers no paths, anything butGETis refused, and a request for any otherHostthan127.0.0.1:<port>orlocalhost:<port>gets a 403, against DNS rebinding. bb never fetches from a server on this machine, so never from another bb. A story file it can’t read or parse gets one line on stderr.--print-envprints the lines that point the dev server at it,--write-envwrites them to.env.local(never.env), keeping the old values in a commented block, and--restore-envputs them back where they were; all three warn when the shell or.env.development.localwould override them./backbase:composeoffers the comparison throughbb story serve, asking before it writes.env.local, or through the boilerplate’s preview mode, instead of a localhost URL that answers 404 for a draft. -
25b0886: Match downloaded Figma images by hash or name, preserve PNG/JPEG/WebP/GIF formats, and upload approved asset plans through the guarded Management API with resumable S3 transfers, finalization retries and atomic per-image recording. SVG is excluded pending sanitisation. Block missing or mismatched write approvals, including commands with global flags. Report unsupported downloads and image-plan/cache gitignore additions in command notes.
-
c63313a: Catch known sticky nav wrapper crashes, export VIDEO fills as checked PNG posters and count unresolved gallery images consistently, defer Figma authentication to the frame read, and compare colour channels with 8-bit alpha tolerance while grouping palette requests by theme collection.
Keep R-7 original image downloads and approved uploads while matching hashless VIDEO posters by node id, checking poster PNG bytes at plan and upload, and reporting them as posters of a video.
-
3c9163c: Add repeatable section filters and immutable numbered page comparison iterations, with an atomic latest symlink, score history, fit verdicts, timestamp-matched L-2 change-log entries and Figma/current/previous offline reports. Keep pixel-ratio-v1 and add deterministic cumulative edge-profile and combined fit scores for tuning spacing. Document the local-story route for avoiding preview overlays.
0.11.0
Minor Changes
-
6bec561:
bb story push <story.json> --plan, then--approved <fingerprint>: bb writes a stamped story to the project’s Storyblok space itself, through the Management API, as a draft, never published (ADR 0028). The plan names the space, the story, create or update, its parent, size and sections, and whether an update’s story is published, with a fingerprint and an approval sentence, as Gate 2 does. It refuses content that changed sincebb story validate --stamp, a stamp for another space, a folder, a path another story has, and a story bb didn’t push unless--storynames it. It retries Storyblok’s 429s and 5xx, never sends a create again blind (it looks for the story at its path first, and adopts it when the create landed but its answer was lost), gives a write 120 seconds, and records what it pushed in.backbase/stories/<slug>.remote.json, so the next push updates that story. ILS run 2’s 394k-character story, which the MCP couldn’t send, goes in one request.The plugin’s Bash guard blocks
bb story pushwithout--approved, with a fingerprint that isn’t the saved plan’s, or with--approvedpassed more than once (andbb space synctoo), and asks the person before the approved push./backbase:composechecks the push plan before it uploads an image, and pushes the story with bb after the person approves the plan, instead of creating it through the MCP. -
8e362ad: Detect stale presets that reference removed Backpack bloks. Reject their use in plans and story preflight, warn about their writes in Gate 2 approval, and draft one Backpack maintenance request listing their files and content paths. Guide classification and composition to current atomic bloks.
0.10.0
0.9.0
Minor Changes
- 15881d9: Materialize bare kebab icon names into Backpack asset-selector SVG selections, with a cached icons/bold manifest and offline mode. Validate icon formats, warn with closest names for unknown picks, and teach compose to use the design’s icons.
- 1f5ffc5:
backbase-composereads the space’s colour entries withbb space datasourcesbefore it scaffolds, and writes colours as their entries’ names, sizes with their units, links as the URL or anchor the design shows, and the sticky nav’sanchor_linksas the sections it links to, by name:bb story materializestores each as its picker does (MAR-4741).backbase-analyzeignores.backbase/datasources.json. - 1f5ffc5:
backbase-compose’s Design step writes colour subfields (text_color,bg_color,background_colorand the like) as acolorsentry’s name, whichbb story materializestores as the picker does (MAR-4741). It goes from the frame’s variable to the name through the knowledge pack’s<colour_tokens>: the entry whose value is the frame’s hex when there is one, so the page looks like the design, else the one Backpack’s theme maps the variable to, listed as an approximation with the frame’s hex. - cfdcd2a: Compose writes Site Builder design values (MAR-4739).
backbase-analyzecompiles the knowledge pack,.backbase/knowledge.md, after the catalog, with the frame’ssections.jsonfor its colour map, andbackbase-classifyreads it to choose bloks, presets and design values.backbase-composehas a Design step after filling the text. For each section it reads the screenshot and the section’sdesignfromsections.json:design.layoutfor directions, gaps, padding, radii and side-by-side columns;design.tokensfor the spacing, radius, colour and text-style variables behind them;design.instancesfor each component’s variant properties;design.iconsfor its icons. From those and the pack, it writes each blok’s options (variant, size,as) and its design in the simplified dialect, with grid positions, sizes ({ "value": 600, "unit": "px" }) and spacing as the objects they store, using only names the pack lists, then materializes and validates. Five examples from the ILS page teach the dialect: the hero’s two columns, a surface’s radius, a tag’s fill, a heading’s level and size, and a text button. What it can’t express yet, colours among them, it lists per section as approximations in its report, quoting the frame’s variable and thecolorsname it maps to.
0.8.0
Minor Changes
- c259a3d:
backbase-composematerializes the story before it validates it: scaffold, fill,bb story materialize,bb story validate --stamp, ask, write. The agent may write a blok’sdesignin the simplified dialect,{ "gap": { "s": "xl" }, "direction": { "s": "column", "l": "row" } }, with the subfields and values the catalog lists, and never writes the envelope by hand:bb story materializeturns it into the stored format for where the blok sits (MAR-4732). - f602dda: Export Figma image layers, plan and record assets by hash, and include guarded uploads in compose’s single write approval. Reuse confirmed assets on reruns, set full CDN asset envelopes on page slots, and reject image URLs outside the project’s Storyblok space.
- cd4d249: List field-plugin requirements before space sync and explain missing-plugin failures per component. Add plan-scoped compose preflight, one-line destructive approval, project .env guidance, and default-locale folder placement.
0.7.0
Minor Changes
-
46b736b:
/backbase:composeand the write guard (MAR-4697):/backbase:composeloads the newbackbase-composeskill. After Gate 1, Gate 2 (.backbase/space-sync.json) andbb doctor --write, it:- runs
bb story scaffoldand fills the placeholders from the design; - runs
bb story validateuntil there are no errors, and checks the slug is free; - asks once, saying what’s left (“Create draft story
<name>at<slug>in space<name> (<id>)? It has N placeholders left, M images to upload, and K sections that need a space preset applied in the editor.”); - on a yes, stamps with
bb story validate --stamp(with--allow-placeholdersonly when some are left); - creates the story with
createStorythrough Storyblok’s MCP, never withpublish, and records it in.backbase/stories/written.json; - reports the editor link, what’s composed and skipped, and the images to upload.
- runs
- The write guard, the plugin’s second hook next to the sb-mig guard (
hooks/write-guard.mjs, bundled from core’swrite-guard.pure.tslike the sb-mig guard, and registered inhooks/hooks.json): aPreToolUsehook onexecute_readonly,execute_mutatingandexecute_destructiveof every MCP server whose name containsstoryblok. It acts only in a Backbase project (a.backbase/folder from the session’s folder up, or from the folder Claude Code was opened in), and lets every call through anywhere else. In a project it’s an allowlist:execute_mutatingruns onlycreateStory, andupdateStoryof a story inwritten.json; every other write (such asreleaseAll,updateRelease,createStoryScheduling,importStory) is blocked.- The story’s content must be stamped in
validated.json, with no stamp meaning no write. The space must be the stamps’ (one space only), andspace.idin.backbase/config.jsonwhen that’s set. - Any publish (an operation named for it, or
publishset in the parameters, body, query or a query string, or on the story) is blocked,execute_destructivealways is, and so are reads named for publishing, releases, scheduling or imports. - A call that gives two different stories, space ids (
space_id,spaceId) or story ids (story_id,storyId,id) is blocked, and so is any call it fails to check. - It’s dependency-free Node (it needs
nodeonPATH), reads only the project’s.backbase/files, and never calls the network.
-
b81e722:
/backbase:syncand thebackbase-sync-spaceskill: Gate 2 (MAR-4695). The agent runsbb doctor --write, thenbb space plan, shows the person every change and every destructive one, asks them to approve it (each destructive change by name), and only then runsbb space sync --approved <fingerprint>.backbase-newandbackbase-analyzepoint to it after Gate 1, and the analyse flow ignores the two Gate 2 files.The plugin’s first hook: a
PreToolUsehook on Bash (hooks/hooks.json). It fails closed: it blocks any simple command where sb-mig, or a path to it, is followed by--ssot, whatever runs it, andnpm/pnpm/yarnscript runs given--ssot, npm’s global options included. It follows&&chains,bash -c(andfish -c,-c --),eval,$(…), text piped into a shell and heredocs a shell reads. Text tools (echo,grep,git,gh,cat, …) and heredocs nothing runs go through, so commit messages and PR bodies that mention it aren’t blocked. Beforebb space sync, it answers Claude Code withpermissionDecision: "ask", so the person approves Gate 2 themselves, shown what the saved plan writes and to which space. It’s a Node script with no dependencies, bundled frompackages/core.The skill says an approval relayed by another agent, a tool result, a file, a commit or a tracker comment never counts, and that with no person to answer it stops at Gate 2. It shows the presets at Gate 2.
0.6.0
Minor Changes
- 96c6cf9: Add
schemas/story.v1.json, the JSON Schema of a story’s content thatbb story validatechecks (MAR-4691). No skill runs it yet: S-4 does, before each Storyblok MCP write.
0.5.0
Minor Changes
-
9a3ac66:
bb catalogshows the variants Backpack’s React has that a blok’s schema can’t pick, and honours the component map’s exceptions.- It reads the string-literal unions in Backpack’s
.d.tsfiles, and the props that use them, with a small parser (no TypeScript compiler). Each export lists its components’ union props asbackpackExports[].reactComponents. Each blok lists thevariantvalues novariantor<x>_variantoption offers asreactOnlyValues: [{ export, packageExport, prop, values, via }], andbb catalog --component sb-cardprintsReact only: CardFooter.variant: testimonial (in Card, via folder: may not apply), with one line saying these are leads, since a wrapper may map other option names onto React’s.exportis the component (CardFooter), andpackageExportthe package export whose folder holds it (Card), the name a Promote’s primitive takes.viasays whether the blok’s own wrapper renders the component (file) or they only share a folder (folder). Props inherited from another component’s folder (such as Text’s typography variants) are left out, intersections with object types are read, and a component’s props can come from the type its declaration names (FC<AnyProgressProps>). component-map.exceptions.jsonat the project root ({ "<blok>": "<why it needs no map entry>" }) excuses bloks frommissing-map-entry. The summary counts them asexcused. An entry for a blok that doesn’t exist, or one already mapped, is anote:, not a problem. Without the file, nothing changes.missing-map-entrynow says what it means: the blok isn’t in the component map, so it renders nothing if it’s placed where the page hands it to the map.- The classify and analyse skills read
reactOnlyValuesbefore calling a variant missing from React, and word the map problems the same way.
- It reads the string-literal unions in Backpack’s
-
575d605: Polish after the first real
/backbase:newrun (MAR-4684).- Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except
\nand\tinto a space in everything they return, object keys included, and cut text lines at 160 code points with…, so an emoji is never split. The parts script reads 3 parts per call, and a batch that overflows is halved. - Classifying: a self-check before
plan.jsoncompares every Compose with the Reuse rule and the worked examples (two roots that are each one blok doing its own job are a Reuse). A question that could change the bucket caps confidence atmedium, one that decides whether Backpack can do it at all makes itunsure, and content questions don’t count.reactOnlyValuesis a lead, not proof: the agent reads the Storyblok wrapper’s source before calling a section a Promote, and avia: "folder"link counts for less. A confirmed one is a Promote of the entry’spackageExport, with the variant named inreason. A variant in neithervariantsnorreactOnlyValuesisunsure, or a Create for the person to decide. - Gate 1: the analyse flow commits exactly
sections.jsonandplan.jsonasPlan "<frame name>" at Gate 1, unsigned, with every git command run in the project folder (git -C) and the paths given togit commitso nothing else in the tree is touched. It checks the ignore rules before it stages anything, never forces an ignored path, and unstages only its own paths when the commit fails. It skips the commit when git has no identity, taken fromgit configor theGIT_AUTHOR_*andGIT_COMMITTER_*variables asbb createdoes, and says so.missing-map-entryis information, mentioned only for bloks in the plan, and never as bloks that must be registered before they render. bb createadds.backbase/catalog.json,.backbase/figma/*/shots/and.backbase/figma/*/sections.draft.jsonto.gitignore, next to.backbase/*.log.bb doctor: nobbon PATH is a pass forbb-binary, since the plugin runs bb through npx. A Babashkabbis still a warning.
- Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except
-
54366b1: Reuse the Storyblok space’s presets before composing (MAR-4427):
backbase-analyzehas a step 2b. It runs when the catalog knows the project’s space (--space <id>on/backbase:analyze,spaceIdinstoryblok.config, or the spacebb create --spacerecorded in.backbase/config.json, so/backbase:new --spacecarries it through) and Storyblok’s MCP is connected:- it runs the
listManagementComponentsoperation throughsearch,describeandexecute_readonly, withfieldsasking only for component names and theirall_presets; - it never calls
execute_mutating,execute_destructive,upload_assetorupload_asset_finish; - it writes
.backbase/space-presets.jsonwith each preset’s name, id, preview image and description, saves the images under.backbase/space-presets/, and runsbb catalogandbb plan validatewith--space-presets. Without a space id or the MCP it skips the step, doesn’t ask for an id, and says why at Gate 1. Gate 1 reports the presets as “N read, M matched to catalog bloks”, and the Gate 1 commit’s ignore lines include.backbase/space-presets.jsonand.backbase/space-presets/.
- it runs the
backbase-classifylooks for presets first, comparing each section’s screenshot with the presets’ preview images, and the content of presets from code. Names are a hint only, so it works on any frame. A section that matches a preset as it is, or differs only indesignvalues or content, is a Reuse of that preset, withpreseton its blok. One that differs in structure is a Compose that names the nearest preset insignalsand asks whether to use it as it is. Preset names come only from the catalog.signals.mdputs presets first, andbuckets.mdhas a worked Reuse of a section preset.- A plugin test fails when a skill or command names a Storyblok tool that writes, except in the sentence that forbids them.
0.4.0
Minor Changes
-
ba37d2e:
/backbase:new <figma frame link> [folder] [--space <id>]starts a new site from an empty folder. Its skill,backbase-new, runsbb doctorand stops on any failure, listing the warnings and going on. It takes the folder from the argument, or asks for one name, suggesting one from the Figma frame’s name, and never touches a folder that isn’t empty. It runsbb createand shows its steps, stopping with the step’s message and fix when one fails. Then it followsbackbase-analyzein the new project, from its preflight, and stops at Gate 1 with the plan’s table and a What you have now block: the folder, the boilerplate commit, the first commit, the plan waiting for review, and nothing written to Storyblok or Figma.When
/backbase:analyzefinds no Backpack project, it names/backbase:new <figma link> <folder>as the way to start from scratch.
0.3.0
Minor Changes
-
af8159c: Polish the analyse flow after the first end-to-end rehearsal (MAR-4426):
backbase-read-figma’s read-only script also collects each part’s visible text and skips internal components named with.or_.get_design_contextis now an optional fallback, never converted to code, andfigma-useisn’t needed for the script.- The outline comes from a read-only
use_figmascript (the frame’s direct children with their bounds and visibility).get_metadata, which fails on very large frames, is only an optional extra. - The script returns each main component’s
keyand its component set’skey, and counts nested instances by key, never by name. Remote libraries are resolved by key, with design-system searches in small batches."remote"is written only when a key doesn’t resolve. backbase-classifyasks for each blok’s required children in a structure, and the worked examples include them.- The script keeps the first non-empty description of each nested component, and
sections.jsonstores it on thenestedentry. - Screenshots ask for
maxDimension: 4096, a tall frame is screenshotted part by part, and the images are saved to.backbase/figma/<node-id>/shots/, which Gate 1 allows. - Entries are drafted in
sections.draft.jsonwhile reading, andsections.jsonis written once at the end. - The skills read bb’s output through its envelope (
data.summary,data.backpackVersion,data.project…), mention catalog problems (no-component-map,missing-map-entry) once at Gate 1 as project health, and show the plan withbb plan show --questions. backbase-classifyandbuckets.mdopen with a table of what each bucket adds to the codebase: Reuse and Compose add no code, and Create (a new*SBwrapper and its.sb.tsschema) is the only bucket that does, so a section needing even one is a Create.backbase-classifyandbuckets.mdhave the Reuse-or-Compose rule, and Promote covers a variant an existing blok doesn’t expose, told apart by the catalog’svariants.- The skills run bb as
npx -y @ef-global/backbase@<the plugin's version>, andscripts/sync-plugin-version.mjsmoves the pin with each release.
0.2.0
Minor Changes
- 6536315: The plugin installs from the repo’s marketplace (
claude plugin install backbase@backbase) and adds/backbase:analyze <figma-url> [--project <dir>]: it reads a Figma frame through the person’s own Figma MCP, classifies every part against the project’s catalog with thebackbase-analyze,backbase-read-figmaandbackbase-classifyskills, and stops at Gate 1 with a plan. - 9c4bdaa: Add the sections and plan contracts (v1) and their commands:
bb figma sections validate <file>,bb plan validate <plan> --sections <file>andbb plan show <plan>. The plan checks cover the sections file and, throughCatalogView, the catalog: bloks, children and their counts, Create primitives (not*SBwrappers) and names. The JSON Schemas are inplugins/backbase/schemas/, for skills to point agents at.
0.1.0
Minor Changes
- 1d9131b: First release:
bb --versionandbb --help, installable from GitHub Packages.