The original implementation tried to integrate `--set` with the prompt /
result-template / schema-strict-partition system: pre-seeding answers,
filtering questions, evaluating function defaults, tracking override
keys for partition exemption, mirroring carry-forward in two collection
helpers, threading state through Config + ui.js + collection helpers +
manifest writer. ~900 lines spawned across 4 review rounds, with bugs
the bots kept finding because every change touched a different layer.
The simpler model: `--set` is a post-install patch. The installer runs
its normal flow untouched, then `applySetOverrides` upserts each value
into `_bmad/config.toml` (team scope) or `_bmad/config.user.toml` (user
scope) AND into `_bmad/<module>/config.yaml` so declared keys carry
forward via the existingValue path on the next install.
What gets ripped out
- All `setOverrides` plumbing through OfficialModules (constructor
field, applyOverridesAfterSeeding, _trackUnknownKeysAsOverrides,
declaredResultKeys, override classification + pre-write +
question-filter + two-pass function-defaults + carry-forward in
collectModuleConfig, _trackUnknownKeysAsOverrides calls in
collectModuleConfigQuick, headless-branch additions in
Installer.build). official-modules.js reset to its pre-#1663 baseline
(commit 48a7ec8b).
- `setOverrideKeys` field on Config, threading from ui.js, partition
exemption parameter on `manifest-generator.writeCentralConfig`.
- The "ignored under quick-update" warning in install.js — `--set` is
now a uniform post-install patch, so it works the same way for
quick-update as for a regular install.
What stays
- `tools/installer/set-overrides.js` parser with the prototype-pollution
guard, prefixed by the new `applySetOverrides` / `upsertTomlKey` /
`tomlString` / `tomlHasKey` helpers.
- `tools/installer/list-options.js` — small standalone discovery
helper, untouched.
- The `--set` and `--list-options` CLI flag registration in
`commands/install.js`.
- ui.js `collectModuleConfigs` retains the early-feedback warning for
overrides targeting modules not in the install set (and now also
filters them out of `setOverrides` before threading).
Routing rules (post-install patch)
- If `_bmad/config.user.toml` already has `[section] key`, update it
there (so user-scope keys like `core.user_name` and
`bmm.user_skill_level` keep their proper file).
- Otherwise update `_bmad/config.toml` (team scope, default).
- A module without `_bmad/<module>/config.yaml` (i.e. not installed)
is skipped silently — no orphan `[modules.notamodule]` sections.
Tradeoffs documented in `docs/how-to/install-bmad.md`
- Values are written verbatim — no `result:` template rendering. Pass
`--set bmm.project_knowledge='{project-root}/research'` if you want
the rendered form.
- Carry-forward is automatic for declared schema keys (per-module yaml
→ existingValue → prompt default → accepted under --yes). For keys
outside any module's schema, the value lands in `config.toml` for
the current install but won't be re-emitted on the next install.
Re-pass `--set` if you need it sticky.
- No "key not in schema" validation — whatever you assert is written.
Tests: Suite 44 rewritten. 355 passing (was 351). Coverage now focused
on what matters: parser + pollution guard, tomlString escaping,
upsertTomlKey across insert/replace/missing-section/empty-file/
preserved-newline cases, applySetOverrides happy path + uninstalled-
module skip + missing-user-toml-creation + empty-input no-op,
discoverOfficialModuleYamls / formatOptionsList sanity.
E2E smoke verified across all 6 scenarios:
1. fresh install with mixed declared + undeclared --set → correct files
2. quick-update no --set → declared keys persist via per-module yaml
3. quick-update WITH --set → applies (used to be warned + dropped)
4. --set for unselected module → warned, no orphan section
5. prototype pollution → exit 1
6. --list-options bmm exit 0, --list-options nope exit 1
Net: -158 lines vs HEAD. The complex integration was load-bearing for
edge cases nobody actually needed; the simple post-install patch
covers the real use case (script a config value from CI) without the
schema gymnastics.
(J) Prototype pollution guard (CodeRabbit major).
`--set __proto__.x=1` previously mutated Object.prototype because
`overrides.__proto__` returned Object.prototype on a plain object,
and assigning `[key]=value` polluted every plain object in the process.
Verified the attack reproduces on f1c9e12 and is now blocked: parser
rejects __proto__/prototype/constructor segments, and the maps are
Object.create(null) for defense-in-depth.
(I) Non-zero exit when --list-options <module>'s yaml is unparseable
(CodeRabbit major). formatOptionsList tracks moduleScopedFailure and
returns ok:false in that case; install.js exits 1.
(F) Dynamic defaults can now see --set sibling values (Augment medium).
buildQuestion's function default falls back to
`this.collectedConfig[mod][otherKey]`, but overrides were only in
`allAnswers` (local) at default-evaluation time. Pre-write override
raw values to collectedConfig before the prompt batch so the
fallback resolves. Post-prompt template processing overwrites with
the rendered version.
(E) applyOverridesAfterSeeding no longer bypasses carry-forward when
the schema can't be loaded (Augment low). Restructured: schema-load
is now best-effort; without schema, declaredKeys is an empty Set, so
all overrides are flagged as "unknown" and carry-forward runs against
every prior key. Comment now matches behavior.
(G) Flag placeholder --set <spec> instead of <module.key=value>
(Augment low) — angle brackets in the placeholder were misleading;
the description spells out the spec format.
(H) README wording: "every available key" → "locally-known official
keys (built-in modules plus any external officials cached on this
machine)" (CodeRabbit minor) — accurately reflects scope.
Tests: +2 cases for prototype-pollution rejection. Total 343 passing.
Carry forward unknown --set keys across upgrades (CodeRabbit major).
Without this, an unknown key like --set bmm.future_thing=hello landed in
config.toml on run #1 but was silently dropped on the next install
because collectModuleConfig rebuilds collectedConfig from prompt answers
only. collectModuleConfig now copies any non-declared keys from
_existingConfig into collectedConfig and tracks them in setOverrideKeys
so the manifest writer's schema-strict partition keeps them.
Guard single-select rendering with Array.isArray (CodeRabbit major):
a malformed truthy non-array would have aborted --list-options.
Unify core override handling: move the inline post-collection block
from ui.js into OfficialModules.applyOverridesAfterSeeding so core and
non-core take a single validated path. Removes duplicated schema-load
logic and inline requires from ui.js.
Remove dead code: findOfficialModuleYaml and readDeclaredKeys in
set-overrides.js were exported but never imported. Drop them and
their path/fs/yaml/project-root imports — the module is now pure
string-parsing with zero deps.
Doc fix: change "silently ignored" to "ignored with a warning" for
the --action quick-update note (Augment + CodeRabbit).
Polish: clearer flag placeholder (--set <module.key=value> instead of
the misleading <key=value>), trim-asymmetry rationale comment in
parseSetEntry, dedupe rationale in list-options.
Tests: +6 cases — collectModuleConfig --set application end-to-end
(prompt-skip with template rendering), and carry-forward of unknown
keys from _existingConfig. Total 333 passing.
`--set <module>.<key>=<value>` (repeatable) sets any module config option
non-interactively. Scales to every module without growing the CLI surface
per option, and persists into _bmad/config.toml so values survive upgrades.
`--list-options [module]` prints every available --set key for built-in
and locally-cached official modules (community/custom users read their own
module.yaml). Pass a module code to scope the listing.
Validation rules, all non-fatal:
- Module not in --modules → warn and drop the value.
- Key not declared in module.yaml → warn but persist (forward-compat).
The manifest writer's schema-strict partition exempts these so they
survive into config.toml even though the schema doesn't know them.
- Malformed --set syntax → exit non-zero up front.
The legacy core shortcuts (--user-name, --output-folder, etc.) remain
supported as aliases for `--set core.<key>=<value>`. --set with
--action quick-update is ignored with a warning since quick-update
preserves the existing answers by design.
Files:
- tools/installer/set-overrides.js (new): parser
- tools/installer/list-options.js (new): discovery + formatter
- tools/installer/commands/install.js: flags + early validation
- tools/installer/ui.js: parse, warn-on-unselected, thread to OfficialModules
- tools/installer/modules/official-modules.js: pre-fill answers, persist unknowns
- tools/installer/core/config.js + installer.js: carry setOverrideKeys through
- tools/installer/core/manifest-generator.js: partition exempts override keys
- test/test-installation-components.js: +15 cases (Suite 44)
- docs/how-to/install-bmad.md, README.md: --set as preferred non-interactive path
Closes#1663