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
initwizard is hand-rolled on top ofinquireinrtb-cli. Every downstream tool that wants its own multi-step prompt today copies that pattern. There is noWizardwidget yet despite §8.1 of the framework spec referring to one asrtb-tui::Wizard. - Users can declare credentials via
CredentialRefand resolve them throughResolver, but cannot add, remove, list, or test stored credentials from the CLI. Operators currently have to usesecret-toolorkeyringdirectly. - Telemetry consent is a static
CollectionPolicyonTelemetryContext— there is no persisted user opt-in. The two-level opt-in advertised inrtb-telemetry's docs assumes anenable / disable / status / resetsubcommand that doesn't exist. configships asconfig showonly. The framework spec §8.7 promisedconfig get / set / schema / validatesince v0.1; onlyshowlanded.- Output rendering across
update,docs serve,mcp list,version, etc. is bespoke per crate. A reusable table helper aroundtabledwould let every command hand back uniformtext/jsonoutput.
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::OperationCanceledon any step is interpreted as "go back one step" rather than terminating the wizard. The first step's cancel terminates the wizard withWizardError::Cancelled. - State threading. Wizard state
Sis mutable across steps; each step'spromptmethod 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,
Spinnerno-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(callsrender_table) and--output json(callsrender_json). Behaviour mirrors the framework spec §8 promise.
What rtb-tui v0.1 explicitly does not ship:
- Full
ratatuiwidgets —rtb-docs::browseris the onlyratatuiconsumer at v0.4 and doesn't use general-purpose widgets. Aratatuiwidget library is a v0.5+ concern if real demand surfaces. - Progress bars beyond the
Spinnerno-TTY guard.indicatifintegration 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
inquirelook is fine. - Ratatui widget library. v0.5+ if a real consumer materialises.
- Telemetry retroactive backfill. Toggling
enabledoes not retroactively flush events recorded underDisabled— that would break the audit-trail guarantee. config edit. Pulling up$EDITORis 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-zerosplit; 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 existingsubcommand_passthroughpattern (the same shapeupdate/docs/mcpuse today).rtb-app::ToolMetadata— new optionaltelemetry_notice: Option<&'static str>field. Additive; existing builders inheritNone.rtb-app::Features— addsFeature::Credentialsand (still) honours the existingFeature::Telemetry. Default-set additions land in §16 of the framework spec.rtb-credentials— no API change.CredentialStorealready exposesget / 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— addsConfig::schema() -> serde_json::Value(currently a TODO in the v0.1 spec) andConfig::write(path, value)forconfig set. Both behind a newmutableCargo feature.rtb-telemetry— adds a tinyconsentmodule that reads/writes the consent file. TheApplication::builderglue lives inrtb-cli, not inrtb-telemetry.rtb-cli— every new subtree registers viaBUILTIN_COMMANDS. The umbrella'stuiCargo feature (currently transitively-on viadocs) flips to default-on directly soWizardis reachable without enablingdocs.- Examples.
examples/minimalgains smoke tests forcredentials --help,telemetry --help,config --help— plus a realcredentials addround-trip againstMemoryStore(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 deserialisedConfig<C>and yields(field_path, CredentialRef)pairs. Heavyweight; needs a custom visitor wired through every config that touches credentials.schemars-driven walk overConfig::schema(), looking for nodes typed asCredentialRefand dereferencing each path againstserde_json::to_value(&config). Tempting (zero per-tool work) but brittle:$refresolution,oneOf/anyOfforOption, customJsonSchemaimpls, 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-tuiv0.1 spec lands asAPPROVEDand ships withWizard,render_table,render_json,Spinner, ≥ 90% line coverage, and an integration test that drives a 3-step wizard end-to-end.- The four
rtb-clisubtrees (credentials,telemetry,config get/set/schema/validate, the--output text|jsonflag) ship with per-subcommand acceptance criteria in their per-crate specs (TBD when each spec lands). examples/minimalsmoke tests cover the help output for every new subtree plus thecredentials addround-trip againstMemoryStore.cargo denyclean — 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
CredentialBearingtrait downstream tools implement (§4.1). The schemars-walking option was tempting but brittle in the face of$ref/oneOf/Optionplumbing; the serde-visitor option was heavyweight. A#[derive(CredentialBearing)]proc-macro is deferred to v0.5 pending real demand. - O2 —
config setwrite destination. Resolved as a single canonical user-file path (<config_dir>/<tool>/config.yaml), with--config-file PATHto override. Matchesgit config --global; predictability beats flexibility for an interactive command. - O3 — telemetry consent file location. Resolved as
ProjectDirs::config_dir()(alongsideconfig.yaml), notdata_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 byinit;dialoguerbrings nothing extra. - O5 —
--outputflag placement. Resolved as a top-level flag declared once with clap'sArg::global(true), propagating to every subcommand. Bothmytool --output json subcommandandmytool subcommand --output jsonparse identically. Subcommands that have structured output honour the flag; interactive ones silently ignore it.
7. Slicing into PRs¶
Two PRs, ordered by dependency:
rtb-tuiv0.1 —Wizard,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 inrtb-cli. Lands first because thecredentials addandinitflows in PR 2 depend onWizard.rtb-cliops subtrees —credentials,telemetry,config get/set/schema/validate, the--outputglobal flag. Includes thertb-configmutablefeature 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.