Skip to content

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:

  1. The rtb binary is itself an RTB tool — built on rtb-cli's Application::builder, eating its own dog food. It depends on rtb-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 what gtb does in the Go project and is the right shape for a binary whose entire purpose is to scaffold copies of itself.

  2. No separate new command — new is generate project. The two operations are semantically identical (render a template tree into a destination); only the kind differs. Unifying under rtb 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 verb project matches gtb's canonical form (gtb's skeleton is a deprecated alias being removed in a future version) — see (3) below.

  3. 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 (no Props struct, no cobra builder pattern, no setup.Command wrapper) — 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:

  4. gtb generate projectrtb generate project (same verb; gtb's skeleton is a deprecated alias on its way out).
  5. gtb generate commandrtb generate command (same).
  6. gtb generate flagrtb generate flag (same).
  7. gtb generate docsrtb generate docs (same).
  8. gtb remove commandrtb remove command (same; we add the remove top-level verb that the original draft was missing).
  9. gtb regenerate project|manifestrtb regenerate project|manifest (same).
  10. Common flags --dry-run, --provider, --model, -f/--features, --overwrite ask|allow|deny, --private, --env-prefix, --help-type slack|teams|none — same names and semantics.
  11. huh.NewGroup / NewInput / NewSelect / NewMultiSelect / NewConfirminquire::Text / Select / MultiSelect / Confirm (the existing rtb-tui::Wizard already wraps inquire in this pattern).
  12. gtb's --git-backend github|gitlab becomes rtb's --vcs-backend github|gitlab|bitbucket|gitea|codeberg — same flag idea, broader value set matching rtb-vcs's supported providers.
  13. gtb's --go-version becomes rtb's --rust-version / --msrv.

2. The design tension

Three real choices shape this milestone, all worth surfacing before code:

  1. How opinionated rtb generate project is. 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, maybe ai-tool) and a custom-feature picker. Too few presets and the scaffolder is a glorified cp -r; too many and the template surface area becomes a maintenance burden of its own.

  2. How rtb regenerate behaves 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 by Repo::diff) is what the [[rtb-vcs v0.5 Repo memory]] anticipated. The "three-way merge" model is closer to what generators like cookiecutter and copier do — 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.

  3. What the difference is between a Kind baked into the binary and a preset on disk. A Kind (e.g., command, config-field) is a concept the scaffolder understandsrtb knows how to register a new Command against BUILTIN_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 a Kind impl. 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. MYTOOLMYTOOL_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:

  1. Render the AI output into the target file(s).
  2. Run cargo check -p <tool> against the result.
  3. If failures → capture diagnostics + the offending code, feed back to rtb-ai, ask for a corrected version. Apply, go to (2).
  4. On clean compile, run cargo clippy -p <tool> -- -D warnings. Loop on failures.
  5. On clean clippy, run cargo test -p <tool> (if tests were generated). Loop on failures.
  6. 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):

  1. 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.
  2. AI-prose pass (runs when --provider is 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 rtb binary at compile time via rust-embed (consistent with rtb-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::buildermain.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):

  1. Wire rtb-cli-bin onto rtb-cli — add the rtb-cli dep, replace the bespoke clap setup in main.rs with Application::builder().metadata(…).version(…).run().await, verify rtb doctor / rtb version etc. surface from the framework for free. Features per Q-10/Q-15: Init stays enabled (default; per Q-15 users opt out per-tool, not in the scaffolder), Ai and Telemetry enabled (gtb parity per Q-10/Q-11). No scaffolder commands yet — this is the foundation.
  2. Embed plumbingrust-embed over templates/, vfs overlay, minijinja env, escapers, validate.rs. Still no commands.
  3. rtb generate project (minimal preset only) — end-to-end against one preset; manifest written; Repo::init invoked; non- interactive path covered by assert_cmd + insta snapshots.
  4. Wizard integration — per-kind step sequences, Escape-to-back convention, --non-interactive failure mode (single diagnostic with full missing-fields list).
  5. rtb generate command + protect / unprotect sub-verbs — extend manifest's [generated] and [protected] maps. No AI yet.
  6. rtb generate flag — interactive flag-form, type select, default-is-code handling. Updates the target command's clap definition + options struct.
  7. rtb generate config-field — typed config field on the tool's AppConfig struct + default in default.yaml.
  8. AI plumbing — ai/ modulertb-ai client wrapper, --provider / --model persistent flags on generate, provider resolution from env (Q-7 → install path is cargo install rtb-cli-bin, so resolution mirrors any other rtb tool). No command-level use yet; just the wiring.
  9. generate command --prompt / --script — AI codegen path (Q-11). First draft only; verifier loop comes next.
  10. Verifier / repair loopai/verifier.rs drives cargo check → AI → cargo clippy → AI → cargo test → AI, capped by --max-repair-iterations. --agentless short-circuits.
  11. rtb generate flag --description AI-drafting — narrow application of the AI plumbing to flag descriptions (Q-11).
  12. rtb generate docs — extraction pass — clap metadata + doc comments + manifest walk → structural Markdown skeleton. No AI.
  13. rtb generate docs --provider — AI prose pass — narrative fills, per-page or batch diff prompts (Q-12). --page-by-page review flag.
  14. rtb remove command — counterpart to generate command; honours protected unless --force.
  15. rtb remove flag — counterpart to generate flag (Q-13).
  16. rtb regenerate projectRepo::diff driven; the three --overwrite modes (ask/allow/deny) land here; protected files always skipped (per Q-14 / gtb's ErrCommandProtected semantics); --non-interactive falls through to deny for edited generator-owned files (per Q-16).
  17. rtb regenerate project --update-docs — pipe through to the generate docs two-pass flow on the regen target's source set (Q-12).
  18. rtb regenerate manifest — walk source tree, infer generator-owned files, rewrite .rtb/manifest.yaml.
  19. 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 command and remove flag ship in v0.6.0.
  • Post-generation hooks beyond the verifier loop — formatter runs, external tool installs. The Q-11 verifier (cargo check / clippy / test driven by rtb-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.yaml under a path and regenerates each). v0.7.

Promoted INTO scope by §9 resolutions (was previously out of scope):

  • rtb regenerate project --update-docs and the generate docs AI-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-bin itself is a binary crate, so its only "public API" is the CLI grammarrtb 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 on generate command (--prompt, --script, --agentless, --max-repair-iterations), AI-aware flags on generate docs (--page-by-page, --update-docs), and the --non-interactive fall-through semantics on regenerate project. The widened grammar is what we will freeze in 1.0.
  • rtb-clirtb-cli-bin gains a new dependency on rtb-cli (replacing the bespoke clap binary with Application::builder). This does not affect rtb-cli's own public API; it just consumes it. The framework now has one more example of a real rtb-cli-based tool — the scaffolder eats its own dog food.
  • rtb-vcs — no new public types needed; Repo::init and Repo::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 via cargo install rtb-cli-bin.
  • .rtb/manifest.yaml — pre-1.0 the schema is allowed to evolve; downstream tools should not parse this file outside rtb. Versioned via the rtb_version field 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)? Recommend minimal + cli to 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-clean is 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.yaml schema-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-interactive mode, fail-with-help, or fail-with-list-of-missing- fields? Recommend the latter — easier to drive from CI.
  • [Q-6] rtb generate project git behaviour. Always Repo::init unless --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-base does NOT depend on rtb-cli-bin. Users install via cargo 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 newer rtb scaffolded a project, an older rtb later runs regenerate. Hard-error on version-down, or warn? Recommend hard-error with a clear "update rtb first" diagnostic.
  • [Q-10] Inherited-command surface. The rtb binary 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 doctor validates the user's environment, rtb update self-updates the scaffolder, rtb docs surfaces 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 docs scope 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 remove scope at v0.6.0. Only remove command, or also remove flag / remove config-field? Recommend remove command only — the others depend on AST surgery against existing source that's likely fiddlier than its slice value.
  • [Q-14] protect/unprotect semantics. When a command is protected, does rtb regenerate project also skip the command's registration in BUILTIN_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 is init,update,mcp,docs,doctor,changelog,keychain; rtb's closest equivalent is cli,update,mcp,docs,doctor,credentials,tui. Confirm — particularly whether tui should be on by default (modest dep cost; nice scaffolded UX) and whether vcs should be off (heavy gix dep; opt-in).
  • [Q-16] --overwrite ask|allow|deny default. gtb defaults to ask. 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 checkcargo clippycargo 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 cli preset's cargo check isn't asserted in CI because it pulls rust-tool-base from 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-deny waiver audit (task #115 — yaml-rust, bincode 1.3, rustls-pemfile, proc-macro-error2).