Skip to content

feat(skills): add bookable-resources skill - #9

Open
papelipe wants to merge 2 commits into
mainfrom
feat/bookable-resources-skill
Open

papelipe wants to merge 2 commits into
mainfrom
feat/bookable-resources-skill

Conversation

@papelipe

Copy link
Copy Markdown
Member
Q A
Branch? main
Bug fix? no
New feature? yes
BC breaks? no
Fixed tickets #...

Bookable resources are in no skill at all. An agent asked to build a rental store today has to discover policies, pools, holds and the confirmation dance from the schema — and three of the failure modes are silent. Two storefront builds went through that: Tools Universe (24 rental machines across five depots, sold by day/weekend/week/4 weeks) and the rental work in Car Parts Universe.

Adds bookable-resources: SKILL.md plus references/policies-and-pools.md (Core setup), references/booking-flow.md (Shop API) and references/modelling.md.

The one thing you cannot get from the schema

confirmCartBooking writes orderId onto the reservations only if the cart already has one — and the cart is linked in the background a few hundred milliseconds after createFromCart returns. The obvious order (bookSkuItem → place → confirmCartBooking → createFromCart) therefore leaves every reservation at { state: CONFIRMED, orderId: null }, and the admin shows "No order — not checked out" for a booking that was paid for.

The skill documents the flow Tools Universe ships: keep the first confirm (it proves the holds still stand before the shopper is charged), then poll cart(id) { orderId } and confirm again. If that timing is considered a bug rather than the contract, this is the page to change once it is fixed.

Verified against the live APIs

On the tools-universe tenant, 2026-09-24 — including two test reservations taken and released:

Claim What the API answered
Policy durations are seconds heavy-equipment: advanceWindow 10368000 → humanized 120 days; cancellationWindow 172800 → 2 days; pendingHoldDuration 900 → 15 minutes
bookingPolicies is a connection, deletes are blocked while in use totalCount: 3, stats.referencingProductCount 15 / 4 / 5
A pool is units or capacity, and unit meta carries identity BookableUnitPool, poolSize: 5, units with depot, serial, hours, lastService — served back by Discovery
checkBooking needs language, returns { ok, reason } Without it: Field "language" of required type "String!" was not provided. With it: { ok: true }
bookSkuItem refusals are results, not errors Union of Cart, ReservationConflict, NotBookable, InvalidRange, InvalidUnitId
The reservation id is on the line's meta meta: { reservationIds: "18a6b9d4-…", "booking.window": "2026-10-01T08:00:00.000Z/2026-10-01T16:00:00.000Z" } — and the line's unitId is not returned, so the caller must store it
cancelReservation applies the cancellation window to a hold that was never bought Booking 7 days out → cancelled fine. Booking tomorrow, two-day window → CancellationWindowClosed, i.e. the shopper cannot remove their own basket line. Re-hydrating without the line releases it (reservation → null)
confirmCartBooking result Cart | NotPlaced | NothingToConfirm | ReservationNoLongerHeld
hydrate takes the cart id inside CartInput, cancelReservation takes reservationId Corrected in the examples

Both test carts were emptied afterwards; the holds are gone.

Also in the skill

  • A capability probe: a tenant that is not served for bookings answers ExperimentalFeaturesNotAvailableError from bookingPolicies, so match the type rather than a message.
  • Booking is on the product, price on the variant — so rental periods are variants. NotBookable on a product that looks configured usually means it is not published, or the query's language is wrong.
  • A policy edit does not reach products until reapplyBookablePolicy; products carry a policySnapshot with its version.
  • hydrate is the whole cart: a booking line you leave out is cancelled, which is the supported way to remove one from the basket, and re-hydrating slides the hold's expiry.
  • Discovery serves the static pool, never availability.
  • No fulfill after createFromCart, consistent with fix(skills): correct five places where the skills contradict the API #8.

Version

3.7.0 in plugin.json, marketplace.json and mcp-servers/crystallize/package.json. The last one was left on 3.5.0 by #3 and is brought back into lockstep.

oxfmt --check passes on use-crystallize/skills and README.md.

🤖 Generated with Claude Code

papelipe and others added 2 commits September 24, 2026 12:22
Bookable resources — products held for a window of time instead of counted
in stock — had no coverage in any skill. Two storefront builds (Tools
Universe rentals, and the rental work in Car Parts Universe) had to
discover the whole surface from the schema, and three of its failure modes
are silent.

New skill `bookable-resources`, with references for the Core setup, the
Shop API flow and modelling.

Verified on 2026-09-24 against the live Core API and Shop API on the
tools-universe tenant, including two test reservations taken and released.
Confirmed directly:

  - Policy durations are seconds. `heavy-equipment` reads back as
    advanceWindow 10368000 = 120 days, cancellationWindow 172800 = 2 days,
    pendingHoldDuration 900 = 15 minutes. `humanized` gives the same
    values as { value, unit }, which is how you catch a wrong unit.
  - `bookingPolicies` is a connection, and `stats.referencingProductCount`
    is what blocks a delete.
  - A pool is units or capacity, never both; unit `meta` carries depot,
    serial and service data, and Discovery serves it back on the item.
  - `checkBooking` requires `language` and returns { ok, reason }, not a
    union. `bookSkuItem` returns Cart | ReservationConflict | NotBookable |
    InvalidRange | InvalidUnitId — refusals are results, not errors.
  - The reservation id is on the cart LINE's meta (`reservationIds`),
    alongside a `booking.window` the API writes itself. The line's unitId
    is not returned, so the caller has to store it.
  - `cancelReservation(cartId, reservationId)` applies the cancellation
    window to a hold that was never bought: a booking for tomorrow under a
    two-day window answers CancellationWindowClosed, so a shopper cannot
    empty their own basket. Re-hydrating without the line releases it, and
    the reservation then reads null.
  - `confirmCartBooking` answers Cart | NotPlaced | NothingToConfirm |
    ReservationNoLongerHeld.

The one thing a storefront cannot get right by reading the schema is the
double confirmation: `confirmCartBooking` writes `orderId` onto the
reservations only if the cart already has one, and the cart is linked in
the background a few hundred ms after `createFromCart` returns. A single
confirm leaves { state: CONFIRMED, orderId: null } and the admin shows
"No order — not checked out" for a booking that was paid for. The skill
documents polling the cart and confirming again, as Tools Universe does.

Version to 3.7.0 in the three manifests. mcp-servers/crystallize was left
on 3.5.0 by #3 and is brought back into lockstep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- No feature flag gates bookings; RBAC does. Replace the
  ExperimentalFeaturesNotAvailableError probe with the FORBIDDEN case.
- setBookable, reapplyBookablePolicy and clearBookable write the draft;
  each needs a publish before the Shop sees it.
- BookablePoolKindChangeError only fires capacity -> units while a
  capacity pool is published; document clear, publish, drain, set.
- bookableProducts lists published configurations only; the pool is
  shared by every language of the product.
- The first confirmCartBooking commits the slot (CONFIRMED never
  expires); mark it optional, place payment in the sequence, and show
  the /booking/admin cancel for a failed payment.
- Expired holds become EXPIRED and are re-taken on hydrate; placed
  carts refuse hydrate, cancel and rebook.
- Add the hydrate shape for a booking line (lineId, window, unit), the
  exact cancellation rule, why orderId must be polled, and that
  bookSkuItem ignores group and type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants