v0.6 scope — the rtb scaffolder¶
Status: IMPLEMENTED — all 22 slices (8a–8v) of §8 landed across
MRs !19 through !28 on 2026-06-08 / 2026-06-09. rtb generate
project | command | flag | config-field | docs, rtb remove command
| flag, rtb regenerate manifest | project, the minimal + cli
presets, marker-driven file edits, and end-to-end AI codegen with
the verifier loop are all wired up. 194 tests in rtb-cli-bin cover
the surface, including wiremock-driven AI E2E.
1. Motivation¶
rtb-cli-bin ships the rtb binary — the scaffolder + regenerator
analogue to gtb in the Go project. Framework spec §16 roadmap reserves
v0.6 for the scaffolder commands. This spec turns that one-liner into a
concrete scope.
The crate skeleton is already in place: crates/rtb-cli-bin/
declares the rtb binary, depends on rtb-app, rtb-error, clap,
tokio, miette, tracing, minijinja, inquire, serde,
serde_yaml. src/main.rs is a stub printing a version line. Templates
(templates/) do not yet exist. The framework spec §12 (Command
authoring experience), CLAUDE.md "Code generation (scaffolder)" section,
and the [[rtb-vcs v0.5 Repo memory]] (which deliberately included
Repo::init + Repo::diff for this milestone) inform the surface
sketched below.
Three framing decisions that shape everything below:
-
The
rtbbinary is itself an RTB tool — built onrtb-cli'sApplication::builder, eating its own dog food. It depends onrtb-cli(added in this milestone), so the framework's default- enabled runtime features —Init,Version,Update,Docs,Mcp,Doctor— are inherited automatically.rtb doctor,rtb version,rtb update, etc., are not commands the scaffolder re-implements; they are the framework's commands that every tool gets for free, including this one. This matches whatgtbdoes in the Go project and is the right shape for a binary whose entire purpose is to scaffold copies of itself. -
No separate
newcommand —newisgenerate project. The two operations are semantically identical (render a template tree into a destination); only the kind differs. Unifying underrtb generate <kind>gives one mental model — "rtb generates things from templates" — and makes future kinds (generate release-source,generate command-suite, …) trivial to add. The verbprojectmatches gtb's canonical form (gtb'sskeletonis a deprecated alias being removed in a future version) — see (3) below. -
Sibling, not twin, to
gtb. This milestone deliberately borrows gtb's command grammar (verb structure, kind names, common flag names, interactive-form patterns) so that anyone fluent in gtb has near-zero learning curve on rtb — and vice versa. What we don't borrow is Go-isms (noPropsstruct, no cobra builder pattern, nosetup.Commandwrapper) — those are replaced by RTB-native equivalents (App, clap derive,Command+BUILTIN_COMMANDS). The rule of thumb: users see familiar surface, contributors see idiomatic Rust. Concrete examples: gtb generate project↔rtb generate project(same verb; gtb'sskeletonis a deprecated alias on its way out).gtb generate command↔rtb generate command(same).gtb generate flag↔rtb generate flag(same).gtb generate docs↔rtb generate docs(same).gtb remove command↔rtb remove command(same; we add theremovetop-level verb that the original draft was missing).gtb regenerate project|manifest↔rtb regenerate project|manifest(same).- Common flags
--dry-run,--provider,--model,-f/--features,--overwrite ask|allow|deny,--private,--env-prefix,--help-type slack|teams|none— same names and semantics. huh.NewGroup/NewInput/NewSelect/NewMultiSelect/NewConfirm→inquire::Text/Select/MultiSelect/Confirm(the existingrtb-tui::Wizardalready wrapsinquirein this pattern).gtb's--git-backend github|gitlabbecomesrtb's--vcs-backend github|gitlab|bitbucket|gitea|codeberg— same flag idea, broader value set matchingrtb-vcs's supported providers.gtb's--go-versionbecomesrtb's--rust-version/--msrv.
2. The design tension¶
Three real choices shape this milestone, all worth surfacing before code:
-
How opinionated
rtb generate projectis. The cheap path is a single workspace-style template that always produces the same shape. The more useful path offers a small set of presets (minimal,cli,library, maybeai-tool) and a custom-feature picker. Too few presets and the scaffolder is a glorifiedcp -r; too many and the template surface area becomes a maintenance burden of its own. -
How
rtb regeneratebehaves when the human has edited the scaffolded code. The "stomp" model (overwrite) is brittle and will immediately be turned off by users. The "diff and prompt" model (informed byRepo::diff) is what the [[rtb-vcs v0.5 Repo memory]] anticipated. The "three-way merge" model is closer to what generators likecookiecutterandcopierdo — and is what downstream GTB-flavoured tools want — but it is meaningfully more work and probably belongs in a v0.6.x follow-up rather than the v0.6.0 cut. -
What the difference is between a
Kindbaked into the binary and a preset on disk. AKind(e.g.,command,config-field) is a concept the scaffolder understands —rtbknows how to register a newCommandagainstBUILTIN_COMMANDS, where the config struct lives, etc. A preset is one whole project template (e.g.,minimal,cli).generate project <name>consumes a preset;generate command <name>consumes aKindimpl. Keeping these two things distinct keeps the design honest as the kinds list grows.
All three are tracked in §7 (Open questions) for human review.
3. Sketch of the proposed surface¶
The rtb binary is itself an rtb-cli::Application — every framework
default command (doctor, version, update, docs, mcp, init)
ships out of the box because the binary builds on Application::builder.
This milestone adds two scaffolder commands on top via
BUILTIN_COMMANDS: generate and regenerate. Each is described as
a contract, not as an implementation.
3.1 rtb generate <kind> — scaffold from templates¶
Three persistent flags on the generate parent (gtb-aligned):
- --dry-run — preview without writing.
- --provider openai|gemini|claude|ollama — AI provider for
help-text / docstring generation (off by default; opt-in per kind).
- --model <name> — model override for --provider.
Kinds for v0.6.0:
3.1.1 rtb generate project [name] — fresh project¶
The most-used kind. Renders a template preset into a new directory,
with optional git init + initial commit. Flag set (gtb parity):
- -n, --name <name> — project name.
- -r, --repo <org/repo> — repository in org/repo form.
- --vcs-backend github|gitlab|bitbucket|gitea|codeberg
(default github; broader than gtb's --git-backend because
rtb-vcs supports more providers).
- --host <host> — VCS host (defaults from backend).
- --private — mark repo as private (requires a token for updates).
- -d, --description <text> — short description (default: "A tool
built with rtb").
- -p, --path <path> — destination path (default .).
- -f, --features <list> — comma-separated features to enable.
Default set: cli,update,mcp,docs,doctor,credentials,tui. Valid
values mirror the rtb umbrella's [features] block: cli,
update, docs, mcp, ai, credentials,
credentials-linux-persistent, tui, telemetry, vcs.
- --rust-version <semver> — rust-version for the generated
Cargo.toml (defaults to the running toolchain).
- --msrv <semver> — alias for --rust-version.
- --help-type slack|teams|none — help-channel type (default none).
- --overwrite ask|allow|deny — file-conflict policy (default ask).
- --slack-channel <ch>, --slack-team <name>
- --teams-channel <ch>, --teams-team <name>
- --env-prefix <PREFIX> — env-var prefix for config overrides
(e.g. MYTOOL → MYTOOL_FOO_BAR).
- --preset <name> — template preset (default minimal).
- --no-git — opt out of Repo::init + initial commit.
Behaviour: creates <path>/<name>/, validates <name> against
the user-facing identifier rules in validate.rs (NFC-normalised,
character-class checked — CLAUDE.md template-security guardrails),
refuses reserved names (rtb, rust-tool-base, anything matching
rtb-*), walks the embedded templates/<preset>/ tree via
rust-embed + vfs, renders each file through minijinja with the
field-specific escapers, writes .rtb/manifest.yaml, calls
Repo::init. Missing fields prompted for via the rtb-tui::Wizard
unless --non-interactive (CI-friendly).
3.1.2 rtb generate command <name> — add a subcommand¶
Inside an existing scaffolded project (refuses if
.rtb/manifest.yaml is absent). Writes a new Command impl + the
linkme::distributed_slice(BUILTIN_COMMANDS) registration,
matching the framework spec §12 command-authoring contract.
Flag set + interactive form (gtb-aligned): --short, --long,
--aliases a,b, --parent <path> (for nested commands, e.g.
kube/ctx), --args <ExactArgs(1)|ArbitraryArgs|…>, --with-assets,
--persistent-pre-run, --pre-run, --with-initializer,
--with-config-validation, --force (overwrite). The
multi-select-style options follow gtb's huh form; in rtb they're
either flags (CI) or inquire::MultiSelect (interactive).
AI-assisted codegen (per Q-11):
- --prompt <text-or-path> — natural-language description of the
command's behaviour, OR a path to a file containing one. The AI
drafts the Command::run body + tests against the description.
- --script <path> — path to an existing bash/python/js script to
convert. The AI ports the script's behaviour into a Rust Command
impl, preserving semantics.
- --agentless — opt out of the verifier/repair loop (see below).
One-shot generation only; useful when you trust the AI's output
or want to inspect/edit before running cargo check yourself.
Verifier / repair loop (Q-11, mirrors gtb's
internal/generator/verifier/agent.go): when --prompt or
--script is used and --agentless is not set, the generator
runs an autonomous repair loop after the first AI draft:
- Render the AI output into the target file(s).
- Run
cargo check -p <tool>against the result. - If failures → capture diagnostics + the offending code, feed back
to
rtb-ai, ask for a corrected version. Apply, go to (2). - On clean compile, run
cargo clippy -p <tool> -- -D warnings. Loop on failures. - On clean clippy, run
cargo test -p <tool>(if tests were generated). Loop on failures. - Cap at
--max-repair-iterations N(default 5). On cap-hit, leave the last attempt on disk + print the unresolved diagnostics; user takes over.
The loop is opt-in (only fires when AI is in play) and bounded
(can't run forever). --dry-run short-circuits the loop after step 1
— prints the proposed code without writing or compiling.
Sub-verbs (gtb parity):
- rtb generate command protect <command-path> — marks a generated
command as protected in .rtb/manifest.yaml; rtb regenerate
refuses to regenerate that specific command (per Q-14).
- rtb generate command unprotect <command-path> — reverse.
3.1.3 rtb generate flag <flag-name> — add a flag¶
Inside an existing project. Adds a flag to an existing Command's
clap definition (and the corresponding field on its options struct).
Flag form (mirrors gtb's FlagFormInput): --command <path>
(target command), --type string|bool|int|float|stringSlice|…,
--description <text>, --persistent, --shorthand <c>,
--required, --default <value>, --default-is-code (true if the
default is a Rust expression rather than a literal). Interactive
form prompts for whichever aren't passed.
AI-assisted help-text drafting (per Q-11): when --provider /
--model is set on the parent generate (and --description is
not supplied), rtb-ai drafts a candidate description from the
flag name + type + the target command's context. The Wizard
presents it for the user to accept / edit / regenerate before the
flag is written. --non-interactive mode either uses the AI draft
verbatim (if --provider is set) or falls back to the missing-
fields diagnostic (per Q-5).
3.1.4 rtb generate config-field <path> — typed config field¶
Inside an existing project. Adds a typed field to the tool's
AppConfig struct + a default in the embedded default.yaml. Flags:
--type bool|string|int|f64|…, --description, --default. No
gtb counterpart — this is an rtb-specific kind because rtb-config
uses typed structs while gtb-config uses dynamic accessors. Marked as
rtb-only in the help text.
3.1.5 rtb generate docs — component / how-to docs¶
Inside an existing project. Generates Markdown docs for the
project's components / commands. Writes under docs/components/
and docs/how-to/ matching the microsite layout used by rtb itself.
Two passes, gated by --provider (per Q-12 = full parity):
- Extraction pass (always runs) — walks the project source,
pulls clap subcommand metadata + flag definitions +
///doc comments + the manifest's[generated]index. Renders the structural skeleton of each Markdown page from this — headings, flag tables, link cross-references, status pills. Pure mechanical transform; reproducible. - AI-prose pass (runs when
--provideris set) — for each page the extraction pass produced, the AI fills in narrative sections: a short introductory paragraph, "when to use this", worked examples, edge-case notes. The user is shown a diff per page (or per section if--page-by-page) and accepts / rejects / regenerates before the file is written.
Flags: -p, --path <path>, --page-by-page (review each
generated page individually rather than the batch), --provider /
--model (inherited from parent), --update-docs (see §3.3.1 —
when invoked through regenerate project --update-docs, both
passes target only pages whose source code has changed since the
manifest's last-generated hash).
Common behaviour across all kinds:
- Field-specific escapers run at non-code render sites —
escape_yaml, escape_toml, escape_markdown, escape_shell_arg
— mirroring the GTB template_escape.go rules.
- Updates .rtb/manifest.yaml to record what was generated (the
source of truth for rtb regenerate).
- Prompts (via inquire-backed rtb-tui::Wizard) for any field not
supplied by flags. --non-interactive requires every field by
flag — designed for CI scaffolding.
3.2 rtb remove <kind> — counterpart to generate¶
Mirrors gtb's remove top-level verb. Removes generator-owned
artefacts from a project, keeping .rtb/manifest.yaml consistent.
Kinds for v0.6.0:
- rtb remove command <path> — removes a generated Command
(source file + BUILTIN_COMMANDS registration entry + manifest
[generated] entry). Refuses if the command is protected (per
3.1.2's protect/unprotect) unless --force.
- rtb remove flag <command-path> <flag-name> — removes a flag
from a generated command's Args struct and from any rendered
template fragments referencing it (per Q-13). Like remove command,
refuses on a protected parent command unless --force. Updates
the manifest's per-command flag inventory.
Common flags: --dry-run, --force.
Out of contract for v0.6.0: rtb remove config-field. Deferred
until the field-level diffing story is in (per Q-13).
3.3 rtb regenerate <kind> — drift detection + reconciliation¶
Persistent flag: --dry-run (preview without writing).
Kinds for v0.6.0:
3.3.1 rtb regenerate project — re-render the tool¶
Behaviour:
- Reads .rtb/manifest.yaml.
- Renders the template tree fresh into a temp dir at the manifest's
recorded preset + field values.
- Uses Repo::diff (rtb-vcs v0.5) to compute the three-way set —
{current working tree, last-generated state, freshly-regenerated
state} — and presents one of three modes:
- Show (--overwrite=deny or --dry-run): print the diff,
no writes.
- Apply-clean (--overwrite=ask, the default per Q-16):
overwrite untouched generator-owned files; for edited ones,
prompt the user (interactive). Under --non-interactive the
prompt cannot fire, so the mode falls through to deny for
those edited files — they are left untouched and the diff is
printed for the operator's later review.
- Apply-force (--overwrite=allow or --force): overwrite
everything; warn hard.
- Per-command protected: honoured (per Q-14, mirroring gtb's
ErrCommandProtected): any path listed under protected: in the
manifest is refused outright — regenerate does not overwrite it
regardless of --overwrite/--force. The user must rtb generate
command unprotect <path> first if they really want it regenerated.
An informational Skipped (protected): <path> line is printed per
refusal so the operator knows what was held back.
- Manifest is rewritten on success to record the new last-generated
state hash.
Flag set (gtb-aligned): -p, --path <path>, --force,
--overwrite ask|allow|deny (default ask, per Q-16),
--update-docs (re-render the generate docs output as well —
docs are now in-scope per Q-12 and §3.1.5's two-pass model).
3.3.2 rtb regenerate manifest — rebuild the manifest¶
Walks the project's source tree, infers which files would be
generator-owned (by comparing against fresh template renders), and
rewrites .rtb/manifest.yaml. Useful after manual edits / hand
ports / version-skew situations. Flag: -p, --path <path>.
3.4 Interactive forms — Wizard / inquire mapping¶
For every kind, fields not supplied by flags are collected through an
rtb-tui::Wizard (which wraps inquire). The form patterns are a
1:1 conceptual port of gtb's huh patterns:
| gtb (huh) | rtb (inquire / Wizard) | Use case |
|---|---|---|
huh.NewGroup(…) |
Wizard::new().step(WizardStep::…) |
Grouping related fields |
huh.NewInput().Title(…).Description(…).Value(&x).Validate(fn) |
inquire::Text::new(title).with_help_message(desc).with_validator(fn) |
Free-text input |
huh.NewSelect[T]().Options(…) |
inquire::Select::new(prompt, vec![…]) |
Single choice |
huh.NewMultiSelect[T]().Options(…) |
inquire::MultiSelect::new(prompt, vec![…]) |
Multi-select |
huh.NewConfirm().Affirmative("X").Negative("Y").Value(&b) |
inquire::Confirm::new(prompt).with_default(false) |
Yes/no |
Escape-to-back behaviour follows the Wizard's existing convention
(map InquireError::OperationCanceled to "back one step"). The
form sequence per kind is documented inline in the kind's source
file, not in a central registry — same shape gtb uses.
--non-interactive mode: every required field must be supplied
by flag. Missing fields produce one diagnostic with the full list
of missing fields, not one error per field — easier to drive from
CI.
3.5 Templates (crates/rtb-cli-bin/templates/)¶
- One directory per preset (
minimal/,cli/,library/, …). - Files use
minijinja(Jinja2-compatible runtime templates). The filename itself can carry placeholders ({{ project_name }}/). - Embedded into the
rtbbinary at compile time viarust-embed(consistent withrtb-assets). Allows airgapped scaffolding. - Every preset declares a small
_preset.yaml(or.toml) at its root listing the fields it needs + default values. The scaffolder reads this to know what to prompt for and what to write into the manifest.
3.6 Manifest (<scaffolded-project>/.rtb/manifest.yaml)¶
# .rtb/manifest.yaml — source of truth for `rtb regenerate`.
rtb_version: "0.6.0"
preset: minimal
preset_version: 1
fields:
project_name: widget
author: "Matt Cockayne <[email protected]>"
license: MIT
# … etc, mirroring the preset's _preset.yaml
# blake3 hash of the last-rendered output, indexed by relative path.
# `rtb regenerate` compares the working-tree state against these to
# distinguish "user edits" from "out-of-date generator output".
generated:
src/main.rs: "b3:abc123…"
Cargo.toml: "b3:def456…"
# …
# Commands the user has called `rtb generate command protect` on.
# `rtb regenerate` leaves these alone entirely (see Q-14).
protected:
- <command-path>
The schema is versioned via rtb_version so future format changes
can be migrated. Pre-1.0 we explicitly reserve the right to evolve the
keys; downstream tools should not parse this file outside rtb.
3.7 rtb-cli-bin module layout¶
Mirrors the v0.5 module-layout discipline: one module per concern, no
"manager" / "service" splits. The binary builds on
rtb_cli::Application::builder — main.rs looks like any downstream
RTB tool's main.rs, registering only the scaffolder-specific
commands; everything else (doctor, version, …) comes from
rtb-cli's BUILTIN_COMMANDS.
crates/rtb-cli-bin/src/
main.rs — `rtb_cli::Application::builder().run().await`
validate.rs — NFC + char-class + reserved-name checks
template/
mod.rs — minijinja env + escaper registration
render.rs — walk + render a preset tree
embed.rs — rust-embed-backed preset loader
manifest.rs — read/write/validate `.rtb/manifest.yaml`
ai/
mod.rs — `rtb-ai` client wrapper (provider/model resolution)
codegen.rs — prompt→Rust / script→Rust drafting (Q-11)
docs_prose.rs — narrative-pass for `generate docs` (Q-12)
verifier.rs — autonomous repair loop (cargo check / clippy / test)
commands/
mod.rs — re-exports
generate/
mod.rs — `rtb generate <kind>` dispatch + persistent flags
project.rs — `project` kind (was `rtb new` in earlier drafts)
command.rs — `command` kind (with `protect`/`unprotect` sub-verbs)
flag.rs — `flag` kind
config_field.rs — `config-field` kind (rtb-only)
docs.rs — `docs` kind
remove/
mod.rs — `rtb remove <kind>` dispatch
command.rs — `command` kind
flag.rs — `flag` kind (per Q-13)
regenerate/
mod.rs — `rtb regenerate <kind>` dispatch + persistent flags
project.rs — `project` kind
manifest.rs — `manifest` kind
wizard.rs — Wizard step definitions per kind (uses `rtb-tui::Wizard`)
error.rs — `thiserror` + `miette::Diagnostic`
Adding a new kind in v0.7+ = one new file under the right
commands/<verb>/ subdir plus an entry in the verb's kind enum and
the manifest's [generated] indexer.
4. Implementation outline¶
Lean enough to slice (§8):
- Wire
rtb-cli-binontortb-cli— add thertb-clidep, replace the bespoke clap setup inmain.rswithApplication::builder().metadata(…).version(…).run().await, verifyrtb doctor/rtb versionetc. surface from the framework for free. Features per Q-10/Q-15:Initstays enabled (default; per Q-15 users opt out per-tool, not in the scaffolder),AiandTelemetryenabled (gtb parity per Q-10/Q-11). No scaffolder commands yet — this is the foundation. - Embed plumbing —
rust-embedovertemplates/,vfsoverlay, minijinja env, escapers,validate.rs. Still no commands. rtb generate project(minimal preset only) — end-to-end against one preset; manifest written;Repo::initinvoked; non- interactive path covered byassert_cmd+instasnapshots.Wizardintegration — per-kind step sequences, Escape-to-back convention,--non-interactivefailure mode (single diagnostic with full missing-fields list).rtb generate command+protect/unprotectsub-verbs — extend manifest's[generated]and[protected]maps. No AI yet.rtb generate flag— interactive flag-form, type select, default-is-code handling. Updates the target command's clap definition + options struct.rtb generate config-field— typed config field on the tool'sAppConfigstruct + default indefault.yaml.- AI plumbing —
ai/module —rtb-aiclient wrapper,--provider/--modelpersistent flags ongenerate, provider resolution from env (Q-7 → install path iscargo install rtb-cli-bin, so resolution mirrors any other rtb tool). No command-level use yet; just the wiring. generate command --prompt/--script— AI codegen path (Q-11). First draft only; verifier loop comes next.- Verifier / repair loop —
ai/verifier.rsdrivescargo check→ AI →cargo clippy→ AI →cargo test→ AI, capped by--max-repair-iterations.--agentlessshort-circuits. rtb generate flag --descriptionAI-drafting — narrow application of the AI plumbing to flag descriptions (Q-11).rtb generate docs— extraction pass — clap metadata + doc comments + manifest walk → structural Markdown skeleton. No AI.rtb generate docs --provider— AI prose pass — narrative fills, per-page or batch diff prompts (Q-12).--page-by-pagereview flag.rtb remove command— counterpart togenerate command; honoursprotectedunless--force.rtb remove flag— counterpart togenerate flag(Q-13).rtb regenerate project—Repo::diffdriven; the three--overwritemodes (ask/allow/deny) land here;protectedfiles always skipped (per Q-14 / gtb'sErrCommandProtectedsemantics);--non-interactivefalls through todenyfor edited generator-owned files (per Q-16).rtb regenerate project --update-docs— pipe through to thegenerate docstwo-pass flow on the regen target's source set (Q-12).rtb regenerate manifest— walk source tree, infer generator-owned files, rewrite.rtb/manifest.yaml.- Second template preset (
cli) once 1–18 are green — proves the preset abstraction.
Each slice ships its own assert_cmd E2E scenario per the engineering
standards (CLI commands + lifecycle changes must include one). AI
slices (8–13) additionally stub the provider in tests via
wiremock — the integration tests must not call out to a real
provider.
5. Out of scope (deferred — v0.6.x or later)¶
- True three-way merge on
rtb regenerate(vs the show / apply- clean / apply-force modes above). Likely v0.7 once the data model is validated. rtb remove config-field— deferred per Q-13 until field-level diffing exists.remove commandandremove flagship in v0.6.0.- Post-generation hooks beyond the verifier loop — formatter runs,
external tool installs. The Q-11 verifier (
cargo check/clippy/testdriven byrtb-ai) is in scope; everything else is not. - Editor integrations (VS Code extension, JetBrains plugin).
- Network template registry — fetching presets from a remote URL. Embedded-only at v0.6.0.
- Template-author tooling (
rtb template lint/rtb template test). Authoring templates stays a CLAUDE.md-documented workflow at v0.6.0. - Cross-project regeneration sweeps (one command that finds every
.rtb/manifest.yamlunder a path and regenerates each). v0.7.
Promoted INTO scope by §9 resolutions (was previously out of scope):
rtb regenerate project --update-docsand thegenerate docsAI-prose pass (per Q-12).- AI-assisted command codegen (
--prompt/--script) and the autonomous repair loop (per Q-11). These are gtb USPs; they ship with v0.6.0. rtb remove flag(per Q-13).
6. Public-API stability impact¶
rtb-cli-bin— first stable surface;rtb-cli-binitself is a binary crate, so its only "public API" is the CLI grammar —rtb generate <kind> [name] [flags],rtb remove <kind> [flags],rtb regenerate <kind> [flags], and the inherited framework defaults (rtb doctor,rtb version,rtb update,rtb docs,rtb mcp,rtb init). The §9 resolutions widen the v0.6.0 grammar beyond the original sketch: new sub-verbs (generate command protect/unprotect,remove flag), AI flags ongenerate command(--prompt,--script,--agentless,--max-repair-iterations), AI-aware flags ongenerate docs(--page-by-page,--update-docs), and the--non-interactivefall-through semantics onregenerate project. The widened grammar is what we will freeze in 1.0.rtb-cli—rtb-cli-bingains a new dependency onrtb-cli(replacing the bespoke clap binary withApplication::builder). This does not affectrtb-cli's own public API; it just consumes it. The framework now has one more example of a realrtb-cli-based tool — the scaffolder eats its own dog food.rtb-vcs— no new public types needed;Repo::initandRepo::diff(both stable in v0.5) are the contact points.rtb-app— no public-API change anticipated.- No new dependency of the umbrella crate on
rtb-cli-bin. Downstream tools do not pull minijinja / inquire / etc. via the umbrella's default features. Scaffolder is consumed viacargo install rtb-cli-bin. .rtb/manifest.yaml— pre-1.0 the schema is allowed to evolve; downstream tools should not parse this file outsidertb. Versioned via thertb_versionfield so future migrations can be detected.
7. Open questions¶
Each is a real choice for the human reviewer. Marked [Q-N] so they can be addressed in §9 (Resolutions).
- [Q-1] Preset set for v0.6.0. One (
minimal) or two (minimal+cli)? Recommendminimal+clito prove the preset abstraction without over-committing template content. - [Q-2] Regenerate reconciliation mode at v0.6.0. Three options
(show / apply-clean / apply-force) or just show + apply-force?
Recommend all three —
apply-cleanis the one downstream tools will actually use day-to-day. - [Q-3] Manifest format. TOML (CLAUDE.md), YAML, or JSON? Recommend
TOML for human-editability + alignment with
Cargo.toml. The_preset.yamlschema-declaration file uses YAML because it carries free-form prompt definitions; this is a deliberate split. - [Q-4] Hashing in the
[generated]block. blake3 (workspace already has it) is fast and stable; sha256 is more conventional. Recommend blake3 for parity with the asset pipeline; document the choice. - [Q-5] Interactive prompts behaviour. When a field is missing in
--non-interactivemode, fail-with-help, or fail-with-list-of-missing- fields? Recommend the latter — easier to drive from CI. - [Q-6]
rtb generate projectgit behaviour. AlwaysRepo::initunless--no-git, or default to no-git unless--git? Recommend "init unless opted out" to match GTB and reader expectations. - [Q-7] Where the scaffolder ships. The umbrella
rust-tool-basedoes NOT depend onrtb-cli-bin. Users install viacargo install rtb-cli-bin. Confirm; document in CLAUDE.md. - [Q-8] Reserved project names — refuse
rtb,rust-tool-base,rtb-*. Anything else? Confirm. - [Q-9] Version-skew on
rtb regenerate— a newerrtbscaffolded a project, an olderrtblater runsregenerate. Hard-error on version-down, or warn? Recommend hard-error with a clear "updatertbfirst" diagnostic. - [Q-10] Inherited-command surface. The
rtbbinary picks up the framework's default-enabled commands (Init,Version,Update,Docs,Mcp,Doctor). Should any of these be disabled for the scaffolder specifically? Recommend no — they're all useful on the scaffolder binary itself (rtb doctorvalidates the user's environment,rtb updateself-updates the scaffolder,rtb docssurfaces the scaffolder's own embedded docs). - [Q-11] AI integration (
--provider/--model) at v0.6.0. gtb threads AI into help-text + docstring generation via these flags. Should rtb v0.6.0 ship AI-assisted scaffolding at parity, or land the flags as no-op stubs (forward-compat) and wire them in v0.6.x? Recommend stubs at v0.6.0 — the publish + manifest mechanics are the load-bearing work; AI is value-add that should not slip the milestone. - [Q-12]
rtb generate docsscope at v0.6.0. Pure extraction (clap metadata +///doc comments → Markdown), or also AI-assisted prose via--provider? Recommend pure extraction at v0.6.0, AI prose at v0.6.x. - [Q-13]
rtb removescope at v0.6.0. Onlyremove command, or alsoremove flag/remove config-field? Recommendremove commandonly — the others depend on AST surgery against existing source that's likely fiddlier than its slice value. - [Q-14]
protect/unprotectsemantics. When a command isprotected, doesrtb regenerate projectalso skip the command's registration inBUILTIN_COMMANDS, or just the command's source file (re-registering anyway)? Recommend "skip both" — protect means "leave this alone entirely". - [Q-15] Feature flag default set in
rtb generate project. gtb's default isinit,update,mcp,docs,doctor,changelog,keychain; rtb's closest equivalent iscli,update,mcp,docs,doctor,credentials,tui. Confirm — particularly whethertuishould be on by default (modest dep cost; nice scaffolded UX) and whethervcsshould be off (heavy gix dep; opt-in). - [Q-16]
--overwrite ask|allow|denydefault. gtb defaults toask. Recommend matching.
8. Slicing plan¶
Same pattern v0.5 used — one slice per feat(cli-bin): commit / MR,
each ending green:
| Slice | Contents | Acceptance |
|---|---|---|
| 8a | Wire rtb-cli-bin onto rtb-cli::Application::builder (Init/Ai/Telemetry features per Q-10/Q-11/Q-15) |
E2E: rtb doctor, rtb version, rtb --help all surface from the framework |
| 8b | rust-embed + minijinja plumbing + validate.rs |
unit-tested; no scaffolder commands yet |
| 8c | rtb generate project (minimal preset, non-interactive only) |
E2E: rtb generate project -n widget … && cd widget && cargo check |
| 8d | Manifest read/write + Repo::init integration |
E2E: generated project has .rtb/manifest.yaml + clean git history |
| 8e | Wizard per-kind step sequences + --non-interactive mode |
TUI golden tests; --non-interactive produces single missing-fields diagnostic |
| 8f | rtb generate command <name> + protect / unprotect |
E2E: new subcommand appears in <tool> --help; protected status round-trips |
| 8g | rtb generate flag (no AI) |
E2E: new flag appears on target command's --help |
| 8h | rtb generate config-field <path> |
E2E: field appears in <tool> config show |
| 8i | AI plumbing — ai/ module, --provider/--model flags wired through generate (Q-11) |
unit: provider resolution; wiremock-backed integration; no command consumes it yet |
| 8j | rtb generate command --prompt / --script — first AI draft only |
E2E (wiremock): scripted prompt → rendered Rust file matches snapshot |
| 8k | Verifier / repair loop (ai/verifier.rs) — cargo check / clippy / test cycle, --max-repair-iterations, --agentless opt-out (Q-11) |
E2E: forced-error fixture converges; cap-hit produces actionable diagnostic |
| 8l | rtb generate flag --description AI-drafting (Q-11) |
E2E: prompt-less flag with --provider set produces accepted draft |
| 8m | rtb generate docs — extraction pass, no AI |
E2E: writes docs/components/<crate>.md for every workspace crate |
| 8n | rtb generate docs --provider — AI prose pass + --page-by-page (Q-12) |
E2E (wiremock): narrative sections rendered; per-page review honoured |
| 8o | rtb remove command <path> |
E2E: subcommand disappears; protected refuses without --force |
| 8p | rtb remove flag <command> <flag> (Q-13) |
E2E: flag disappears; protected parent refuses without --force |
| 8q | rtb regenerate project — show/--overwrite=deny mode |
E2E: no writes; diff printed |
| 8r | rtb regenerate project --overwrite=ask + --non-interactive deny fall-through (Q-16) |
E2E: edits preserved or overwritten per prompt; --non-interactive falls through to deny on edited files |
| 8s | rtb regenerate project --overwrite=allow (force) + per-command protected refusal (Q-14) |
E2E: warning + everything overwritten EXCEPT items on protected: list |
| 8t | rtb regenerate project --update-docs — pipe through to §3.1.5 docs flow (Q-12) |
E2E: only changed pages re-rendered; manifest hashes update |
| 8u | rtb regenerate manifest |
E2E: walks source tree, rewrites manifest, idempotent |
| 8v | Second template preset (cli) |
end-to-end scaffold + regenerate green for both presets |
Each slice gets feat(cli-bin):. E2E scenarios live under
crates/rtb-cli-bin/tests/e2e/ per the engineering-standards rule.
AI-touching slices (8i–8n) stub providers via wiremock; the
integration tests must not call live providers.
9. Resolutions¶
| Q | Decision | Notes |
|---|---|---|
| Q-1 | (B) minimal + cli |
Ship two presets in v0.6.0. Proves the preset abstraction under two shapes before the surface freezes; minimal is a strict subset of cli, doubles as the smallest-possible-RTB-tool documentation example. |
| Q-2 | (A) all three modes (deny / ask / allow) |
Parity with gtb. ask (default) is the mode humans use day-to-day; deny (preview / --dry-run) and allow (force) cover the CI/automation poles. Per-file inquire::Select for the interactive prompt; --non-interactive refuses on conflicts cleanly. |
| Q-3 | (B) YAML — .rtb/manifest.yaml |
gtb-consistency takes priority over ecosystem-purity here: the manifest format is a user-facing surface that anyone hopping between gtb and rtb projects sees first. YAML also reads cleaner for the [generated] file→hash map (no quoted dotted keys). _preset.yaml and the manifest now share a format — one fewer thing to learn. |
| Q-4 | (A) blake3, with a "b3:" algorithm prefix |
Faster and idiomatic for internal-content hashing; both blake3 and sha2 are already in the workspace so there's no dep cost either way. gtb writes raw sha256 in its own manifest, so cross-tool manifest interop is out of scope for v0.6.0 — explicitly noted as a future consideration. The "b3:" prefix is the escape hatch: a future migrator can detect a foreign sha256: prefix and convert if interop ever becomes a goal. |
| Q-5 | (B) single missing-fields list | One miette::Diagnostic listing every missing required flag at once. Saves N CI iterations for users who hit it (1 fix vs N), and the structured help = block tells them exactly which flags to add. Format: error: --non-interactive requires --name, --repo, --description + code + help. |
| Q-6 | (A) Repo::init + initial commit by default, --no-git to opt out |
Matches cargo new and user expectation. Smart-default: if Repo::open finds a parent .git, skip init automatically (handles the "scaffold into a monorepo" case without forcing everyone else to type --git). gtb hasn't shipped this yet — rtb gets it in early; gtb will follow. |
| Q-7 | Confirmed — cargo install rtb-cli-bin |
Umbrella rust-tool-base does NOT depend on rtb-cli-bin; downstream tools never pull minijinja / inquire / templates into their binaries via the umbrella. Doc update implied: CLAUDE.md gains a short paragraph noting cargo install rtb-cli-bin as the install path. |
| Q-8 | Refuse: rtb, rust-tool-base, rtb-*, + Rust reserved (test, proc-macro, proc_macro, core, std, alloc, extern, crate, self, super, Self, _) |
First three: avoid collisions with the umbrella + framework namespace. The Rust reserved list mirrors what cargo new itself enforces — anything that would confuse cargo build. cargo and cargo-* are left allowed so users can legitimately scaffold cargo subcommands. crates.io existence is not checked (network flake; user gets the registry's own error at publish time). |
| Q-9 | (A) hard-error on any version-down | An older rtb cannot know what newer manifests/templates encode (e.g. a protected flag added in a later release would be silently dropped on "best-effort" regen). Diagnostic includes the required version range + exact cargo install rtb-cli-bin --version "^X.Y" command. The reverse direction (newer rtb, older manifest) is always allowed via in-band manifest schema migration — we own both ends. Pre-1.0 every minor counts as breaking; revisit a major-only rule post-1.0. |
| Q-10 | Disable Init. Keep Version, Update, Docs, Mcp, Doctor. Additionally enable Ai + Telemetry. |
Matches gtb's own feature set verbatim (Disable(InitCmd) + Enable(AiCmd) + Enable(TelemetryCmd)). Init is the first-run config wizard — rtb has no per-user config of its own, defaults are baked in. Doctor stays (gtb keeps it; useful for environment validation: cargo present, toolchain version, etc.). Ai/Telemetry enabled to match gtb and to give the scaffolder its own telemetry channel + AI access for the codegen + repair-loop work (per Q-11). |
| Q-11 | (B) Wire AI fully — including code generation and self-repair loop | AI is a gtb USP, not nice-to-have help text. v0.6.0 ships parity: --provider, --model, --prompt <text\|path>, --script <path>, --agentless on generate command (and generate flag for help-text drafting). The agentic verifier/repair loop drives cargo check → cargo clippy → cargo test iteratively, feeding failures back to rtb-ai until the generated code is clean (mirrors gtb's internal/generator/verifier/agent.go). --agentless opts out for users who want one-shot generation. This significantly expands the milestone; slicing plan in §8 updated to reflect the new work. |
| Q-12 | (C) Full parity — extraction + AI prose + --update-docs on regenerate project |
Since Q-11 put the AI plumbing on the critical path anyway, the marginal cost from "extraction + AI prose" to "full parity including --update-docs" is small (a different prompt + a different input-gathering step). Without --update-docs the AI-generated docs go stale the moment the code changes — that's the loop-closer that makes AI docs actually useful long-term. Slicing plan in §8 updated. |
| Q-13 | (B) remove command + remove flag |
Slightly beyond gtb's current remove command-only shape. remove flag slots cleanly into the manifest infrastructure we're building anyway (each generated flag has a manifest entry) and avoids the "I can generate but not ungenerate" trap on the second-most-common kind. remove config-field deferred to v0.7 — AST surgery on the Config struct + default.yaml is fiddlier and we want to see real usage before locking the design. |
| Q-14 | (B) Skip command source + registration. Per-command flag; templates use one-command-per-file. | Matches gtb exactly. gtb stores Protected *bool per command on the manifest entry; during regenerate, if the existing manifest has Protected=true and the user hasn't passed an explicit override, the generator returns ErrCommandProtected and refuses to regenerate that specific command. No AST surgery — gtb leans on its template convention of one-command-per-file so "skip this command" effectively means "skip this file." rtb adopts the same convention: every generated command lives in pkg/cmd/<name>/mod.rs (or equivalent) with the #[distributed_slice] registration adjacent, so protection translates cleanly to file-level skip without collateral damage to siblings. |
| Q-15 | Two-layer defaults. Cargo features baked into the generated Cargo.toml [dependencies.rust-tool-base]: minimal = cli, update, docs, doctor; cli = cli, update, docs, mcp, doctor, credentials, tui. Off in both defaults: vcs (heavy gix dep — opt-in), telemetry (privacy default — opt-in), ai (heavy AI deps — opt-in). Runtime Features in the generated main.rs's Application::builder().features(…): Init stays enabled in the cli preset (it's framework-default-on, so the generated main.rs just doesn't call .disable(Feature::Init) — matches what gtb does for projects it scaffolds, distinct from rtb/gtb's own binaries which disable Init per Q-10). The user can opt out of Init (or any other runtime feature) by editing the generated Application::builder() block — explicit and discoverable. |
|
| Q-16 | ask is the default; ask + --non-interactive falls through to deny |
Matches gtb. Interactive users get a useful default (prompted on real conflicts, silent on clean updates); CI users get safety-by-default (no surprise writes) because the fallthrough means ask can't silently pick a side when no human is there to answer. Falls cleanly out of the per-file inquire::Select from Q-2 — when stdin isn't a TTY (proxy for --non-interactive), the conflict is treated as deny and the regenerate run exits non-zero with the list of conflicted files. |
Downstream framework-spec amendments triggered by these resolutions get listed below the table — same pattern as the v0.5 scope spec.
Landed¶
All 22 slices of §8 are implemented. The MR map (chronological,
matching the commit log on main):
| MR | Slice(s) | Title |
|---|---|---|
| !19 | spec finalisation + 8a | Application::builder foundation |
| !20 | 8b | template-render foundation + validators |
| !21 | 8c | rtb generate project --preset minimal |
| !22 | 8d | .rtb/manifest.yaml + Repo::init |
| !23 | — | cargo-deny waiver prune (CI unblocker) |
| !24 | 8e | Wizard + non-interactive missing-fields diagnostic |
| !25 | 8f | rtb generate command + protect / unprotect |
| !26 | 8g | rtb generate flag (no AI) |
| !27 | 8h, 8m, 8o, 8p, 8q, 8r, 8s, 8t, 8u, 8v + AI flag surface | the batch |
| !28 | 8i, 8j, 8k, 8l, 8n | wire rtb-ai end-to-end |
Subsequent material changes to scope or grammar require a follow-up amendment to this spec, not a fresh draft.
Known follow-ups not in scope of v0.6¶
- Multi-segment command paths on
generate command/remove command(currently rejected with a clear diagnostic). - Real-provider validation against live
ANTHROPIC_API_KEY/OPENAI_API_KEY. Wiremock proves the wire format; a user-run smoke test confirms the live happy path. - The
clipreset'scargo checkisn't asserted in CI because it pullsrust-tool-basefrom crates.io; the embed-walk unit test confirms the template files are bundled, and rendering is exercised in unit tests against an offline fixture. cargo-denywaiver audit (task #115 — yaml-rust, bincode 1.3, rustls-pemfile, proc-macro-error2).