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>∈ thertb_app::Featurevariants 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 underCI=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¶
- Read
.rtb/manifest.yaml; this is a cli-preset project (error if the project's preset has noFeaturessurface). - Compute the desired feature→state map vs
Feature::defaults()(Init, Version, Update, Docs, Mcp, Doctor). The generated.features(...)expresses deviations from default only. - Rewrite the
// rtb:features-begin/-endmarker region insrc/main.rs(new — added to the cli preset, §4) to the minimalFeaturesBuilderchain 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). - Record the toggle state in the manifest (
features:block) so it survives regenerate (regenerate re-applies it like other markers). - Honour
protected+--force;--dry-runprints 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(...):
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/disablefamily;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(...)disablesFeature::Mcp+ the tool stillcargo checks +manifest.featuresrecords it;rtb enable aiadds it;--dry-runno-ops;protected+--force; the toggle survivesregenerate project; CI guard rejects the no-arg interactive form underCI=true.
8. Out of scope¶
- The
minimalpreset (noFeatures). - 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
Featurethis command canenablemust 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.