writeGlobalUserCore now line-surgery upserts only the [core] keys it owns
into the existing file content — no read-parse-merge-emit round-trip. Hand
edits in shapes the minimal parseSimpleToml dropped (arrays, single-quoted
strings, dotted/quoted keys, \uXXXX escapes, custom sections) survive
byte-for-byte. Dead emitTomlSections helper removed.
writeModuleTomls emits core's shipped defaults under top-level [core], not
nested [modules.core] — matches where writeCentralConfig writes core deltas
and where resolve_config.py consumers read core.* from.
tomlString auto-infers TOML type from --set values: true/false → bool,
integer pattern → int, decimal pattern → float, otherwise quoted string.
Explicit-string escape hatch: --set k='"true"' writes "true" as a string.
Previously every --set value was wrapped in quotes, so --set bmm.workers=4
wrote "4" and downstream numeric comparisons broke.
upsertTomlKey's inline-comment detector now tracks double-quoted string
state via findInlineCommentStart, so a # inside a basic-string value
(legal per the TOML spec, contrary to the previous defending comment)
is no longer truncated. tomlHasKey and upsertTomlKey both use a new
isSectionHeader matcher that tolerates trailing inline comments on the
header line and walks every section occurrence — fixes mis-routing on
hand-edited files with [core] # comment headers or duplicate sections.
Python test harnesses (test_resolve_config.py, test_resolve_customization_cascade.py)
now force BMAD_HOME to the synthetic isolation path unconditionally
instead of env.get(BMAD_HOME, default), which was a no-op when the
developer had BMAD_HOME set in their shell.
Adds 21 JS assertions covering the new behaviors directly: tomlString
type inference, # inside string values, inline-comment headers, and
writeGlobalUserCore round-trip preservation of hand-edited shapes.
Tests: 405 JS + 21 Python pass. Lint, markdownlint, prettier green.
Nine fixes covering bugs caught by my code-review, Augment, and CodeRabbit:
quickUpdate identity overwrite (official-modules.js): collectModuleConfigQuick
now lazy-loads globalConfig and falls back through ~/.bmad/config.user.toml
before getDefaultUsername(), so `bmad update` no longer silently overwrites
the user's global identity with the OS username.
computeProcessedDefault placeholder asymmetry (manifest-generator.js): build
a shipped-defaults crossKeyDefaults map and feed BOTH writeCentralConfig and
writeModuleTomls through it. Previously stripDefaults resolved {output_folder}
against user answers while module.toml resolved it against shipped defaults —
overriding output_folder silently reverted every derived path on read.
parseTomlScalar escape order (global-config.js): single-pass regex replacer
so `\\n` round-trips as backslash+n instead of collapsing to a newline.
Windows paths and any TOML string with literal `\n`/`\t`/`\r` survive.
writeGlobalUserCore nested-table corruption (manifest-generator.js): emit
nested objects as proper dotted sub-tables (`[modules.bmm]`) instead of
serializing them as `"[object Object]"` strings inside the parent section.
extractAgentBlocks dead code (manifest-generator.js): deleted. The agent
preservation invariant was intentionally retired by Task F (agents live in
module.toml floor); the function had no callers.
applySetOverrides 3-tier routing (set-overrides.js): consults
~/.bmad/config.user.toml first, then project config.user.toml, then routes
known core scope:user keys (user_name, communication_language per core
schema) to global when neither file owns them. Prevents
`bmad install --set core.user_name=Alice` from polluting project team config.
BMAD_HOME tilde expansion (global-config.js): JS resolver now matches
Python's Path.expanduser() so installer and resolver agree on the global
location when BMAD_HOME is set in non-shell contexts (.env, Docker, Windows).
resolve_config.py fail-fast (resolve_config.py): when --project-root is
explicitly passed but _bmad/ doesn't exist, exit 1 with a clear error
instead of silently returning {}. Global-only mode (no --project-root)
remains permissive.
_MODULE_SKILLS_RE hyphen support (resolve_customization.py): allow hyphens
in module slugs (e.g. `foo-bar-skills/`) so qualified-name skill matching
works for hyphenated modules.
Tests: 383 JS tests + 21 Python tests pass; lint, markdownlint, prettier all
green. Suite 44 now isolates BMAD_HOME and asserts the new 3-tier routing
contract for `--set` overrides.
Closes#1663.
Adds two installer flags so module config options can be set without
interactive prompts. Designed for CI scripts, Dockerfiles, and
enterprise rollouts where the user wants to bake answers into the
install command rather than answer prompts.
`--set <module>.<key>=<value>` (repeatable) sets any module config
option. `--list-options [module]` lists every key the installer can
discover locally — built-in modules (`core`, `bmm`) plus any cached
official modules. One flag scales to every module without growing the
CLI surface per option.
```bash
npx bmad-method install --yes \
--modules bmm --tools claude-code \
--set bmm.project_knowledge=research \
--set bmm.user_skill_level=expert \
--set core.user_name=Brian
```
## How it works
`--set` is a post-install patch. The installer runs its normal flow
untouched, then `applySetOverrides` upserts each value into the
relevant config files:
- `_bmad/config.toml` (team scope, default)
- `_bmad/config.user.toml` (user scope, when the key already lives
there — so user-scope keys like `core.user_name` and
`bmm.user_skill_level` keep their proper file)
- `_bmad/<module>/config.yaml` (so declared schema keys carry forward
via the existingValue path on the next install)
A module without `_bmad/<module>/config.yaml` is skipped silently —
no orphan sections in `config.toml` for uninstalled modules.
## Tradeoffs documented in install-bmad.md
- **Verbatim values.** `--set bmm.project_knowledge=research` writes
`"research"`, not `"{project-root}/research"`. The `result:`
template is not applied. Pass it explicitly if you want the
rendered form: `--set bmm.project_knowledge='{project-root}/research'`.
- **Carry-forward, declared keys.** Free — values land in the
per-module `config.yaml`, so the next install reads them as
`existingValue` and they become the prompt default (accepted under
`--yes`).
- **Carry-forward, undeclared keys.** Best-effort. The value lives in
`config.toml` for the current install but won't be re-emitted on
the next install (the manifest writer's schema-strict partition
drops unknown keys). Re-pass `--set` if needed.
- **No "key not in schema" validation.** Whatever you assert is
written.
## Security
Prototype-pollution defense: `--set __proto__.x=1` would otherwise
reach `overrides.__proto__[x] = 1` and pollute `Object.prototype`,
cascading into every plain-object lookup in the process. Defense-in-
depth via parser-level reserved-name rejection (`__proto__`,
`prototype`, `constructor`) AND `Object.create(null)` for the
override maps. Verified the attack reproduces without the guard and
is blocked with it.
## What's intentionally NOT integrated
`--set` deliberately does not touch the prompt / template / schema
collection flow. No pre-seeding answers, no question filtering, no
function-default evaluation, no schema-strict partition exemption.
That earlier integration approach was tried and scrapped: it
spread state across `Config`, `OfficialModules`,
`manifest-generator`, both collection helpers, and required parallel
plumbing for quick-update — every bug fix touched a different layer.
The post-install patch model covers the actual user need (set a
config value from CI) in ~330 lines of `set-overrides.js` without
the schema gymnastics.
## Files
- `tools/installer/set-overrides.js` (new): parser, prototype-pollution
guard, `applySetOverrides` post-install patch, `upsertTomlKey` /
`tomlString` / `tomlHasKey` line-based TOML helpers
- `tools/installer/list-options.js` (new): module.yaml discovery +
formatter for `--list-options`
- `tools/installer/commands/install.js`: register `--set` /
`--list-options` flags, early validation, `--list-options` exit-code
handling (await `stream.write` callback then `process.exitCode` to
avoid truncating piped output), thread `setOverrides` through to
quick-update
- `tools/installer/core/config.js`: carry `setOverrides` field for
the post-install patch step
- `tools/installer/core/installer.js`: invoke `applySetOverrides`
after `writeCentralConfig` (covers regular install + quick-update
via the shared install path)
- `tools/installer/ui.js`: parse `--set` for early validation, warn
about overrides targeting modules not in `--modules`, drop those
entries before threading
- `docs/how-to/install-bmad.md`, `README.md`: usage, routing rules,
carry-forward semantics, tradeoffs
## Test plan
Suite 44 (24 cases): parser, prototype-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
(hermetic via `BMAD_EXTERNAL_MODULES_CACHE` temp dir). 355 total
passing. Lint + prettier + markdownlint clean.
E2E smoke verified across:
- [x] `--set` writes correct files (team toml / user toml / per-module
yaml) for declared and undeclared keys
- [x] Quick-update without `--set` carries forward declared keys via
`existingValue` path
- [x] Quick-update WITH `--set` applies cleanly (uniform behavior
across action types)
- [x] `--set` for unselected module: warned, no orphan section
- [x] Prototype pollution: rejected with non-zero exit
- [x] `--list-options bmm` exit 0 with full output through pipe;
`--list-options nope` exit 1
- [x] Translated docs (`docs/{cs,fr,vi-vn,zh-cn}/`) intentionally not
touched — they'll lag behind English until the translation pipeline
runs