Repository navigation
v2.1.0 — catalog management in the cockpit
Catalog management reaches the cockpit. Offering CRUD was previously CLI-only (bss admin catalog *) and browser-only (the v1.6.1 CSR Catalog screen) — the conversational surface didn't have it. It does now, under propose-then-/confirm.
Highlights
- Four verbs on the operator LLM surface.
catalog.add_offering,catalog.add_price,catalog.window_offering, and a newcatalog.retire_offering. The first three were registered and in theoperator_cockpitprofile since v0.7 but sat inLLM_HIDDEN_TOOLS, so the model never saw them; retire had no tool at all. /confirmreplaces invisibility as the gate. A strictly stronger control: the operator reads the staged args before the write lands, where hiding just meant the capability didn't exist. No catalog write has a one-call undo —add_pricecan only be superseded, never removed, andlist_active_offeringsfeeds order creation, so a bad row is sellable the instant it lands.- Multi-turn elicitation — the agent asks, it doesn't assume. Ask for "a new 5GB plan" and it comes back for the plan id, price, and allowances rather than inventing them.
- Never customer-facing. Not in
customer_self_serve; asserted in both directions.
Elicitation, and why it's enforced twice
The orchestrator emits a permissive function schema (additionalProperties: true), so the description prose is the model's only arg contract. Both layers matter:
- The four tool descriptions are explicit "ask, never assume" contracts — never derive a plan id from the name, never price by analogy with an existing plan, never assume allowances typical for a tier.
tools::require_fieldsgates server-side, naming every absent field at once so the model asks one question instead of probing.add_offeringadditionally rejects a plan that grants no allowance (pass0for a deliberate omission);window_offeringrejects both bounds absent, treating an explicitnullas a deliberate clear.
An elicitation bounce is reported with is_error = false (ToolError::is_elicitation) so clarifying questions don't burn the 3-strike failure-bail budget. The first implementation assumed "absent from FAILURE_MARKERS" was sufficient — but is_failure_result is is_error || markers and every Err sets is_error, so three clarifying questions in one turn killed the turn with a "couldn't recover" panel. Caught by the new loop test, not by review.
Documentation fixes
- HANDBOOK §8.1 gained the chat path alongside the CLI and browser routes.
- HANDBOOK §8.1a was actively wrong and is removed. It claimed
bss admin catalog add-offeringhad no--data-roaming-mbflag (v0.20 added it) and told operators to run rawINSERT INTO catalog.bundle_allowance— bypassing the policy layer, a doctrine violation — with a "recommended" fallback pointing at a Python package deleted at v2.0.0. - Knowledge index reindexed. It had been stale since 2026-07-19, predating the v2.0.0 release, so
knowledge.searchwas serving pre-2.0 content. Also note: retrieval is Tier-0 Postgres FTS withplainto_tsquery, which ANDs every term — a question containing one out-of-corpus word returns zero hits, not a near miss. Retire/decommission/withdraw/remove synonyms were added to the docs for that reason. The provisioned pgvector column remains unpopulated (the Tier-1 embedder pass is unbuilt).
Compatibility
SemVer minor, backward compatible. No wire contract, event, payload, or DB schema change. BSS_RELEASE 2.0.0 → 2.1.0.
Gate
cargo fmt --all --check · clippy -D warnings (0) · cargo test workspace 540 pass / 0 fail · 14 doctrine guards.
The hero scenario suite was not run for this cut — it requires mock payment/KYC providers and the deploy carries a live BSS_PAYMENT_PROVIDER=stripe. The change is confined to the LLM tool surface, which the hero flows don't exercise; it was verified live against the running stack instead (tool visible, elicitation asks rather than assumes, destructive gate held with nothing written, retire endpoint reachable, knowledge.search retrieves and cites the new content).
See DECISIONS 2026-07-22.