Skip to content

Rust Tool Base — Requirements & Specification

Status: Draft v0.1. Normative for the 0.x line. Audience: contributors to rust-tool-base; downstream CLI authors evaluating adoption.


0. Scope & non-goals

0.1 Goal

Provide a batteries-included, opinionated Rust application framework for building production-grade CLI tools and adjacent applications (daemons, agents, MCP servers), with the lifecycle (version, update, docs, init, MCP, telemetry) wired by default.

0.2 Non-goals

  • Not a port of Go Tool Base. RTB targets the same outcomes, but uses idiomatic Rust mechanisms throughout. Go paradigms that are inappropriate in Rust are replaced, not transliterated.
  • Not a web framework. Integrates axum for tools that need a serve-style subcommand; does not replace axum.
  • Not a TUI library. Uses ratatui, inquire, termimad.
  • Not a dependency-injection container. The App context is a plain Rust struct passed by cheap clone.
  • No custom async runtime. Picks tokio.
  • No custom logging abstraction. Uses tracing.

0.3 Paradigm swaps vs. GTB (normative)

GTB (Go) RTB (Rust) Rationale
Props struct with any-typed config Generic App<C: AppConfig> + strongly-typed serde config Types over strings; compile-time checking
Functional options (func(opts *X)) Typestate builders via bon Required fields enforced at compile time
context.Context threaded through every call tokio_util::sync::CancellationToken + async fn propagation with ? Structured concurrency; no value-bag
Package-level init() self-registration #[linkme::distributed_slice] No life-before-main; link-time safety
Containable interface for config figment::Figment + caller's serde::Deserialize type Typed access, not dynamic accessors
ErrorHandler.Check() at Execute() main() -> miette::Result<()> + installed miette hook Errors as values, reported at the edge
afero.Fs everywhere vfs only for overlay; std::fs/tokio::fs otherwise Pay overlay cost only where needed
interface{} slog key-value pairs tracing structured fields Compile-time macro expansion
cmd.go + main.go split per subcommand Rust module per command, file count by size Rust modules already scope things
chan + goroutines tokio::sync::mpsc/broadcast/watch + tasks Native async primitives

1. Glossary

  • Application — the downstream binary built on RTB.
  • App context (App) — cheap-clonable struct carrying framework services (config, assets, logging, shutdown token, tool metadata).
  • Command — a type implementing rtb_cli::Command that is registered with an Application.
  • Feature — a runtime switch (rtb_app::Feature) for built-in commands and subsystems. Orthogonal to Cargo features.
  • Cargo feature — a compile-time on/off for a slice of RTB, exposed by the rtb umbrella crate (cli, update, docs, mcp, ai, credentials, tui, telemetry, vcs, full).

2. Workspace & crate topology

2.1 Workspace root

Single Cargo workspace at Cargo.toml. resolver = "2". Pinned toolchain via rust-toolchain.toml (stable). MSRV is whatever that stable channel supports at the time of a release; documented in each crate's rust-version field.

2.2 Crate list

Crate Role Hard deps (public API)
rtb-error Error enum, Result, miette glue miette, thiserror
rtb-app App, ToolMetadata, VersionInfo, Features, registration slices rtb-error, rtb-config, rtb-assets
rtb-config Layered typed config; hot-reload figment, notify, arc-swap, serde
rtb-assets rust-embed + vfs overlay rust-embed, vfs, format deps
rtb-cli Application builder, clap integration, built-in commands clap, tracing, tokio, miette, rtb-app
rtb-update Self-update (archive + signature + atomic swap) self_update, self-replace, ed25519-dalek, sha2
rtb-vcs Git + GitHub + GitLab abstractions gix, octocrab, gitlab, secrecy
rtb-ai Multi-provider AI client; structured output genai, async-openai, schemars, jsonschema
rtb-mcp MCP server that exports registered commands rmcp, rtb-cli
rtb-docs ratatui-based docs browser; streaming AI Q&A ratatui, termimad, rtb-assets, rtb-ai (feature-gated)
rtb-tui Wizard, tables, spinners inquire, ratatui, tabled
rtb-credentials CredentialStore trait + KeyringStore impl keyring, secrecy, zeroize
rtb-telemetry Opt-in telemetry sinks + OTLP layer machine-uid, sha2, opentelemetry_*, tracing-opentelemetry
rtb Umbrella — re-exports everything behind Cargo features all of the above
rtb-cli-bin The rtb scaffolder/regenerator binary clap, minijinja, inquire

2.3 Directory layout

Mirrors the GTB layout conceptually but uses Rust conventions (no pkg//cmd//internal/ split; Rust's module system handles that).

rust-tool-base/
├── Cargo.toml
├── crates/                  # library + binary crates
├── examples/                # reference tool(s) built on rtb
├── docs/                    # mkdocs-compatible user docs
├── assets/init/config.yaml  # default embedded config
├── .github/workflows/       # ci, release
├── deny.toml
├── rustfmt.toml
├── rust-toolchain.toml
├── justfile
├── LICENSE
└── SECURITY.md

2.4 Feature flags on the rtb umbrella

Feature Default? Enables
cli yes rtb-cli, clap
update yes rtb-update, rtb-vcs release providers
docs yes rtb-docs
mcp yes rtb-mcp
credentials yes rtb-credentials
ai no rtb-ai
tui no (brought in transitively by docs) rtb-tui
telemetry no rtb-telemetry
vcs no (brought in transitively by update) rtb-vcs explicitly
full no all of the above

3. Core application context (rtb-app)

3.1 App struct

#[derive(Clone)]
pub struct App {
    pub metadata: Arc<ToolMetadata>,
    pub version:  Arc<VersionInfo>,
    pub config:   Arc<Config>,          // typed; see §4
    pub assets:   Arc<Assets>,          // overlay FS; see §5
    pub shutdown: CancellationToken,    // tokio_util; see §11
}

Normative:

  • All fields are Arc-wrapped so App is cheap to clone. Command handlers take App by value.
  • App is not Default. It is constructed only via the Application builder in rtb-cli.
  • There is no Arc<dyn …> in App by design. Runtime polymorphism (e.g. a dyn CredentialStore) lives inside the relevant subsystem, not on the context.

3.2 Config generic parameter

In the public API, App is actually App<C = ()> where C: AppConfig. AppConfig is a trait with a blanket impl for any T: DeserializeOwned + Send + Sync + 'static. This keeps App strongly typed to the downstream tool's config struct without sacrificing ergonomics.

pub trait AppConfig: DeserializeOwned + Send + Sync + 'static {}
impl<T: DeserializeOwned + Send + Sync + 'static> AppConfig for T {}

pub struct App<C: AppConfig = ()> {
    // …
    pub config: Arc<Config<C>>,
}

3.3 ToolMetadata

Built with bon::Builder. See crates/rtb-app/src/metadata.rs. All fields except name and summary are optional. release_source is required iff Feature::Update is enabled at runtime (checked in Application::build).

3.4 Features

A HashSet<Feature> wrapped in a newtype with a FeaturesBuilder exposing .enable(f)/.disable(f). The set is owned by the Application builder, not placed on App itself — handlers do not need to query features.

3.5 Command registration

RTB provides two orthogonal registration mechanisms:

  1. Explicit (preferred for downstream tools): Application::builder().command::<MyCommand>() — an inherent trait- object Box<dyn Command> is added.

  2. Distributed-slice (for RTB's built-ins and for plugin crates):

use linkme::distributed_slice;

#[distributed_slice]
pub static BUILTIN_COMMANDS: [fn() -> Box<dyn Command>];

#[distributed_slice(BUILTIN_COMMANDS)]
fn register_version() -> Box<dyn Command> { Box::new(VersionCmd) }

Application::build walks BUILTIN_COMMANDS and filters by Features. No life-before-main, no mutex-guarded registry.

3.6 Command trait

#[async_trait::async_trait]
pub trait Command: Send + Sync + 'static {
    /// Uniquely identifies the subcommand path, e.g. "deploy", "config get".
    fn spec(&self) -> CommandSpec;

    /// Execute with the app context and the already-parsed clap matches.
    async fn run(&self, app: App<Self::Config>, matches: &clap::ArgMatches) -> miette::Result<()>;

    type Config: AppConfig = ();
}

A CommandSpec describes the static command surface: name, about, aliases, and the Feature it gates on. MCP-tool exposure is opt-in via the Command::mcp_exposed / Command::mcp_input_schema default trait methods (see rtb-mcp). Downstream authors will eventually derive Command via a #[rtb::command] attribute macro that generates spec() and the MCP hooks from struct fields — see §12.


4. Configuration (rtb-config)

4.1 Design statement

Configuration is typed and layered. Dynamic Containable.GetString- style access is explicitly rejected.

4.2 Layering order (last-wins)

  1. Embedded default (assets/init/config.yaml, or caller-supplied &str).
  2. User/system file(s) (~/.config/<tool>/config.{yaml,toml,json} via directories::ProjectDirs; plus any explicit --config <path>).
  3. Environment variables, prefix "<TOOL>_" (configurable).
  4. Command-line flags.

4.3 Builder

let cfg: Config<MyConfig> = Config::<MyConfig>::builder()
    .embedded_default(include_str!("../assets/init/config.yaml"))
    .user_files_yaml(ProjectDirs::from("dev", "me", "mytool"))
    .env_prefixed("MYTOOL_")
    .cli_overrides(&matches)           // serde_json::Value from clap
    .watch(true)
    .build()?;

4.4 Hot reload

  • notify-debouncer-full polls the registered user files.
  • On change, the new figment::Figment is re-extracted into C.
  • The parsed value is swapped atomically into arc_swap::ArcSwap<C>.
  • Subscribers call cfg.subscribe() -> watch::Receiver<Arc<C>> (backed by tokio::sync::watch) to react.
  • No observer pattern in the GTB sense. The watch channel is the idiomatic Rust alternative — pull-based, Clone-friendly, survives subscriber death gracefully.

4.5 Profiles

For layered profiles (think "dev", "prod") use Figment::select(profile) from figment. RTB exposes this as Config::with_profile(&str) on the builder; no runtime Sub() accessor.

4.6 Schema + validation

  • Downstream tools may derive schemars::JsonSchema on their config.
  • Config::schema() returns the JSON Schema (used by config schema subcommand and by MCP introspection).
  • Validation is compile-time via serde deserialisation. For richer invariants, implement TryFrom<Raw> for your Config.

5. Assets & overlay FS (rtb-assets)

5.1 Layering

#[derive(rust_embed::RustEmbed)]
#[folder = "assets/"]
pub struct EmbeddedAssets;

let embedded: VfsPath = EmbeddedFS::<EmbeddedAssets>::new().into();
let userdir:  VfsPath = PhysicalFS::new(
    ProjectDirs::from("dev", "me", "mytool").unwrap().data_dir(),
).into();
let overlay:  VfsPath = OverlayFS::new(&[userdir, embedded]).into();

5.2 Structured-data merging

For each of .yaml/.json/.toml, RTB merges across layers using json-patch::merge on serde_json::Value (the two non-JSON formats round- trip via serde_json). Last-registered (top) layer wins at the leaf; maps merge recursively.

5.3 Binary assets

For non-structured blobs, last-registered-wins shadowing only — no concatenation.

5.4 CSV

CSVs are appended with header-deduplication across layers (mirroring GTB's behaviour).

5.5 Dev vs. release

rust-embed compiles in bytes in release mode. In debug mode the same API reads from disk, so authors see live edits without rebuilding. RTB does not override this behaviour.


6. Logging & diagnostics (tracing + miette)

6.1 Logging

  • The Application installs a tracing_subscriber::registry() with these layers (gated by config/env):
  • fmt::layer().with_target(false).compact() for pretty terminal output when stderr is a TTY.
  • fmt::layer().json() when log.format = json or stderr is not a TTY.
  • tracing_opentelemetry::layer().with_tracer(…) when the telemetry Cargo feature is on and OTLP is configured.
  • Level is controlled by RUST_LOG (standard) or log.level.

RTB does not define its own Logger trait. Callers use the tracing macros (info!, warn!, error!, span!) directly.

6.2 Diagnostics

  • Every crate derives thiserror::Error + miette::Diagnostic.
  • rtb-cli::Application::run() installs:
  • miette::set_hook(Box::new(|_| Box::new(GraphicalReportHandler::new())))
  • miette::set_panic_hook()
  • Errors carry #[diagnostic(code(...), help(...), url(...))]. The custom hook additionally consults ToolMetadata::help (the GTB "support channel" concept) and appends a contact line.

7. Error type (rtb-error)

  • Error enum with #[non_exhaustive].
  • pub type Result<T, E = Error> = std::result::Result<T, E>;
  • Other(Box<dyn Diagnostic + Send + Sync + 'static>) variant preserves downstream typed diagnostics.

RTB does not export a WithHint() helper — miette::Diagnostic's help attribute fills that role.


8. Built-in commands

Status (as of v0.3): version, doctor, init, config show, update, docs, and mcp all ship as real implementations. update is registered by rtb-update (v0.2), docs by rtb-docs (v0.2), and mcp by rtb-mcp (v0.3). changelog is not yet wired. --output json is deferred. See §16 for the roadmap.

All built-ins are registered into BUILTIN_COMMANDS behind Cargo features and runtime-filtered by Features. Each supports --output text|json (v0.2).

8.1 init

  • Prompts via inquire::Wizard (RTB's rtb-tui::Wizard).
  • Writes the merged default config to ~/.config/<tool>/config.yaml.
  • Invokes registered Initialisers (trait object in BUILTIN_INITIALISERS distributed slice, mirroring BUILTIN_COMMANDS).
  • Skippable per-initialiser via --skip=<name>.

8.2 version

  • Prints version, commit, date, rustc, and the detected target triple.
  • With --check, issues a ReleaseProvider::latest() call.

8.3 update

  • Uses self_update to find the matching release asset.
  • Downloads, verifies SHA-256, then Ed25519 signature against the pinned public key in ToolMetadata.
  • Extracts via tar + flate2 (or zip).
  • Swaps with self-replace.
  • Supports --file <archive> for offline installs.
  • Throttled: writes a timestamp to ProjectDirs::cache_dir() and won't re-check within update.check_interval hours (default 24).

8.4 docs

  • TUI browser (ratatui) over the overlay-merged /docs tree.
  • Two-pane layout: tree sidebar + termimad-rendered markdown.
  • / for fuzzy search.
  • With the ai Cargo feature: docs ask "<question>" runs a RAG loop over the merged tree via rtb-ai.

8.5 mcp

  • Boots an rmcp server over stdio (default). Transport::Sse and Transport::Http variants are present on the API; full streamable-HTTP wiring lands in v0.3.x.
  • Subcommands: mcp serve [--transport stdio|sse|http] [--bind ADDR] and mcp list (prints every exposed tool's name + description + JSON schema as one JSON object per line).
  • Per-command opt-in via the Command::mcp_exposed (default false) and Command::mcp_input_schema (default None) trait methods on rtb_app::command::Command.
  • See rtb-mcp v0.1 spec for the full contract.

8.6 doctor

  • Runs HealthCheck trait objects registered in BUILTIN_HEALTH_CHECKS.
  • Built-ins: config validity, keychain reachability, release-provider reachability, filesystem permissions.
  • Emits a report table (tabled) + JSON envelope.

8.7 config

  • config get <jsonpath>, config set <jsonpath> <value>, config schema, config validate.
  • Mutations write to the highest-priority writable user file.

8.8 changelog

  • Parses CHANGELOG.md (Keep-a-Changelog / conventional-commit format).
  • Prints the entries for the current version, or for a --since range.

9. VCS & release providers (rtb-vcs)

Status:shipped. Release-provider slice shipped at 0.2.0 (release backends + ReleaseProvider trait, consumed by rtb-update). Git-operations slice shipped at the v0.5 milestone — Repo type with init / open / clone / walk / diff / blame / status / commit / fetch / checkout / push. See the v0.5 scope addendum for the per-commit history and design resolutions; see docs/components/rtb-vcs.md for the user-facing reference.

9.1 Release provider trait

#[async_trait::async_trait]
pub trait ReleaseProvider: Send + Sync {
    async fn latest_release(&self) -> Result<Release, ProviderError>;
    async fn release_by_tag(&self, tag: &str) -> Result<Release, ProviderError>;
    async fn list_releases(&self, limit: usize) -> Result<Vec<Release>, ProviderError>;
    async fn download_asset(&self, asset: &ReleaseAsset)
        -> Result<(Box<dyn AsyncRead + Send + Unpin>, u64), ProviderError>;
}

Selected at runtime from ToolMetadata::release_source. Implementations live behind Arc<dyn ReleaseProvider>; the downstream tool never imports octocrab or gitlab directly. Built-in backends register at link time via linkme::distributed_slice on RELEASE_PROVIDERSrtb_vcs::lookup and rtb_vcs::registered_types are the discovery entry points.

9.2 Git ops — Repo

The Repo type is the async git wrapper. Public method shape:

impl Repo {
    pub async fn init(path, InitOptions) -> Result<Self, RepoError>;
    pub async fn open(path) -> Result<Self, RepoError>;
    pub async fn clone(url, dst, CloneOptions) -> Result<Self, RepoError>;
    pub fn walk(&self, revspec) -> Result<CommitWalk, RepoError>;     // async stream
    pub async fn diff(&self, a, b) -> Result<Diff, RepoError>;
    pub async fn blame(&self, path, revspec) -> Result<Blame, RepoError>;
    pub async fn status(&self) -> Result<RepoStatus, RepoError>;
    pub async fn commit(&self, paths, message) -> Result<String /* OID */, RepoError>;
    pub async fn fetch(&self, remote, FetchOptions) -> Result<(), RepoError>;
    pub async fn checkout(&self, revspec, CheckoutOptions) -> Result<(), RepoError>;
    pub async fn push(&self, remote, refspec, PushOptions) -> Result<(), RepoError>;
}

Backend choice (per A8 wrap-not-leak):

  • gix is the read-path backend (open, walk, diff, status, anonymous clone). Gated on the git Cargo feature (default-on per A7).
  • gix-blame powers blame, with per-line BlameLine output rather than gix's hunk-level entries.
  • Write paths shell out to git. commit, fetch, checkout, authenticated clone, push invoke the host git CLI under tokio::task::spawn_blocking. Rationale: gix 0.72 has no high-level helpers for staging + commit + auth and rebuilding them would be 50+ lines of fiddly plumbing per op. Migration to pure-gix is internal — public API stable.
  • Errors are wrapped, not leaked. RepoError carries semantic variants with stringified cause; gix / git2 / git stderr never reach the public API.
  • The originally-planned git2-fallback Cargo feature was obsoleted (v0.5 A4) — consistent shell-out for all write ops removed the need for libgit2.

9.3 Token resolution

Auth-bearing Repo methods (clone, fetch, push) take an optional CredentialRef on their options struct (CloneOptions::with_credential, etc.). When set, rtb-vcs resolves the credential via rtb_credentials::Resolver::with_platform_default() — the same env → keychain → literal → fallback-env precedence chain as everywhere else in the framework.

The resolved SecretString is passed to the git subprocess via:

  • RTB_VCS_GIT_TOKEN=<secret> in the subprocess env.
  • -c credential.helper='!f() { echo username=x-access-token; echo password=$RTB_VCS_GIT_TOKEN; }; f' in argv (helper snippet contains no secret).
  • GIT_TERMINAL_PROMPT=0 to fail fast on auth errors.

Username is hardcoded as x-access-token — GitHub PAT convention, accepted by GitLab / Gitea / Codeberg. Tools needing other usernames can wrap Repo::clone / fetch / push themselves.

Unresolvable credentials surface as RepoError::Auth before any network IO; resolved-but-rejected credentials surface as CloneFailed / FetchFailed / PushFailed with git's stderr in cause.


10. AI client (rtb-ai)

Status:deferred to v0.3. rtb-ai exists as a stub crate. The spec below is the design target; nothing in this section is yet implemented. Credentials for AI providers will flow through rtb-credentials's Resolver — that part is v0.1 shipped. See §16 for the full roadmap.

10.1 Providers

Provider set matches GTB: Claude (Anthropic), ClaudeLocal, OpenAI, OpenAICompatible, Gemini. Added: Ollama (via genai).

10.2 API surface

pub struct AiClient { /* … */ }

impl AiClient {
    pub async fn chat(&self, prompt: &str) -> Result<String>;
    pub async fn ask<T: JsonSchema + DeserializeOwned>(&self, prompt: &str) -> Result<T>;
    pub async fn chat_stream(&self, prompt: &str) -> Result<impl Stream<Item = Result<String>>>;
    pub async fn react<Ts: ToolSet>(&self, tools: Ts, prompt: &str) -> Result<ReactOutcome>;
}
  • ask::<T> inserts a JSON Schema (generated via schemars) in the request and validates the response with jsonschema before deserialising. This is RTB's "structured output" guarantee.
  • react::<Ts> orchestrates a bounded ReAct loop (config.ai.max_steps). Parallel tool execution via tokio::join!-style fan-out, capped by config.ai.max_parallel_tools.

10.3 Anthropic-specific features

For prompt caching, extended thinking, citations, managed agents, and the Files API, the claude backend drops down to direct reqwest calls against the Anthropic Messages API. This is isolated behind the same AiClient façade, so callers remain portable across providers.


11. Concurrency & lifecycle

11.1 Runtime

tokio multi-thread flavour. rtb-cli exposes a run() that enters a Runtime if one is not already present; prefer the #[tokio::main] pattern for downstream tools.

11.2 Shutdown

  • App::shutdown is a CancellationToken.
  • rtb-cli wires tokio::signal::ctrl_c() + (on Unix) SIGTERM via signal-hook to shutdown.cancel().
  • Subsystems derive child tokens (shutdown.child_token()). A parent cancellation cascades.
  • Long-running work uses tokio::select! to race against shutdown.cancelled(). No "controller" service-manager type is provided; the JoinSet in std/tokio is sufficient.

11.3 No Controller / service manager

GTB's controls package supervises long-running services (HTTP, workers). In Rust this is the natural fit for tokio::task::JoinSet plus the tokio_graceful_shutdown crate when tiered shutdown is desired. RTB exposes a thin helper rtb_cli::services::run_services(Vec<BoxedService>, CancellationToken) but does not define a new abstraction.


12. Command authoring experience

Status (as of v0.1): the #[rtb::command] attribute macro is deferred to v0.2+ (pending a real usage pattern to crystallise around). v0.1 command authoring is hand-written impl Command with inline CommandSpec + a #[distributed_slice(BUILTIN_COMMANDS)] factory. Error ergonomics (§12.2) ARE shipped. See examples/minimal for the v0.1 pattern.

12.1 Macro (deferred)

use rtb::prelude::*;

#[rtb::command]
#[command(about = "Deploy the thing")]
pub struct Deploy {
    /// Environment name.
    #[arg(long, short)]
    env: String,

    /// Dry run.
    #[arg(long)]
    dry_run: bool,
}

#[async_trait::async_trait]
impl rtb::Command for Deploy {
    type Config = crate::Config;
    async fn run(&self, app: App<crate::Config>, _: &ArgMatches) -> miette::Result<()> {
        info!(env = %self.env, "deploying");
        Ok(())
    }
}
  • #[rtb::command] expands into:
  • clap::Args derive on the struct.
  • CommandSpec construction.
  • A fn __rtb_register() inserted into BUILTIN_COMMANDS via linkme.
  • #[rtb::command(mcp)] additionally derives schemars::JsonSchema, overrides Command::mcp_exposed to true, and emits an mcp_input_schema body that returns the derived JSON Schema.

Status (as of v0.3): the #[rtb::command] attribute macro is not yet shipped — author commands by hand-implementing Command for now. Until the macro lands, MCP-exposed commands implement mcp_exposed / mcp_input_schema directly:

impl Command for Deploy {
    fn mcp_exposed(&self) -> bool { true }
    fn mcp_input_schema(&self) -> Option<serde_json::Value> {
        Some(serde_json::to_value(schemars::schema_for!(DeployArgs)).unwrap())
    }
    /* … */
}

12.2 Error ergonomics

Return miette::Result<()>; use ? freely. For ad-hoc hints:

return Err(miette::miette!(
    help = "run `mytool init` first",
    code = "mytool::no_config",
    "no config file found in {}",
    path.display()
));

13. Security requirements (normative)

  1. #![forbid(unsafe_code)] in every workspace crate. Enforced via workspace.lints.rust.
  2. Secrets: API tokens, keychain lookups, and Git credentials must transit the codebase as secrecy::SecretString. Debug must render [REDACTED].
  3. TLS: reqwest + axum-server must be built with the rustls-tls feature. native-tls is explicitly disallowed.
  4. Update verification: SHA-256 digest and Ed25519 signature are required before self-replace runs. Tools may ship their own public key via ToolMetadata::update_verification_key.
  5. Telemetry: opt-in at both author and user levels. Machine ID is salted-SHA-256 of machine-uid; never the raw ID.
  6. cargo-deny runs in CI with the policy in deny.toml.
  7. Regex compiled from user input uses RegexBuilder::size_limit(1 MiB) + dfa_size_limit(8 MiB).

14. CI / release engineering

  • ci.yaml: rustfmt, clippy (-D warnings), cargo nextest on {linux, macOS, windows} x stable, cargo-deny, cargo doc with -D warnings.
  • release.yaml: tag-triggered cargo-dist (renamed dist) producing signed multi-platform artefacts; publishes to crates.io in dependency order.
  • Version bumping via cargo-release.
  • CHANGELOG.md authored in Keep-a-Changelog format and parsed by the changelog subcommand.

15. Acceptance criteria — 0.1 release

Status:0.1.0 shipped 2026-04-22. All 7 criteria below are closed (1–7). Criterion 8 (rtb-cli-bin scaffolder) deferred to v0.6 per the roadmap revision. See CHANGELOG.md for the per-crate detail.

Minimum shippable scope:

  1. ✅ Workspace compiles clean (just ci).
  2. rtb-app exposes App, ToolMetadata, Features, Command trait, BUILTIN_COMMANDS. (Application::builder lives in rtb-cli rather than rtb-app.)
  3. rtb-config supports env + user file + embedded default layering, typed via serde::Deserialize. Explicit reload() ships; reactive subscribe() + notify-driven auto-reload deferred to v0.2.
  4. rtb-assets exposes the overlay FS with YAML/JSON deep merging. TOML deep-merge deferred to v0.2.
  5. rtb-error exposes the Error enum and miette hook helpers.
  6. rtb-cli wires the above and ships version, doctor, init, config show. update, docs, mcp ship as FeatureDisabled stubs that get replaced by their respective crates' v0.2+ registrations (see §8).
  7. examples/minimal runs end-to-end on Linux/macOS/Windows and is covered by an assert_cmd smoke test in examples/minimal/tests/smoke.rs.
  8. rtb-cli-bin scaffolds a new project (rtb new). Deferred to v0.6; v0.1 ships the binary as a stub to reserve the rtb command name.

16. Roadmap

Shipped

  • 0.1.0 (2026-04-22) — rtb-error, rtb-app, rtb-config, rtb-assets, rtb-cli, rtb-credentials, rtb-telemetry. 151 acceptance criteria green. See CHANGELOG.md and docs/development/specs/2026-04-22-*.md for per-crate detail.
  • 0.2.0 (2026-05-01) — rtb-redact, rtb-vcs v0.1 (release slice), rtb-update v0.1, rtb-docs v0.1, rtb-config hot-reload, OTLP + HTTP JSON sinks in rtb-telemetry. update, docs, and mcp stubs removed from rtb-cli. See 2026-04-23-v0.2-scope.md for scope rationale.
  • 0.3.0 (2026-05-09) — the "operator surface" release. Bundles framework-spec milestones 0.3 and 0.4 into a single shipped workspace version:
  • Spec milestone 0.3rtb-ai v0.1 (genai + Anthropic-direct for caching), rtb-mcp v0.1 (rmcp SDK; commands self-register as MCP tools via Command::mcp_exposed). Structured output via schemars + jsonschema. See 2026-05-01-v0.3-scope.md.
  • Spec milestone 0.4rtb-tui v0.1 (Wizard, table/json render helpers, TTY-aware Spinner); rtb-cli credentials / telemetry / extended config subtrees; global --output text|json flag. App gains credentials_provider
    • App::credentials(); rtb-credentials adds CredentialBearing + Resolver::probe; rtb-config adds the mutable feature with Config::schema / Config::write; rtb-telemetry adds the persisted consent module. See 2026-05-06-v0.4-scope.md.
  • Schemars 0.8 → 1.x and tantivy 0.22 → 0.26 upstream bumps landed on develop after release; they ship in the next workspace version.
  • 0.4.1 (2026-05-09) — App typed-config integration. App gains type-erased typed-config storage with App::typed_config<C> / App::config_as<C> recovery, App::config_schema / config_value rendering, and Application::builder().config<C>(...). rtb-cli's config show / get / set / schema / validate light up the schema-aware paths automatically when the host tool opts in. See 2026-05-09-v0.4.1-scope.md.
  • 0.5.0rtb-vcs v0.2 (git-operations slice). The Repo type with init / open / clone / walk / diff / blame / status / commit / fetch / checkout / push. Read paths via gix + gix-blame; write paths via shell-out to git (consistent backend choice obsoleted the originally-planned git2-fallback opt-in — see A4). Auth via rtb-credentials::Resolver
  • inline credential.helper. See 2026-05-11-v0.5-scope.md.

Pending

  • 0.6rtb-cli-bin scaffolder with rtb new, rtb generate, rtb regenerate.
  • 1.0 — API freeze, semver commitment, full docs site.

Appendix A — Crate selection rationale

See docs/about/ecosystem-survey.md for a condensed table with alternatives considered.

Appendix B — Explicit anti-patterns

The following Go-isms are rejected in RTB. Contributors proposing them must justify against the alternative listed.

Anti-pattern Preferred Rust alternative
map[string]any config serde::Deserialize struct + figment::Figment
Functional options (func(*Options)) bon::Builder typestate
Package init() for registration linkme::distributed_slice
context.Context threading CancellationToken + async ?
ErrorHandler.Check() funnel main() -> miette::Result<()> with installed hook
interface{} DI container Strongly-typed App<C> struct
any slog field values tracing structured fields
chan struct{} for cancellation CancellationToken::cancelled()
goroutine pool with WaitGroup JoinSet with cancellation
Sub(key) dynamic config figment::select(profile) + nested typed structs
if err := check(err); err != nil ? operator + miette
Two-file command split (cmd.go + main.go) Single Rust module, split by size
mu sync.Mutex guard everywhere Prefer Arc<T> immutable data + tokio::sync::watch / ArcSwap

Appendix C — Open questions

  • O1: Should Application::builder() accept a pre-built tracing registry, or always own the subscriber? Leaning toward: accept one.
  • O2: Should the scaffolder rtb tool vendor its templates or fetch from Git? Leaning toward: vendored, offline-friendly.
  • O3: MCP tool schemas — derive from schemars::JsonSchema always, or allow hand-authored JSON Schema override? Likely both.
  • O4: Plugin discovery beyond linkme — do we need a dlopen-style runtime plugin story for downstream tools that want third-party commands? Punt until a real user asks.