Skip to content

Commit 00fdf12

Browse files
Carlos D. Escobar-Valbuenaclaude
andcommitted
feat(bench): live mode — Databricks Gateway + OpenAI-compatible provider abstraction (0.11.0)
Closes the live-mode gap from BRO-1205. v0.10.0 shipped StubLiveRunner + StubLLMJudgeEvaluator placeholder stubs raising NotImplementedError; v0.11.0 ships the real thing — validated end-to-end against real Databricks Anthropic Claude (5/5 live tests green; 221 real tokens on a Haiku call; Haiku-agent + Sonnet-judge run reached quality cliff at rc=0). The contract bstack adopts is OpenAI Chat Completions API v1 — the de facto LLM standard in 2026 (Databricks, OpenAI, vLLM, Together, Fireworks, Anyscale, llama.cpp, Anthropic-via-Bedrock all serve identical JSON). Mirrors Stimulus's apps/api/src/utils/databricks_openai.py pattern. Substrate: - scripts/bench/providers/{base,databricks,registry}.py + __init__.py - Provider ABC + ChatMessage/Usage/ChatCompletion types (OpenAI-shaped) - DatabricksGatewayProvider: OpenAI SDK pointed at {HOST}/serving-endpoints - get_provider() factory with lazy module loading - MockProvider for offline tests - ProviderNotConfigured / ProviderNotInstalled / ProviderError taxonomy - estimate_cost_usd() with per-model pricing table - scripts/bench/agent_runner.py: LiveProviderRunner replaces StubLiveRunner. Delegates to a Provider, captures real token usage, estimates cost. - scripts/bench/evaluator.py: LLMJudgeEvaluator replaces StubLLMJudgeEvaluator. Structured JSON judge prompt, fallback for prose-wrapped output, 0.6 cliff, graceful handling of provider errors + parse failures. - scripts/bench/orchestrator.py: --provider, --model, --judge-model, --allow-same-judge-model flags. P20 model-isolation enforcement at CLI layer: rc=8 when judge=agent without explicit override + rationale. Rationale captured in config.json for audit. New exit codes 8/9/10. - bin/bstack-bench: new flag surface in --help; live invocation examples (direct + Railway credential broker pattern). - references/provider-standards.md: documents OpenAI-compatible contract, how to plug a new provider, P20 enforcement rules, Railway invocation. - specs/bench-skill-evolution.md: exit codes 8/9/10 added; live mode section with Railway-credential-broker invocation. Tests: - tests/bench-providers.test.sh (NEW, 10 assertions, offline): unknown-provider rc=2, missing --model rc=2, mock provider end-to-end (real chat() call), P20 violation rc=8, P20 override rationale captured, P20 distinct-models accepted, DATABRICKS_TOKEN missing rc=9, list_providers() shape, doc presence, public API symbol coverage. - tests/bench-live.test.sh (NEW, 5 assertions, gated BSTACK_BENCH_LIVE=1 + env): DatabricksGatewayProvider instantiates with real creds, chat() returns PONG with parseable usage, Phase 1 run produces non-canned token counts (221 real tokens on Haiku), LLMJudgeEvaluator end-to-end (Haiku agent / Sonnet judge), P20 enforcement holds in live mode. - tests/bench-mvp.test.sh: tests #13 + #15 updated for v0.11.0 semantics — live runner / llm-judge without --provider now fails fast at the CLI layer with rc=2 instead of reaching the stub. Cleaner. Live validation (commit-time): railway run --service stimulus-api -- BSTACK_BENCH_LIVE=1 bash tests/bench-live.test.sh → 5/5 PASS — real Databricks calls, ~$0.005 cost. Local validation: bash tests/bench-mvp.test.sh → 18/18 bash tests/bench-providers.test.sh → 10/10 bash scripts/doctor.sh --quiet → 81/82 (1 pre-existing gap, unrelated) Anti-patterns avoided: - openai SDK is a soft dep (ImportError → ProviderNotInstalled with hint) - No .env loading at runtime — caller's concern (Railway / direnv / shell) - P20 enforcement at CLI layer (rc=8) before any LLM cost is incurred - StubLiveRunner + StubLLMJudgeEvaluator kept as legacy fallbacks for backwards-compat with v0.10.0 callers; new code paths route to live. Ticket: BRO-1211 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a736a7b commit 00fdf12

16 files changed

Lines changed: 1848 additions & 61 deletions

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
# Changelog
22

3+
## 0.11.0 — 2026-05-20
4+
5+
### Live mode for `bstack bench` — Databricks Gateway provider + OpenAI-compatible abstraction (BRO-1211)
6+
7+
Closes the live-mode gap left open by v0.10.0 (BRO-1205). v0.10.0 shipped `StubLiveRunner` + `StubLLMJudgeEvaluator` that raised NotImplementedError. v0.11.0 ships the real thing: an industry-standard provider abstraction with **Databricks Model Serving Gateway** as the first concrete provider. Live mode validated end-to-end against real Databricks Anthropic Claude endpoints (5/5 live tests green; 221 real tokens on a `databricks-claude-haiku-4-5` call; Haiku-agent + Sonnet-judge run reached quality cliff at rc=0).
8+
9+
The contract bstack adopts is **OpenAI Chat Completions API v1** — the de facto LLM standard in 2026, served by Databricks, OpenAI, Anthropic-via-Bedrock, Together, Fireworks, Anyscale, vLLM, llama.cpp, etc. Future providers (anthropic, openai, openai-compat, bedrock) plug in by implementing `Provider.chat()`.
10+
11+
- **NEW** `scripts/bench/providers/` package (stdlib + optional `openai` SDK):
12+
- `base.py``Provider` ABC + OpenAI-compatible `ChatMessage` / `Usage` / `ChatCompletion` types + `ProviderError` / `ProviderNotConfigured` / `ProviderNotInstalled` taxonomy + `estimate_cost_usd()` with per-model pricing table.
13+
- `databricks.py``DatabricksGatewayProvider`: wraps the OpenAI SDK with `base_url = {DATABRICKS_HOST}/serving-endpoints` + `api_key = DATABRICKS_TOKEN`. Mirrors Stimulus's `apps/api/src/utils/databricks_openai.py` pattern. Known models hardcoded: `databricks-claude-{haiku-4-5, sonnet-4, opus-4-5}` + `databricks-meta-llama-4-maverick`.
14+
- `registry.py``get_provider(name, **kwargs)` factory with lazy module loading. Built-in providers: `databricks`, `mock`. Runtime extension via `register_provider()`.
15+
- `__init__.py` — public API exports.
16+
- **NEW** `references/provider-standards.md` — documents OpenAI-compatible contract bstack adopts, how to add a new provider, P20 model-isolation enforcement rules, and Railway credential-broker invocation pattern.
17+
- **NEW** `tests/bench-providers.test.sh` — 10 offline tests covering unknown-provider error, missing `--model`, mock provider end-to-end (real `chat()` call), P20 violation (rc=8), P20 override rationale captured in config, P20 distinct-models accepted, `DATABRICKS_TOKEN` absent (rc=9), `list_providers()` shape, provider-standards doc presence, public API symbol coverage. All green.
18+
- **NEW** `tests/bench-live.test.sh` — 5 live integration tests (gated by `BSTACK_BENCH_LIVE=1` + `DATABRICKS_HOST` + `DATABRICKS_TOKEN`). Validates: `DatabricksGatewayProvider` instantiates with real creds, minimal `chat()` returns PONG with parseable usage stats, Phase 1 run produces non-canned token counts (real Databricks usage), `LLMJudgeEvaluator` end-to-end (Haiku agent + Sonnet judge), P20 enforcement holds in live mode. **All 5 passed against real Databricks at ship time** — this PR ships proven-working live mode, not stubs.
19+
- **CHANGED** `scripts/bench/agent_runner.py``LiveProviderRunner` replaces `StubLiveRunner` (which is kept as legacy fallback). Delegates to a Provider, captures real token usage from `completion.usage`, writes deliverables, estimates cost from per-model pricing table.
20+
- **CHANGED** `scripts/bench/evaluator.py``LLMJudgeEvaluator` replaces `StubLLMJudgeEvaluator`. Builds structured judge prompt from rubric criteria, parses JSON verdict (with fallback for prose-wrapped output), computes weighted pass rate, applies 0.6 cliff. Handles judge-side provider errors + parse failures gracefully (no traceback).
21+
- **CHANGED** `scripts/bench/orchestrator.py` — adds `--provider`, `--model`, `--judge-model`, `--allow-same-judge-model RATIONALE` flags. Enforces P20 model isolation: judge model MUST differ from agent model unless explicit override with rationale (captured in `config.json` for audit). New exit codes 8 (P20 violation), 9 (provider not configured), 10 (SDK not installed).
22+
- **CHANGED** `bin/bstack-bench` — surfaces new flags in `--help`; documents new exit codes; adds live-mode invocation examples (direct + Railway credential broker pattern).
23+
- **CHANGED** `tests/bench-mvp.test.sh` — tests #13 + #15 updated for v0.11.0 semantics: live runner / llm-judge without `--provider` now fails *fast* at the CLI layer with rc=2 + "--provider required" instead of reaching the stub. Cleaner error, surfaces missing config before any task runs.
24+
25+
### Design choices
26+
27+
- **OpenAI Chat Completions API is the contract.** Picked because Databricks, OpenAI, vLLM, Together, Fireworks, Anyscale, llama.cpp, and Anthropic-via-Bedrock all serve identical request/response JSON. Choosing the same shape means new providers ship with zero translation layer.
28+
- **`openai` SDK is a soft dependency.** Imported lazily inside `DatabricksGatewayProvider.__init__`; raises `ProviderNotInstalled` with install hint when missing. CI doesn't install it (mock provider covers offline tests). Live runs need it.
29+
- **Railway as credential broker.** Recommended invocation: `railway run --service stimulus-api -- bstack bench run ...`. Credentials never written to disk in the bstack tree. Direct env export works identically.
30+
- **P20 enforcement at CLI layer.** Same model for agent + judge is the same-model-echo-chamber failure mode P20 exists for. Rejected with rc=8 unless `--allow-same-judge-model "rationale"` is passed; rationale is captured in `config.json` for audit.
31+
- **Mock provider is built in.** Deterministic in-process provider; tests + CI never need network or credentials. Same registry, same factory, same API as `databricks`.
32+
- **No `.env` file loading at runtime.** Bstack reads `os.environ`; how vars get there is the caller's concern (Railway, direnv, sops, 1Password, manual export — all work).
33+
- **Stimulus pattern mirrored.** `DatabricksGatewayProvider` directly mirrors `apps/api/src/utils/databricks_openai.py` — same base_url construction (`{HOST}/serving-endpoints`), same auth (token as `api_key`), same model name conventions.
34+
35+
### What this enables (next BRO-1205 followups, now unblocked)
36+
37+
- Per-skill telemetry counters can land — substrate now produces real token usage to populate them.
38+
- Crystallize (P16) FIX/DERIVED/RETIRE sub-modes can read from real bench runs, not synthetic numbers.
39+
- Cross-provider benchmarking — agent on Databricks Claude, judge on OpenAI GPT-4o (when `openai` provider lands).
40+
- Cost-per-quality measurement — bench reports now include real `cost_usd` from `estimate_cost_usd()`.
41+
42+
### Test counts
43+
44+
- `tests/bench-mvp.test.sh`: 18 → 18 (unchanged, two assertions retargeted for v0.11.0 semantics)
45+
- `tests/bench-providers.test.sh`: NEW, 10 assertions
46+
- `tests/bench-live.test.sh`: NEW, 5 assertions (gated, ran green at ship time)
47+
- Total: 33 offline + 5 live (gated) = 38 assertions
48+
49+
### Linked artifacts
50+
51+
- Linear: BRO-1211 (this PR); BRO-1205 (predecessor — MVP)
52+
- Spec: `specs/bench-skill-evolution.md` (updated)
53+
- Reference: `references/provider-standards.md` (NEW)
54+
- Stimulus mirror: `apps/api/src/utils/databricks_openai.py` (reference implementation)
55+
356
## 0.10.0 — 2026-05-20
457

558
### Skill-evolution benchmark substrate (BRO-1205)

SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Then, in your agent session:
3939
/bstack bench run → two-phase skill-evolution benchmark (P11)
4040
/bstack bench compare → Phase 1 vs Phase 2 REPORT.md
4141
/bstack bench tasks list → registered task sets
42+
/bstack bench run --runner live --provider databricks --model ...
43+
→ real LLM via OpenAI-compatible provider (≥ 0.11.0)
4244
```
4345

4446
## What bstack enforces

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.10.0
1+
0.11.0

bin/bstack-bench

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#
77
# Subcommands:
88
# run [--tasks SET] [--runner R] [--evaluator E] [--phase {1|2|both}]
9+
# [--provider P] [--model M] [--judge-model M] [--allow-same-judge-model RATIONALE]
910
# [--budget-usd N] [--resume RUN_ID] [--no-dry-run]
1011
# Two-phase bench against a task set.
1112
# compare [--run-id RUN_ID] Build REPORT.md from existing phase results.
@@ -25,6 +26,9 @@
2526
# 5 resume / status run-id not found
2627
# 6 all task runs failed (structurally broken — e.g. stub runner without SDK)
2728
# 7 compare requires both phase 1 + phase 2 results
29+
# 8 P20 violation: judge model equals agent model without --allow-same-judge-model
30+
# 9 provider not configured (missing DATABRICKS_TOKEN, etc.)
31+
# 10 provider SDK not installed (e.g. `pip install openai`)
2832

2933
set -euo pipefail
3034

@@ -72,6 +76,8 @@ bstack-bench — skill-evolution benchmark dispatcher
7276
7377
Usage:
7478
bstack bench run [--tasks SET] [--runner R] [--evaluator E]
79+
[--provider P] [--model M] [--judge-model M]
80+
[--allow-same-judge-model RATIONALE]
7581
[--phase {1|2|both}] [--budget-usd N]
7682
[--resume RUN_ID] [--no-dry-run]
7783
bstack bench compare [--run-id RUN_ID]
@@ -81,12 +87,22 @@ Usage:
8187
8288
Defaults:
8389
--tasks bstack-smoke --runner dry-run --evaluator rubric-match
84-
--phase both --dry-run (live mode is a stub in v0.10.0)
90+
--phase both --dry-run
91+
92+
Live mode (v0.11.0+):
93+
bstack bench run --runner live --evaluator llm-judge \\
94+
--provider databricks \\
95+
--model databricks-claude-haiku-4-5 \\
96+
--judge-model databricks-claude-opus-4-5
97+
# Or, with Railway as credential broker:
98+
railway run --service stimulus-api -- bstack bench run --runner live ...
8599
86100
State:
87101
~/.config/bstack/bench/runs/<run-id>/ (override via BSTACK_BENCH_HOME)
88102
89-
Spec: specs/bench-skill-evolution.md Ticket: BRO-1205
103+
Spec: specs/bench-skill-evolution.md
104+
Providers: references/provider-standards.md
105+
Tickets: BRO-1205 (MVP), BRO-1211 (live mode)
90106
EOF
91107
}
92108

references/provider-standards.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# LLM Provider Standards (bench)
2+
3+
> **Audience**: bstack maintainers + anyone wiring a new live LLM backend
4+
> into `bstack bench`. Agent-readable substrate (markdown, per P18).
5+
6+
## The contract bstack adopts
7+
8+
**OpenAI Chat Completions API v1** — the de facto LLM provider contract in 2026.
9+
10+
```
11+
POST {base_url}/chat/completions
12+
Authorization: Bearer <token>
13+
Content-Type: application/json
14+
15+
{
16+
"model": "<model-id>",
17+
"messages": [
18+
{"role": "system", "content": "..."},
19+
{"role": "user", "content": "..."}
20+
],
21+
"max_tokens": 4096,
22+
"temperature": 0.0
23+
}
24+
```
25+
26+
Response:
27+
28+
```
29+
{
30+
"id": "...",
31+
"model": "<resolved-model-id>",
32+
"choices": [
33+
{
34+
"index": 0,
35+
"message": { "role": "assistant", "content": "..." },
36+
"finish_reason": "stop"
37+
}
38+
],
39+
"usage": {
40+
"prompt_tokens": 19,
41+
"completion_tokens": 6,
42+
"total_tokens": 25
43+
}
44+
}
45+
```
46+
47+
## Why this contract
48+
49+
1. **Industry alignment** — the same JSON shape is served by:
50+
- **Databricks Model Serving** (Anthropic Claude, Meta Llama, etc. behind one gateway)
51+
- **OpenAI** itself
52+
- **Together**, **Fireworks**, **Anyscale**, **Groq**, **Perplexity**
53+
- **vLLM**, **llama.cpp**, **TGI** (self-hosted)
54+
- **Anthropic via AWS Bedrock** (with a thin adapter)
55+
- **Vertex AI** model garden (with a thin adapter)
56+
2. **Token semantics are uniform**`usage.{prompt,completion,total}_tokens` is universal; bench's per-task cost accounting reads consistently.
57+
3. **Anthropic's `messages.create` is a rotated version of the same shape** — providers that don't expose an OpenAI front-end (raw Anthropic SDK, Vertex) translate inside the provider class; bench code above stays clean.
58+
59+
## Provider abstraction (Python)
60+
61+
```python
62+
from bench.providers import Provider, ChatMessage, ChatCompletion, get_provider
63+
64+
provider = get_provider("databricks") # or "mock", future: "anthropic", "openai", ...
65+
66+
response: ChatCompletion = provider.chat(
67+
messages=[
68+
ChatMessage(role="system", content="..."),
69+
ChatMessage(role="user", content="..."),
70+
],
71+
model="databricks-claude-haiku-4-5",
72+
max_tokens=4096,
73+
temperature=0.0,
74+
)
75+
76+
print(response.content)
77+
print(response.usage.total_tokens)
78+
print(response.model) # resolved model ID (richer than the alias)
79+
print(response.finish_reason)
80+
```
81+
82+
Three types are public:
83+
84+
- `ChatMessage` — role-tagged message
85+
- `Usage``prompt_tokens` + `completion_tokens` + `total_tokens`
86+
- `ChatCompletion``content` + `model` + `usage` + `finish_reason` (+ `raw` for forward-compat)
87+
88+
Three exception types:
89+
90+
- `ProviderNotInstalled` — optional SDK missing (e.g. `pip install openai`)
91+
- `ProviderNotConfigured` — required env vars missing (e.g. `DATABRICKS_TOKEN`)
92+
- `ProviderError` — wraps upstream SDK errors uniformly
93+
94+
## Built-in providers (v0.11.0)
95+
96+
| Name | Backend | Required env | Models |
97+
|---|---|---|---|
98+
| `databricks` | Databricks Model Serving (OpenAI-compatible) | `DATABRICKS_HOST`, `DATABRICKS_TOKEN` | `databricks-claude-haiku-4-5`, `databricks-claude-sonnet-4`, `databricks-claude-opus-4-5`, `databricks-meta-llama-4-maverick` |
99+
| `mock` | In-process deterministic stub || `mock-small`, `mock-large` |
100+
101+
Future providers (planned, not yet shipped):
102+
103+
| Name | Backend | Required env |
104+
|---|---|---|
105+
| `anthropic` | Anthropic API direct | `ANTHROPIC_API_KEY` |
106+
| `openai` | OpenAI API direct | `OPENAI_API_KEY` |
107+
| `openai-compat` | Generic OpenAI-compatible endpoint | `OPENAI_BASE_URL`, `OPENAI_API_KEY` |
108+
| `bedrock` | Anthropic via AWS Bedrock | `AWS_*` |
109+
110+
## How to add a new provider
111+
112+
1. Create `scripts/bench/providers/<name>.py` with a class that subclasses `Provider` and implements `configured()`, `list_models()`, and `chat()`.
113+
2. Soft-import the SDK in `chat()` (or `__init__`) and raise `ProviderNotInstalled` if missing — never make the SDK a hard dep.
114+
3. Read credentials from env vars in `__init__`; raise `ProviderNotConfigured` if missing.
115+
4. Map upstream errors to `ProviderError` (preserve original via `raise ... from exc`).
116+
5. Add an entry in `scripts/bench/providers/registry.py:_BUILTIN_PROVIDERS` of the form `"<name>": "bench.providers.<name>:<ClassName>"`.
117+
6. Add a row to the table above + the cost table in `base.py:_COST_TABLE_USD_PER_MILLION`.
118+
7. Add provider-specific tests in `tests/bench-providers.test.sh`.
119+
120+
## Recommended invocation patterns
121+
122+
### Direct (env already exported)
123+
124+
```bash
125+
export DATABRICKS_HOST=https://...azuredatabricks.net
126+
export DATABRICKS_TOKEN=dapi...
127+
bstack bench run --runner live --provider databricks \
128+
--model databricks-claude-haiku-4-5 \
129+
--judge-model databricks-claude-opus-4-5 \
130+
--phase 1
131+
```
132+
133+
### Railway as credential broker (recommended for shared dev envs)
134+
135+
When credentials live in Railway (the bstack-broomva-stimulus convention), use `railway run` to inject env vars without writing them to disk:
136+
137+
```bash
138+
railway run --service stimulus-api -- bstack bench run \
139+
--runner live --provider databricks \
140+
--model databricks-claude-haiku-4-5 \
141+
--judge-model databricks-claude-opus-4-5 \
142+
--phase 1
143+
```
144+
145+
### 1Password / sops / direnv / vault
146+
147+
Any tool that exports env vars works. The provider class never sees the credential storage system — it only reads `os.environ`.
148+
149+
## P20 model-isolation enforcement
150+
151+
Bench enforces **Cross-Review (P20)** at the layer where it matters most: the LLM judge.
152+
153+
**Rule:** when `--evaluator llm-judge` is selected, the judge model MUST differ from the agent model. Same model judging itself is exactly the single-model-echo-chamber failure P20 exists to prevent.
154+
155+
```bash
156+
# ❌ Rejected — same model agent + judge
157+
bstack bench run --runner live --provider databricks \
158+
--model databricks-claude-haiku-4-5 \
159+
--evaluator llm-judge --judge-model databricks-claude-haiku-4-5
160+
# → exit 8: "judge model equals agent model"
161+
162+
# ✅ Accepted — distinct models
163+
bstack bench run --runner live --provider databricks \
164+
--model databricks-claude-haiku-4-5 \
165+
--evaluator llm-judge --judge-model databricks-claude-opus-4-5
166+
167+
# ⚠️ Override only with --allow-same-judge-model (must pass rationale)
168+
bstack bench run --runner live --provider databricks \
169+
--model databricks-claude-haiku-4-5 \
170+
--evaluator llm-judge --judge-model databricks-claude-haiku-4-5 \
171+
--allow-same-judge-model "smoke test only — not a quality measurement"
172+
# → warning logged + rationale captured in run config.json
173+
```
174+
175+
Same-provider, different-model is the cheapest path to compliance. Cross-provider (e.g. agent on Databricks Claude, judge on OpenAI GPT-4o) is the strongest. Bench logs both paths.
176+
177+
## Anti-patterns
178+
179+
- **Don't** hardcode credentials anywhere in bstack. The provider class reads `os.environ`; how those env vars get there is the caller's concern.
180+
- **Don't** make any SDK a hard dependency of bstack. Soft-import only.
181+
- **Don't** bypass the registry — always call `get_provider(name)`, never instantiate `DatabricksGatewayProvider(...)` directly from bench code (tests are the exception).
182+
- **Don't** treat `raw` as part of the public contract. It exists for one-off forward-compat reads (logprobs, structured outputs) and may be `None` for stub/mock providers.
183+
- **Don't** allow same-model judge silently. P20 violation requires explicit `--allow-same-judge-model` opt-out with rationale.
184+
185+
## References
186+
187+
- OpenAI Chat Completions API: https://platform.openai.com/docs/api-reference/chat
188+
- Databricks Foundation Model APIs: https://docs.databricks.com/en/machine-learning/foundation-models/index.html
189+
- Stimulus reference implementation: `apps/api/src/utils/databricks_openai.py` (in the stimulus repo)
190+
- bstack bench spec: `specs/bench-skill-evolution.md`
191+
- P20 Cross-Review primitive: `SKILL.md` § Bstack Core Automation Primitives

0 commit comments

Comments
 (0)