Skip to content

feat: bcli pack system, ask oracle, and bcli-site v0 scaffold#22

Merged
igor-ctrl merged 17 commits into
mainfrom
worktree-agent-a5724d8b39c9ef6d9
May 25, 2026
Merged

feat: bcli pack system, ask oracle, and bcli-site v0 scaffold#22
igor-ctrl merged 17 commits into
mainfrom
worktree-agent-a5724d8b39c9ef6d9

Conversation

@igor-ctrl
Copy link
Copy Markdown
Owner

Summary

Three coordinated additions toward bcli-as-an-agent (Parts 0–3 of an approved plan; Parts 4 bcli agent REPL and 5 cloud runtime are deferred to their own plans):

  • bcli.context — shared infra. Typed ContextBundle, 3-layer redaction (audit keys + telemetry patterns + new URL/GUID/attachment scrub), last-error capture without tracebacks by default, opt-in http-tail. Future bcli ask and bcli agent both consume it.
  • bcli pack — installable bundles of saved queries + batch templates + endpoint registry presets + AGENTS.md/CLAUDE.md fragments. JSON ledger at ~/.config/bcli/packs/<profile>/<pack>.json for safe install/uninstall, per-fragment targets: declaration (default [agents]), registry-preset conflict detection (--replace-owned --accept-conflicts), entry-point group bcli.packs. Ships two built-in packs: starter-generic (6 queries against standard v2.0 endpoints, day-1 onboarding for any tenant) and cronus-demo (Microsoft CRONUS-sample-data workflows).
  • bcli ask — one-shot LLM oracle that bundles last-error + http-tail + profile snapshot, redacts, and ships to Claude or OpenAI. Factory pattern mirrors bcli.extract exactly. New entry-point group bcli.ask.context_providers lets downstream packages (e.g. bcli-beautech-bootstrap) inject domain glossaries — opt-in via user config, never auto-enabled.
  • bcli-site/ — Astro + Tailwind v0 marketing scaffold (files only, no pnpm install run). Hero + install + 3 example commands. GH Actions build workflow stubbed; Vercel deploy step commented for the user to wire up with secrets.

Approved plan with full context including the /codex review (9 findings, all integrated as R1–R9): /Users/igor/.claude/plans/how-would-the-bcli-zippy-lantern.md.

Independently inspired by @steipete's pattern of "small sharp CLIs for every external tool, designed for humans AND agents."

What's in the 12 commits

Commit Part What
f3f448f 0 bcli.context SDK — ContextBundle + 3-layer redaction
fa0ebcb 0 Wire last-error capture + http-tail bootstrap into CLI
2eb26a3 0 Tests: dataclass round-trip, 3-layer redaction, audit trail
e3b1714 0 CHANGELOG
e3eb1eb 1 bcli.packs SDK — Pack/Manifest/Ledger + installer (R2, R3, R7, R8)
c61627b 1 bcli pack list / info / install / uninstall CLI
a6a8c46 1 Built-in packs: starter-generic + cronus-demo
ebc0544 1 Tests + pyproject pack wheel layout + CHANGELOG
30e7545 2 bcli ask oracle — Claude/OpenAI backends + dry-run + R8 providers
d306517 2 --no-context truly suppresses last-error; --include-debug reads sidecar
44a13f9 3 bcli-site v0 — Astro + Tailwind landing scaffold
bd633e9 Implementation summary

Test plan

  • uv pip install -e ".[dev]" clean
  • uv run pytest908 passed, 5 skipped, 0 failed
  • uv run ruff check src/bcli/{context,packs,ask} src/bcli_cli/commands/{pack_cmd,ask_cmd}.py — all green
  • bcli pack list — both built-in packs visible
  • bcli pack info starter-generic — manifest reads correctly (3 fragments / 6 queries / 2 batches)
  • bcli pack install starter-generic --dry-run — splices into AGENTS.md only (R3 default target), merges 6 queries, writes 2 batches; nothing actually written
  • bcli ask --help — full flag surface
  • bcli ask --dry-run --no-context "smoke test" — bundle renders, no network
  • Reviewer: try bcli pack install starter-generic for real (against a sandbox profile), then bcli skill install to project as slash commands, then bcli pack uninstall starter-generic to verify ledger-driven removal
  • Reviewer: set [ask] backend = "claude" + ANTHROPIC_API_KEY, run bcli ask "what is bcli pack?" and confirm the model gets the bundle
  • Reviewer: confirm bcli-site/ files parse (no pnpm install needed for review; CI workflow is stubbed)

Known follow-ups (called out in IMPLEMENTATION-SUMMARY.md)

  1. bcli describe excerpt isn't yet wired into the default bcli ask context bundle. The stub exists; integration is a small follow-up PR.
  2. bcli pack uninstall --force for partial-marker recovery. Ledger code handles it; the UX flag isn't wired yet.

Neither blocks landing this PR; both are clearly scoped follow-ups.

Out of scope

  • Beautech companion (Part 1B–4B from the plan) — separate repo (bcli-beautech-bootstrap), needs its own PR once this lands and ships to PyPI.
  • bcli agent REPL loop (Part 4) and cloud runtime (Part 5) — deliberately deferred to their own plans. src/bcli/context/ is shaped so Part 4 can import it directly.

Release

Recommend bumping pyproject.toml 0.4.0 → 0.5.0 when merging, since bcli pack, bcli ask, and bcli.context are net-new SDK surface.

igor-ctrl added 12 commits May 24, 2026 14:02
Lands the shared model-bound context layer (Part 0 of the bcli ask /
agent plan). Standalone in this commit — no CLI consumers yet. The
package shape and redaction posture are exercised by tests so the
next two commits (CLI hook-up, tests) and the upcoming Part 2
`bcli ask` can build against a stable surface.

- `bcli.context._protocol` — typed dataclasses (R4): ContextBundle,
  TokenBudget, BundlePolicy, BundleSource, RedactionRecord,
  ProfileSnapshot, HttpEvent, LastErrorRecord, Attachment. Frozen,
  JSON-serialisable, with `to_dict()` and `to_prompt_text()` renderers.
- `bcli.context._redact` — three-layer redaction (R5) composing
  bcli/audit/_redact.py (layer 1 keys), bcli/telemetry/events.py
  (layer 2 patterns), and a new URL/GUID/attachment scrubber
  (layer 3). Every redaction lands in the bundle's audit trail with
  a stable `rule_id`.
- `bcli.context._last_error` — captures BCLIError exits to
  `~/.config/bcli/last-error.json`. No tracebacks by default (R6);
  --debug runs also write a `last-error-debug.json` at mode 0600.
- `bcli.context._http_tail` — opt-in RotatingFileHandler on
  `bcli.http` for the last ~200 events. Off by default.
- `bcli.context._bundle` — pure builder: token-budgeted priority
  truncation (question > last_error > profile > http > describe >
  attachments), every step source-attributed.
- ContextConfig added to bcli.config._model with knobs: tail,
  redact_company_ids, attachment_max_bytes. Backwards-compatible
  via Pydantic default_factory.
- Central BCLIError handler in bcli_cli.app calls
  bcli.context.capture_last_error so every error exits leaves a
  redacted JSON record at ~/.config/bcli/last-error.json (and the
  debug sidecar when --debug was active).
- Root callback bootstraps the http-tail handler only when
  [context] tail = true. Default off — read-only home dirs, CI, and
  ephemeral containers see zero overhead. All hooks are
  try/except-wrapped so a misconfigured [context] section never
  blocks the CLI.
…rail

40 tests covering Part 0 surface:

- test_protocol.py — ContextBundle round-trips through JSON;
  to_prompt_text renders priority sections; tracebacks excluded
  unless policy.include_debug.
- test_redact.py — adversarial layered redaction: nested JSON with
  Bearer + JWT + client_secret; URL-encoded tokens; base64-wrapped
  JWTs; GUID policy gate; attachment truncation. Audit trail's
  rule_id set is asserted frozen + complete; redacted values never
  leak via repr.
- test_last_error.py — capture without traceback by default, debug
  sidecar at mode 0600 only when --debug active, BC-message token
  pattern redaction, read-only config dir safety.
- test_http_tail.py — handler idempotent, NDJSON round-trip, bad
  lines skipped, RotatingFileHandler size cap, limit slicing.
- test_bundle.py — minimal-question bundle, last-error read from
  disk, budget-driven describe-excerpt drop, attachment scan + cap,
  layered audit trail completeness, --no-context policy path.
…3, R7, R8)

Lands the SDK surface for the pack mechanism. Three sub-modules
mirror the existing extract/telemetry package shape:

- `_protocol.py` — frozen dataclasses for Pack, PackManifest,
  PackContents, AgentFragment (with per-fragment `targets:`,
  default [agents] per R3), PackQuery, PackBatch,
  PackRegistryPreset. VALID_TARGETS = {agents, claude}.
- `_loader.py` — parses pack.yaml + content files, validates
  fragment targets, supports either list-of-strings or list-of-
  dicts for each section.
- `_registry.py` — discovers packs from three sources: built-in
  (packs/ at repo root, or bcli/packs/_builtin in the wheel),
  entry-point group `bcli.packs`, and explicit paths. Broken packs
  log a warning and are skipped.
- `_ledger.py` — frozen Ledger dataclass + round-trip JSON I/O at
  ~/.config/bcli/packs/<profile>/<pack>.json. Records every
  artefact path with rendered_hash + owner for provenance-driven
  uninstall (R2).
- `_installer.py` — plan_install / execute_install / install_pack /
  uninstall_pack. Atomic writes via the same tmp+os.replace pattern
  as skill_init_cmd. Conflict detection on registry presets refuses
  overwrites unless --replace-owned --accept-conflicts (R7).
  Marker blocks splice into AGENTS.md / CLAUDE.md per fragment's
  targets list (R3); idempotent re-install via marker pair +
  content_hash.

The package has no CLI consumers yet — the next commit wires up
bcli pack.
Typer group registered alongside skill / extract. Mirrors the
skill_init UX: per-command confirmation prompts (skipped with
--yes), dry-run mode renders the plan without touching disk,
helpful diff when info-querying an installed pack.

Flags on `install`:
- --profile / -p (defaults to active profile)
- --target (project root; defaults to CWD if .claude/ present, else $HOME)
- --dry-run
- --replace-owned + --accept-conflicts (R7 — two-flag override)
- --yes / -y

Flags on `uninstall`:
- --profile / -p
- --yes / -y

Pack recommendations (R8) surface as a one-line hint after install:
"This pack recommends enabling these `bcli ask` context providers".
They are never auto-enabled — user config in [ask] context_providers
is the binding decision.
Two reference packs in the OSS repo. Both fit through the standard
v2.0 endpoint registry so they install cleanly against any BC tenant.

starter-generic (v0.1.0) — the day-1 onboarding pack:
- 6 saved queries — vendor-by-no, customer-by-no, open-pos,
  ar-aging-buckets, recent-posted-invoices, inventory-on-hand
- 2 read-only batches — weekly-ar-snapshot, month-end-readonly-audit
- 3 AGENTS.md fragments — endpoint-discovery, filter-syntax-cheatsheet,
  common-errors

cronus-demo (v0.1.0) — Microsoft CRONUS-tenant demo:
- 1 month-end batch (lifted from examples/month-end-cronus.yaml)
- 1 sample queries file (lifted from examples/queries/sample.yaml)
- 2 fragments — cronus-orientation and month-end-walkthrough

Beautech-flavoured packs land in bcli-beautech-bootstrap via the
bcli.packs entry-point group; this OSS repo only ships mechanism +
two generic, standard-API-only examples.
- test_loader.py — manifest schema validation, missing fields,
  bad targets, broken YAML
- test_registry.py — built-in discovery, skips non-pack dirs,
  warns on broken manifests
- test_installer.py — install writes all artefacts with provenance;
  fragment targets route blocks to AGENTS.md vs CLAUDE.md;
  idempotent re-install; conflict refusal + --replace-owned
  override; uninstall round-trips; dry-run writes nothing
- test_builtins.py — both shipped packs install end-to-end against
  a tmp config dir, co-exist on the same profile, and uninstall
  cleanly

Also:
- pyproject.toml — hatch force-include maps packs/ → bcli/packs/_builtin
  in the wheel so the shipped CLI sees them. sdist explicitly
  includes packs/, examples/, docs/.
- CHANGELOG.md — Part 1 entry under [Unreleased].
…oviders

Part 2 of the pack/ask/site plan. Single-call second-opinion
oracle that bundles the operator's recent context (last-error,
http-tail, profile, describe) via Part 0's bcli.context, ships it
to a configured LLM, and prints the answer.

SDK (bcli.ask):
- _protocol.py — AskBackend Protocol + NullAsker (mirror of
  extract/_protocol.py)
- _factory.py — get_asker dispatch with _BUILTIN_BACKENDS dict,
  module:Class fallback, Null fallback on any failure, one-shot
  warning (mirror of extract/_factory.py)
- _claude.py — Anthropic backend; messages.create with bundle
  rendered as Markdown user-turn
- _openai.py — OpenAI Responses API backend
- _providers.py — bcli.ask.context_providers entry-point group
  (R8). Discovery + user-gated execution; pack recommendations
  surface as hints only and are NEVER auto-enabled.

CLI (bcli_cli.commands.ask_cmd):
- bcli ask "<question>" [--no-context] [--attach PATH]
  [--backend NAME] [--dry-run] [--include-bodies]
  [--include-debug] [--max-tokens N]

Config:
- AskConfig added under [ask] — backend, model, api_key_env,
  max_tokens, include_describe, include_http_tail,
  context_providers, base_url, organization.

pyproject:
- New extras [ask-claude], [ask-openai], [ask] (meta).
- [dev] now also includes [ask].

Tests (16):
- test_factory.py — Null fallback paths, custom backend import,
  malformed spec, from_config raise, missing from_config.
- test_cli_dry_run.py — --dry-run prints the bundle without
  reaching the backend; attachments redacted + included.
- test_providers.py — entry-point discovery, opt-in execution,
  provider failure silently logged, profile + last-error reach
  the provider callable.
Single-page landing site for bcli.sh. Files only — pnpm install
deliberately skipped since the agent harness has no guaranteed
network egress; first developer touch runs `pnpm install`.

Stack:
- Astro 4 + @astrojs/tailwind 5 + TypeScript 5 (strict)
- Tailwind palette/font scoped to bcli's terminal-y vibe (ink/mist
  neutrals + accent blue/amber)

Content (R9-compliant — describes shipped features only):
- Hero: tagline "Business Central from the terminal. Designed for
  humans and AI agents."
- Install: pip + uv tool install, noting the bc-cli vs bcli
  distribution-name story
- Three example commands: pack install, saved query, ask oracle
- Features grid: packs, ask oracle, MCP server, discovery-first
- Footer with GitHub link

Build pipeline:
- .github/workflows/site.yml — actions/setup-node@v4 + corepack +
  pnpm install + pnpm build on changes under bcli-site/**. Vercel
  deploy step is wired but commented out (TODO once secrets exist).
- The workflow follows GitHub's command-injection guidance: secrets
  flow through env: blocks, never directly interpolated into run:.

gitignore:
- bcli-site/{node_modules,dist,.astro,pnpm-lock.yaml,package-lock.json,yarn.lock}
…eads sidecar

Two advisor-flagged bugs in bcli ask:

1. --no-context was leaking last-error.json into the bundle.
   build_bundle's "read from disk when caller passes None" default
   meant the CLI's `last_error=None` arg silently restored the
   prior failure even though --no-context says otherwise. Fix: new
   skip_last_error=True parameter on build_bundle that disables
   the implicit disk read; CLI threads it from the --no-context
   flag.

2. --include-debug was wired but inert. CLI now reads
   ~/.config/bcli/last-error-debug.json (mode 0600 sidecar) when
   the flag is set. Pairs with the existing
   policy.include_debug=True which already gates the traceback
   excerpt in to_prompt_text().

Two new regression tests in test_cli_dry_run.py:
- test_no_context_suppresses_existing_last_error — writes a real
  last-error.json, asserts the phrase appears in the default
  bundle but is gone with --no-context.
- test_include_debug_reads_traceback_sidecar — asserts Traceback
  absent without the flag, present with it.

Also: IMPLEMENTATION-SUMMARY.md updated to record the fixes and a
wheel-build smoke-test confirming packs ship at
bcli/packs/_builtin/ in the produced wheel.
@igor-ctrl igor-ctrl force-pushed the worktree-agent-a5724d8b39c9ef6d9 branch from d306517 to 0cdd892 Compare May 24, 2026 19:06
igor-ctrl added 5 commits May 24, 2026 14:13
- pyproject.toml added [ask], [ask-claude], [ask-openai] extras and pulled
  bc-cli[ask] into [dev]; uv.lock wasn't regenerated so `uv sync --locked`
  rejected the lockfile on CI.
- site.yml used node 20; corepack pulls pnpm 11.3.0 which requires
  node >=22.13 (imports node:sqlite, an only-in-22 builtin). Bumped to 22.

No package additions — both ask extras reuse anthropic / openai already
pulled in by the extract extras.
- 4 unused imports in tests (F401, autofixed by ruff)
- 1 unused local variable in test_installer.py (F841, manual drop)
- bcli-site/package.json: declare onlyBuiltDependencies=[esbuild, sharp]
  so pnpm 10+ runs their install scripts instead of failing the build.
  Both are first-party Astro deps with trusted publishers.

37 tests in test_packs/ + test_ask/ still pass post-cleanup.
pnpm 11.3 dropped the `pnpm.*` key from package.json (warning emitted in
CI: "The 'pnpm' field in package.json is no longer read by pnpm"). The
new home is pnpm-workspace.yaml even for single-package projects. Drop
the dead package.json key.

Both esbuild and sharp are first-party Astro deps that legitimately need
install-time build scripts — without the allowlist, pnpm refuses to run
them and the build fails with ERR_PNPM_IGNORED_BUILDS.
pnpm 11.3 didn't pick up onlyBuiltDependencies from a package-local
pnpm-workspace.yaml — build still failed with ERR_PNPM_IGNORED_BUILDS.
.npmrc is the canonical package-local pnpm config and is reliably read
from the working directory pnpm install runs in.
pnpm 10+ enforces an "approve-builds" gate that requires onlyBuiltDependencies
to be configured before install scripts (esbuild, sharp) can run. corepack
downloaded pnpm 11.3.0 by default; neither package.json nor pnpm-workspace.yaml
nor .npmrc made pnpm 11 honor the allowlist in CI (config read order varies
across the 10/11 transition).

pnpm 9.15.4 predates that strict-builds behavior and runs install scripts
without the approval gate. corepack honors the packageManager field, so
this pins deterministically across local dev and CI.

.npmrc kept in place — harmless for pnpm 9, future-proof if we re-bump.
@igor-ctrl igor-ctrl merged commit 3734f09 into main May 25, 2026
4 checks passed
@igor-ctrl igor-ctrl deleted the worktree-agent-a5724d8b39c9ef6d9 branch May 25, 2026 01:55
@igor-ctrl igor-ctrl mentioned this pull request May 25, 2026
4 tasks
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.

1 participant