Add themes/dynamic-bundles skill and workflow - #141
Conversation
Universal Dynamic Bundles skill: classify a company's bundle shapes, write
records through Surface A, generate one reusable theme implementation, and
prove the money path through the checkout API.
Verified live against Chipotle (company 980243433) on 2026-08-06: six
variant->bundle conversions, four fixed catering kits, one real add-to-cart
failure reproduced and root-caused.
The three theme-targeted workflow steps use the repo's kind-target shape
({"type":"kind","kind":"theme","fallbackToManager":true}) rather than
{"type":"theme"}, which appears nowhere in the catalog.
ShadReyes
left a comment
There was a problem hiding this comment.
The catalog additions are structurally valid, and the workflow parses against the current Mist WorkflowDefinitionSchema, but this is not wired for a reliable end-to-end Mist run yet.
-
Workflow steps cannot access the shipped playbooks, schema, or templates. The manifest declares these only as references/assets of
themes/dynamic-bundles, while every workflow step is an isolated inline-prompt step. Mist materializes assets only whenrun_skillexecutes, into that chat's active project. Workflow step chats receive their inline prompt, caller context, and dependency outputs—not the initiating skill's materialized assets. As a result, the theme step instructions to copytemplates/...point at files that do not exist in the theme worker sandbox; the same applies to workflow references toplaybooks/...andschemas/.... -
The advertised human approval gate cannot happen at the stated point.
context.approvedis fixed whenrun_workflowstarts. The plan is produced inside the background workflow and the write step dispatches automatically afterward, soapproved: trueapproves before the plan exists. If approval is absent, the write step reports a no-op but its dependents still run. This needs either a planning/apply split, a launcher that produces and presents the plan before starting the mutating workflow, or an actual pause/resume mechanism. -
The workflow is not pinned to the active theme. The preflight discovers the active theme only as step output. Mist resolves a theme step from initial
context.theme_id; without it, it selects the first local theme checkout or scaffolds one. Therefore these steps can edit an unrelated theme. Also,fallbackToManager: truedoes not provide a manager fallback for theme targets—Mist deliberately keeps theme work in a real theme project. -
Safety gates default to continuing after failed QA. All steps omit
qa, so Mist defaults toenabled: true,strictness: "standard", andonFail: "continue". A failed preflight, write verification, or cart/money verification becomesneeds-review, satisfies dependencies, and allows later mutation steps to continue. Hard gates should explicitly useqa.onFail: "stop". -
The optional source-analysis step is not conditionally skipped. Printing a
STEP_OUTPUT: ... skippedline does not mark a Mist workflow step as condition-skipped; onlyrunIfdoes. Without a source URL, the step still runs and its unconditional acceptance criteria are likely to fail, leaving normal no-source runs ascompleted-with-issues. Pass a boolean source flag in launcher context and gate this step withrunIf.
The manifest paths are safe, the reference corpus is within the Mist inline budget, repository validation passes, and the exact workflow JSON loads successfully. The blockers are specifically the runtime handoff, asset/reference materialization, approval lifecycle, and target-selection semantics above.
…ution
Skill: adds playbooks 09-pricing-patterns and 10-ui-and-price-surfaces from a
second live company conversion, and rewrites SKILL.md §6 as a launcher contract
(theme_id, approved, source_url) rather than a step list.
Workflow rev 2: 10 steps, explicit qa on every step, variant-completeness
preflight, rebalanced-base pricing arithmetic with a base >= 0 assertion, a
price-surface inventory step, fresh-GET verification, and a negative-case cart
test. Restores plan-translation's dependency on study-company, and drops
target: {"type":"theme"} -- a shape the documented schema does not define, which
would have passed CI and misrouted at runtime.
Workflow rev 3 addresses the review's runtime-handoff findings:
- Every step opens with run_skill("themes/dynamic-bundles"). A step chat does
not inherit the launching skill's materialized files, so playbooks/,
templates/ and schemas/ were dead references. SKILL.md §5a remains the
fallback and now says so.
- write-bundles gates on human_in_the_loop with the plan's real numbers.
context.approved is fixed before run_workflow starts and cannot be consent to
a plan that does not exist yet; it now only means "this run may write".
- Steps 5-9 carry runIf {flag: approved}, so an unapproved run condition-skips
the mutating tail instead of no-opping through it while dependents continue.
Step 10 stays ungated so such a run still reports.
- preflight reconciles context.theme_id against the active theme and stops on a
mismatch; all four theme steps verify the checkout's own theme id before
writing. Mist resolves theme steps from initial context, not step output.
- source-analysis carries runIf {flag: source_url}; a STEP_OUTPUT line is prose,
not a skip. Its acceptance passes the no-source path explicitly, since older
builds drop unknown keys and run the step.
- qa.onFail: stop now also covers preflight and plan-translation.
Also fixes apply-manifest.py writing the manifest with ensure_ascii=False,
which re-encoded every existing em-dash and buried the new entries under ~30
lines of unrelated diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a universal Dynamic Bundles skill and its execution workflow.
The skill takes a company's existing bundle implementation — or a source configurator page —
and recreates it as Fluid Dynamic Bundles, with one theme implementation that renders every
current and future bundle rather than a template per bundle.
Everything asserted below was confirmed live against Chipotle (company
980243433) on2026-08-06: six variant→bundle conversions, four fixed catering kits built, one real
add-to-cart failure reproduced and root-caused. Where a claim is inference rather than a call I
made, it says so.
What's here
themes/dynamic-bundles/SKILL.mdthemes/dynamic-bundles/playbooks/01-07themes/dynamic-bundles/playbooks/08-api-write-recipe.mdthemes/dynamic-bundles/playbooks/09-pricing-patterns.mdthemes/dynamic-bundles/playbooks/10-ui-and-price-surfaces.mdthemes/dynamic-bundles/templates/bundle_buildersection, its four-layer JS engine, token-only CSS, host templatethemes/dynamic-bundles/schemas/themes/dynamic-bundles/PLAN.mdworkflows/dynamic-bundles.workflow.jsonThe findings that shaped it
Classify the shape before writing anything. The single most expensive mistake available is
applying one pattern to products that merely look uniform. Chipotle had seven near-identical
"menu item" products that split three ways across three different option ids — four component
axes (→ bundles) and two flattened categories (→ collections). Converting a category into one
bundle destroys a browsable listing and advertises one wrong price. The test is are these
option values components of one sellable item, or separate sellable items?, and it must be run
per product with the option id cited. Workflow step 2 fails if a run answers in aggregate.
Create is one call; convert is two.
bundle: truepersists onPOST create_bundle_productalongside groups — but onPATCH update_bundle_productit doesnot: the groups are created while
is_bundlestaysfalse,bundle_configstays{}, and noBundlerow syncs. A second groups-free flag call fixes it. Between the two calls an unroutedproduct renders
$0.00with no picker, which is why routing happens per product rather thanbatched at the end.
Three pricing behaviours that silently move the shelf price.
includedgroups add the sum of all their components' resolved prices to the floor.fixed_price(max_only) groups add nothing.dynamic_pricegroups add their cheapest item even atmin_selections: nil— anoptional paid add-on group moved a burrito's advertised floor from $9.35 to $12.30, so every
collection tile misquoted a product buyable at $9.35.
fixed_price: "0.00"is ignored. A "free" group falls back to eachcomponent's own variant price; one $2.75 component pushed a bundle from
$53–$64to$55.75–$66.75.Valid config is not evidence the storefront can sell it. A six-group bundle read back
perfectly from
/api/v2025-06/bundlesand still failed for every shopper:That is an
includedgroup being sent by the client. The cart attaches included componentsitself — the successful response listed Cheese, Romaine and Tortillas under that group although
they were never in the request. Two shapes verified 200 with correct totals: a configurable
bundle sending
customizablegroups only ($53.00), and a pure fixed kit sending nobundled_itemsat all ($52.00).The corollary matters as much as the fix: an all-
includedproduct has nothing to select, soany UI gating add-to-cart on "all required groups chosen" leaves every fixed kit permanently
unclickable — while the API side looks perfectly healthy. The workflow's
cart-and-moneysteptherefore requires a successful cart call for one configurable bundle and one pure fixed
kit; testing one shape is a fail.
Corrected while writing this up: an earlier draft of the skill claimed
price_range/bundle_price_rangeare computed withcountry_code: niland read$0.00.They aren't — across nine bundles they came back correct and country-resolved, and a wrong
one (
$55.75–$66.75) is what exposed the pricing defect above. Only top-levelprice/display_priceare unusable. That distinction is now explicit, because an agentfollowing the old text would discard the best verification signal available.
Second conversion (2026-08-07/08) — what it changed
A second company was converted end to end (~13 live carts) and forced four corrections, two of
them to claims made in the first draft of this PR.
Upgrade pricing was the biggest gap. The skill documented fixed vs dynamic group pricing as
a data field but never said how to make a dearer choice cost more. Flipping choice groups to
dynamic_pricealone double-charges — each item contributes its full variant price on topof an anchor that already holds the headline ($11.19 would have charged $16.27). The working
pattern is a rebalanced base:
anchor.fixed_price = headline − Σ(defaults), every choicegroup dynamic,
is_defaultinsideconfig. Verified across carts at $11.19 default / $11.79one tier up / $9.39 one down. Value bundles can compute a negative base, which is normal —
honour the source's declared default, and escalate if it is still negative rather than clamping.
New playbook
09-pricing-patterns.md.price_range— I was wrong twice, in opposite directions. The first draft called a$0.00range a platform read defect. The second conversion showed it is not: the field sums each item's
config.priceand correctly reports zero when every item is zero. But my own correction thenover-claimed the other way — I reinstated it as "the primary write-verification signal" on the
strength of nine bundles that were, I now find, read from PATCH echoes. A fresh
GETtodayon Chipotle 89901, stored config byte-identical and
updated_atunchanged since the write,returns
price_range: 0.0where the echo said$52.00. The cart charges $52.00 correctly —that part was verified with a real cart POST. So: the range is trustworthy for dynamic
groups, reads
$0.00for all-fixed bundles, and a PATCH echo is never evidence. Thatlast rule is now in
SKILL.md§5a and enforced by the workflow's write-step QA.Sequencing and membership. Groups built before their component products had full variant
sets pointed at the wrong variant, and one group had 43 items where the source offered 117 —
two rebuild passes. Variant completeness is now a blocking G0 preflight check. Separately,
group membership must be read from the source per bundle, never derived from the component
product's variant list: different bundles offer different subsets, and deriving silently adds
options the merchant does not sell.
Portability.
playbooks/,templates/andreference/were unreachable (ENOENT) fromevery workflow step sandbox, so the write contract had to be reverse-engineered from existing
bundles. The full contract table is now inlined in
SKILL.md§5a, and §10 tells the reader toexpect this rather than stall. Related: the skill arrived in the target company but the
workflow did not — the two catalogs sync separately — so §6 now says the workflow may be
absent and how to recover.
Also folded in: one card per parent product with a variant sub-picker (a group holding three
variants of one product was rendering three identical cards); variant images rather than the
parent's; the anchor item sized from the same token as the option grid (it was rendering 36×36
beside 144px options); a price-surface inventory step, because the reference page had four
composed sections with only one wired to the engine and no price at all in the PDP header; and
country_codebeing required on cart creation.Review round 1 — the runtime-handoff findings, addressed
The review was right that the workflow parsed but was not wired for a reliable end-to-end run.
Five findings, all now fixed in the workflow (rev 3) and reflected in
SKILL.md§6. Onecorrection: the "all steps omit
qa" finding described the 9-step rev 1 that was on thebranch — the rev 2 rewrite this update lands already carried an explicit
qablock on everystep. The other four were live and are fixed with mechanisms the repo already documents.
1. Steps could not reach the shipped playbooks, templates or schema. Correct, and it had
already bitten us in the field — it is why the write contract is inlined in
SKILL.md§5a. Thefix is the idiom
streamlined-onboard-launchandspeed-importalready use: every step's promptnow opens by calling
run_skill("themes/dynamic-bundles"), which materializes the skill body andits references into that step's chat. §5a stays as the safety net if the call fails, and now says
so rather than describing ENOENT as the expected state.
2. The approval gate approved a plan that did not exist yet. Correct —
context.approvedisfixed when
run_workflowis called, several steps beforeplan-translationproduces anything.It now means only this run may write.
write-bundlescallshuman_in_the_loopwith theplan's real numbers — per bundle: headline, base arithmetic, before → intended-after span — and
gets approval for that before its first write. If
human_in_the_loopis unavailable in thebuild, that is explicitly not approval: the step writes nothing and says why. Separately, the
"no-op but dependents still run" hole is closed by finding 5's mechanism: steps 5–9 carry
runIf: {flag: "approved"}, so an unapproved run condition-skips the whole mutating tail. Step10 is deliberately left ungated, so such a run still completes and reports what it would have
written.
3. The workflow was not pinned to the active theme. Correct and the most dangerous of the
five, since the failure is silent and lands in the wrong company's theme.
preflightnowreconciles
context.theme_idagainstGET /api/application_themes/activeand stops the runif it is missing or disagrees. All four theme-targeted steps additionally verify the checkout's
own theme id against preflight's before touching a file, and each has an acceptance criterion
that fails an unverified assumption.
SKILL.md§6 now documentstheme_idas required launchercontext.
On
fallbackToManager: the review says Mist deliberately keeps theme work in a real themeproject and gives no manager fallback, but
README.mddocuments it as degrading "to the managerproject instead of failing when none exists." Those disagree. I have kept the flag, because it is
the shape all 13 theme-targeted steps in this repo use and the theme-identity guard now makes the
wrong-project case loud either way — but if the README is stale, it should be fixed, since it
is what the next author will build against.
4. QA gates defaulted to continuing. Applies to rev 1 (0 of 9 steps had a
qablock); rev 2gave all 10 an explicit one. Acting on the underlying point, the hard gate now covers all three
steps where continuing on unverified work does damage rather than leaving a mess:
preflight(the blocking variant-completeness check),
plan-translation(the writes execute this plan), andwrite-bundles. Worth flagging for maintainers:README.mdnotes older desktop builds silentlydowngrade
qa.onFail: "stop"to"continue", which is exactly why the real protection here ishuman_in_the_looprather than the gate alone.5.
source-analysiswas not actually condition-skipped. Correct — aSTEP_OUTPUT: skippedline is prose, not a skip. It now carries
runIf: {flag: "source_url"}, gating directly on thekey itself since a non-empty string is truthy, so no
deriveContextentry is needed. The promptkeeps its self-skip and the acceptance criterion now passes the no-source path explicitly,
because
README.mdwarns that older builds drop unknown keys and run the step anyway.Notes for review
Writes go through Surface A only.
/api/v2025-06/bundlesis read-cross-check: it skipsvalidation and writes
bundles.settingswithout mirroring toproducts.bundle_config, soexclusivity written there is honoured by the portal and silently ignored by the cart.
Everything before
write-bundlesis read-only. That step refuses to write unless the callerpassed
context.approved: true; the human confirmation belongs to the calling skill. It is alsothe workflow's only
onFail: "stop"gate — deliberately unlike the launch workflows, which neverstop a run. Building a theme on top of bad bundle records is not worth continuing through.
playbooks/rather thanreferences/.SKILL.mdand the playbooks cross-reference eachother by that path in ~20 places; the manifest's
referencesarray points at the real paths,which is what
validate_catalog.pychecks. Happy to rename if you'd prefer strict consistencywith the other skills.
CI validates this workflow only as JSON.
validate_catalog.pychecks new workflow entriesfor slug/path/
updated_at, that the file exists, and that it parses with no duplicate keys. Thedeep step-shape assertions are hardcoded to
streamlined-onboard-launch.workflow.json, sonothing machine-checks these ten steps. Rather than match one file by eye, I surveyed the field
values every workflow in this repo actually uses and matched them:
targetis{"type": "manager"}or{"type": "kind", "kind": "theme", "fallbackToManager": true}— theonly two shapes in the repo, and the four theme steps use the latter.
qa.strictnessandqa.onFailare likewise drawn from the values already in use. An earlier draft of this workflowused
target: {"type": "theme"}, which appears nowhere in the repo; that would have passed CIand misrouted at runtime, and it is fixed here.
This skill uses
/api/company/v1/products/...deliberately, and CI does not flag it.validate_shared_skill_contracts()bans that fragment, but only inside its hardcodedSHARED_CONTRACT_FILESlist, which this skill is not in. The ban is right for the onboarding andclone skills — they should use
/api/v202604/company/products. It cannot apply here:create_bundle_product/update_bundle_productexist only on the v1 surface, and it is theonly surface that writes
products.bundle_config, where the cart reads exclusivity. Please donot add this skill to
SHARED_CONTRACT_FILES— it would fail immediately, and "fixing" theendpoint would point the skill at something that does not exist.