Skip to content

Scaffolder feature-toggle — rtb enable/disable <feature>

Status: APPROVED — GTB-parity gap from the v0.22.0 audit (Phase 1). Reversal: Phase 1 first assessed persistent feature-toggling as an intentional non-goal (features chosen in main.rs builder code). That was too hasty. Reclassified a GOAL — the deciding evidence is that GTB has matured to the point of proving a feature toggle is required for generated code: real downstream tools need to turn built-ins on/off after scaffolding without hand-editing main.rs, and GTB shipping + sustaining enable/disable demonstrates the need is real, not theoretical. It is also idiomatic for RTB: the scaffolder already edits the generated main.rs via marker regions + manifest + regenerate (generate command/flag/setting); a feature toggle is the same shape applied to the .features(...) builder block. Decisions resolved 2026-06-23 (§6). Ready for TDD.

Resolutions (binding): - Author-side scaffolder verb rewriting the generated .features(...) block — NOT a runtime/config-persisted toggle (that would clash with the typed-config + builder-code paradigm) (Q-1). - cli preset only — the minimal preset has no rtb dep / no Features (Q-2). - Unified enable/disable command also owns the MCP per-command case: enable/disable mcp <path> delegates to the exposure behaviour in 2026-06-23-mcp-command-exposure-gating.md; enable/disable mcp (no path) toggles the Mcp feature (Q-3). Mirrors GTB.

1. Motivation

GTB (v0.18) ships gtb enable/disable <feature>...: author-side toggles that flip properties.features in the manifest, re-render the generated root, survive regenerate, with an interactive multi-select and a CI guard. RTB has the runtime Feature/Features/FeaturesBuilder (rtb-app) and tools select features in their main.rs Application::builder().features(...) — but there is no tool to toggle them; an author must hand-edit main.rs. This adds the missing verb, the RTB way.

2. Surface

rtb enable  <feature>... [-p <path>] [--dry-run] [--force]
rtb disable <feature>... [-p <path>] [--dry-run] [--force]
rtb enable  mcp <command-path>...     # per-command MCP exposure (see MCP spec)
rtb disable mcp <command-path>...
  • <feature> ∈ the rtb_app::Feature variants currently toggleable in a scaffolded tool: Init, Version, Update, Docs, Mcp, Doctor, Ai, Telemetry, Config, Changelog (validated; unknown name = error with the list).
  • No args → interactive multi-select of candidate features (those not already in the target state), mirroring GTB's pickFeatures. CI guard: refuse the interactive picker under CI=true / non-interactive (require explicit names).
  • enable/disable mcp <command-path> (path present) → per-command MCP exposure, defined in the MCP gating spec (not duplicated here).

3. Mechanism

  1. Read .rtb/manifest.yaml; this is a cli-preset project (error if the project's preset has no Features surface).
  2. Compute the desired feature→state map vs Feature::defaults() (Init, Version, Update, Docs, Mcp, Doctor). The generated .features(...) expresses deviations from default only.
  3. Rewrite the // rtb:features-begin/-end marker region in src/main.rs (new — added to the cli preset, §4) to the minimal FeaturesBuilder chain that yields the desired set, e.g. .features(Features::builder().disable(Feature::Mcp).enable(Feature::Ai).build()). Empty deviation set → omit the .features(...) call (defaults).
  4. Record the toggle state in the manifest (features: block) so it survives regenerate (regenerate re-applies it like other markers).
  5. Honour protected + --force; --dry-run prints the intended change.

No #[rtb::command] macro is involved — this edits the builder chain text in a marker region, consistent with generate setting.

4. Template change (cli preset)

Add a marker region to crates/rtb-cli-bin/templates/cli/src/main.rs.j2 inside the Application::builder() chain (around the optional .features(...)), e.g. between .version(...) and .config(...):

    // rtb:features-begin
    // rtb:features-end

Default render = empty region (tool uses Feature::defaults()). generate project records no features: block until a toggle runs.

5. Manifest schema

Add to Manifest:

#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub features: BTreeMap<String, bool>,   // feature name -> enabled (deviations from default)

6. Resolutions (resolved 2026-06-23)

  • [Q-1] Mechanism → author-side scaffolder verb rewriting the .features(...) marker region + manifest. (Rejected: runtime config-persisted feature state — clashes with the paradigm; rejected: hand-edit only — that's the gap.)
  • [Q-2] Scope → cli preset only (minimal has no Features).
  • [Q-3] Command unification → one enable/disable family; mcp <path> delegates to the MCP gating spec; mcp (no path) toggles the feature.
  • [Q-4] Interactive + CI guard → yes (mirror GTB): multi-select on no args, refuse interactive under CI/non-interactive.

7. Testing (TDD)

  • Unit: feature-name validation; desired-set → minimal builder-chain rendering (deviations only; empty set omits .features); manifest round-trip.
  • E2E (assert_cmd): scaffold a cli-preset tool → rtb disable mcp → generated .features(...) disables Feature::Mcp + the tool still cargo checks + manifest.features records it; rtb enable ai adds it; --dry-run no-ops; protected+--force; the toggle survives regenerate project; CI guard rejects the no-arg interactive form under CI=true.

8. Out of scope

  • The minimal preset (no Features).
  • Runtime/end-user feature toggling persisted in config (paradigm non-goal — features are builder-code-side).
  • Per-command MCP exposure mechanics (owned by the MCP gating spec; this spec only routes enable/disable mcp <path> to it).

9. Amendment (2026-06-26) — feature-has-backing-command invariant

The GTB v0.22.0 parity audit (Phase 2) found that Feature::Changelog is toggleable here but had no registered command behind it — so rtb enable changelog would produce a dead flag in the generated tool. Add an invariant to this verb:

  • Every Feature this command can enable must map to a registered builtin command (or a documented backing-less feature, e.g. a pure cross-cutting concern). enable <feature> validates the mapping and warns (not hard-fail) when enabling a feature whose command is not yet implemented, naming it explicitly ("enabled, but no command ships yet").
  • The toggleable-feature list (§2) is derived from Feature-with-backing-command, with any backing-less feature flagged in a single source-of-truth table shared with the MCP-gating spec's resolver (so both verbs agree on what "has a command" means).

Changelog ceases to be the counterexample once 2026-06-26-rtb-cli-changelog-subcommand.md lands; the invariant remains as a guard against future flags-without-commands.