Skip to content

v0.4 scope — rtb-tui v0.1 + rtb-cli ops subcommands

Status: IMPLEMENTED — both slices landed. rtb-tui v0.1 shipped via PR #32; rtb-cli ops subtree shipped on feat/rtb-cli-ops-v0.1. See each per-crate spec for caveats vs the scope addendum (notably the App<C> typed-config gap that defers config schema / typed validate to a follow-up slice). Parent contract: rust-tool-base.md §16 Roadmap. Driver: closing the "tool operator" loop. v0.3 closed the AI / MCP loop for tool consumers; v0.4 closes the day-to-day operations loop for tool users — managing credentials, telemetry consent, and config from inside the CLI rather than by editing files.


1. Motivation

v0.3 made every RTB-built tool an MCP server and an AI client, but the human-facing operator surface still has gaps:

  • The init wizard is hand-rolled on top of inquire in rtb-cli. Every downstream tool that wants its own multi-step prompt today copies that pattern. There is no Wizard widget yet despite §8.1 of the framework spec referring to one as rtb-tui::Wizard.
  • Users can declare credentials via CredentialRef and resolve them through Resolver, but cannot add, remove, list, or test stored credentials from the CLI. Operators currently have to use secret-tool or keyring directly.
  • Telemetry consent is a static CollectionPolicy on TelemetryContext — there is no persisted user opt-in. The two-level opt-in advertised in rtb-telemetry's docs assumes an enable / disable / status / reset subcommand that doesn't exist.
  • config ships as config show only. The framework spec §8.7 promised config get / set / schema / validate since v0.1; only show landed.
  • Output rendering across update, docs serve, mcp list, version, etc. is bespoke per crate. A reusable table helper around tabled would let every command hand back uniform text / json output.

v0.4 closes those gaps with one new shipped crate (rtb-tui v0.1) and three new built-in subcommand subtrees inside rtb-cli. None of the work introduces new external dependencies — every required crate is already in Cargo.lock (inquire, tabled, console).

2. Scope — what ships

2.1 rtb-tui v0.1 — reusable widgets

The shipped surface, all behind the existing tui Cargo feature on the rtb umbrella:

pub struct Wizard<S> { /* … */ }
impl<S> Wizard<S> {
    pub fn builder() -> WizardBuilder<S>;          // bon-derived
    pub async fn run(self) -> Result<S, WizardError>;
}
pub trait WizardStep<S>: Send + Sync {
    fn prompt(&self, state: &mut S) -> Result<StepOutcome, inquire::InquireError>;
}
pub enum StepOutcome { Next, Back, Cancel }

pub fn render_table<R: tabled::Tabled>(rows: &[R]) -> String;
pub fn render_json<R: serde::Serialize>(rows: &[R]) -> String;

pub struct Spinner { /* … */ }   // console-backed; suppresses when stderr isn't a TTY.

Behavioural promises:

  • Escape ↔ back navigation. inquire::InquireError::OperationCanceled on any step is interpreted as "go back one step" rather than terminating the wizard. The first step's cancel terminates the wizard with WizardError::Cancelled.
  • State threading. Wizard state S is mutable across steps; each step's prompt method receives &mut state. Dependent steps read from earlier-set fields (e.g. step 2 default-fills from step 1's selection).
  • TTY-aware spinners. When stderr is not a TTY, Spinner no-ops — important for CI logs and when a tool is invoked over an MCP transport.
  • Output dual-mode. Every table consumer wires both --output text (calls render_table) and --output json (calls render_json). Behaviour mirrors the framework spec §8 promise.

What rtb-tui v0.1 explicitly does not ship:

  • Full ratatui widgets — rtb-docs::browser is the only ratatui consumer at v0.4 and doesn't use general-purpose widgets. A ratatui widget library is a v0.5+ concern if real demand surfaces.
  • Progress bars beyond the Spinner no-TTY guard. indicatif integration is not in scope.

2.2 rtb-cli credentials subtree

Backed by rtb_credentials::CredentialStore. Subcommands:

Subcommand Behaviour
credentials list Walk the merged config for every CredentialRef field; print service / account / source / status (resolved / missing / literal-rejected). Dual-mode.
credentials add <ref-name> Wizard prompts for storage mode (env / keychain) and the secret. Writes to the store, never echoes the secret.
credentials remove <ref-name> Deletes from the underlying store — keychain only; literals must be removed via config edit.
credentials test <ref-name> Runs Resolver::resolve and reports the resolved-by-which-precedence-step result. Never prints the secret.
credentials doctor Aggregates per-credential test calls into a tabled summary; surfaces as a HealthCheck so mytool doctor picks it up too.

Reaching every CredentialRef from the CLI uses an explicit CredentialBearing trait — see §4.1. Downstream tools list their credentials in five lines; credentials list walks the merged config via that trait. No proc-macro at v0.4.

The subtree is gated behind a new runtime Feature::Credentials (compile-gated by the existing credentials Cargo feature on rtb).

2.3 rtb-cli telemetry subtree

Backed by a new persisted-consent file at <ProjectDirs::config_dir()>/<tool>/consent.toml — alongside config.yaml rather than tucked under data_dir. Consent is user-visible policy that operators should be able to inspect and audit; the state-dir option (~/.local/share/<tool>/) was rejected as actively unhelpful for an opt-in privacy primitive. File:

# version: schema for forward-compat
version = 1
# state: "enabled" | "disabled" | "unset"
state = "enabled"
# decided_at: ISO-8601, optional
decided_at = "2026-05-08T12:34:56Z"

Subcommands:

Subcommand Behaviour
telemetry status Prints current state + when it was decided. Dual-mode.
telemetry enable Writes state = "enabled", prints the privacy notice from ToolMetadata::telemetry_notice (new, optional &'static str).
telemetry disable Writes state = "disabled".
telemetry reset Removes the consent file → state reverts to unset.

Application::builder reads the consent file at startup and threads the resulting CollectionPolicy into the TelemetryContext it builds. When the file is unset, the policy is Disabled — opt-in remains the default. This is a purely additive change; tools that don't enable the telemetry Cargo feature on rtb are unaffected.

2.4 rtb-cli config subtree extension

config show already ships. v0.4 adds:

Subcommand Behaviour
config get <jsonpath> Resolves a JSON path against the merged typed config; prints the value (text or JSON).
config set <jsonpath> <value> Writes to the highest-priority writable user file. Refuses to write into the embedded-default layer. Refuses paths that aren't represented in Config::schema().
config schema Prints the JSON Schema produced by Config::schema().
config validate [--file PATH] Validates a candidate config (defaults to the merged result) against the schema. Exits non-zero on violation.

config set writes to a single canonical user-file path: <ProjectDirs::config_dir()>/<tool>/config.yaml. Tools that want to write elsewhere pass --config-file PATH. The "write to whichever layered file is highest-priority writable" alternative was rejected — predictability beats flexibility for an interactive command, and it matches git config --global behaviour.

2.5 rtb-cli rendering helpers

A thin module rtb_cli::render with two functions, used by every subcommand that prints structured data:

pub fn output<R: serde::Serialize + tabled::Tabled>(
    mode: OutputMode,
    rows: &[R],
);
pub enum OutputMode { Text, Json }

OutputMode is parsed from the --output text|json flag declared once at the top of the clap tree with Arg::global(true). clap propagates it to every subcommand automatically, so users can write mytool --output json subcommand or mytool subcommand --output json and either form works. Default is Text. Subcommands that print structured data honour the flag; interactive ones (init, mcp serve, update run --progress) silently ignore it. This closes the framework spec's "every built-in supports --output text|json" promise — v0.1 through v0.3 each wired their own.

3. Out of scope (deferred)

  • Wizard skinning / theming. The default inquire look is fine.
  • Ratatui widget library. v0.5+ if a real consumer materialises.
  • Telemetry retroactive backfill. Toggling enable does not retroactively flush events recorded under Disabled — that would break the audit-trail guarantee.
  • config edit. Pulling up $EDITOR is straightforward but bikesheds on cross-platform editor selection. Punt.
  • Credential rotation policies. v0.4 ships add/remove/list/test — automatic rotation is a separate concern.
  • Exit-code conventions overhaul. v0.4 keeps the existing 0 / non-zero split; codifying a richer table is a v1.0-readiness item.

4. Cross-cutting changes

  • rtb-app::Command — no new trait methods. Every v0.4 subtree fits in the existing subcommand_passthrough pattern (the same shape update / docs / mcp use today).
  • rtb-app::ToolMetadata — new optional telemetry_notice: Option<&'static str> field. Additive; existing builders inherit None.
  • rtb-app::Features — adds Feature::Credentials and (still) honours the existing Feature::Telemetry. Default-set additions land in §16 of the framework spec.
  • rtb-credentials — no API change. CredentialStore already exposes get / set / delete; the v0.4 CLI subtree consumes it as-is. The "list every known credential" path lives in the new config-introspection seam (§4.1 below).
  • rtb-config — adds Config::schema() -> serde_json::Value (currently a TODO in the v0.1 spec) and Config::write(path, value) for config set. Both behind a new mutable Cargo feature.
  • rtb-telemetry — adds a tiny consent module that reads/writes the consent file. The Application::builder glue lives in rtb-cli, not in rtb-telemetry.
  • rtb-cli — every new subtree registers via BUILTIN_COMMANDS. The umbrella's tui Cargo feature (currently transitively-on via docs) flips to default-on directly so Wizard is reachable without enabling docs.
  • Examples. examples/minimal gains smoke tests for credentials --help, telemetry --help, config --help — plus a real credentials add round-trip against MemoryStore (no keychain in CI).

4.1 Config introspection seam

credentials list needs to enumerate every CredentialRef in the merged config. The chosen mechanism is an explicit CredentialBearing trait on the typed config:

// in rtb-credentials
pub trait CredentialBearing {
    fn credentials(&self) -> Vec<(&'static str, &CredentialRef)>;
}

Downstream tools spell their credentials out — typically five lines:

impl CredentialBearing for MyConfig {
    fn credentials(&self) -> Vec<(&'static str, &CredentialRef)> {
        vec![
            ("anthropic", &self.anthropic.api),
            ("github",    &self.github.token),
        ]
    }
}

App::credentials() delegates to the trait. credentials list / test / doctor consume that listing.

Alternatives considered and rejected:

  • serde-trait visitor that walks the deserialised Config<C> and yields (field_path, CredentialRef) pairs. Heavyweight; needs a custom visitor wired through every config that touches credentials.
  • schemars-driven walk over Config::schema(), looking for nodes typed as CredentialRef and dereferencing each path against serde_json::to_value(&config). Tempting (zero per-tool work) but brittle: $ref resolution, oneOf/anyOf for Option, custom JsonSchema impls, and JSON-pointer-vs-Rust path mismatches turn it into a long road of edge cases for a feature that is fundamentally simple.

A #[derive(CredentialBearing)] proc-macro is deferred to v0.5 — only worth shipping if v0.4 surfaces real demand for it.

5. Acceptance criteria

The slice is implemented when:

  • rtb-tui v0.1 spec lands as APPROVED and ships with Wizard, render_table, render_json, Spinner, ≥ 90% line coverage, and an integration test that drives a 3-step wizard end-to-end.
  • The four rtb-cli subtrees (credentials, telemetry, config get/set/schema/validate, the --output text|json flag) ship with per-subcommand acceptance criteria in their per-crate specs (TBD when each spec lands).
  • examples/minimal smoke tests cover the help output for every new subtree plus the credentials add round-trip against MemoryStore.
  • cargo deny clean — no new licence or advisory hits introduced.
  • The framework spec §16 gains a v0.4 Shipped entry once the slice merges.

6. Resolutions

All five open questions resolved 2026-05-06. Recorded here for the audit trail — the spec body above carries the live behaviour.

  • O1 — credential introspection. Resolved via an explicit CredentialBearing trait downstream tools implement (§4.1). The schemars-walking option was tempting but brittle in the face of $ref / oneOf / Option plumbing; the serde-visitor option was heavyweight. A #[derive(CredentialBearing)] proc-macro is deferred to v0.5 pending real demand.
  • O2 — config set write destination. Resolved as a single canonical user-file path (<config_dir>/<tool>/config.yaml), with --config-file PATH to override. Matches git config --global; predictability beats flexibility for an interactive command.
  • O3 — telemetry consent file location. Resolved as ProjectDirs::config_dir() (alongside config.yaml), not data_dir(). Consent is user-visible policy that operators should be able to inspect and audit; hiding it under state-dir works against the openness principle of an opt-in privacy primitive.
  • O4 — wizard library. Resolved as inquire — no change. Already in use by init; dialoguer brings nothing extra.
  • O5 — --output flag placement. Resolved as a top-level flag declared once with clap's Arg::global(true), propagating to every subcommand. Both mytool --output json subcommand and mytool subcommand --output json parse identically. Subcommands that have structured output honour the flag; interactive ones silently ignore it.

7. Slicing into PRs

Two PRs, ordered by dependency:

  1. rtb-tui v0.1Wizard, render_table, render_json, Spinner, the per-crate spec (docs/development/specs/YYYY-MM-DD-rtb-tui-v0.1.md), and the rendering-helpers module in rtb-cli. Lands first because the credentials add and init flows in PR 2 depend on Wizard.
  2. rtb-cli ops subtreescredentials, telemetry, config get/set/schema/validate, the --output global flag. Includes the rtb-config mutable feature and the schemars-walk credential introspection. Closes the v0.4 scope.

The v0.3-style "release after slices land" pattern applies — once both PRs are in develop, cut a release/v0.4.0 branch, bump versions, consolidate the changelog, tag.

8. Approval gate

This addendum is APPROVED as of 2026-05-06 — open questions §6 resolved. The slice is implemented when (a) the two per-crate specs (rtb-tui v0.1 and rtb-cli v0.4 ops subtree) land as DRAFT, (b) both per-crate slices ship green tests + ≥ 90% coverage, © §16 of the framework spec gains a v0.4 Shipped entry once the slices merge, (d) the v0.4 status above flips to IMPLEMENTED.