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

From packages/bb/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_plan and show_review, each linking a ui://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. --root sets 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 --root means 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 (or all) writes .backbase/report.html with 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.written lists the page, and data.views and data.summary say 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. --open opens it in the default browser.
    • Plain words, from one glossary. packages/core/src/glossary.pure.ts holds the output contract’s table (the backbase-output skill’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. --slug takes 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 runs bb report at 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’s glossary.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 create ignores .backbase/report.html and .backbase/reports/, and so does the analyse flow’s .gitignore step.
  • 19bf028: Every skill now talks to the person the same way, through one output contract, the new backbase-output skill (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>.md and is linked, and “show section 6” gets just that part. The reports are gate-1.md, gate-2.md, push.md, review.md and done.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-component and backbase-upstream fill 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 create adds .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.

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 --write and bb create pin it, raising an older chaperoneVersion and keeping a newer one. A project still pinned below it fails every schema under schema-name-matches-folder, since an older Chaperone ignores the preset’s case transform: bb doctor’s project-chaperone now names the old pin and that symptom, and npx @ef-global/backbase doctor --write fixes it.
    • schema-is-registered is native registered-in-map. It reads src/components/component-list.ts and the project’s component-map.exceptions.json: a listed schema passes, an unlisted one fails.
    • A missing component-map.exceptions.json is now an error. Chaperone reports “Cannot check registration”, which no baseline can record. bb doctor reports the missing file under project-chaperone, and bb doctor --write and bb create create it as {}, never overwriting an existing one. (bb check components --registration still treats a missing file as {}.)
    • schema-name-matches-folder is a native file-pairing with a pascal transform. <kebab>.sb.ts must sit in src/components/<Pascal>/, with exactly that case, so Fancard/fan-card.sb.ts fails on macOS as it does on Linux CI.
    • The three command rules list one result per component. wrapper-visibility-keeps-display, schema-is-placeable and flex-child-has-design-field run bb check components --visibility, bb check placeable and bb check flex-design with --json and "output": "results". With --json, those checks’ envelope adds results: one { file, message, fingerprint } per violation, at the schema or wrapper, with fingerprints such as schema-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.mjs still says “@ef-global/backbase isn’t installed” for each command rule when bb’s CLI is missing.
  • 41a9646: bb create runs 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 runs node and git. packages-token runs right before the install, when the project’s .npmrc reads GH_PACKAGES_TOKEN: a token GitHub Packages refuses stops the run, and a timeout or another failure is a warning, so the install still runs. github never stops a clone that would work: it runs only after an HTTPS clone fails, to add its fix. data.checks lists every check it ran, and a check that warns is in data.warnings. runMachineChecks takes only, and packages-token gives its cause in facts.cause.

  • d380b83: /backbase:edit can 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 field bb story ops apply would 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.
      • --origin records where the image came from, such as its source and licence, and --fresh starts the manifest over for a new edit.
    • 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-Length says.
    • The upload is compose’s upload. The manifest is an images file bb assets plan takes, with the story in place of the frame. The images go into Edits / <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.json already 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 --plan lists 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.json records it. It refuses a new image without alt text (alt-missing).
    • New projects ignore .backbase/edits/*/images/, and bb assets add adds that line to .gitignore in existing ones.

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 the backpack-theme plugin rebuilt from the datasources only when a person opened Settings and saved. bb space sync now 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 --sections reads the Settings story the site reads (settings, or <locale>/settings per locale folder, as bb doctor finds it) and lists its id and slug and each token of theme.selected that changes, mode, before and after. It plans the story only when selected would 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, and selected before 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 planned selected, and keeps its theme when a theme write failed. When sb-mig’s plan writes colors-theme-dark or colors-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 the theme field (the model and selected the plugin saves) and keeps every other edit in the draft. .backbase/space-sync.json records the selected and the model it replaced, as previous and previousModel, and bb drops bb 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 a theme field, 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:sync and /backbase:compose end 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.

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 requests now 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 plan drafts it as an issue for ef-global/backpack, and /backbase:upstream requests files 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 (LinkedIn is linkedin-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, as bb story materialize does. 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 create writes .claude/launch.json with the boilerplate’s dev server: its dev script, run with the package manager its lockfile names (npm run dev), on the port the script gives (3000, -p3000 too), 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, and bb 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 from bb create. Running either twice changes nothing.

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/backbase to the running version in devDependencies and 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 rewrites node_modules, and it runs through a shell on Windows, where npm is npm.cmd. bb create does the same for a new project, and also writes the Chaperone preset and .prettierignore. bb doctor’s project-chaperone check fails when the preset doesn’t resolve or bb’s CLI isn’t installed, naming bb doctor --write as 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:component and /backbase:new no longer leave the agent to run those checks by hand.

    A new project-prettierignore check, which bb doctor --write fixes, adds .next, .turbo and node_modules to .prettierignore, counting **/node_modules and 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 calls prettier --check ., which reads .prettierignore alone, so .next/cache was 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 measure pairs 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 is unpaired, with the reason, and gets no deltas. Two cards whose copy was partly edited still pair with their own frames. Each measured blok lists its texts and how each paired.
    • bb page measure reads 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 its bars, 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; --sections still works for this release, deprecated, as a budget of that many sections’ tries), and the report lists any section left untried, and why. Core’s planRefine, nextSection and refineBudgetFrom hold the rule.
    • bb page review captures 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) and xxxl (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 plan names each one in offScale, only for a frame px no step of the scale is, and bb plan requests drafts 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 carry offScale (ILS run 2: 8 stale fits become 11; run 3: 7 become 11): run bb plan validate <plan> --sections <file> --write-design-fit again, then bb plan requests. New bb request plan --issues <gh issue list JSON> drafts each request as an issue for ef-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, which bb plan requests keeps; replacing another link needs --force. ADR 0030: the agent files a design request after the person’s yes. /backbase:upstream requests files 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 sync writes 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 own component-failed error, next to the theme’s result, with --exclude as 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, and bb 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, and space-sync.json records the components left out with their reasons.
    • bb space plan and a new bb doctor check, space-field-plugins, read the account’s and organisation’s field plugins (GET /v1/field_types, GET /v1/org_field_types, paged by Total), 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 --exclude suggestion.
    • When Storyblok’s 422 says the space lacks a field plugin, the component-failed error names it, space-sync.json lists it (fieldPluginFailures), and /backbase:sync offers 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:sync offers the exclusion when a component fails or lacks a plugin, and it and /backbase:compose end with the person’s step after theme writes: open Settings, check the colours, save and publish.
  • 5769871: bb component new no longer turns a Wrap’s grid or flex root into a block when its visibility is Show. The wrapper passes Backpack’s getVisibilityClasses the root’s own display as originalDisplayProperty: read from Backpack’s styles for a Wrap, on the element that gets the wrapper’s className (grid for FanStack), flex for a Create. When bb can’t tell, it puts the visibility on a wrapper that is display: contents when shown. The wrapper names the classes the helper builds at runtime, so Tailwind builds them.

    bb check components --visibility, and the preset’s new wrapper-visibility-keeps-display rule, flag a local wrapper that calls getVisibilityClasses (or the deprecated useVisibility) without originalDisplayProperty on a root that isn’t a block, naming the file and the display to pass. /backbase:component offers 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:upstream takes a Wrap or a Create a project built to Backpack (C-6, ADR 0029): an issue in ef-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), the sb- names (fan-stack.sb.ts → sb-fan-stack.sb.ts, the blok’s name and component fan-stack → sb-fan-stack, display name Fan 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’s parameters.siteBuilder, the exact exports and typesVersions entries, 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 own StoryblokComponent and storyblokEditable among them, which Backpack’s ESLint forbids), warnings for a parent or a local blok Backpack lacks, the branch, the base (the clone’s origin/HEAD), the commit in Backpack’s convention, and the story migration note. It refuses a name Backpack already has. --write writes the moved files into the clone, only one in the temp folder, and only on the plan’s branch.
    • The backbase-upstream skill 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 or GH_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 FanStack is a Wrap: Backpack’s React draws it and only Storyblok lacks it. bb writes src/components/FanStack/FanStackSB.tsx, which imports @ef-global/backpack/FanStack and maps the blok’s fields to its props, and fan-stack.sb.ts, and no FanStack.tsx. The fields are the export’s own props, read from Backpack’s type declarations: children becomes the items bloks field, which the wrapper renders with StoryblokComponent. An optional boolean with no boolean default, like FanStack’s active (unset: fan out on scroll), becomes an Automatic / On / Off option, Automatic by default, which the wrapper passes as undefined: a plain boolean field would be born false and keep the stack shut. --variant prop=value sets a prop in the wrapper. --from Slider,SegmentedControls,Button is a Create: it writes TripBuilder.tsx too, built from those primitives, each imported from its own Backpack export. --fields spec.json gives the fields as Storyblok fields by name, each with an optional prop.

    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 and data.designFrom name. Both also add '<name>': <Name>SB to component-list.ts with an edit that leaves the rest of the file alone, and put the name under each --in parent (sb-section by default) in backbase.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 into backbase.sb.resolvers.ts, or into the project’s own resolvers file, because sb-mig reads only the first. Then it runs the project’s components:discover, and the project’s own prettier over every file bb wrote and all-components.ts, which discover writes with double quotes: the boilerplate’s npm run build passes straight after. Running it again changes neither the placements nor the resolver; the helpers are in placements.ts, for bb adopt. It refuses a name that starts with sb- or ends in -sb, one Backpack has, and one the project has. --dry-run prints 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 catalog lists the new component with its parents. The Backpack 4.11.0 snapshot in evals/fixtures now has each public entry’s index.d.ts, and evals/fixtures/boilerplate is 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 review puts 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 is not-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>/, with index.json, worst first by height delta, each section with its previous image. --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 measured grew only 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 is not-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 with bb story serve where 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.
    • --section on the page commands takes a section’s review key too.
  • 6ea2251: bb page measure and bb page compare never 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/image placeholders, 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/image until 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’s networkidle never 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 with page-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-unsettled captures it anyway, marked settled: false: bb page measure --against judges nothing from such a measure, nor against one, and with --iteration the capture stays out of history.json, the verdicts and latest/. measure.json and compare.json record settled and settle: { 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:refine and /backbase:edit treat page-not-settled as 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 its story.slug, and once pushed, its slug in Storyblok. bb story validate, serve, push, page measure and page compare refuse a story whose names differ with slug-mismatch, naming all three and the fix. New: bb story slug <file> <new-slug> is the one way a slug changes: it sets story.slug, renames the file and its report, push record, ops journal, snapshots and design log, and drops the stamp; for a pushed story, the next bb story push takes the slug to Storyblok, shown in its plan as a slug change. After a hand-edit of story.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 pull refuses 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 serve serves each story at its slug, refuses to start on slug-mismatch or serve-conflict, puts story._backbase = { source: "local", file, servedAt } on what it answers from a file (nothing on Storyblok’s answers), and keeps the last answers per slug at GET /_backbase/served?slug=…. Measure and compare check the page came from the --story file (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 handle slug-taken with bb story slug after asking.

0.16.0

Minor Changes

  • 6c252ba: bb brings its own browser for bb page measure and bb page compare (MAR-4848), so measuring a page changes neither the project nor Playwright’s shared cache. bb depends on playwright-core 1.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 own node_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 on PATH no longer counts. An explicit PLAYWRIGHT_BROWSERS_PATH still 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 install installs Chromium’s headless shell only (about 120 MB) with bb’s own playwright-core, into bb’s cache, or an explicit PLAYWRIGHT_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 doctor gains a page-browser check: it passes when the Playwright bb would use has its headless Chromium installed, and warns, with the command, when it hasn’t. /backbase:compose and /backbase:refine ask once before running the install, and no longer tell the person to add Playwright to the project.

  • bb5504e: bb page measure leads with the section heights. data.heights says how many sections are within ±2px of the frame’s height, and how many more are within a token step (4px), where done is: “1 of 7 within ±2px; 3 more within a token step”. measure.txt and 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 beside data.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] at s and m, [0,16] at l in Backpack 4.11.0), and an sb-image takes viewport_size 100vw. It’s written at the frame’s breakpoint (its section’s Breakpoint variant, or else its width against Backpack’s md and lg) 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 and viewport_size’s default. bb plan validate --write-design-fit stores what to write in the plan entry’s new designFit.bleed, only when something bleeds, so other stored fits stay current; bb plan show lists it under Full bleed, and /backbase:compose writes 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 show marks each question Recommended: … or Answered: …, counts only the open ones in the table, and ends with a Confirmation that names each recommended answer (Confirm, with: the image bleeds); --json gives data.openQuestions and data.confirmation. At Gate 1, /backbase:analyze lists 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> and bb 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 is STORYBLOK_PREVIEW_SECRET from the shell, else the project’s .env.local, else its .env, and the origin is https://localhost:3000, or --url <origin>. The secret, as it is and as the URL encodes it, is masked as *** in data.url (new on both commands), in measure.json, compare.json, index.html and the history, in the output and in errors, such as Playwright’s when the dev server isn’t up. When it isn’t set, the no-preview-secret error 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 exports draftUrl, pure.

    After the push, /backbase:refine measures the draft itself with bb page measure --draft <data.fullSlug> instead of handing the person a command with a secret to fill in, and /backbase:edit does the same for a story composed in the project. /backbase:compose offers bb page compare --draft for the pushed draft.

  • 65ab854: The knowledge pack knows the section grid and flex alignment (MAR-4844). <section_grid> says what a position’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 at s and m (full bleed [0,6]) and 0–16 at l (full bleed [0,16]). A number past the last line is main-start/main-end, the content’s edge, with no error, so [0,16] at m bleeds 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 an sb-flex-group, align is justify-content and centres a column vertically when the children hug (item_flex shrink). justify renders nothing while cross_axis_stretch holds align-items at stretch. In a row the properties swap, and so do a flex group’s fields, so align stays the vertical one where direction is written at that breakpoint. Both come from the render table, so a new Backpack’s table brings its own. bb catalog render-semantics now reads the grid from the installed Backpack’s section helper (mapLayout), and records justify-* and items-* 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 in bb 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’s renderSemantics has the grid, and alignment: false for such a table. The ILS-scope diet bound moves to 62,500 characters. /backbase:compose step 2b and /backbase:edit point to both parts before a position or an alignment is written.

  • 1426e44: A saved catalog or knowledge pack is never used against a different installed Backpack (MAR-4851). bb catalog records 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’s package.json, its type declarations and every file its schemas and datasources load (their presets, plugin schemas and shared fields); storyblok.config and 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 under node_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 as fingerprint, 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 --catalog compares 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 with catalog-stale. The message names both versions, or the files that changed, and gives the command that rebuilds it, with --space-presets when 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: its data.fingerprint leaves out each file’s hash, which the file keeps. For the pack it compares the fingerprint only, not --root or --sections. A catalog saved with --space is 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 doctor has a catalog check: it warns about a stale .backbase/catalog.json, with the command that rebuilds it, and when it can’t compare it, such as with a storyblok.config that won’t load.

Patch Changes

  • c70fba9: bb story design get with a project render table from before bb read alignment classes (MAR-4844) now says so: a renderNote that what align or justify renders is unknown for that table, to write it again with bb catalog render-semantics, and alignment: false in 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 fill places a preset frame’s copy (MAR-4847). On the reference frame it filled 4 of 31 placeholders and left 27 ambiguous, 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-text before 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 new copyStyles pairs 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 a Header 4 title); 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 ambiguous only when two texts really compete or a role contradicts the order. On evals/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.

  • a47173c: bb story fill writes 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 new SHOWN_TEXT table: a button’s label (sb-text-button’s text), a link’s text (sb-link-text, now a body or label role), a tag’s label, and a headline’s, a text’s or a body text’s content. 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 an ambiguous placeholder 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 no Body for 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.fieldsFilled in the report and on their own lines, Filled empty …, so a person can tell them from placeholders. Those still empty are under fill.fieldsLeft, as Still empty …, with a reason (frame-has-none, ambiguous, no-layer-of-its-role or no-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 as frame-has-none.

  • fe504b5: The design read now keeps the auto-layout inside library instances, and bb story spacing plan pads a card’s footer from it (MAR-4852). On reference frame 1, each sb-card-primitive-footer needed pt m. The plan couldn’t find it, because backbase-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 design keeps 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, one Footer), 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.

  • 1a24a9c: bb page measure reads 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’s item_spacing pads 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, as bb story design get does. Where no table has a row for a blok whose children’s gaps are measured, the report warns and names it.

  • 046b818: Hardening --draft on bb page measure and bb page compare, from #95’s review (MAR-4846):

    • A STORYBLOK_PREVIEW_SECRET shorter than 4 characters is refused as no-preview-secret, with why: masking a secret like en would 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 as encodeURIComponent does (a%20b(c)). A form inside another no longer leaves a tail (abc% masked secret=***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’s trailingSlash: true 308s to it first), or with no redirect before it. A page that isn’t /<full slug>, such as a login a redirect led to, is preview-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), is preview-blocked, never preview-refused. core exports previewLanding, 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-blocked and a too-short secret mean, and say the draft wasn’t measured.

  • 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.json from its cwd, so the guard follows a plain cd <dir> && chain at the very start of the command when bb story push comes 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: a cd with ~, $, a backtick, - or a redirect; one followed by ;, || or a subshell; a pushd; anything else before the push; or a relative cd into a folder that isn’t there, or while CDPATH is set. Approved writes in one command that run in different folders are blocked. bb space sync and bb assets upload find 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 scaffold and bb story fill record it that way. bb story spacing plan and bb page measure use 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 proposed pb 0 and the measure reported a width of 960 and padding right 0 → 32. Now the section measures 1024 wide with 32 padding, and the plan leaves its pb alone.

  • 94c4bcb: bb story spacing plan now 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 a Label and 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_asset and upload_asset_finish, so mcp__storyblok__execute_batch passed. The hook now runs before every tool of a server whose name has storyblok in it. Tools named as reads pass: search and describe, and on another server named for Storyblok, such as a docs server, get_…, list_…, search…, describe… and read…. 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. Its execute_ tools are now checked whatever the server is named, since only Storyblok’s MCP has them, and so are its upload_asset and upload_asset_finish when given a space_id, as Storyblok’s are. Another CMS’s upload_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_readonly of an operation not named as a read (get…, list…, retrieve… and the like) now gets the same in-project checks as an execute_mutating write, since Storyblok’s MCP doesn’t document refusing a write sent through execute_readonly. When the guard can’t tell which project a call is for, only reads pass: execute_readonly of an operation named for publishing, releases, scheduling or imports is blocked there too.

  • 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 in backbase-tests with the project in backbase-tests/sitebuilder-presets-frame wasn’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, the colors-theme-light token’s user value when it has one, else its default, through every var(…). Where they differ it plans the user value: set, change (it replaces an override) or reset (an empty value, when the default is the frame’s colour). The value is var(--color-<primitive>) when the frame’s colour is exactly a colors entry’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 a custom-colors entry 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 its user dimension). It shows each token, what it is today and what it becomes, and the custom colours to add. The fingerprint covers it, with the user value each write replaces. Each user value 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 the bb space sync command 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: the custom-colors entries (additive), then each user value with the user dimension’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 whose user value changed since the plan. Only #RRGGBB, rgba(…), var(--…) or a reset’s empty value are ever sent. theme-write-failed names each write that failed, and .backbase/space-sync.json records them, with each token’s previous value. 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 now exact, with via: "theme" and the override in theme: { token, value }, instead of approximate. A translucent paint, or a primitive the frame binds directly, stays approximate. So D-2’s designFit in plan.json, bb plan show and bb plan validate count it as fitting: on the ILS plan, the hero has 1 approximation instead of 3.
    • The sync skill passes --sections when 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.
  • 6a265e6: /backbase:refine is a measured audit now (MAR-4816). It measures every section with bb page measure, runs bb story spacing plan for each section first (its envelope is a try), writes one ops envelope for each section’s largest deltas, applies it with bb 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 with bb 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. fit is 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. --threshold is 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’s beyondStep list uses too. A section is done only when its height is within a step too; with every delta inside a step and the height not, it’s heightUnexplained. 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.judgement says what’s still a token step off. Each delta has an id, <blok uid>/<what>, the same in every measure, and each section a total, its px off together. --iteration latest now records the iteration’s number (it was null behind compare’s latest symlink) and keeps the measure in iterations/<n>/ too.

  • 266307a: The skills write ops (MAR-4820): /backbase:compose, /backbase:edit and /backbase:refine change a story only through bb story ops apply, and every change is one journal entry that bb story ops undo takes back. A new skill, backbase-ops, teaches them how: read the targets with bb story targets, write one envelope (each op, with an example that a test applies to ILS run 3’s story), repair a repairable answer in at most 4 rounds, accept unchanged, 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 after bb story scaffold, alongside bb story fill and before bb 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 with bb story ops undo --entry last, and the budget counts envelopes. Refine keeps, keeps part of, or undoes a try by what bb page measure says 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 set and revert are thin wrappers now. set checks its assignments as before, with the same errors, then applies them as one envelope (updateDesign, or setProp for an option field) through ops apply’s own code, writing exactly what it wrote before; a call that changes nothing is unchanged, and nothing is written. revert is bb story ops undo for 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 compare reads 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 log lists them from the journal, and bb page compare matches its entries to sections by the uids each one touched. bb story ops undo takes --note. bb story ops apply and design set read .backbase/datasources.json only 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 a duplicateNode copy 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 map bb page measure compares through) it reads what each value renders, in px, from the render table, and chooses the field, the blok and the token: a container’s gap or item_spacing when the table says it spaces the children (item_spacing pads each row above and below, so 16px between rows is xs), else each child’s mb; padding on the blok that renders it, a card body’s default 24 included; item_flex Fill or Hug in a row, and direction: row where the frame is a row; an item_basis fraction 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 as ties. It writes an ops envelope for bb story ops apply, one updateDesign per 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 measure now lists, as beyondStep, 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:compose runs 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-semantics reads 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; --story writes the probes as one story for bb 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 catalog shows each blok’s row as renderSemantics, bb story design get says 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 in bb catalog, the pack and bb story design get. /backbase:compose and /backbase:refine get “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 targets and bb story ops come in O-2). An edit is one envelope, { summary, ops, warnings }, of ten typed ops: setProp, updateDesign, insertChild, removeNode, reorderChildren, moveNode, wrapChildren, duplicateNode, unwrapChildren and insertPreset. 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 is applied, unchanged (the envelope changed nothing), repairable (with the reasons) or terminal.

    updateDesign stores exactly what bb story design set stores, through the same code (setStoryDesignValues), and null clears a breakpoint as gap@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. A duplicateNode copy equals its source but for its new _uids. insertPreset takes 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 as bb page compare does, light and at the frame’s width, scrolls it through for lazy images and waits for fonts and images, then reads every data-blok-uid element’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 in skipped, 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. A display: contents element’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.json and measure.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 to bb 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 scaffold and bb story fill record the map in the story’s report, as bloks: { "<_uid>": { figmaNode, section, path } }, never in the story’s content. Core’s extendBlokMap adds 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 targets and bb story ops apply / undo / log: an agent edits a local story with one envelope of typed ops. targets lists 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. apply applies an envelope all or nothing against those targets and answers applied, unchanged, repairable (with up to 30 legal targets for a uid it wasn’t offered) or refused; 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 for undo, which refuses when the story changed since unless --force. log lists 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 or mailto:), 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 new backbase-refine skill renders the local story with bb story serve (after the person’s yes to .env.local), compares every section with bb 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 with bb story design get, makes at most 3 changes with bb 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 with bb story design revert and 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.local back and stops what it started. It shows the compare report and bb 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 logged bb story design set call 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 with revertOf. To make this possible, set’s log entries now record unset for a value that had none, lifted for a field the lift changed, and subfields, each design subfield touched, whole, before and after. Because set writes the lean shape, a revert puts the subfield back whole: a null or "" over a default stays, and what wasn’t there goes. set returns its entry’s index, and bb story design log numbers 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 eval runs it.

  • ad178ce: bb story materialize writes lean design envelopes: plugin, version and fields, 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, title and description are left out, and fields is 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 set writes 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 passes bb story validate: materialize trims it (data.counts.trimmed), and with --sections doesn’t list a section in data.outside for 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 validate reports 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.json recording the space, the story, its updated_at and 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 in body, by its sb-section name or _uid, plus :page for the page’s own fields, and :order for 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 --plan now guards an update: when the story’s updated_at moved since the pull or push (or its content’s hash did, for a record with no updated_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 in data.outside the ones completing would have changed.

    The plugin adds /backbase:edit and its backbase-edit skill: 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:compose points 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>.json on every reload, read fresh each time, with nothing pushed. Every blok gets an _editable, so the page has data-blok-uid on each section for bb page compare; the story id is the file’s own, else the one bb story push recorded, else a stable one bb makes up. The stories it doesn’t have, such as settings, are fetched once as drafts from the real Delivery API with the project’s public delivery token and cached in .backbase/cache/serve/ (--offline serves only the cache). Lists answer empty, GraphQL answers no paths, anything but GET is refused, and a request for any other Host than 127.0.0.1:<port> or localhost:<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-env prints the lines that point the dev server at it, --write-env writes them to .env.local (never .env), keeping the old values in a commented block, and --restore-env puts them back where they were; all three warn when the shell or .env.development.local would override them.

    /backbase:compose offers the comparison through bb 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.json keeps every text line of a section in copy, in layer order with its Figma node id, none cut and repeats kept. It’s optional within v1, so older files stay valid, and bb figma sections design writes 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-figma reads each one with PAGE until none is left, and bb figma sections design puts the pages back together. It names the pages still to read, and truncated now 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’s copy, 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, under fill in 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, set and log for 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 since bb story validate --stamp, a stamp for another space, a folder, a path another story has, and a story bb didn’t push unless --story names 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 push without --approved, with a fingerprint that isn’t the saved plan’s, or with --approved passed more than once (and bb space sync too), and asks the person before the approved push. /backbase:compose checks 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 scaffold then skipped all three, so Campus, Why choose EF? and Testimonials were missing from the story.

    • bb story scaffold composes a Promote’s structure, its stand-in, exactly like a Compose. The report lists it in composed, with standIn: { promote, reason } (the Backpack export it asks for, and why), and the command prints it as 5. Package/Campus (stand-in for promote: Chip). A Promote with no stand-in is still skipped, with the reason Promote 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 validate warns about a Promote with no stand-in (promote-left-out), with or without a catalog. It doesn’t fail the plan.
    • bb plan show ends its Gate 1 view with a Promote list: each Promote is either composed from its stand-in until Backpack has Chip in Storyblok, or this section will be left out of the page until Backpack has FanStack in Storyblok.
    • bb page compare reads the scaffold’s report, <slug>.report.json beside the story or --report <file>, and maps exactly the sections in its composed list, 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 exports parseScaffoldedSections, and mapCompareSections takes the composed section ids.
    • Skills: backbase-classify always gives a Promote the Compose that stands in when the catalog can approximate the section, and says what happens without one. backbase-analyze shows the Promote list at Gate 1 in those words, never just “has a stand-in”. backbase-compose reads standIn and reports a stand-in as composed (stand-in for promote: Card).
    • core exports isOnPage, promoteRequest, promoteLeftOut, warnPromotesLeftOut and the Promote type.
  • 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.json carries each section’s design fit, and bb plan show and the analyse flow present it.

    • The contract: a plan entry gains an optional designFit, D-1’s matcher output trimmed: the section’s fit (fits, approximate or gap), how many facts are gaps and how many approximations, 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 its count; exact facts left out), and the matcher’s notes. 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.json is regenerated.
    • bb plan validate works 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 --datasources or .backbase/datasources.json, or the catalog’s in code when there are none. --write-design-fit stores 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 a design-fit-not-stored error says so. Without it, a missing fit is a design-fit-missing warning and one that no longer matches the structure a design-fit-stale warning. Gaps never fail the plan. data.designFit lists each entry’s fit and status.
    • bb plan show adds 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. --approximations lists the other approximations, a colour as #e0d7f9 → Color/Red/Lightest (100) (#fff5f6). data.designFit has each section’s counts, remedies and notes.
    • Skills: backbase-classify stores the fits after choosing the structure, and where another blok or preset would avoid an approximation or a gap, chooses it and says so in signals. 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, describeFit and renderFitCell.
  • 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 requestFacts so 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 in plan.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’s design from sections.json (P-3a) beside the planned blok tree from plan.json, pairs each Figma layer with a planned blok by order and kind (an auto-layout frame with sb-section, sb-flex-group, sb-surface or sb-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). datasources are the colour datasources by slug, as bb space datasources reads 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, and value says 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, with remedy: "plan" and the alternative blok and subfield (sb-content-group has no radius, sb-surface has design.border_radius). An overlay or a gradient is judged against the blok and the bloks it sits in: a card image’s overlay is its sb-card-media’s.
    • Colours are P-5’s choice (chooseFrameColour over colorPickerEntries, so custom-colors come first): the entry whose value is the frame’s hex is exact, the theme’s entry for another hex approximate, 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 fit is its worst fact’s, fits, approximate or gap, and notes say 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’s gap options label. colorPickerEntries takes any { name, value } entries, the catalog’s from code too.
    • On the ILS frame: the hero’s tags are approximate (✱ System/Surface/Brand/Accent/3 paints #e0d7f9, and the theme’s Color/Red/Lightest (100) is #fff5f6), What’s included’s green and blue tags are exact and its dotted divider is a gap, Other Packages’ card overlays are the card media’s, and the price card’s gradient frame is a gap on sb-surface.
    • Nothing calls it yet: the plan and the skills read it from D-2.
  • 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’s units:, a number’s min:/max:, and defaults: for the breakpoints the schema writes at birth), its placements, its parents:, 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 is asset-selector(icons), never the plugin’s settings.
    • With --sections, the pack maps each colour variable the frame uses (design.tokens.color in sections.json, from P-3a’s design read) through Backpack’s theme datasources to the colors name a picker stores: ✱ System/Surface/Brand/Accent/3 is color-system-surface-brand-accent-3, which colors-theme-light points at Color/Red/Lightest (100). A token whose colour colors has 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 --out it prints the pack; --json gives it as data.text, with each section’s size, the colour map and diagnostics for a maintainer. A root the catalog doesn’t have is an unknown-root error naming the root bloks it does.
    • The catalog reads the datasources in code, Backpack’s lib/storyblok/datasources/*.sb.datasource.cjs and the project’s own *.sb.datasource.ts, into datasources: [{ slug, name, source, entries: [{ name, value }] }], the project’s own over Backpack’s. Each design subfield also keeps a size’s units, a number’s bounds and the editor’s labels, and an unrestricted bloks field is marked unrestricted.
    • bb story validate warns, 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 stay design-invalid-value errors. A catalog saved without datasources checks no colours.
    • core exports figmaVariableToThemeToken, resolveThemeColours, bridgeFigmaColours, frameColourVariables and frameColourHexes.
    • bb create adds .backbase/knowledge.md to the project’s .gitignore.
  • 1f5ffc5: Core’s chooseFrameColour picks the colors entry 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, | ≈ approximation where 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 by Color/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. A title3 headline is as:"h4" (9/9). A design value is written at s and 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 marked CURATED, ruled by hand in new-blok: media beside the words centres both halves with item_align, a background hero bottoms its words, white text over its photo always comes with the dark theme, the headline stays header2, and minimum_height is 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 --knowledge stays under its 60,000-character diet with the house style: 59,700 for the ILS project with its colour map.
    • bb story validate warns, 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 design where 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, renderHouseExemplars and describeHouseDeviations. The exemplars, Backpack’s presets distilled to their shape, are generated and checked, and wait for room in the pack.
  • 4dcc876: sections.json carries each section’s design intent (MAR-4733):

    • The sections contract gains an optional design per section, within v1: it’s additive, so every sections.json written before it stays valid. It has layout (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) and truncated. bb figma sections validate checks it, and counts the sections that have one. schemas/sections.v1.json is regenerated.
    • bb figma sections design <sections.json> [--raw <file>] reads the design script’s output (figma-design.raw.json next to sections.json, or --raw), normalizes it offline and writes each read section’s design into sections.json, keeping every other field. A section whose read failed keeps its fields, loses its design, 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 create adds .backbase/figma/*/figma-design.raw.json to .gitignore.
    • The plugin’s backbase-read-figma skill has a Design step: a read-only use_figma script in references/design-script.md, one part per call, saved with the libraries resolved by key, then bb 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; a ROOT that 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-classify reads design.instances and design.tokens as evidence for presets and variants.
    • The outline and parts scripts’ clean tests 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 \u escape and that each still parses once escapes are turned into characters.
  • 1f5ffc5: bb story materialize writes 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 (or custom-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 a color-not-materialized error 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 with dimension_value: null.
    • Sizes and bounds: a backpack-size written 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 %, vh or vw, and a number subfield past its schema’s min_value or max_value (sb-hero’s minimum_height, 0 to 100), are clamped, with a number-clamped warning. The catalog keeps each size’s units and each number’s bounds, so a catalog.json saved before this is refused.
    • Links: a link written as an https:, http: or tel: 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-placeholder included, stays the empty link, never a URL. Anything else, javascript: included, is multilink-malformed.
    • Sections: the sticky nav’s anchor_links and hidden_in_sections take a list of sections, by name, label or anchor, and get the section selector’s { "selectedSections", "plugin", "version" }, by each sb-section’s name. A name no section has warns (section-unknown).

    bb space datasources reads the entries of Backpack’s colour datasources from the project’s space, read-only, into .backbase/datasources.json. bb story materialize and bb story validate read it, or the file --datasources names, and refuse one from another space. bb create ignores it.

    Core has themeColorEntryName, which names the theme datasource entry a Figma colour variable is: ✱ System/Text/Default is color-system-text-default.

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’s default_value, else [] for bloks, options and multiasset, { "type": "doc", "content": [] } for rich text, {} for custom, "" for text, textarea, option and number, false for a boolean, and no key for tabs and sections. A value that’s there is never touched. Every design becomes 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 (position outside an sb-section, item_flex outside 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, stored values that 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’s type, fieldType, options (per breakpoint where they’re declared that way), datasource, defaultValue and placements, with door B’s placements for a blok that declares none. Every field keeps its defaultValue, and every custom field its fieldType. A catalog.json saved by an older bb is refused: save it again with bb catalog --json.
    • bb story scaffold completes 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-level sb-section gets its id in name, 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’s anchor_links and hidden_in_sections list sections by it. The report adds each section’s anchor, { id, label }, what it dropped from presets, and completion.
    • bb story validate reports missing-field, design-not-materialized, design-placement, design-unknown-subfield and design-invalid-value, all errors, so --stamp refuses while any are left. The first three say to run bb story materialize. A design field in another breakpoints dialect is a design-dialect warning.
  • 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 compare to 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’s sections.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 as alt; 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); and todo, 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 validate reports a placeholder warning for each placeholder the scaffold left (text starting [Backbase placeholder, or a link to #backbase-placeholder), and counts them in data.counts.placeholders.
    • bb story validate --stamp: once a story passes with no errors, and no placeholders unless --allow-placeholders is passed, it records the sha256 of its content, as canonical JSON, in .backbase/stories/validated.json as { "<hash>": { file, at, spaceId } }, where spaceId is the project’s configured space (spaceId in storyblok.config, then space.id in .backbase/config.json; never a flag, so a stamp can’t name another space). Without a space it doesn’t stamp, and fails. --json adds data.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 plan checks what bb doctor --write checks, then that the project’s own sb-mig (node_modules/.bin/sb-mig) is 6.5.0-beta.25 or later, and runs sb-mig sync datasources --all --dry-run --json, then sb-mig sync components --all --presets --dry-run --json, with nothing on stdin and the space bb checked as STORYBLOK_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-destructive wasn’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 reports ok: false or 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: create and overwrite, by component), the render shows them, the fingerprint covers every component’s presets, and data.written.presets says what the sync wrote.
    • plan-changed no longer prints the new plan’s fingerprint: a new plan needs the person’s approval first.
    • What bb writes to .backbase/space-plan.json and .backbase/space-sync.json is masked like its output: the env’s secrets, the token from .env and storyblok.config’s oauthToken.
    • bb evaluates storyblok.config (in bb doctor, bb catalog and the space commands) with the project’s .env loaded first, as sb-mig does, never over the shell’s variables. A spaceId: process.env.X set only in .env now gives bb the space sb-mig writes to.
    • bb never passes --ssot to sb-mig: no flag asks for it, and the arguments are built in one place that throws if they’d hold it.
    • bb create adds .backbase/space-plan.json and .backbase/space-sync.json to .gitignore.
    • bb doctor --json: the space-access check’s result has facts: { 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-token warns when the shell and the project’s .env both set STORYBLOK_OAUTH_TOKEN to different values: the shell’s silently wins for sb-mig and bb. It compares them without showing either. Under --write this warning doesn’t block, since space-access checks the token sb-mig will use.

0.6.0

Minor Changes

  • 26295b3: bb doctor checks 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 find STORYBLOK_OAUTH_TOKEN in 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-access sends 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 --write is 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 exports readyToWrite, 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 under content, or a whole story under story.content.
    • It builds the catalog the way bb catalog does, or takes --catalog <file>, such as the .backbase/catalog.json saved 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, or content["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-type and uid (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, and unchecked-children, for a bloks field restricted by tag or by a component group the catalog can’t resolve.
    • unknown-field warns about a key the schema doesn’t have, and fails when the key holds bloks, such as a misspelled contents, since they’d never render; the bloks in it are still checked. _uid, component and _editable are always allowed, and a translation such as title__i18n__de is checked as its field, never as required.
    • min-items only warns about an empty bloks field that isn’t required.
    • --json gives data: { file, catalogFile, backpackVersion, root, problems, counts: { bloks, errors, warnings } }, with every problem and its severity, and the errors in errors.
    • 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.v1 contract describes the file. Its JSON Schema is the plugin’s schemas/story.v1.json.
    • bb catalog records where an option field’s options come from when they aren’t its own: source (such as internal or internal_stories) and datasource, the datasource’s slug.
    • bb catalog makes a schema without is_nestable nestable unless it’s a root, as Storyblok does; it used to make it not nestable.
    • bb catalog resolves a bloks field restricted by component group (restrict_type: "groups") to the nestable bloks whose component_group_name or component_group_uuid is in its component_group_whitelist. When a group matches none, or the field restricts by tag, its allowedChildren is null with restrictedTo: "groups" or "tags", instead of [], which rejected every child.

0.5.0

Minor Changes

  • 9a3ac66: bb catalog shows 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.ts files, and the props that use them, with a small parser (no TypeScript compiler). Each export lists its components’ union props as backpackExports[].reactComponents. Each blok lists the variant values no variant or <x>_variant option offers as reactOnlyValues: [{ export, packageExport, prop, values, via }], and bb catalog --component sb-card prints React 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. export is the component (CardFooter), and packageExport the package export whose folder holds it (Card), the name a Promote’s primitive takes. via says 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.json at the project root ({ "<blok>": "<why it needs no map entry>" }) excuses bloks from missing-map-entry. The summary counts them as excused. An entry for a blok that doesn’t exist, or one already mapped, is a note:, not a problem. Without the file, nothing changes.
    • missing-map-entry now 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 reactOnlyValues before calling a variant missing from React, and word the map problems the same way.
  • 575d605: Polish after the first real /backbase:new run (MAR-4684).

    • Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except \n and \t into 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.json compares 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 at medium, one that decides whether Backpack can do it at all makes it unsure, and content questions don’t count. reactOnlyValues is a lead, not proof: the agent reads the Storyblok wrapper’s source before calling a section a Promote, and a via: "folder" link counts for less. A confirmed one is a Promote of the entry’s packageExport, with the variant named in reason. A variant in neither variants nor reactOnlyValues is unsure, or a Create for the person to decide.
    • Gate 1: the analyse flow commits exactly sections.json and plan.json as Plan "<frame name>" at Gate 1, unsigned, with every git command run in the project folder (git -C) and the paths given to git commit so 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 from git config or the GIT_AUTHOR_* and GIT_COMMITTER_* variables as bb create does, and says so. missing-map-entry is information, mentioned only for bloks in the plan, and never as bloks that must be registered before they render.
    • bb create adds .backbase/catalog.json, .backbase/figma/*/shots/ and .backbase/figma/*/sections.draft.json to .gitignore, next to .backbase/*.log.
    • bb doctor: no bb on PATH is a pass for bb-binary, since the plugin runs bb through npx. A Babashka bb is still a warning.
  • 54366b1: Reuse the Storyblok space’s presets before composing (MAR-4427):

    • A new space-presets.v1 contract, { 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’s schemas/space-presets.v1.json.
    • plan.v1 structure nodes take an optional preset, the name of the blok’s preset they reuse. Existing plans stay valid.
    • bb catalog and bb plan validate find the project’s Storyblok space id in --space <id>, then spaceId in storyblok.config, then space.id in the .backbase/config.json that bb create --space writes. bb never reads .env. data.space in the catalog has its id and its source: "flag", "config", "backbase-config" or "none".
    • bb create also ignores .backbase/space-presets.json and .backbase/space-presets/ (its preview images), so Gate 1 never commits them: like catalog.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 its spacePresets, with their images, next to its presets from code. data.space.presets says which space they were read from and when. data.summary.spacePresets has two counts: read (every preset in the file) and matched (those whose blok is in the catalog). A blok the space has presets for and the catalog doesn’t have is an unknown-preset-component problem. Presets read from another space than the project’s fail the command.
    • bb plan validate --space-presets <file> checks every preset in a structure against its blok’s presets, in code and in the space. One it doesn’t have is an unknown-preset error 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 show shows a preset after its blok: sb-section [SHOWCASE / 3-cards].
    • bb never calls Storyblok: the agent reads the presets and bb reads the file.

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 .env from STORYBLOK_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 .npmrc reads GH_PACKAGES_TOKEN and 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 to AGENTS.md and 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. --json reports 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, so commit.gpgsign can’t wait on a passphrase. It needs a complete identity, for the author and the committer, from git config or the GIT_AUTHOR_* and GIT_COMMITTER_* env vars. data.leftBehind says when a failed run left a half-made project in the folder.

  • 133c653: bb doctor checks that a machine is ready for Backbase:

    • Node 20 or later, and git.
    • A bb on 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 gh or GH_TOKEN/GITHUB_TOKEN.
    • SSH to GitHub.
    • A GH_PACKAGES_TOKEN that 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. --json carries every check’s result. --fix applies 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 create and 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 help in a terminal shows it beside the title, and bb droid rolls 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 when BB_NO_DROID is 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):

    • structure in plan v1 takes one root blok or an array of roots, for a section that needs two top-level bloks. The checks, bb plan show and plan.v1.json follow.
    • bb plan show shows each structure two levels deep. --full shows the whole tree, and --questions lists each section’s questions under the table instead of in a cell. Its JSON adds data.sectionQuestions.
    • bb plan validate runs 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 validate warns, in data.warnings and 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 --component shows each bloks field’s minimum and maximum next to required.
    • bb catalog lists each blok’s variants: option fields that select a variant, including the ones inside design, such as design.variant on sb-button.
    • bb catalog reports a whitelisted child that no schema defines as an unknown-allowed-child problem and no longer offers it as a placement.
    • backpackExports[].schemaNames lists 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.json documents component.library as "local", "remote" or a library name, and null only when unknown.
    • nested[] in sections v1 takes an optional description, 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.addSecret masks secrets that don’t come from the env. Adds bb help [command...], bb version, bb check pure-tests, bb check commands and bb 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> and bb plan show <plan>. The plan checks cover the sections file and, through CatalogView, the catalog: bloks, children and their counts, Create primitives (not *SB wrappers) and names. The JSON Schemas are in plugins/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 --version and bb --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_plan and show_review, each linking a ui://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. --root sets 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 --root means 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 (or all) writes .backbase/report.html with 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.written lists the page, and data.views and data.summary say 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. --open opens it in the default browser.
    • Plain words, from one glossary. packages/core/src/glossary.pure.ts holds the output contract’s table (the backbase-output skill’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. --slug takes 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 runs bb report at 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’s glossary.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 create ignores .backbase/report.html and .backbase/reports/, and so does the analyse flow’s .gitignore step.
  • 19bf028: Every skill now talks to the person the same way, through one output contract, the new backbase-output skill (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>.md and is linked, and “show section 6” gets just that part. The reports are gate-1.md, gate-2.md, push.md, review.md and done.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-component and backbase-upstream fill 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 create adds .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.

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 --write and bb create pin it, raising an older chaperoneVersion and keeping a newer one. A project still pinned below it fails every schema under schema-name-matches-folder, since an older Chaperone ignores the preset’s case transform: bb doctor’s project-chaperone now names the old pin and that symptom, and npx @ef-global/backbase doctor --write fixes it.
    • schema-is-registered is native registered-in-map. It reads src/components/component-list.ts and the project’s component-map.exceptions.json: a listed schema passes, an unlisted one fails.
    • A missing component-map.exceptions.json is now an error. Chaperone reports “Cannot check registration”, which no baseline can record. bb doctor reports the missing file under project-chaperone, and bb doctor --write and bb create create it as {}, never overwriting an existing one. (bb check components --registration still treats a missing file as {}.)
    • schema-name-matches-folder is a native file-pairing with a pascal transform. <kebab>.sb.ts must sit in src/components/<Pascal>/, with exactly that case, so Fancard/fan-card.sb.ts fails on macOS as it does on Linux CI.
    • The three command rules list one result per component. wrapper-visibility-keeps-display, schema-is-placeable and flex-child-has-design-field run bb check components --visibility, bb check placeable and bb check flex-design with --json and "output": "results". With --json, those checks’ envelope adds results: one { file, message, fingerprint } per violation, at the schema or wrapper, with fingerprints such as schema-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.mjs still says “@ef-global/backbase isn’t installed” for each command rule when bb’s CLI is missing.
  • 41a9646: bb create runs 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 runs node and git. packages-token runs right before the install, when the project’s .npmrc reads GH_PACKAGES_TOKEN: a token GitHub Packages refuses stops the run, and a timeout or another failure is a warning, so the install still runs. github never stops a clone that would work: it runs only after an HTTPS clone fails, to add its fix. data.checks lists every check it ran, and a check that warns is in data.warnings. runMachineChecks takes only, and packages-token gives its cause in facts.cause.

  • d380b83: /backbase:edit can 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 field bb story ops apply would 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.
      • --origin records where the image came from, such as its source and licence, and --fresh starts the manifest over for a new edit.
    • 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-Length says.
    • The upload is compose’s upload. The manifest is an images file bb assets plan takes, with the story in place of the frame. The images go into Edits / <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.json already 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 --plan lists 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.json records it. It refuses a new image without alt text (alt-missing).
    • New projects ignore .backbase/edits/*/images/, and bb assets add adds that line to .gitignore in existing ones.

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 the backpack-theme plugin rebuilt from the datasources only when a person opened Settings and saved. bb space sync now 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 --sections reads the Settings story the site reads (settings, or <locale>/settings per locale folder, as bb doctor finds it) and lists its id and slug and each token of theme.selected that changes, mode, before and after. It plans the story only when selected would 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, and selected before 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 planned selected, and keeps its theme when a theme write failed. When sb-mig’s plan writes colors-theme-dark or colors-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 the theme field (the model and selected the plugin saves) and keeps every other edit in the draft. .backbase/space-sync.json records the selected and the model it replaced, as previous and previousModel, and bb drops bb 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 a theme field, 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:sync and /backbase:compose end 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.

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 requests now 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 plan drafts it as an issue for ef-global/backpack, and /backbase:upstream requests files 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 (LinkedIn is linkedin-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, as bb story materialize does. 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 create writes .claude/launch.json with the boilerplate’s dev server: its dev script, run with the package manager its lockfile names (npm run dev), on the port the script gives (3000, -p3000 too), 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, and bb 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 from bb create. Running either twice changes nothing.

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/backbase to the running version in devDependencies and 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 rewrites node_modules, and it runs through a shell on Windows, where npm is npm.cmd. bb create does the same for a new project, and also writes the Chaperone preset and .prettierignore. bb doctor’s project-chaperone check fails when the preset doesn’t resolve or bb’s CLI isn’t installed, naming bb doctor --write as 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:component and /backbase:new no longer leave the agent to run those checks by hand.

    A new project-prettierignore check, which bb doctor --write fixes, adds .next, .turbo and node_modules to .prettierignore, counting **/node_modules and 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 calls prettier --check ., which reads .prettierignore alone, so .next/cache was 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 measure pairs 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 is unpaired, with the reason, and gets no deltas. Two cards whose copy was partly edited still pair with their own frames. Each measured blok lists its texts and how each paired.
    • bb page measure reads 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 its bars, 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; --sections still works for this release, deprecated, as a budget of that many sections’ tries), and the report lists any section left untried, and why. Core’s planRefine, nextSection and refineBudgetFrom hold the rule.
    • bb page review captures 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) and xxxl (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 plan names each one in offScale, only for a frame px no step of the scale is, and bb plan requests drafts 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 carry offScale (ILS run 2: 8 stale fits become 11; run 3: 7 become 11): run bb plan validate <plan> --sections <file> --write-design-fit again, then bb plan requests. New bb request plan --issues <gh issue list JSON> drafts each request as an issue for ef-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, which bb plan requests keeps; replacing another link needs --force. ADR 0030: the agent files a design request after the person’s yes. /backbase:upstream requests files 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 sync writes 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 own component-failed error, next to the theme’s result, with --exclude as 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, and bb 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, and space-sync.json records the components left out with their reasons.
    • bb space plan and a new bb doctor check, space-field-plugins, read the account’s and organisation’s field plugins (GET /v1/field_types, GET /v1/org_field_types, paged by Total), 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 --exclude suggestion.
    • When Storyblok’s 422 says the space lacks a field plugin, the component-failed error names it, space-sync.json lists it (fieldPluginFailures), and /backbase:sync offers 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:sync offers the exclusion when a component fails or lacks a plugin, and it and /backbase:compose end with the person’s step after theme writes: open Settings, check the colours, save and publish.
  • 5769871: bb component new no longer turns a Wrap’s grid or flex root into a block when its visibility is Show. The wrapper passes Backpack’s getVisibilityClasses the root’s own display as originalDisplayProperty: read from Backpack’s styles for a Wrap, on the element that gets the wrapper’s className (grid for FanStack), flex for a Create. When bb can’t tell, it puts the visibility on a wrapper that is display: contents when shown. The wrapper names the classes the helper builds at runtime, so Tailwind builds them.

    bb check components --visibility, and the preset’s new wrapper-visibility-keeps-display rule, flag a local wrapper that calls getVisibilityClasses (or the deprecated useVisibility) without originalDisplayProperty on a root that isn’t a block, naming the file and the display to pass. /backbase:component offers 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:upstream takes a Wrap or a Create a project built to Backpack (C-6, ADR 0029): an issue in ef-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), the sb- names (fan-stack.sb.ts → sb-fan-stack.sb.ts, the blok’s name and component fan-stack → sb-fan-stack, display name Fan 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’s parameters.siteBuilder, the exact exports and typesVersions entries, 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 own StoryblokComponent and storyblokEditable among them, which Backpack’s ESLint forbids), warnings for a parent or a local blok Backpack lacks, the branch, the base (the clone’s origin/HEAD), the commit in Backpack’s convention, and the story migration note. It refuses a name Backpack already has. --write writes the moved files into the clone, only one in the temp folder, and only on the plan’s branch.
    • The backbase-upstream skill 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 or GH_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 review puts 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 is not-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>/, with index.json, worst first by height delta, each section with its previous image. --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 measured grew only 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 is not-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 with bb story serve where 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.
    • --section on the page commands takes a section’s review key too.
  • 6ea2251: bb page measure and bb page compare never 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/image placeholders, 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/image until 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’s networkidle never 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 with page-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-unsettled captures it anyway, marked settled: false: bb page measure --against judges nothing from such a measure, nor against one, and with --iteration the capture stays out of history.json, the verdicts and latest/. measure.json and compare.json record settled and settle: { 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:refine and /backbase:edit treat page-not-settled as 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 its story.slug, and once pushed, its slug in Storyblok. bb story validate, serve, push, page measure and page compare refuse a story whose names differ with slug-mismatch, naming all three and the fix. New: bb story slug <file> <new-slug> is the one way a slug changes: it sets story.slug, renames the file and its report, push record, ops journal, snapshots and design log, and drops the stamp; for a pushed story, the next bb story push takes the slug to Storyblok, shown in its plan as a slug change. After a hand-edit of story.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 pull refuses 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 serve serves each story at its slug, refuses to start on slug-mismatch or serve-conflict, puts story._backbase = { source: "local", file, servedAt } on what it answers from a file (nothing on Storyblok’s answers), and keeps the last answers per slug at GET /_backbase/served?slug=…. Measure and compare check the page came from the --story file (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 handle slug-taken with bb story slug after asking.

0.16.0

Minor Changes

  • 6c252ba: bb brings its own browser for bb page measure and bb page compare (MAR-4848), so measuring a page changes neither the project nor Playwright’s shared cache. bb depends on playwright-core 1.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 own node_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 on PATH no longer counts. An explicit PLAYWRIGHT_BROWSERS_PATH still 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 install installs Chromium’s headless shell only (about 120 MB) with bb’s own playwright-core, into bb’s cache, or an explicit PLAYWRIGHT_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 doctor gains a page-browser check: it passes when the Playwright bb would use has its headless Chromium installed, and warns, with the command, when it hasn’t. /backbase:compose and /backbase:refine ask once before running the install, and no longer tell the person to add Playwright to the project.

  • bb5504e: bb page measure leads with the section heights. data.heights says how many sections are within ±2px of the frame’s height, and how many more are within a token step (4px), where done is: “1 of 7 within ±2px; 3 more within a token step”. measure.txt and 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 beside data.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] at s and m, [0,16] at l in Backpack 4.11.0), and an sb-image takes viewport_size 100vw. It’s written at the frame’s breakpoint (its section’s Breakpoint variant, or else its width against Backpack’s md and lg) 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 and viewport_size’s default. bb plan validate --write-design-fit stores what to write in the plan entry’s new designFit.bleed, only when something bleeds, so other stored fits stay current; bb plan show lists it under Full bleed, and /backbase:compose writes 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 show marks each question Recommended: … or Answered: …, counts only the open ones in the table, and ends with a Confirmation that names each recommended answer (Confirm, with: the image bleeds); --json gives data.openQuestions and data.confirmation. At Gate 1, /backbase:analyze lists 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> and bb 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 is STORYBLOK_PREVIEW_SECRET from the shell, else the project’s .env.local, else its .env, and the origin is https://localhost:3000, or --url <origin>. The secret, as it is and as the URL encodes it, is masked as *** in data.url (new on both commands), in measure.json, compare.json, index.html and the history, in the output and in errors, such as Playwright’s when the dev server isn’t up. When it isn’t set, the no-preview-secret error 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 exports draftUrl, pure.

    After the push, /backbase:refine measures the draft itself with bb page measure --draft <data.fullSlug> instead of handing the person a command with a secret to fill in, and /backbase:edit does the same for a story composed in the project. /backbase:compose offers bb page compare --draft for the pushed draft.

  • 65ab854: The knowledge pack knows the section grid and flex alignment (MAR-4844). <section_grid> says what a position’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 at s and m (full bleed [0,6]) and 0–16 at l (full bleed [0,16]). A number past the last line is main-start/main-end, the content’s edge, with no error, so [0,16] at m bleeds 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 an sb-flex-group, align is justify-content and centres a column vertically when the children hug (item_flex shrink). justify renders nothing while cross_axis_stretch holds align-items at stretch. In a row the properties swap, and so do a flex group’s fields, so align stays the vertical one where direction is written at that breakpoint. Both come from the render table, so a new Backpack’s table brings its own. bb catalog render-semantics now reads the grid from the installed Backpack’s section helper (mapLayout), and records justify-* and items-* 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 in bb 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’s renderSemantics has the grid, and alignment: false for such a table. The ILS-scope diet bound moves to 62,500 characters. /backbase:compose step 2b and /backbase:edit point to both parts before a position or an alignment is written.

  • 1426e44: A saved catalog or knowledge pack is never used against a different installed Backpack (MAR-4851). bb catalog records 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’s package.json, its type declarations and every file its schemas and datasources load (their presets, plugin schemas and shared fields); storyblok.config and 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 under node_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 as fingerprint, 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 --catalog compares 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 with catalog-stale. The message names both versions, or the files that changed, and gives the command that rebuilds it, with --space-presets when 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: its data.fingerprint leaves out each file’s hash, which the file keeps. For the pack it compares the fingerprint only, not --root or --sections. A catalog saved with --space is 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 doctor has a catalog check: it warns about a stale .backbase/catalog.json, with the command that rebuilds it, and when it can’t compare it, such as with a storyblok.config that won’t load.

Patch Changes

  • c70fba9: bb story design get with a project render table from before bb read alignment classes (MAR-4844) now says so: a renderNote that what align or justify renders is unknown for that table, to write it again with bb catalog render-semantics, and alignment: false in 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 fill places a preset frame’s copy (MAR-4847). On the reference frame it filled 4 of 31 placeholders and left 27 ambiguous, 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-text before 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 new copyStyles pairs 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 a Header 4 title); 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 ambiguous only when two texts really compete or a role contradicts the order. On evals/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.

  • a47173c: bb story fill writes 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 new SHOWN_TEXT table: a button’s label (sb-text-button’s text), a link’s text (sb-link-text, now a body or label role), a tag’s label, and a headline’s, a text’s or a body text’s content. 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 an ambiguous placeholder 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 no Body for 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.fieldsFilled in the report and on their own lines, Filled empty …, so a person can tell them from placeholders. Those still empty are under fill.fieldsLeft, as Still empty …, with a reason (frame-has-none, ambiguous, no-layer-of-its-role or no-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 as frame-has-none.

  • fe504b5: The design read now keeps the auto-layout inside library instances, and bb story spacing plan pads a card’s footer from it (MAR-4852). On reference frame 1, each sb-card-primitive-footer needed pt m. The plan couldn’t find it, because backbase-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 design keeps 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, one Footer), 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.

  • 1a24a9c: bb page measure reads 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’s item_spacing pads 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, as bb story design get does. Where no table has a row for a blok whose children’s gaps are measured, the report warns and names it.

  • 046b818: Hardening --draft on bb page measure and bb page compare, from #95’s review (MAR-4846):

    • A STORYBLOK_PREVIEW_SECRET shorter than 4 characters is refused as no-preview-secret, with why: masking a secret like en would 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 as encodeURIComponent does (a%20b(c)). A form inside another no longer leaves a tail (abc% masked secret=***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’s trailingSlash: true 308s to it first), or with no redirect before it. A page that isn’t /<full slug>, such as a login a redirect led to, is preview-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), is preview-blocked, never preview-refused. core exports previewLanding, 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-blocked and a too-short secret mean, and say the draft wasn’t measured.

  • 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.json from its cwd, so the guard follows a plain cd <dir> && chain at the very start of the command when bb story push comes 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: a cd with ~, $, a backtick, - or a redirect; one followed by ;, || or a subshell; a pushd; anything else before the push; or a relative cd into a folder that isn’t there, or while CDPATH is set. Approved writes in one command that run in different folders are blocked. bb space sync and bb assets upload find their saved plans the same way. Every block names the plan file it read.

  • 94c4bcb: bb story spacing plan now 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 a Label and 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_asset and upload_asset_finish, so mcp__storyblok__execute_batch passed. The hook now runs before every tool of a server whose name has storyblok in it. Tools named as reads pass: search and describe, and on another server named for Storyblok, such as a docs server, get_…, list_…, search…, describe… and read…. 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. Its execute_ tools are now checked whatever the server is named, since only Storyblok’s MCP has them, and so are its upload_asset and upload_asset_finish when given a space_id, as Storyblok’s are. Another CMS’s upload_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_readonly of an operation not named as a read (get…, list…, retrieve… and the like) now gets the same in-project checks as an execute_mutating write, since Storyblok’s MCP doesn’t document refusing a write sent through execute_readonly. When the guard can’t tell which project a call is for, only reads pass: execute_readonly of an operation named for publishing, releases, scheduling or imports is blocked there too.

  • 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 in backbase-tests with the project in backbase-tests/sitebuilder-presets-frame wasn’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, the colors-theme-light token’s user value when it has one, else its default, through every var(…). Where they differ it plans the user value: set, change (it replaces an override) or reset (an empty value, when the default is the frame’s colour). The value is var(--color-<primitive>) when the frame’s colour is exactly a colors entry’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 a custom-colors entry 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 its user dimension). It shows each token, what it is today and what it becomes, and the custom colours to add. The fingerprint covers it, with the user value each write replaces. Each user value 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 the bb space sync command 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: the custom-colors entries (additive), then each user value with the user dimension’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 whose user value changed since the plan. Only #RRGGBB, rgba(…), var(--…) or a reset’s empty value are ever sent. theme-write-failed names each write that failed, and .backbase/space-sync.json records them, with each token’s previous value. 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 now exact, with via: "theme" and the override in theme: { token, value }, instead of approximate. A translucent paint, or a primitive the frame binds directly, stays approximate. So D-2’s designFit in plan.json, bb plan show and bb plan validate count it as fitting: on the ILS plan, the hero has 1 approximation instead of 3.
    • The sync skill passes --sections when 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.
  • 6a265e6: /backbase:refine is a measured audit now (MAR-4816). It measures every section with bb page measure, runs bb story spacing plan for each section first (its envelope is a try), writes one ops envelope for each section’s largest deltas, applies it with bb 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 with bb 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. fit is 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. --threshold is 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’s beyondStep list uses too. A section is done only when its height is within a step too; with every delta inside a step and the height not, it’s heightUnexplained. 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.judgement says what’s still a token step off. Each delta has an id, <blok uid>/<what>, the same in every measure, and each section a total, its px off together. --iteration latest now records the iteration’s number (it was null behind compare’s latest symlink) and keeps the measure in iterations/<n>/ too.

  • 266307a: The skills write ops (MAR-4820): /backbase:compose, /backbase:edit and /backbase:refine change a story only through bb story ops apply, and every change is one journal entry that bb story ops undo takes back. A new skill, backbase-ops, teaches them how: read the targets with bb story targets, write one envelope (each op, with an example that a test applies to ILS run 3’s story), repair a repairable answer in at most 4 rounds, accept unchanged, 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 after bb story scaffold, alongside bb story fill and before bb 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 with bb story ops undo --entry last, and the budget counts envelopes. Refine keeps, keeps part of, or undoes a try by what bb page measure says 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 set and revert are thin wrappers now. set checks its assignments as before, with the same errors, then applies them as one envelope (updateDesign, or setProp for an option field) through ops apply’s own code, writing exactly what it wrote before; a call that changes nothing is unchanged, and nothing is written. revert is bb story ops undo for 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 compare reads 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 log lists them from the journal, and bb page compare matches its entries to sections by the uids each one touched. bb story ops undo takes --note. bb story ops apply and design set read .backbase/datasources.json only 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 a duplicateNode copy 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 map bb page measure compares through) it reads what each value renders, in px, from the render table, and chooses the field, the blok and the token: a container’s gap or item_spacing when the table says it spaces the children (item_spacing pads each row above and below, so 16px between rows is xs), else each child’s mb; padding on the blok that renders it, a card body’s default 24 included; item_flex Fill or Hug in a row, and direction: row where the frame is a row; an item_basis fraction 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 as ties. It writes an ops envelope for bb story ops apply, one updateDesign per 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 measure now lists, as beyondStep, 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:compose runs 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-semantics reads 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; --story writes the probes as one story for bb 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 catalog shows each blok’s row as renderSemantics, bb story design get says 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 in bb catalog, the pack and bb story design get. /backbase:compose and /backbase:refine get “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 new backbase-refine skill renders the local story with bb story serve (after the person’s yes to .env.local), compares every section with bb 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 with bb story design get, makes at most 3 changes with bb 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 with bb story design revert and 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.local back and stops what it started. It shows the compare report and bb 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 logged bb story design set call 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 with revertOf. To make this possible, set’s log entries now record unset for a value that had none, lifted for a field the lift changed, and subfields, each design subfield touched, whole, before and after. Because set writes the lean shape, a revert puts the subfield back whole: a null or "" over a default stays, and what wasn’t there goes. set returns its entry’s index, and bb story design log numbers 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 eval runs 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.json recording the space, the story, its updated_at and 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 in body, by its sb-section name or _uid, plus :page for the page’s own fields, and :order for 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 --plan now guards an update: when the story’s updated_at moved since the pull or push (or its content’s hash did, for a record with no updated_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 in data.outside the ones completing would have changed.

    The plugin adds /backbase:edit and its backbase-edit skill: 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:compose points 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>.json on every reload, read fresh each time, with nothing pushed. Every blok gets an _editable, so the page has data-blok-uid on each section for bb page compare; the story id is the file’s own, else the one bb story push recorded, else a stable one bb makes up. The stories it doesn’t have, such as settings, are fetched once as drafts from the real Delivery API with the project’s public delivery token and cached in .backbase/cache/serve/ (--offline serves only the cache). Lists answer empty, GraphQL answers no paths, anything but GET is refused, and a request for any other Host than 127.0.0.1:<port> or localhost:<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-env prints the lines that point the dev server at it, --write-env writes them to .env.local (never .env), keeping the old values in a commented block, and --restore-env puts them back where they were; all three warn when the shell or .env.development.local would override them.

    /backbase:compose offers the comparison through bb 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 since bb story validate --stamp, a stamp for another space, a folder, a path another story has, and a story bb didn’t push unless --story names 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 push without --approved, with a fingerprint that isn’t the saved plan’s, or with --approved passed more than once (and bb space sync too), and asks the person before the approved push. /backbase:compose checks 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-compose reads the space’s colour entries with bb space datasources before 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’s anchor_links as the sections it links to, by name: bb story materialize stores each as its picker does (MAR-4741). backbase-analyze ignores .backbase/datasources.json.
  • 1f5ffc5: backbase-compose’s Design step writes colour subfields (text_color, bg_color, background_color and the like) as a colors entry’s name, which bb story materialize stores 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-analyze compiles the knowledge pack, .backbase/knowledge.md, after the catalog, with the frame’s sections.json for its colour map, and backbase-classify reads it to choose bloks, presets and design values. backbase-compose has a Design step after filling the text. For each section it reads the screenshot and the section’s design from sections.json: design.layout for directions, gaps, padding, radii and side-by-side columns; design.tokens for the spacing, radius, colour and text-style variables behind them; design.instances for each component’s variant properties; design.icons for 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 the colors name it maps to.

0.8.0

Minor Changes

  • c259a3d: backbase-compose materializes the story before it validates it: scaffold, fill, bb story materialize, bb story validate --stamp, ask, write. The agent may write a blok’s design in 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 materialize turns 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:compose and the write guard (MAR-4697):

    • /backbase:compose loads the new backbase-compose skill. After Gate 1, Gate 2 (.backbase/space-sync.json) and bb doctor --write, it:
      1. runs bb story scaffold and fills the placeholders from the design;
      2. runs bb story validate until there are no errors, and checks the slug is free;
      3. 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.”);
      4. on a yes, stamps with bb story validate --stamp (with --allow-placeholders only when some are left);
      5. creates the story with createStory through Storyblok’s MCP, never with publish, and records it in .backbase/stories/written.json;
      6. reports the editor link, what’s composed and skipped, and the images to upload.
    • The write guard, the plugin’s second hook next to the sb-mig guard (hooks/write-guard.mjs, bundled from core’s write-guard.pure.ts like the sb-mig guard, and registered in hooks/hooks.json): a PreToolUse hook on execute_readonly, execute_mutating and execute_destructive of every MCP server whose name contains storyblok. 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_mutating runs only createStory, and updateStory of a story in written.json; every other write (such as releaseAll, 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), and space.id in .backbase/config.json when that’s set.
      • Any publish (an operation named for it, or publish set in the parameters, body, query or a query string, or on the story) is blocked, execute_destructive always 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 node on PATH), reads only the project’s .backbase/ files, and never calls the network.
  • b81e722: /backbase:sync and the backbase-sync-space skill: Gate 2 (MAR-4695). The agent runs bb doctor --write, then bb space plan, shows the person every change and every destructive one, asks them to approve it (each destructive change by name), and only then runs bb space sync --approved <fingerprint>. backbase-new and backbase-analyze point to it after Gate 1, and the analyse flow ignores the two Gate 2 files.

    The plugin’s first hook: a PreToolUse hook 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, and npm/pnpm/yarn script runs given --ssot, npm’s global options included. It follows && chains, bash -c (and fish -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. Before bb space sync, it answers Claude Code with permissionDecision: "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 from packages/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 that bb story validate checks (MAR-4691). No skill runs it yet: S-4 does, before each Storyblok MCP write.

0.5.0

Minor Changes

  • 9a3ac66: bb catalog shows 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.ts files, and the props that use them, with a small parser (no TypeScript compiler). Each export lists its components’ union props as backpackExports[].reactComponents. Each blok lists the variant values no variant or <x>_variant option offers as reactOnlyValues: [{ export, packageExport, prop, values, via }], and bb catalog --component sb-card prints React 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. export is the component (CardFooter), and packageExport the package export whose folder holds it (Card), the name a Promote’s primitive takes. via says 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.json at the project root ({ "<blok>": "<why it needs no map entry>" }) excuses bloks from missing-map-entry. The summary counts them as excused. An entry for a blok that doesn’t exist, or one already mapped, is a note:, not a problem. Without the file, nothing changes.
    • missing-map-entry now 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 reactOnlyValues before calling a variant missing from React, and word the map problems the same way.
  • 575d605: Polish after the first real /backbase:new run (MAR-4684).

    • Reading Figma: the read-only scripts turn U+2028, U+2029 and every other control character except \n and \t into 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.json compares 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 at medium, one that decides whether Backpack can do it at all makes it unsure, and content questions don’t count. reactOnlyValues is a lead, not proof: the agent reads the Storyblok wrapper’s source before calling a section a Promote, and a via: "folder" link counts for less. A confirmed one is a Promote of the entry’s packageExport, with the variant named in reason. A variant in neither variants nor reactOnlyValues is unsure, or a Create for the person to decide.
    • Gate 1: the analyse flow commits exactly sections.json and plan.json as Plan "<frame name>" at Gate 1, unsigned, with every git command run in the project folder (git -C) and the paths given to git commit so 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 from git config or the GIT_AUTHOR_* and GIT_COMMITTER_* variables as bb create does, and says so. missing-map-entry is information, mentioned only for bloks in the plan, and never as bloks that must be registered before they render.
    • bb create adds .backbase/catalog.json, .backbase/figma/*/shots/ and .backbase/figma/*/sections.draft.json to .gitignore, next to .backbase/*.log.
    • bb doctor: no bb on PATH is a pass for bb-binary, since the plugin runs bb through npx. A Babashka bb is still a warning.
  • 54366b1: Reuse the Storyblok space’s presets before composing (MAR-4427):

    • backbase-analyze has a step 2b. It runs when the catalog knows the project’s space (--space <id> on /backbase:analyze, spaceId in storyblok.config, or the space bb create --space recorded in .backbase/config.json, so /backbase:new --space carries it through) and Storyblok’s MCP is connected:
      • it runs the listManagementComponents operation through search, describe and execute_readonly, with fields asking only for component names and their all_presets;
      • it never calls execute_mutating, execute_destructive, upload_asset or upload_asset_finish;
      • it writes .backbase/space-presets.json with each preset’s name, id, preview image and description, saves the images under .backbase/space-presets/, and runs bb catalog and bb plan validate with --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.json and .backbase/space-presets/.
    • backbase-classify looks 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 in design values or content, is a Reuse of that preset, with preset on its blok. One that differs in structure is a Compose that names the nearest preset in signals and asks whether to use it as it is. Preset names come only from the catalog.
    • signals.md puts presets first, and buckets.md has 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, runs bb doctor and 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 runs bb create and shows its steps, stopping with the step’s message and fix when one fails. Then it follows backbase-analyze in 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:analyze finds 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_context is now an optional fallback, never converted to code, and figma-use isn’t needed for the script.
    • The outline comes from a read-only use_figma script (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 key and its component set’s key, 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-classify asks 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.json stores it on the nested entry.
    • 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.json while reading, and sections.json is 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 with bb plan show --questions.
    • backbase-classify and buckets.md open with a table of what each bucket adds to the codebase: Reuse and Compose add no code, and Create (a new *SB wrapper and its .sb.ts schema) is the only bucket that does, so a section needing even one is a Create.
    • backbase-classify and buckets.md have the Reuse-or-Compose rule, and Promote covers a variant an existing blok doesn’t expose, told apart by the catalog’s variants.
    • The skills run bb as npx -y @ef-global/backbase@<the plugin's version>, and scripts/sync-plugin-version.mjs moves 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 the backbase-analyze, backbase-read-figma and backbase-classify skills, 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> and bb plan show <plan>. The plan checks cover the sections file and, through CatalogView, the catalog: bloks, children and their counts, Create primitives (not *SB wrappers) and names. The JSON Schemas are in plugins/backbase/schemas/, for skills to point agents at.

0.1.0

Minor Changes

  • 1d9131b: First release: bb --version and bb --help, installable from GitHub Packages.