Skip to content

feat(installer): add --set and --list-options for non-interactive config - #2353

Closed
bmadcode wants to merge 7 commits into
mainfrom
feat-set-flag-1663
Closed

feat(installer): add --set and --list-options for non-interactive config#2353
bmadcode wants to merge 7 commits into
mainfrom
feat-set-flag-1663

Conversation

@bmadcode

Copy link
Copy Markdown
Collaborator

Summary

Closes #1663.

Reporter wanted a --project-knowledge CLI flag for non-interactive bmm installs. The discussion on the issue surfaced two paths: per-option flags (doesn't scale — every module's module.yaml can declare arbitrarily many prompts) or a config-file approach. This PR pivots to a third shape that's the best of both: a single repeatable --set <module>.<key>=<value> flag that scales to every module, every option, present and future.

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

--list-options [module] is the partner discovery flag — prints every available --set key for built-in and locally-cached official modules. Community/custom users read their own module.yaml directly; we don't enumerate those (no network, no extra plumbing).

Design choices

Decision Rationale
--set k=v over inline JSON / config file Shell-friendly (no JSON quoting hell), scales without growing the CLI surface, repeatable. Inline JSON breaks on Windows cmd; config file adds more I/O than CI scripts need.
Warn-and-persist on unknown keys Forward-compatibility: a user can --set a key for a module that doesn't declare it yet. Lands in config.toml with a warning, available when the module gains the key.
Drop values for unselected modules Selection is the gate. --set widgets.foo=bar without --modules widgets would create an orphan TOML section with no schema; honest warning beats invisible junk.
No validation against single-select choices Same warn-and-persist principle — let users assert future values without the installer second-guessing.
--list-options is local-only Reads built-ins + ~/.bmad/cache/external-modules/. No registry fetch, no clone of unknown modules. Community/custom is on the user.

Legacy --user-name, --output-folder, etc. remain as aliases for --set core.<key>=<value> (zero-churn for existing CI scripts). --set with --action quick-update warns and skips, matching quick-update's "preserve existing answers" semantic.

Implementation

  • tools/installer/set-overrides.js (new): parser, throws on malformed input
  • tools/installer/list-options.js (new): module.yaml discovery + formatter
  • tools/installer/commands/install.js: register flags, early syntax validation, --list-options exit path, quick-update warning
  • tools/installer/ui.js: parse --set, warn-on-unselected, post-collection core merge (since core is skipped when seeded by --yes defaults)
  • tools/installer/modules/official-modules.js: pre-fill allAnswers from overrides so the prompt loop and --yes skipPrompts path both see them as already-set; persist unknown keys
  • tools/installer/core/{config,installer}.js: carry setOverrideKeys through to manifest writer
  • tools/installer/core/manifest-generator.js: partition() exempts override-asserted keys from the schema-strict filter so unknown keys survive into config.toml

Test plan

Suite 44 (15 new cases) covers parsing, discovery, formatter rendering, and the manifest writer's exemption logic. End-to-end smoke verified manually:

  • --set bmm.project_knowledge=research[modules.bmm] project_knowledge = "{project-root}/research" (template renders)
  • --set core.user_name=Brian[core] user_name = "Brian" (overrides --yes system-username default)
  • --set bmm.user_skill_level=expertconfig.user.toml (user-scope respected)
  • --set core.unknown_thing=xyz → warned, persisted to config.toml
  • --set freddy-got-fingered.foo=bar (module not selected) → warned, dropped
  • Malformed --set → exits non-zero with a clear error
  • --set with --action quick-update → warning, ignored
  • --list-options bmm → renders bmm.project_knowledge, bmm.user_skill_level (with single-select choices)
  • All 417 existing tests still pass; lint/format/markdownlint clean

Docs

  • docs/how-to/install-bmad.md — flag reference table updated, new "Module config overrides" section with example, validation rules, and quick-update note
  • README.md — added a --set example below the existing non-interactive snippet
  • Translated copies (docs/{cs,fr,vi-vn,zh-cn}/) intentionally not touched — they'll lag behind English until your translation pipeline runs

…fig (#1663)

`--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
@augmentcode

augmentcode Bot commented Apr 28, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Adds scalable non-interactive installer configuration via repeatable --set <module>.<key>=<value> overrides and a local discovery command --list-options [module].

Changes:

  • Adds hardened tools/installer/set-overrides.js to parse/validate --set entries (incl. prototype-pollution guard) and group overrides by module.
  • Adds tools/installer/list-options.js to discover locally-available official module.yaml files (built-ins + cached externals) and render available keys for --set.
  • Extends the installer CLI (tools/installer/commands/install.js) with --set and --list-options, including early syntax validation and correct non-zero exit codes for scoped listing failures.
  • Threads override state through the UI/config pipeline (tools/installer/ui.js, tools/installer/core/config.js) so both interactive and --yes flows can apply overrides and preserve forward-compatible unknown keys.
  • Updates module config collection (tools/installer/modules/official-modules.js) to pre-seed overrides, honor them in headless collection, evaluate dynamic defaults under --yes, and carry forward unknown persisted keys.
  • Updates central config writing (tools/installer/core/manifest-generator.js) to keep user-asserted unknown keys via setOverrideKeys through schema-strict filtering.
  • Adds a new test suite covering parsing, discovery/listing output, exit-code semantics, and override-key persistence.
  • Updates docs/README to document the new flags, discovery behavior, and cache limitations.

Technical Notes: --list-options is intentionally local-only (no registry fetch); unknown --set keys warn but persist for forward-compatibility, and quick-update warns + ignores --set to preserve prior answers.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread docs/how-to/install-bmad.md Outdated
Comment thread tools/installer/modules/official-modules.js Outdated
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a repeatable CLI --set <module>.<key>=<value> for non-interactive module config overrides, a --list-options [module] command to enumerate available module keys, plumbing to thread and persist override keys through collection and manifest generation, and tests covering parsing, security, and integration behavior.

Changes

Cohort / File(s) Summary
Docs
README.md, docs/how-to/install-bmad.md
Document --set repeatable override syntax, --list-options [module], examples, legacy-flag equivalence, scoping/validation/persistence rules, and quick-update semantics.
CLI entry & behavior
tools/installer/commands/install.js
Add --set <spec> and --list-options [module]; fail-fast parse/exit on bad --set; print+exit behavior for --list-options; warn and ignore --set for quick-update.
Parsing helpers
tools/installer/set-overrides.js
New parser exports parseSetEntry and parseSetEntries with strict validation, reserved-key rejection, prototype-pollution mitigation (Object.create(null)), and last-write-wins aggregation.
Option listing
tools/installer/list-options.js
New module to discover bundled and cached module.yaml files, infer option types/defaults/choices, deduplicate module codes case-insensitively, and render --list-options output; exports discovery and formatter functions.
Installer core & config flow
tools/installer/ui.js, tools/installer/core/config.js, tools/installer/core/installer.js, tools/installer/core/manifest-generator.js
Parse and pass setOverrides/setOverrideKeys through UI collection into Config and Installer; propagate keys into manifest generation; ensure writeCentralConfig retains user-asserted unknown keys when flagged in setOverrideKeys.
Module collection logic
tools/installer/modules/official-modules.js
Apply pre-seeded overrides to prompts and result templates, warn/persist unknown keys into collected config and setOverrideKeys, provide applyOverridesAfterSeeding for skip-prompts paths, and two-pass default evaluation for --yes default resolution.
Tests
test/test-installation-components.js
Add extensive tests for --set parsing (including embedded =), prototype-pollution defenses, option-listing behavior, integration of overrides through collection, persistence rules, and skip-collection carry-forward semantics.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(63,81,181,0.5)
    participant CLI as CLI
  end
  rect rgba(0,150,136,0.5)
    participant InstallCmd as install.js
    participant SetParser as set-overrides.js
    participant ListOpts as list-options.js
  end
  rect rgba(255,193,7,0.5)
    participant UI as ui.js
    participant Official as official-modules.js
  end
  rect rgba(233,30,99,0.5)
    participant Manifest as manifest-generator.js
    participant Config as config.js
    participant FS as FileSystem
  end

  CLI->>InstallCmd: invoke with --set / --list-options
  alt --list-options
    InstallCmd->>ListOpts: formatOptionsList(module?)
    ListOpts-->>InstallCmd: rendered list (ok true|false)
    InstallCmd-->>CLI: print and exit
  else --set flow
    InstallCmd->>SetParser: parseSetEntries(args)
    SetParser-->>InstallCmd: setOverrides or error
    InstallCmd->>UI: collectModuleConfigs({ setOverrides })
    UI->>Official: collectModuleConfig / applyOverridesAfterSeeding
    Official->>Config: include setOverrides & setOverrideKeys
    UI-->>InstallCmd: { moduleConfigs, setOverrideKeys, setOverrides }
    InstallCmd->>Manifest: generateManifests(..., setOverrideKeys)
    Manifest->>Manifest: writeCentralConfig(..., setOverrideKeys)
    Manifest->>FS: persist _bmad/config.toml (retain asserted unknown keys)
    Manifest-->>InstallCmd: success
    InstallCmd-->>CLI: finish
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding --set and --list-options CLI flags for non-interactive installer config.
Description check ✅ Passed The description provides detailed context, design rationale, implementation overview, test coverage, and documentation updates related to the changeset.
Linked Issues check ✅ Passed Issue #1663 requested a CLI mechanism to set BMM's project knowledge location non-interactively. The PR implements a general --set .= flag that addresses this requirement and scales to all modules.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: CLI flags, parser, discovery/formatter, UI integration, config plumbing, and manifest writer updates. Documentation and tests are in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-set-flag-1663

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tools/installer/list-options.js (1)

62-63: Sort directory entries for deterministic --list-options output

On Line 62 and Line 75, iteration order depends on filesystem readdir() ordering, which can vary by platform. Sorting by entry.name will make output stable and less noisy for docs/tests.

Also applies to: 75-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/list-options.js` around lines 62 - 63, The directory
iteration over entries from fs.readdir(srcModulesDir, { withFileTypes: true })
is not deterministic; sort the returned Dirent array by entry.name before the
for (const entry of entries) loop so --list-options output is stable, and apply
the same fix to the other readdir result used on lines 75–76 (the second Dirent
array variable) by sorting it by name prior to iteration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/how-to/install-bmad.md`:
- Around line 214-216: Update the note that currently states `--set` flags are
"silently ignored" for `bmad install --action quick-update` to accurately
reflect the feature contract: say that `--set` flags are ignored but a warning
is emitted (e.g., "ignored; a warning will be printed"). Edit the sentence
referencing `--set` and `--action quick-update` so it warns scripted users they
may see CLI warning output during quick-update, and keep the guidance to use
`--action update` (or `bmad install`) to change stored values.

In `@tools/installer/core/manifest-generator.js`:
- Around line 491-495: The code currently builds overrideKeys from
setOverrideKeys[moduleName] only, which causes previously persisted unknown keys
from moduleConfigs to be stripped on update; modify the logic that constructs
overrideKeys (used in the loop over Object.entries(cfg || {}), referencing
setOverrideKeys and moduleConfigs) to also include any undeclared keys present
in moduleConfigs[moduleName] (i.e., add keys from moduleConfigs[moduleName] that
are not in scopes or coreKeys into overrideKeys) so that persisted unknown keys
are treated as retention-eligible on subsequent updates.

In `@tools/installer/list-options.js`:
- Around line 123-125: The code assumes item['single-select'] is an array when
computing values and calling .map(); guard this by checking
Array.isArray(item['single-select']) before mapping (or coerce a single value
into an array), so that malformed but truthy schemas don't throw and abort
--list-options; if it's not an array, skip building values (and optionally
console.warn with context) and continue so lines.push(...) only runs when values
is a non-empty array.

---

Nitpick comments:
In `@tools/installer/list-options.js`:
- Around line 62-63: The directory iteration over entries from
fs.readdir(srcModulesDir, { withFileTypes: true }) is not deterministic; sort
the returned Dirent array by entry.name before the for (const entry of entries)
loop so --list-options output is stable, and apply the same fix to the other
readdir result used on lines 75–76 (the second Dirent array variable) by sorting
it by name prior to iteration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1f1f5b5b-5eab-415d-8772-f6a1ea47eec5

📥 Commits

Reviewing files that changed from the base of the PR and between 48a7ec8 and f33d251.

📒 Files selected for processing (11)
  • README.md
  • docs/how-to/install-bmad.md
  • test/test-installation-components.js
  • tools/installer/commands/install.js
  • tools/installer/core/config.js
  • tools/installer/core/installer.js
  • tools/installer/core/manifest-generator.js
  • tools/installer/list-options.js
  • tools/installer/modules/official-modules.js
  • tools/installer/set-overrides.js
  • tools/installer/ui.js

Comment thread docs/how-to/install-bmad.md Outdated
Comment thread tools/installer/core/manifest-generator.js Outdated
Comment thread tools/installer/list-options.js Outdated
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.
@bmadcode

Copy link
Copy Markdown
Collaborator Author

Review fixes pushed

f1c9e12 addresses the bot reviews on this PR. Inline replies posted on each thread; summary:

Thread Status
augmentcode "silently ignored" doc wording ✅ Resolved
augmentcode type coercion (raw strings → non-string TOML) 🟡 Deferred (no module uses boolean/number prompts today; tracked as follow-up)
coderabbitai "silently ignored" doc wording ✅ Resolved
coderabbitai persisted unknown keys vanish on update (🟠 major) ✅ Resolved — collectModuleConfig now carries forward unknown keys from _existingConfig and tracks them in setOverrideKeys
coderabbitai Array.isArray guard for single-select (🟠 major) ✅ Resolved

Beyond the bot comments, also addressed in the same commit:

  • Removed dead code (findOfficialModuleYaml, readDeclaredKeys) from set-overrides.js
  • Unified core override handling: new OfficialModules.applyOverridesAfterSeeding method replaces the inline schema-load that lived in ui.js
  • Clearer --set flag placeholder, dedupe rationale comment in list-options.js

End-to-end carry-forward verified manually. Test suite: +6 cases, 333 total passing.

@coderabbitai full review please

@augmentcode review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

@bmadcode Thanks for the detailed summary of the fixes! The resolved items look solid — particularly the unknown-key carry-forward on update and the Array.isArray guard. Triggering a full review of the updated changes now.


🧠 Learnings used
Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:816-859
Timestamp: 2026-03-27T12:48:02.796Z
Learning: In `tools/installer/core/installer.js`, the `generateModuleConfigs()` method repartitions YAML by splitting the stringified output into lines and separating module-specific vs core config lines by key. This is latently fragile for multi-line YAML values (arrays, objects), but all current configs are simple scalar strings so there is no breakage today. This has been deliberately deferred by alexeyv (PR `#2129`). The safe fix (operate on the data structure before stringifying, using two plain objects) is documented in the review comment. Do not re-flag this as a new issue in future reviews without checking whether configs have grown beyond simple scalars.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:73-86
Timestamp: 2026-03-11T21:28:59.373Z
Learning: In BMAD-METHOD, the missing HALT/error-handling paths in the "Handle Original Document" step of src/core/tasks/bmad-shard-doc/workflow.md (delete and move branches, lines ~73-86) — specifically the absence of failure checks after the delete, mkdir, and move operations before printing confirmation messages — are pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a pure format conversion (XML task → native skill directory) with no behavioral changes. Reviewers must not flag these missing error paths as regressions introduced by the conversion; any behavioral improvements (e.g., adding HALT on failure for filesystem ops) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:40-45
Timestamp: 2026-03-27T12:47:58.429Z
Learning: In `tools/installer/core/installer.js` (BMAD-METHOD), `OfficialModules.build()` is called before `this.customModules.discoverPaths()` populates the custom module path map. This ordering gap has no practical impact because all real call paths (UI-driven installs) pre-collect module configs before `install()` is invoked; the headless/prompt-free code path that would be affected is not currently exercised in production. Do not flag this ordering as a bug in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:35-35
Timestamp: 2026-03-11T21:28:54.206Z
Learning: In BMAD-METHOD, the unpinned `npx kayvan/markdown-tree-parser` invocation (no explicit version) in `src/core/tasks/bmad-shard-doc/workflow.md` (Step 3) is carried over verbatim from the original `src/core/tasks/shard-doc.xml`. PR `#1896` is a pure format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag the missing version pin in this file during format-conversion PRs; pinning (e.g., to `kayvan/markdown-tree-parser1.6.1`) should be addressed in a dedicated behavioral follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1970
File: AGENTS.md:8-9
Timestamp: 2026-03-14T06:22:13.840Z
Learning: In BMAD-METHOD, `AGENTS.md` at the repo root is targeted at AI coding agents, not human contributors. The maintainer (alexeyv) expects capable agents to self-recover from environment issues (e.g., wrong Node.js version) by reading `package.json` / `.nvmrc` directly. Do not flag missing explicit prerequisite versions (e.g., Node >= 20.0.0) in `AGENTS.md` as review issues.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/modules/custom-modules.js:92-98
Timestamp: 2026-03-27T12:48:05.460Z
Learning: In BMAD-METHOD `tools/installer/modules/custom-modules.js`, the hardcoded forward-slash separators used in `_copyWithFiltering()` for filtering `sub-modules/` paths and `-sidecar` directory segments (lines ~92-98) are a known, pre-existing Windows path-separator limitation. This has been explicitly deferred as a follow-up item. Reviewers must not re-flag this as a new issue in future PRs.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1860
File: src/bmm/workflows/4-implementation/code-review/workflow.md:154-180
Timestamp: 2026-03-08T17:16:01.764Z
Learning: In BMAD-METHOD code-review workflow (src/bmm/workflows/4-implementation/code-review/workflow.md), the mixed severity terminology — "CRITICAL finding" used in Step 3 execution logic while the categorization action and output template use "HIGH/MEDIUM/LOW" — is a pre-existing inconsistency inherited verbatim from the original instructions.xml. It is not introduced by the yaml-to-md conversion PR `#1860`. Reviewers should not flag this terminology mismatch during format conversion PRs; any normalization of severity levels should be addressed in a dedicated behavioral follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:1683-1685
Timestamp: 2026-03-27T12:48:03.246Z
Learning: In BMAD-METHOD (`tools/installer/core/installer.js`), `findBmadDir()` intentionally hardcodes `BMAD_FOLDER_NAME` (`_bmad`) as the BMAD installation directory. The old installer on `main` had identical hardcoded `_bmad` lookup. No renamed-folder detection ever existed in the codebase, so flagging `findBmadDir`'s hardcoded lookup as a regression is a false positive. Do not raise this as an issue in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1878
File: src/core/tasks/bmad-index-docs/SKILL.md:3-3
Timestamp: 2026-03-11T15:36:32.294Z
Learning: In BMAD-METHOD, the SKILL.md description for `bmad-index-docs` (src/core/tasks/bmad-index-docs/SKILL.md) — including the phrase "all docs in the folder" and the absence of a clarifying-question instruction when no folder is specified — is carried over verbatim from the original `src/core/tasks/index-docs.xml`. PR `#1878` is a faithful format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag these as regressions introduced by the conversion; any behavioral corrections should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:79-85
Timestamp: 2026-03-11T21:29:04.747Z
Learning: In BMAD-METHOD, the archive branch in src/core/tasks/bmad-shard-doc/workflow.md (Step 6, "If user selects m (move)") does not check whether the archive destination path already exists before moving the original document, risking a silent overwrite on reruns. This is pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a faithful format conversion (XML task → native skill directory) and does not change workflow logic. Reviewers must not flag the missing existence check / overwrite guard as a regression introduced by this conversion; any behavioral fix (e.g., prompting user to confirm overwrite, generating a non-colliding name) should be addressed in a dedicated follow-up PR.

Learnt from: bmadcode
Repo: bmad-code-org/BMAD-METHOD PR: 2284
File: tools/installer/core/installer.js:589-593
Timestamp: 2026-04-19T19:49:07.190Z
Learning: In BMAD-METHOD, the YAML-era `_bmad/custom/.gitignore` seeding (with `*.user.yaml`) was introduced in PR `#2282` and fully reverted before any release. No production user has a `.gitignore` under `_bmad/custom/` containing `*.user.yaml`. Reviewers must not flag the absence of a `*.user.yaml` → `*.user.toml` migration in `_installSharedScripts` (`tools/installer/core/installer.js`) as an upgrade-path issue — there is nothing to migrate.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:705-714
Timestamp: 2026-03-08T08:10:32.911Z
Learning: In BMAD-METHOD (tools/cli/installers/lib/ide/_config-driven.js), no production platform configuration currently uses the `targets` array (multi-target installation path). The `installToMultipleTargets` code path is effectively unreachable for all real platforms, so concerns about multi-target interactions with `installVerbatimSkills` (e.g., source-dir deletion via install_to_bmad=false affecting a second target) are moot. Do not flag multi-target scenarios for this file as blocking issues.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1915
File: test/test-installation-components.js:1827-1867
Timestamp: 2026-03-12T14:18:49.462Z
Learning: In BMAD-METHOD (`tools/cli/installers/lib/core/installer.js`), `renderInstallSummary` has no IDE-dependent branching. The `ides` context field is passed but not consumed in a way that creates a separate no-IDE code path. There is no distinct `_bmad`-only summary branch in `renderInstallSummary` that warrants isolated automated test coverage. Reviewers must not flag the absence of a dedicated no-IDE summary test as a gap in test coverage for this function.
✅ Actions performed

Full review triggered.

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread tools/installer/list-options.js Outdated
*/
async function formatOptionsList(moduleCode) {
const discovered = await discoverOfficialModuleYamls();
const filtered = moduleCode ? discovered.filter((d) => d.code === moduleCode) : discovered;

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tools/installer/list-options.js:151 — formatOptionsList() filters modules with a case-sensitive d.code === moduleCode, even though discovery/deduping is case-insensitive; --list-options BMM (or any case mismatch) can incorrectly report “No locally-known module.yaml”. Consider normalizing the comparison so --list-options behaves consistently with the discovery logic.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in ce12cc1. formatOptionsList now lowercases both sides of the filter:

const needle = moduleCode ? moduleCode.toLowerCase() : null;
const filtered = needle ? discovered.filter((d) => d.code.toLowerCase() === needle) : discovered;

--list-options BMM and --list-options bmm now both find the bmm built-in. Test added that exercises the uppercase path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/installer/core/installer.js (1)

307-312: ⚠️ Potential issue | 🟠 Major

quick-update still drops carried-forward unknown override keys.

At Line 311, manifest generation now relies on config.setOverrideKeys to preserve undeclared --set keys. But quickUpdate() in this file builds its installConfig without that field, so a previously persisted forward-compatible key is rewritten away the next time the user runs --action quick-update.

Suggested fix
 const installConfig = {
   directory: projectDir,
   modules: modulesToUpdate,
   ides: configuredIdes,
   coreConfig: quickModules.collectedConfig.core,
   moduleConfigs: quickModules.collectedConfig,
   actionType: 'install',
   _quickUpdate: true,
   _preserveModules: skippedModules,
   _existingModules: installedModules,
   channelOptions,
+  setOverrideKeys: quickModules.setOverrideKeys || {},
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/core/installer.js` around lines 307 - 312, The quickUpdate()
flow builds an installConfig that omits persisted forward-compatible override
keys so manifestGen.generateManifests later loses them; modify quickUpdate() to
copy config.setOverrideKeys (e.g. setOverrideKeys: config.setOverrideKeys || {})
into the installConfig object you pass to the installer/generator (same shape
expected by manifestGen.generateManifests), ensuring installConfig includes
setOverrideKeys so previously carried-forward unknown --set keys are preserved.
🧹 Nitpick comments (1)
test/test-installation-components.js (1)

2986-3157: Add a regression for --action quick-update ignoring --set.

Suite 44 covers parsing and persistence well, but it never exercises the documented quick-update branch where overrides must warn and be ignored. A focused CLI-level assertion here would catch future wiring changes that accidentally start mutating config during quick updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 2986 - 3157, Add a focused
regression test in Suite 44 that asserts --action quick-update does not apply
--set overrides: instantiate OfficialModules with setOverrides (e.g. { bmm: {
future_thing: 'x' } }), mark skipPrompts/_silentConfig true, simulate the
quick-update path by calling the same code path used for quick updates (invoke
collectModuleConfig('bmm', tmpDir, true, true) or the CLI quick-update handler
if available) and then assert om.collectedConfig.bmm does NOT contain the
override and om.setOverrideKeys.bmm does NOT have('future_thing'); also
capture/log output and assert a warning message about ignoring --set during
quick-update was emitted so the test verifies both the override is ignored and a
warning is produced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/test-installation-components.js`:
- Around line 3033-3047: The test reads the external-module cache causing
nondeterminism; before calling discoverOfficialModuleYamls and
formatOptionsList, save process.env.BMAD_EXTERNAL_MODULES_CACHE, set
process.env.BMAD_EXTERNAL_MODULES_CACHE to a new temp directory, run the
assertions (using discoverOfficialModuleYamls, formatOptionsList, etc.), and in
a finally block restore the original env var (or delete it if undefined) to
ensure hermetic behavior; reference BMAD_EXTERNAL_MODULES_CACHE,
discoverOfficialModuleYamls, and formatOptionsList in the changes.

In `@tools/installer/list-options.js`:
- Around line 153-164: The branch in list-options.js that currently returns a
user-facing string when filtered.length === 0 and moduleCode is set must signal
failure to the process; instead of returning the string, throw an Error (e.g.
new Error(theSameMessage)) from that branch so the error propagates to
tools/installer/commands/install.js and yields a non-zero exit, or (if you
prefer not to throw) set process.exitCode = 1 before returning the message;
update the branch handling filtered.length === 0 (referencing the filtered and
moduleCode variables) to implement one of these fixes so a miss like
--list-options bmn results in a non-zero exit.

In `@tools/installer/modules/official-modules.js`:
- Around line 47-79: When applying overrides in applyOverridesAfterSeeding(),
also carry forward previously persisted unknown keys from
this._existingConfig[moduleName] so they aren't dropped when collection was
skipped: after loading schema (or even if overrides is empty), iterate keys in
this._existingConfig[moduleName] that are not declared in schema and not already
present in this.setOverrides[moduleName], add them into
this.collectedConfig[moduleName], and add them into
this.setOverrideKeys[moduleName] (and log the same warning used for new unknown
keys). Use the existing symbols applyOverridesAfterSeeding, this.setOverrides,
this.collectedConfig, this.setOverrideKeys and this._existingConfig to locate
where to merge these non-schema keys before returning.

---

Outside diff comments:
In `@tools/installer/core/installer.js`:
- Around line 307-312: The quickUpdate() flow builds an installConfig that omits
persisted forward-compatible override keys so manifestGen.generateManifests
later loses them; modify quickUpdate() to copy config.setOverrideKeys (e.g.
setOverrideKeys: config.setOverrideKeys || {}) into the installConfig object you
pass to the installer/generator (same shape expected by
manifestGen.generateManifests), ensuring installConfig includes setOverrideKeys
so previously carried-forward unknown --set keys are preserved.

---

Nitpick comments:
In `@test/test-installation-components.js`:
- Around line 2986-3157: Add a focused regression test in Suite 44 that asserts
--action quick-update does not apply --set overrides: instantiate
OfficialModules with setOverrides (e.g. { bmm: { future_thing: 'x' } }), mark
skipPrompts/_silentConfig true, simulate the quick-update path by calling the
same code path used for quick updates (invoke collectModuleConfig('bmm', tmpDir,
true, true) or the CLI quick-update handler if available) and then assert
om.collectedConfig.bmm does NOT contain the override and om.setOverrideKeys.bmm
does NOT have('future_thing'); also capture/log output and assert a warning
message about ignoring --set during quick-update was emitted so the test
verifies both the override is ignored and a warning is produced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0353ad5-b947-49b8-bc41-d0eef908cf07

📥 Commits

Reviewing files that changed from the base of the PR and between 48a7ec8 and f1c9e12.

📒 Files selected for processing (11)
  • README.md
  • docs/how-to/install-bmad.md
  • test/test-installation-components.js
  • tools/installer/commands/install.js
  • tools/installer/core/config.js
  • tools/installer/core/installer.js
  • tools/installer/core/manifest-generator.js
  • tools/installer/list-options.js
  • tools/installer/modules/official-modules.js
  • tools/installer/set-overrides.js
  • tools/installer/ui.js

Comment thread test/test-installation-components.js Outdated
Comment thread tools/installer/list-options.js
Comment thread tools/installer/modules/official-modules.js Outdated
(A) Carry forward unknown core keys in applyOverridesAfterSeeding
(CodeRabbit major). Mirrors collectModuleConfig's carry-forward so
the skip-collection path used by core (when seeded by --yes / legacy
shortcuts) doesn't drop unknown keys on subsequent installs. Without
this, `--set core.future=x` on run #1 would silently disappear on
the next install.

(B) --list-options now exits non-zero on a single-module miss
(CodeRabbit major). formatOptionsList returns { text, ok }; install.js
exits 1 with text on stderr when ok=false, 0 with text on stdout
otherwise. CI scripts catch typos like `--list-options bmn`.

(C) Hermetic Suite 44 discovery tests (CodeRabbit minor). Point
BMAD_EXTERNAL_MODULES_CACHE at a temp dir and restore in a finally
block so test results don't depend on the developer / CI cache state.

(D) Case-insensitive --list-options filter (Augment). Discovery
already dedupes case-insensitively; the filter now matches the same
way, so `--list-options BMM` and `--list-options bmm` both find the
bmm built-in.

Tests: +7 cases (uppercase listing, ok flag, core carry-forward).
Total 340 passing.
@bmadcode

Copy link
Copy Markdown
Collaborator Author

Round 2 fixes pushed

ce12cc1 addresses the four new bot comments:

Thread Status
coderabbitai applyOverridesAfterSeeding doesn't carry forward (🟠 major) ✅ Resolved — mirrored carry-forward logic; verified --set core.future_thing=hello survives a subsequent install with no --set
coderabbitai --list-options <miss> exits 0 (🟠 major) ✅ Resolved — formatOptionsList returns { text, ok }; install.js exits 1 with text on stderr when ok=false
coderabbitai non-hermetic Suite 44 (🟡 minor) ✅ Resolved — BMAD_EXTERNAL_MODULES_CACHE set to a temp dir with finally-block restore
augmentcode case-sensitive --list-options filter ✅ Resolved — filter lowercased on both sides; BMM and bmm resolve to the same entry

Tests: +7 cases covering uppercase listing, ok-flag, and core carry-forward. Total 340 passing.

@coderabbitai full review please

@augmentcode review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

@bmadcode Thanks for the detailed summary of the Round 2 fixes! The resolutions all look well-considered:

  • applyOverridesAfterSeeding carry-forward — mirroring the same logic from collectModuleConfig is the right approach; good that it was verified end-to-end.
  • --list-options <miss> exit code — returning { text, ok } and exiting 1 on stderr is the correct UNIX convention for a command that finds nothing valid.
  • Suite 44 hermeticity — using BMAD_EXTERNAL_MODULES_CACHE with a temp dir + finally-block restore is a clean isolation approach.
  • Case-insensitive --list-options filter — lowercasing on both sides is the simplest correct fix.

Triggering a full review of the updated changes now.


🧠 Learnings used
Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:40-45
Timestamp: 2026-03-27T12:47:58.429Z
Learning: In `tools/installer/core/installer.js` (BMAD-METHOD), `OfficialModules.build()` is called before `this.customModules.discoverPaths()` populates the custom module path map. This ordering gap has no practical impact because all real call paths (UI-driven installs) pre-collect module configs before `install()` is invoked; the headless/prompt-free code path that would be affected is not currently exercised in production. Do not flag this ordering as a bug in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:816-859
Timestamp: 2026-03-27T12:48:02.796Z
Learning: In `tools/installer/core/installer.js`, the `generateModuleConfigs()` method repartitions YAML by splitting the stringified output into lines and separating module-specific vs core config lines by key. This is latently fragile for multi-line YAML values (arrays, objects), but all current configs are simple scalar strings so there is no breakage today. This has been deliberately deferred by alexeyv (PR `#2129`). The safe fix (operate on the data structure before stringifying, using two plain objects) is documented in the review comment. Do not re-flag this as a new issue in future reviews without checking whether configs have grown beyond simple scalars.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/modules/custom-modules.js:92-98
Timestamp: 2026-03-27T12:48:05.460Z
Learning: In BMAD-METHOD `tools/installer/modules/custom-modules.js`, the hardcoded forward-slash separators used in `_copyWithFiltering()` for filtering `sub-modules/` paths and `-sidecar` directory segments (lines ~92-98) are a known, pre-existing Windows path-separator limitation. This has been explicitly deferred as a follow-up item. Reviewers must not re-flag this as a new issue in future PRs.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:1683-1685
Timestamp: 2026-03-27T12:48:03.246Z
Learning: In BMAD-METHOD (`tools/installer/core/installer.js`), `findBmadDir()` intentionally hardcodes `BMAD_FOLDER_NAME` (`_bmad`) as the BMAD installation directory. The old installer on `main` had identical hardcoded `_bmad` lookup. No renamed-folder detection ever existed in the codebase, so flagging `findBmadDir`'s hardcoded lookup as a regression is a false positive. Do not raise this as an issue in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:73-86
Timestamp: 2026-03-11T21:28:59.373Z
Learning: In BMAD-METHOD, the missing HALT/error-handling paths in the "Handle Original Document" step of src/core/tasks/bmad-shard-doc/workflow.md (delete and move branches, lines ~73-86) — specifically the absence of failure checks after the delete, mkdir, and move operations before printing confirmation messages — are pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a pure format conversion (XML task → native skill directory) with no behavioral changes. Reviewers must not flag these missing error paths as regressions introduced by the conversion; any behavioral improvements (e.g., adding HALT on failure for filesystem ops) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1915
File: test/test-installation-components.js:1827-1867
Timestamp: 2026-03-12T14:18:49.462Z
Learning: In BMAD-METHOD (`tools/cli/installers/lib/core/installer.js`), `renderInstallSummary` has no IDE-dependent branching. The `ides` context field is passed but not consumed in a way that creates a separate no-IDE code path. There is no distinct `_bmad`-only summary branch in `renderInstallSummary` that warrants isolated automated test coverage. Reviewers must not flag the absence of a dedicated no-IDE summary test as a gap in test coverage for this function.

Learnt from: bmadcode
Repo: bmad-code-org/BMAD-METHOD PR: 2284
File: tools/installer/core/installer.js:589-593
Timestamp: 2026-04-19T19:49:07.190Z
Learning: In BMAD-METHOD, the YAML-era `_bmad/custom/.gitignore` seeding (with `*.user.yaml`) was introduced in PR `#2282` and fully reverted before any release. No production user has a `.gitignore` under `_bmad/custom/` containing `*.user.yaml`. Reviewers must not flag the absence of a `*.user.yaml` → `*.user.toml` migration in `_installSharedScripts` (`tools/installer/core/installer.js`) as an upgrade-path issue — there is nothing to migrate.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:705-714
Timestamp: 2026-03-08T08:10:32.911Z
Learning: In BMAD-METHOD (tools/cli/installers/lib/ide/_config-driven.js), no production platform configuration currently uses the `targets` array (multi-target installation path). The `installToMultipleTargets` code path is effectively unreachable for all real platforms, so concerns about multi-target interactions with `installVerbatimSkills` (e.g., source-dir deletion via install_to_bmad=false affecting a second target) are moot. Do not flag multi-target scenarios for this file as blocking issues.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:688-700
Timestamp: 2026-03-08T08:10:35.406Z
Learning: In BMAD-METHOD, source skill directories (those collected by the `type: skill` manifest pipeline) never contain a `SKILL.md` file. Therefore, in `installVerbatimSkills` (`tools/cli/installers/lib/ide/_config-driven.js`), writing the generated SKILL.md before calling `fs.copy()` for the directory contents is safe — there is no risk of a source `SKILL.md` overwriting the generated YAML-safe one. Reviewers should not flag this write-before-copy ordering as a bug.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:35-35
Timestamp: 2026-03-11T21:28:54.206Z
Learning: In BMAD-METHOD, the unpinned `npx kayvan/markdown-tree-parser` invocation (no explicit version) in `src/core/tasks/bmad-shard-doc/workflow.md` (Step 3) is carried over verbatim from the original `src/core/tasks/shard-doc.xml`. PR `#1896` is a pure format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag the missing version pin in this file during format-conversion PRs; pinning (e.g., to `kayvan/markdown-tree-parser1.6.1`) should be addressed in a dedicated behavioral follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1970
File: AGENTS.md:8-9
Timestamp: 2026-03-14T06:22:13.840Z
Learning: In BMAD-METHOD, `AGENTS.md` at the repo root is targeted at AI coding agents, not human contributors. The maintainer (alexeyv) expects capable agents to self-recover from environment issues (e.g., wrong Node.js version) by reading `package.json` / `.nvmrc` directly. Do not flag missing explicit prerequisite versions (e.g., Node >= 20.0.0) in `AGENTS.md` as review issues.

Learnt from: dracic
Repo: bmad-code-org/BMAD-METHOD PR: 1764
File: tools/cli/installers/lib/ide/_config-driven.js:457-462
Timestamp: 2026-02-25T11:19:19.850Z
Learning: In the BMAD installer (tools/cli/installers/lib/ide/_config-driven.js), cleanup() is intentionally decoupled from install(). Legacy target cleanup (via legacy_targets) should always run when defined, even if the current installer config is invalid or installation fails, because it prevents duplicate command names from deprecated locations (e.g., .opencode/agent vs .opencode/agents). This decoupling is by design and should not be flagged as an issue.
✅ Actions performed

Full review triggered.

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

// In-place mutation keeps the rest of this method's `questions` references
// pointing at the filtered list without renaming a local through 100+ lines.
if (seededOverrideKeys.size > 0) {
const remaining = questions.filter((q) => !seededOverrideKeys.has(q.name.replace(`${moduleName}_`, '')));

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--set overrides are pre-seeded into allAnswers and the corresponding questions are removed, but buildQuestion’s dynamic-default function resolves {other_key} via the current prompt batch and then this.collectedConfig[moduleName] (which won’t include the override yet). This can cause dependent defaults to ignore a provided --set value when a module uses same-module placeholder defaults.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in fb57c81. Pre-write the raw override values to this.collectedConfig[moduleName] BEFORE the prompt batch runs:

if (seededOverrideKeys.size > 0) {
  if (!this.collectedConfig[moduleName]) this.collectedConfig[moduleName] = {};
  for (const key of seededOverrideKeys) {
    this.collectedConfig[moduleName][key] = moduleOverrides[key];
  }
}

That way the existing fallback chain in buildQuestion's function default — answers[k] → this.collectedConfig[mod][k] — finds sibling override values when computing dynamic defaults. The post-prompt processing later overwrites these raw values with the template-rendered versions, so the final config still goes through the result-template pipeline (e.g. {project-root}/{value}).

// schema unparseable — skip key-existence validation
}
}
if (!schema || typeof schema !== 'object') return;

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If module.yaml is missing/unparseable, this return exits before the prior-config carry-forward logic later in the function, so previously persisted unknown keys could still be dropped in that scenario. That also contradicts the comment above that says carry-forward should still happen when the schema can’t be loaded.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in fb57c81. Restructured applyOverridesAfterSeeding so the schema-load is best-effort: when unparseable, declaredKeys is left empty, every prior key counts as undeclared, and the carry-forward block at the bottom still runs. Code now matches the comment.

Comment thread tools/installer/commands/install.js Outdated
],
['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
[
'--set <module.key=value>',

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option placeholder string says --set <module.key=value>, but the actual contract/docs/parser are <module>.<key>=<value>; the generated CLI help may mislead users into the wrong format.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in fb57c81. Changed the placeholder to <spec> and clarified the description so the help text now reads:

--set <spec>  Set a module config option non-interactively. Spec format: <module>.<key>=<value> (e.g. bmm.project_knowledge=research). Repeatable. Run --list-options to see available keys.

Avoids the dotted-placeholder ambiguity while keeping the format explicit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 55-61: Update the README wording to remove the claim that
`--list-options` shows "every available key" and instead state its actual scope:
that `--list-options` lists built-in options and any locally cached official
modules' options but does not include uncached official modules or
community/custom modules; reference the flags `--list-options` and `--set` in
the revised sentence so users understand the limitation when using `--set
<module>.<key>=<value>`.

In `@tools/installer/list-options.js`:
- Around line 179-197: When a module-scoped listing fails to parse module.yaml,
the function should return { ok: false } instead of true; update the loop that
iterates "for (const { code, yamlPath, source } of filtered)" to set a failure
flag (e.g., parseFailed) when the catch branch runs for a specific module and
moduleCode is set, and after the loop change the final "return { text:
sections.join('\n'), ok: true }" to return ok: false when that flag is set.
Ensure the logic around "parsed" and the push to "sections" remains the same and
reference the same symbols (filtered, code, yamlPath, source, parsed, sections,
moduleCode) so tooling exits non-zero on unreadable module.yaml.

In `@tools/installer/set-overrides.js`:
- Around line 24-29: The parsing currently returns { module: moduleCode, key,
value } without defending against prototype-polluting names; after extracting
moduleCode and key in the same block that throws for empty values, add a guard
that rejects reserved prototype property names (e.g. "__proto__", "prototype",
"constructor") for either moduleCode or key and throw a clear Error (e.g. `--set
"${entry}": reserved segment not allowed`) to prevent assignments like
overrides.__proto__ from mutating Object.prototype; update the validation near
the moduleCode/key extraction so callers that populate the overrides map never
use these reserved segments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46d7c668-e3f4-4195-9e1a-bcb10280a7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 48a7ec8 and ce12cc1.

📒 Files selected for processing (11)
  • README.md
  • docs/how-to/install-bmad.md
  • test/test-installation-components.js
  • tools/installer/commands/install.js
  • tools/installer/core/config.js
  • tools/installer/core/installer.js
  • tools/installer/core/manifest-generator.js
  • tools/installer/list-options.js
  • tools/installer/modules/official-modules.js
  • tools/installer/set-overrides.js
  • tools/installer/ui.js

Comment thread README.md Outdated
Comment thread tools/installer/list-options.js Outdated
Comment thread tools/installer/set-overrides.js
(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.
@bmadcode

Copy link
Copy Markdown
Collaborator Author

Round 3 fixes pushed

fb57c81 addresses six new bot comments — including a real prototype pollution vulnerability that I confirmed exploitable on the previous commit before fixing.

Thread Status
coderabbitai prototype pollution via __proto__.x=value (🟠 major, security) ✅ Resolved — reserved-name parser guard + Object.create(null) maps; exploit verified blocked
coderabbitai --list-options <m> exits 0 on yaml parse failure (🟠 major) ✅ Resolved — moduleScopedFailure flag, ok: false on parse miss
augmentcode dynamic defaults can't see --set siblings (medium) ✅ Resolved — pre-write override values to collectedConfig before prompt batch so the existing fallback chain in buildQuestion resolves them
augmentcode schema-unparseable bypasses carry-forward (low) ✅ Resolved — schema load is now best-effort; carry-forward runs even when schema is missing
augmentcode --set <module.key=value> placeholder format (low) ✅ Resolved — placeholder is now <spec> with format spelled out in description
coderabbitai README "every key" overstates scope (minor) ✅ Resolved — wording matches your suggestion

Tests: +2 cases for prototype-pollution rejection. 343 total passing.

@coderabbitai full review please

@augmentcode review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

@bmadcode Thanks for the Round 3 summary! The fixes look well-targeted:

  • Prototype pollution — the combination of a reserved-name parser guard (__proto__, constructor, prototype) and Object.create(null) maps is the right defense-in-depth approach. Good that you confirmed the exploit was blocked on the actual commit.
  • --list-options yaml parse failure → ok: false — the moduleScopedFailure flag cleanly separates "module found but YAML broken" from "module not found", both now correctly propagating ok: false and exit code 1.
  • Dynamic defaults seeing --set siblings — pre-writing override values into collectedConfig before the prompt batch is the correct fix; it plugs into the existing buildQuestion fallback chain without special-casing.
  • Best-effort schema load for carry-forward — decoupling carry-forward from schema parseability is a more robust invariant.
  • Placeholder + README wording — both polish items confirmed addressed.

343 passing with 2 new prototype-pollution rejection cases is a solid checkpoint. Triggering the full review now.


🧠 Learnings used
Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:40-45
Timestamp: 2026-03-27T12:47:58.429Z
Learning: In `tools/installer/core/installer.js` (BMAD-METHOD), `OfficialModules.build()` is called before `this.customModules.discoverPaths()` populates the custom module path map. This ordering gap has no practical impact because all real call paths (UI-driven installs) pre-collect module configs before `install()` is invoked; the headless/prompt-free code path that would be affected is not currently exercised in production. Do not flag this ordering as a bug in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:816-859
Timestamp: 2026-03-27T12:48:02.796Z
Learning: In `tools/installer/core/installer.js`, the `generateModuleConfigs()` method repartitions YAML by splitting the stringified output into lines and separating module-specific vs core config lines by key. This is latently fragile for multi-line YAML values (arrays, objects), but all current configs are simple scalar strings so there is no breakage today. This has been deliberately deferred by alexeyv (PR `#2129`). The safe fix (operate on the data structure before stringifying, using two plain objects) is documented in the review comment. Do not re-flag this as a new issue in future reviews without checking whether configs have grown beyond simple scalars.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:73-86
Timestamp: 2026-03-11T21:28:59.373Z
Learning: In BMAD-METHOD, the missing HALT/error-handling paths in the "Handle Original Document" step of src/core/tasks/bmad-shard-doc/workflow.md (delete and move branches, lines ~73-86) — specifically the absence of failure checks after the delete, mkdir, and move operations before printing confirmation messages — are pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a pure format conversion (XML task → native skill directory) with no behavioral changes. Reviewers must not flag these missing error paths as regressions introduced by the conversion; any behavioral improvements (e.g., adding HALT on failure for filesystem ops) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/modules/custom-modules.js:92-98
Timestamp: 2026-03-27T12:48:05.460Z
Learning: In BMAD-METHOD `tools/installer/modules/custom-modules.js`, the hardcoded forward-slash separators used in `_copyWithFiltering()` for filtering `sub-modules/` paths and `-sidecar` directory segments (lines ~92-98) are a known, pre-existing Windows path-separator limitation. This has been explicitly deferred as a follow-up item. Reviewers must not re-flag this as a new issue in future PRs.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:705-714
Timestamp: 2026-03-08T08:10:32.911Z
Learning: In BMAD-METHOD (tools/cli/installers/lib/ide/_config-driven.js), no production platform configuration currently uses the `targets` array (multi-target installation path). The `installToMultipleTargets` code path is effectively unreachable for all real platforms, so concerns about multi-target interactions with `installVerbatimSkills` (e.g., source-dir deletion via install_to_bmad=false affecting a second target) are moot. Do not flag multi-target scenarios for this file as blocking issues.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:688-700
Timestamp: 2026-03-08T08:10:35.406Z
Learning: In BMAD-METHOD, source skill directories (those collected by the `type: skill` manifest pipeline) never contain a `SKILL.md` file. Therefore, in `installVerbatimSkills` (`tools/cli/installers/lib/ide/_config-driven.js`), writing the generated SKILL.md before calling `fs.copy()` for the directory contents is safe — there is no risk of a source `SKILL.md` overwriting the generated YAML-safe one. Reviewers should not flag this write-before-copy ordering as a bug.

Learnt from: jheyworth
Repo: bmad-code-org/BMAD-METHOD PR: 2324
File: tools/installer/ide/_config-driven.js:245-342
Timestamp: 2026-04-26T21:10:45.208Z
Learning: In `tools/installer/ide/_config-driven.js` (BMAD-METHOD), `installVerbatimSkills` intentionally has no per-file try/catch around its copy operations. A partial skill directory copy leaves the skill in a broken state, so failing loudly is by design. Reviewers must not flag the absence of per-file error handling in `installVerbatimSkills` as a bug or suggest making it resilient in the same way as `installCommandPointers`.

Learnt from: n-zhi
Repo: bmad-code-org/BMAD-METHOD PR: 1817
File: tools/cli/installers/lib/ide/templates/combined/antigravity-agent.md:7-10
Timestamp: 2026-03-05T12:15:04.170Z
Learning: In the BMAD-METHOD repository, the unconditional LOAD/READ/FOLLOW activation chain in antigravity-*.md templates (e.g., tools/cli/installers/lib/ide/templates/combined/antigravity-agent.md) is a pre-existing, systemic issue inherited from the corresponding default-*.md templates. It is not introduced by PR `#1817`. A dedicated future PR should address this holistically across all affected templates by implementing integrity checks, an allowlist of permitted agent paths, and strict schema validation of <activation> sections before executing their steps.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:35-35
Timestamp: 2026-03-11T21:28:54.206Z
Learning: In BMAD-METHOD, the unpinned `npx kayvan/markdown-tree-parser` invocation (no explicit version) in `src/core/tasks/bmad-shard-doc/workflow.md` (Step 3) is carried over verbatim from the original `src/core/tasks/shard-doc.xml`. PR `#1896` is a pure format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag the missing version pin in this file during format-conversion PRs; pinning (e.g., to `kayvan/markdown-tree-parser1.6.1`) should be addressed in a dedicated behavioral follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1915
File: test/test-installation-components.js:1827-1867
Timestamp: 2026-03-12T14:18:49.462Z
Learning: In BMAD-METHOD (`tools/cli/installers/lib/core/installer.js`), `renderInstallSummary` has no IDE-dependent branching. The `ides` context field is passed but not consumed in a way that creates a separate no-IDE code path. There is no distinct `_bmad`-only summary branch in `renderInstallSummary` that warrants isolated automated test coverage. Reviewers must not flag the absence of a dedicated no-IDE summary test as a gap in test coverage for this function.

Learnt from: bmadcode
Repo: bmad-code-org/BMAD-METHOD PR: 2284
File: tools/installer/core/installer.js:589-593
Timestamp: 2026-04-19T19:49:07.190Z
Learning: In BMAD-METHOD, the YAML-era `_bmad/custom/.gitignore` seeding (with `*.user.yaml`) was introduced in PR `#2282` and fully reverted before any release. No production user has a `.gitignore` under `_bmad/custom/` containing `*.user.yaml`. Reviewers must not flag the absence of a `*.user.yaml` → `*.user.toml` migration in `_installSharedScripts` (`tools/installer/core/installer.js`) as an upgrade-path issue — there is nothing to migrate.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:1683-1685
Timestamp: 2026-03-27T12:48:03.246Z
Learning: In BMAD-METHOD (`tools/installer/core/installer.js`), `findBmadDir()` intentionally hardcodes `BMAD_FOLDER_NAME` (`_bmad`) as the BMAD installation directory. The old installer on `main` had identical hardcoded `_bmad` lookup. No renamed-folder detection ever existed in the codebase, so flagging `findBmadDir`'s hardcoded lookup as a regression is a false positive. Do not raise this as an issue in future reviews.

Learnt from: dracic
Repo: bmad-code-org/BMAD-METHOD PR: 1764
File: tools/cli/installers/lib/ide/_config-driven.js:457-462
Timestamp: 2026-02-25T11:19:19.850Z
Learning: In the BMAD installer (tools/cli/installers/lib/ide/_config-driven.js), cleanup() is intentionally decoupled from install(). Legacy target cleanup (via legacy_targets) should always run when defined, even if the current installer config is invalid or installation fails, because it prevents duplicate command names from deprecated locations (e.g., .opencode/agent vs .opencode/agents). This decoupling is by design and should not be flagged as an issue.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1878
File: src/core/tasks/bmad-index-docs/SKILL.md:3-3
Timestamp: 2026-03-11T15:36:32.294Z
Learning: In BMAD-METHOD, the SKILL.md description for `bmad-index-docs` (src/core/tasks/bmad-index-docs/SKILL.md) — including the phrase "all docs in the folder" and the absence of a clarifying-question instruction when no folder is specified — is carried over verbatim from the original `src/core/tasks/index-docs.xml`. PR `#1878` is a faithful format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag these as regressions introduced by the conversion; any behavioral corrections should be addressed in a dedicated follow-up PR.
✅ Actions performed

Full review triggered.

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

const seededOverrideKeys = new Set();
const unknownOverrideKeys = [];
for (const [overrideKey, overrideValue] of Object.entries(moduleOverrides)) {
if (declaredPromptKeys.has(overrideKey)) {

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declaredPromptKeys only tracks keys that have item.prompt, so a schema key that’s declared as a static result (no prompt) will be treated as “not a declared config key” and go down the unknown-key warn/persist path. That makes the warning potentially incorrect and can also persist a raw override over a computed static result value.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 7ad054f. Added declaredResultKeys parallel to declaredPromptKeys. A schema entry with result: and no prompt: is now treated as schema-declared:

if (declaredPromptKeys.has(overrideKey) || declaredResultKeys.has(overrideKey)) {
  seededOverrideKeys.add(overrideKey);
} else {
  unknownOverrideKeys.push([overrideKey, overrideValue]);
}

For result-only keys this means: the override is pre-seeded as the answer in allAnswers, the existing result-template loop then renders it ({value} substitutes the user's raw input, cross-references like {project-root} still resolve), and no "not a declared config key" warning fires. Carry-forward block at the bottom of the method also refactored to use new Set([...declaredPromptKeys, ...declaredResultKeys]) instead of re-walking configKeys.

Good catch — the prior behavior was both wrong-warning and a correctness bug (raw value stomped the rendered template).

Comment thread tools/installer/list-options.js Outdated
if (moduleCode) moduleScopedFailure = true;
continue;
}
if (!parsed || typeof parsed !== 'object') {

@augmentcode augmentcode Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In formatOptionsList(), if module.yaml parses successfully but to a non-object (e.g., scalar/array), moduleScopedFailure flips but no error text is appended, so --list-options <module> can fail with little/no explanation. Consider emitting an explicit “unexpected module.yaml shape” message similar to the catch branch so CLI/CI logs are diagnosable.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 7ad054f. The non-object branch now emits a diagnostic mirroring the catch branch, and the type guard also covers arrays (which typeof reports as 'object'):

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  sections.push(`${code} (${source}): module.yaml is not a valid object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`, '');
  if (moduleCode) moduleScopedFailure = true;
  continue;
}

--list-options <module> still exits 1 in this case (per the existing moduleScopedFailureok:false path), but now the user sees why. New test covers the diagnostic + ok:false signal end-to-end via BMAD_EXTERNAL_MODULES_CACHE pointing at a temp dir with a scalar module.yaml.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/installer/core/installer.js (1)

307-312: ⚠️ Potential issue | 🟠 Major

Propagate quick-update override keys into this new manifest path.

This new plumbing still misses the quickUpdate() caller: installConfig there never includes quickModules.setOverrideKeys, so carried-forward unknown keys from prior --set runs still get stripped on a quick-update rewrite because this call falls back to {}.

💡 Suggested follow-up
       coreConfig: quickModules.collectedConfig.core,
       moduleConfigs: quickModules.collectedConfig,
+      setOverrideKeys: Object.fromEntries(
+        Object.entries(quickModules.setOverrideKeys || {}).map(([moduleCode, keys]) => [moduleCode, [...keys]]),
+      ),
       actionType: 'install',
       _quickUpdate: true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/core/installer.js` around lines 307 - 312, The
generateManifests call is dropping quick-update override keys because
installConfig only passes config.setOverrideKeys; update the call-site that
invokes manifestGen.generateManifests (the block that currently passes { ides:
config.ides || [], preservedModules, moduleConfigs, setOverrideKeys:
config.setOverrideKeys || {}, }) to include the quick-update keys as a fallback
— e.g., use quickModules.setOverrideKeys when config.setOverrideKeys is empty —
so quickUpdate() carried keys (quickModules.setOverrideKeys) are propagated into
manifest generation instead of being replaced by an empty object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/how-to/install-bmad.md`:
- Around line 204-205: The sentence describing `--list-options` overstates
availability; update the wording to make the cache requirement explicit by
changing “installed at least once on this machine” to “currently cached official
modules” (or similar phrasing) so the line reads that `--list-options` lists
built-in modules plus any currently cached official modules, and note that
cleared caches or ephemeral CI workers may not show previously installed
officials.

In `@tools/installer/commands/install.js`:
- Around line 60-68: The current handling of the --list-options branch calls
stream.write(...) then immediately process.exit(...), which can truncate
buffered output; change the flow in the options.listOptions block (where
formatOptionsList is called and stream.write is used) to wait for the write to
finish (use the writable stream's callback or await stream.write/draint
handling) and then set process.exitCode to ok ? 0 : 1 and return instead of
calling process.exit; ensure this change is applied to the branch surrounding
formatOptionsList, moduleArg, stream.write and the current process.exit call so
output always flushes before the process ends.

In `@tools/installer/modules/official-modules.js`:
- Around line 1633-1650: The headless/skipPrompts branch currently copies only
non-function defaults which drops same-module dynamic defaults; update the
skipPrompts logic (the block that consumes allAnswers / seededOverrideKeys and
writes into this.collectedConfig) to evaluate function defaults the same way the
interactive path does: ensure seeded override keys are pre-written into
this.collectedConfig[moduleName], then for each schema field if the default is a
function call that function with the same context/answer bag used by
buildQuestion (so it can read {other_key} placeholders and module overrides),
take its returned value (and template-render it if you do that elsewhere), and
write that computed value into both allAnswers and
this.collectedConfig[moduleName] so headless installs preserve dynamic
same-module defaults.
- Around line 23-27: The constructor option setOverrides is stored on
this.setOverrides but the place that instantiates OfficialModules (the call that
currently passes only { channelOptions }) doesn't forward those overrides, so
collectModuleConfig() runs with an empty map and misses CLI --set values; update
the instantiation site that calls new OfficialModules(...) to pass the incoming
setOverrides (e.g. new OfficialModules({ channelOptions, setOverrides })) or
thread the setOverrides through the caller so OfficialModules.constructor
receives it, ensuring collectModuleConfig() can read this.setOverrides for
headless/--yes paths.

---

Outside diff comments:
In `@tools/installer/core/installer.js`:
- Around line 307-312: The generateManifests call is dropping quick-update
override keys because installConfig only passes config.setOverrideKeys; update
the call-site that invokes manifestGen.generateManifests (the block that
currently passes { ides: config.ides || [], preservedModules, moduleConfigs,
setOverrideKeys: config.setOverrideKeys || {}, }) to include the quick-update
keys as a fallback — e.g., use quickModules.setOverrideKeys when
config.setOverrideKeys is empty — so quickUpdate() carried keys
(quickModules.setOverrideKeys) are propagated into manifest generation instead
of being replaced by an empty object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b0713eb-1fb9-418d-a530-db237927b448

📥 Commits

Reviewing files that changed from the base of the PR and between 48a7ec8 and fb57c81.

📒 Files selected for processing (11)
  • README.md
  • docs/how-to/install-bmad.md
  • test/test-installation-components.js
  • tools/installer/commands/install.js
  • tools/installer/core/config.js
  • tools/installer/core/installer.js
  • tools/installer/core/manifest-generator.js
  • tools/installer/list-options.js
  • tools/installer/modules/official-modules.js
  • tools/installer/set-overrides.js
  • tools/installer/ui.js

Comment thread docs/how-to/install-bmad.md Outdated
Comment thread tools/installer/commands/install.js Outdated
Comment thread tools/installer/modules/official-modules.js Outdated
Comment thread tools/installer/modules/official-modules.js Outdated
(1) Use process.exitCode instead of process.exit() after --list-options
write (CodeRabbit major). process.exit() forces immediate termination
even with pending I/O, which can truncate buffered writes when stdout
is piped or captured by CI. Await the write callback, set exitCode,
and return so the event loop drains naturally.

(2) Thread setOverrides through Config → OfficialModules.build for
headless callers (CodeRabbit major). Non-UI entry points (direct
installer.install({...}) without going through ui.collectModuleConfigs)
previously got an empty override map. Config now carries setOverrides
and the headless branch of OfficialModules.build also runs
loadExistingConfig + applyOverridesAfterSeeding('core') to mirror the
UI path's semantics. The UI path is unaffected because it takes the
moduleConfigs early-return.

(3) Evaluate function defaults under skipPrompts and accept-defaults
paths (CodeRabbit major). Both branches were dropping function defaults
silently, so any same-module dynamic default (`{other_key}` placeholder
in default:) disappeared under --yes. Two-pass: write non-function
defaults first so the answer bag is populated, then call function
defaults with that bag. Try/catch around the call surfaces resolution
failures as warnings instead of crashing the install.

(4) Track result-only schema keys as declared (Augment medium). A
schema entry with `result:` and no `prompt:` was being classified as
"unknown" when targeted by --set, producing a wrong warning and
overwriting the computed template output with the raw value. Added
declaredResultKeys parallel to declaredPromptKeys; an override on
either is now seeded as the answer so the result template still
renders ({value} substitution preserved). Carry-forward block
refactored to consume the same set.

(5) Diagnose non-object module.yaml under --list-options (Augment low).
The non-object branch silently flipped moduleScopedFailure with no
output. Now emits "module.yaml is not a valid object (got <type>)"
mirroring the catch branch, and the type guard also catches arrays
which typeof reports as 'object'.

(6) Reword --list-options doc cache scope (CodeRabbit minor).
"Installed at least once on this machine" → "currently cached official
modules" with a note that cache can be cleared or absent on ephemeral
CI workers — accurately reflects what the command can discover.

Tests: +4 cases — Config.build setOverrides threading and default,
formatOptionsList non-object yaml diagnostic and ok:false. Total 347
passing.
@bmadcode

Copy link
Copy Markdown
Collaborator Author

Round 4 fixes pushed

7ad054f addresses the six unaddressed bot comments from the round-3 review pass.

Thread Status
coderabbitai process.exit() truncates buffered --list-options output (🟠 major) ✅ Resolved — await stream.write callback then process.exitCode + return; verified 170-line piped output is intact
coderabbitai setOverrides not threaded into headless Installer.build (🟠 major) ✅ Resolved — Config now carries setOverrides; headless branch also runs loadExistingConfig + applyOverridesAfterSeeding('core') to mirror UI semantics
coderabbitai --yes drops same-module function defaults (🟠 major) ✅ Resolved — two-pass write in both skipPrompts and accept-defaults branches: static defaults first, then function defaults evaluated against the populated answer bag
augmentcode result-only schema keys misclassified as unknown (🟡 medium) ✅ Resolved — added declaredResultKeys; result-only keys with --set now seed the answer so the result template renders, no spurious warning, raw value no longer stomps computed output
augmentcode silent failure for non-object module.yaml in --list-options (🟢 low) ✅ Resolved — emits module.yaml is not a valid object (got <type>) diagnostic; type guard also catches arrays
coderabbitai docs overpromise on --list-options scope (🟡 minor) ✅ Resolved — reworded to "currently cached official modules" with explicit note about cache clearing / ephemeral CI

Tests: +4 cases (Config.build setOverrides threading + default; non-object module.yaml diagnostic + ok:false). Total 347 passing.

CLI smoke: --list-options bmm exit 0, --list-options definitely-not-real exit 1, --set '__proto__.foo=bar' exit 1, --list-options (170 lines piped) intact. Lint + prettier + markdownlint all clean.

@coderabbitai full review please
@augmentcode review

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

@bmadcode Thanks for the detailed Round 4 summary! The fixes look well-targeted:

  • process.exit() truncation — using the write-callback + process.exitCode + return pattern is the correct async-safe approach; glad the 170-line piped output was verified intact end-to-end.
  • setOverrides in headless path — threading it through Config and mirroring loadExistingConfig + applyOverridesAfterSeeding('core') in the headless branch gives proper parity with the UI path.
  • --yes function defaults — the two-pass write (static defaults first, then function defaults evaluated against the populated bag) is the right approach; it ensures cross-key dependencies resolve correctly in non-interactive mode.
  • declaredResultKeys — cleanly separates result-only keys from genuinely unknown ones, eliminating spurious warnings without losing the template-render behavior.
  • Non-object module.yaml diagnostic — emitting a typed diagnostic and catching arrays with the type guard closes that silent failure path nicely.
  • Docs scope — "currently cached official modules" with the explicit cache-clearing/ephemeral-CI note sets accurate expectations.

347 passing with 4 new cases is a solid checkpoint. Triggering the full review now.


🧠 Learnings used
Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:816-859
Timestamp: 2026-03-27T12:48:02.796Z
Learning: In `tools/installer/core/installer.js`, the `generateModuleConfigs()` method repartitions YAML by splitting the stringified output into lines and separating module-specific vs core config lines by key. This is latently fragile for multi-line YAML values (arrays, objects), but all current configs are simple scalar strings so there is no breakage today. This has been deliberately deferred by alexeyv (PR `#2129`). The safe fix (operate on the data structure before stringifying, using two plain objects) is documented in the review comment. Do not re-flag this as a new issue in future reviews without checking whether configs have grown beyond simple scalars.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:40-45
Timestamp: 2026-03-27T12:47:58.429Z
Learning: In `tools/installer/core/installer.js` (BMAD-METHOD), `OfficialModules.build()` is called before `this.customModules.discoverPaths()` populates the custom module path map. This ordering gap has no practical impact because all real call paths (UI-driven installs) pre-collect module configs before `install()` is invoked; the headless/prompt-free code path that would be affected is not currently exercised in production. Do not flag this ordering as a bug in future reviews.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/modules/custom-modules.js:92-98
Timestamp: 2026-03-27T12:48:05.460Z
Learning: In BMAD-METHOD `tools/installer/modules/custom-modules.js`, the hardcoded forward-slash separators used in `_copyWithFiltering()` for filtering `sub-modules/` paths and `-sidecar` directory segments (lines ~92-98) are a known, pre-existing Windows path-separator limitation. This has been explicitly deferred as a follow-up item. Reviewers must not re-flag this as a new issue in future PRs.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:73-86
Timestamp: 2026-03-11T21:28:59.373Z
Learning: In BMAD-METHOD, the missing HALT/error-handling paths in the "Handle Original Document" step of src/core/tasks/bmad-shard-doc/workflow.md (delete and move branches, lines ~73-86) — specifically the absence of failure checks after the delete, mkdir, and move operations before printing confirmation messages — are pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a pure format conversion (XML task → native skill directory) with no behavioral changes. Reviewers must not flag these missing error paths as regressions introduced by the conversion; any behavioral improvements (e.g., adding HALT on failure for filesystem ops) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:688-700
Timestamp: 2026-03-08T08:10:35.406Z
Learning: In BMAD-METHOD, source skill directories (those collected by the `type: skill` manifest pipeline) never contain a `SKILL.md` file. Therefore, in `installVerbatimSkills` (`tools/cli/installers/lib/ide/_config-driven.js`), writing the generated SKILL.md before calling `fs.copy()` for the directory contents is safe — there is no risk of a source `SKILL.md` overwriting the generated YAML-safe one. Reviewers should not flag this write-before-copy ordering as a bug.

Learnt from: bmadcode
Repo: bmad-code-org/BMAD-METHOD PR: 2284
File: tools/installer/core/installer.js:589-593
Timestamp: 2026-04-19T19:49:07.190Z
Learning: In BMAD-METHOD, the YAML-era `_bmad/custom/.gitignore` seeding (with `*.user.yaml`) was introduced in PR `#2282` and fully reverted before any release. No production user has a `.gitignore` under `_bmad/custom/` containing `*.user.yaml`. Reviewers must not flag the absence of a `*.user.yaml` → `*.user.toml` migration in `_installSharedScripts` (`tools/installer/core/installer.js`) as an upgrade-path issue — there is nothing to migrate.

Learnt from: dracic
Repo: bmad-code-org/BMAD-METHOD PR: 1764
File: tools/cli/installers/lib/ide/_config-driven.js:457-462
Timestamp: 2026-02-25T11:19:19.850Z
Learning: In the BMAD installer (tools/cli/installers/lib/ide/_config-driven.js), cleanup() is intentionally decoupled from install(). Legacy target cleanup (via legacy_targets) should always run when defined, even if the current installer config is invalid or installation fails, because it prevents duplicate command names from deprecated locations (e.g., .opencode/agent vs .opencode/agents). This decoupling is by design and should not be flagged as an issue.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/modules/official-modules.js:678-690
Timestamp: 2026-03-27T12:48:10.237Z
Learning: In BMAD-METHOD (`tools/installer/modules/official-modules.js`), the `getFileList()` method uses `path.relative()` which returns platform-specific separators (backslashes on Windows), while downstream filters in `copyModuleWithFiltering()` expect POSIX forward-slash paths (e.g. `sub-modules/`, `agents/`). This Windows path separator mismatch is a pre-existing issue shared with `tools/installer/modules/custom-modules.js` and has been intentionally deferred. Do not flag it as a new issue in either file.

Learnt from: jheyworth
Repo: bmad-code-org/BMAD-METHOD PR: 2324
File: tools/installer/ide/_config-driven.js:245-342
Timestamp: 2026-04-26T21:10:45.208Z
Learning: In `tools/installer/ide/_config-driven.js` (BMAD-METHOD), `installVerbatimSkills` intentionally has no per-file try/catch around its copy operations. A partial skill directory copy leaves the skill in a broken state, so failing loudly is by design. Reviewers must not flag the absence of per-file error handling in `installVerbatimSkills` as a bug or suggest making it resilient in the same way as `installCommandPointers`.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:35-35
Timestamp: 2026-03-11T21:28:54.206Z
Learning: In BMAD-METHOD, the unpinned `npx kayvan/markdown-tree-parser` invocation (no explicit version) in `src/core/tasks/bmad-shard-doc/workflow.md` (Step 3) is carried over verbatim from the original `src/core/tasks/shard-doc.xml`. PR `#1896` is a pure format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag the missing version pin in this file during format-conversion PRs; pinning (e.g., to `kayvan/markdown-tree-parser1.6.1`) should be addressed in a dedicated behavioral follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1878
File: src/core/tasks/bmad-index-docs/SKILL.md:3-3
Timestamp: 2026-03-11T15:36:32.294Z
Learning: In BMAD-METHOD, the SKILL.md description for `bmad-index-docs` (src/core/tasks/bmad-index-docs/SKILL.md) — including the phrase "all docs in the folder" and the absence of a clarifying-question instruction when no folder is specified — is carried over verbatim from the original `src/core/tasks/index-docs.xml`. PR `#1878` is a faithful format conversion (XML task → native skill directory), not a behavior change. Reviewers must not flag these as regressions introduced by the conversion; any behavioral corrections should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:79-85
Timestamp: 2026-03-11T21:29:04.747Z
Learning: In BMAD-METHOD, the archive branch in src/core/tasks/bmad-shard-doc/workflow.md (Step 6, "If user selects m (move)") does not check whether the archive destination path already exists before moving the original document, risking a silent overwrite on reruns. This is pre-existing behavior carried over verbatim from the original src/core/tasks/shard-doc.xml. PR `#1896` is a faithful format conversion (XML task → native skill directory) and does not change workflow logic. Reviewers must not flag the missing existence check / overwrite guard as a regression introduced by this conversion; any behavioral fix (e.g., prompting user to confirm overwrite, generating a non-colliding name) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1915
File: test/test-installation-components.js:1827-1867
Timestamp: 2026-03-12T14:18:49.462Z
Learning: In BMAD-METHOD (`tools/cli/installers/lib/core/installer.js`), `renderInstallSummary` has no IDE-dependent branching. The `ides` context field is passed but not consumed in a way that creates a separate no-IDE code path. There is no distinct `_bmad`-only summary branch in `renderInstallSummary` that warrants isolated automated test coverage. Reviewers must not flag the absence of a dedicated no-IDE summary test as a gap in test coverage for this function.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1896
File: src/core/tasks/bmad-shard-doc/workflow.md:23-30
Timestamp: 2026-03-11T21:28:59.967Z
Learning: In BMAD-METHOD, the shard-doc workflow (src/core/tasks/bmad-shard-doc/workflow.md, converted from src/core/tasks/shard-doc.xml) does not check whether the destination folder is non-empty before proceeding with sharding. This is a pre-existing limitation from the original shard-doc.xml, not introduced by the XML-to-native-skill conversion PR `#1896`. Reviewers must not flag the missing non-empty destination folder guard during format-conversion PRs; any behavioral improvement (e.g., halting or prompting on a non-empty destination) should be addressed in a dedicated follow-up PR.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 1851
File: tools/cli/installers/lib/ide/_config-driven.js:705-714
Timestamp: 2026-03-08T08:10:32.911Z
Learning: In BMAD-METHOD (tools/cli/installers/lib/ide/_config-driven.js), no production platform configuration currently uses the `targets` array (multi-target installation path). The `installToMultipleTargets` code path is effectively unreachable for all real platforms, so concerns about multi-target interactions with `installVerbatimSkills` (e.g., source-dir deletion via install_to_bmad=false affecting a second target) are moot. Do not flag multi-target scenarios for this file as blocking issues.

Learnt from: alexeyv
Repo: bmad-code-org/BMAD-METHOD PR: 2129
File: tools/installer/core/installer.js:1683-1685
Timestamp: 2026-03-27T12:48:03.246Z
Learning: In BMAD-METHOD (`tools/installer/core/installer.js`), `findBmadDir()` intentionally hardcodes `BMAD_FOLDER_NAME` (`_bmad`) as the BMAD installation directory. The old installer on `main` had identical hardcoded `_bmad` lookup. No renamed-folder detection ever existed in the codebase, so flagging `findBmadDir`'s hardcoded lookup as a regression is a false positive. Do not raise this as an issue in future reviews.
✅ Actions performed

Full review triggered.

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread tools/installer/core/installer.js Outdated
ides: config.ides || [],
preservedModules: modulesForCsvPreserve,
moduleConfigs,
setOverrideKeys: config.setOverrideKeys || {},

@augmentcode augmentcode Bot Apr 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tools/installer/core/installer.js:311 — In the headless-collection path (when OfficialModules.build() actually collects configs), override-only keys are accumulated on the officialModules instance, but generateManifests() is currently given config.setOverrideKeys, which may still be {}; this can cause unknown --set keys / carried-forward unknown keys to be dropped during writeCentralConfig()’s schema-strict partitioning.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


let count = 0;
for (const [key, item] of Object.entries(parsed)) {
if (!item || typeof item !== 'object' || !('prompt' in item)) continue;

@augmentcode augmentcode Bot Apr 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tools/installer/list-options.js:123 — formatModuleOptions() only lists schema entries that have prompt, but OfficialModules.collectModuleConfig() now treats result-only schema keys as declared and allows --set to override them; --list-options may therefore omit some valid --set keys and make discovery incomplete.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread test/test-installation-components.js Outdated
const nonObjListing = await formatOptionsList('fakemod');
// Either we got a diagnostic for fakemod, or the entry wasn't
// discovered at all (in which case unknown-module fallback runs).
if (nonObjListing.text.includes('fakemod')) {

@augmentcode augmentcode Bot Apr 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test/test-installation-components.js:3278 — This assertion block is conditional on nonObjListing.text.includes('fakemod'), which means the test can still pass even if cache discovery stops finding the synthesized fakemod/src/module.yaml, reducing coverage of the “non-object YAML should surface a diagnostic + ok:false” behavior.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/installer/core/manifest-generator.js`:
- Around line 491-494: The filtering currently uses the prototype-sensitive
check "key in scopes" which treats inherited properties (e.g., constructor) as
declared; update the declared-key test in the loop that iterates over
Object.entries(cfg) to use an own-property check such as
Object.prototype.hasOwnProperty.call(scopes, key) (or scopes.hasOwnProperty if
preferred) instead of "key in scopes", leaving the surrounding logic with
onlyDeclaredKeys, overrideKeys, isCore, and coreKeys unchanged; ensure you
reference the variables overrideKeys, setOverrideKeys, moduleName, cfg, isCore,
coreKeys, onlyDeclaredKeys, and scopes when making the change.

In `@tools/installer/list-options.js`:
- Around line 121-136: The loop currently skips any schema entries that lack a
'prompt', which omits result-only keys; change the guard so entries are kept if
they have a prompt OR their key is listed in the module's declared result keys
(e.g. check OfficialModules.declaredResultKeys or the appropriate
declaredResultKeys set) — replace the current check (if (!item || typeof item
!== 'object' || !('prompt' in item)) continue;) with a condition that only
continues when item is missing/invalid OR the key is not in declaredResultKeys
and has no prompt, so result-only keys (by key membership in declaredResultKeys)
are included while preserving the existing formatting logic (inferType,
formatPromptText, single-select handling).

In `@tools/installer/modules/official-modules.js`:
- Around line 75-88: Create a single prompt/result-only classifier function
(e.g., isResultOnlyOrPrompt(schemaEntry) used by both the render and seeded-core
paths) and replace the ad-hoc checks that currently only check for 'prompt' in
the schema (the declaredKeys logic) and the seeded-core logic that treats every
non-prompt entry as seedable; use this shared helper to determine whether a key
is declared/result-only. In the render path, when applying an override from
overrides[moduleName] and the classifier marks it as result-only, skip
templating and only inject the override if the string template contains the
"{value}" placeholder; otherwise treat it as a plain seeded-only result (do not
warn or attempt to render). Also update the warning path that calls
prompts.log.warn and this.setOverrideKeys[moduleName].add(key) to rely on the
same classifier so warnings are only emitted for truly undeclared keys (not for
result-only keys).
- Around line 68-73: When parsing module.yaml (the schemaPath block) swallow the
parse exception into a named error (catch (err)) and surface it before falling
back: log a clear warning including schemaPath and err.message/stack (e.g., via
console.warn or the repo's logger) so users see the parse failure, and set
process.exitCode = 1 to mark the tool run as degraded while still allowing the
installer to continue with no-schema behavior; update the try/catch around
yaml.parse(await fs.readFile(schemaPath, 'utf8')) and assignment to schema
accordingly.

In `@tools/installer/ui.js`:
- Around line 729-735: The loop that warns about ignored --set entries
(iterating Object.keys(setOverrides)) currently does not remove those keys, so
setOverrides still contains values for modules not in selectedModuleSet; update
the logic inside that loop (or immediately after it) to delete each ignored
module key from the setOverrides map/object (e.g., delete
setOverrides[moduleCode] or setOverrides.delete(moduleCode) depending on type)
before returning from promptInstall(), ensuring promptInstall() and downstream
code receive the cleaned setOverrides without rejected overrides.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 795d2c8e-d844-4b7c-8220-62c0dc8e8596

📥 Commits

Reviewing files that changed from the base of the PR and between 48a7ec8 and 7ad054f.

📒 Files selected for processing (11)
  • README.md
  • docs/how-to/install-bmad.md
  • test/test-installation-components.js
  • tools/installer/commands/install.js
  • tools/installer/core/config.js
  • tools/installer/core/installer.js
  • tools/installer/core/manifest-generator.js
  • tools/installer/list-options.js
  • tools/installer/modules/official-modules.js
  • tools/installer/set-overrides.js
  • tools/installer/ui.js

Comment on lines +491 to +494
const overrideKeys = new Set(setOverrideKeys[moduleName] || []);
for (const [key, value] of Object.entries(cfg || {})) {
if (!isCore && coreKeys.has(key)) continue;
if (onlyDeclaredKeys && !(key in scopes)) continue;
if (onlyDeclaredKeys && !(key in scopes) && !overrideKeys.has(key)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's locate and read the file in question
cat -n tools/installer/core/manifest-generator.js | sed -n '485,505p'

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1036


🏁 Script executed:

#!/bin/bash
# Search for scopeByModuleKey definition
rg -n "scopeByModuleKey\s*[=:]" tools/installer/core/manifest-generator.js | head -20

Repository: bmad-code-org/BMAD-METHOD

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Search for where scopeByModuleKey is populated
rg -n "scopeByModuleKey\[" tools/installer/core/manifest-generator.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 360


🏁 Script executed:

#!/bin/bash
# Look at the broader context around line 458-461 to understand the structure
cat -n tools/installer/core/manifest-generator.js | sed -n '440,470p'

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1799


🏁 Script executed:

#!/bin/bash
# Let's understand what happens with `key in` for inherited properties
node <<'EOF'
// Simulate the scenario
const scopes = {};
scopes['user-key'] = 'team';

// Check behavior with inherited properties
console.log("'constructor' in scopes:", 'constructor' in scopes);
console.log("'toString' in scopes:", 'toString' in scopes);
console.log("scopes.constructor:", scopes.constructor);
console.log("scopes['constructor']:", scopes['constructor']);

// Check with own property check
console.log("\nUsing hasOwnProperty:");
console.log("Object.prototype.hasOwnProperty.call(scopes, 'constructor'):", Object.prototype.hasOwnProperty.call(scopes, 'constructor'));
console.log("Object.prototype.hasOwnProperty.call(scopes, 'user-key'):", Object.prototype.hasOwnProperty.call(scopes, 'user-key'));
EOF

Repository: bmad-code-org/BMAD-METHOD

Length of output: 360


🏁 Script executed:

#!/bin/bash
# Check if Object.entries includes inherited properties
node <<'EOF'
const obj = {};
// Object.entries only returns own enumerable properties
console.log("Object.entries result includes inherited properties:");
console.log(Object.entries(obj));

// Even with inherited properties
const objWithInherited = Object.create({inherited: 'value'});
objWithInherited.own = 'value';
console.log("\nObject.entries with inherited property in prototype:");
console.log(Object.entries(objWithInherited));

// The `in` operator checks both own and inherited
console.log("\n'inherited' in objWithInherited:", 'inherited' in objWithInherited);
console.log("Object.prototype.hasOwnProperty.call(objWithInherited, 'inherited'):", Object.prototype.hasOwnProperty.call(objWithInherited, 'inherited'));
EOF

Repository: bmad-code-org/BMAD-METHOD

Length of output: 316


🏁 Script executed:

#!/bin/bash
# Let's verify the actual logic flow with the concern
node <<'EOF'
// Simulate the actual scenario in the code
const scopes = {}; // Empty scopes object (no keys added)
// scopes now has inherited properties from Object.prototype

const cfg = { 'constructor': 'some_value', 'normal_key': 'value' };

console.log("Simulating line 494 logic with inherited properties:");
console.log("When key='constructor':");
console.log("  !(key in scopes):", !('constructor' in scopes));
console.log("  Object.prototype.hasOwnProperty.call(scopes, 'constructor'):", Object.prototype.hasOwnProperty.call(scopes, 'constructor'));

console.log("\nWhen key='normal_key' (not in scopes):");
console.log("  !(key in scopes):", !('normal_key' in scopes));
console.log("  Object.prototype.hasOwnProperty.call(scopes, 'normal_key'):", Object.prototype.hasOwnProperty.call(scopes, 'normal_key'));

// The problem: if onlyDeclaredKeys=true and scopes is empty
const onlyDeclaredKeys = true;
const overrideKeys = new Set();

console.log("\nWith current code using 'in' operator:");
for (const [key, value] of Object.entries(cfg || {})) {
  const shouldSkip = onlyDeclaredKeys && !(key in scopes) && !overrideKeys.has(key);
  console.log(`  key='${key}': shouldSkip=${shouldSkip} (because !(key in scopes)=${!(key in scopes)})`);
}

console.log("\nWith proposed fix using hasOwnProperty:");
for (const [key, value] of Object.entries(cfg || {})) {
  const isDeclared = Object.prototype.hasOwnProperty.call(scopes, key);
  const shouldSkip = onlyDeclaredKeys && !isDeclared && !overrideKeys.has(key);
  console.log(`  key='${key}': shouldSkip=${shouldSkip} (because isDeclared=${isDeclared})`);
}
EOF

Repository: bmad-code-org/BMAD-METHOD

Length of output: 721


Use own-property checks for declared-key filtering.

On line 494, key in scopes matches inherited prototype keys (like constructor), causing undeclared keys to be incorrectly treated as declared. Replace with an own-property check:

Fix
      const overrideKeys = new Set(setOverrideKeys[moduleName] || []);
      for (const [key, value] of Object.entries(cfg || {})) {
        if (!isCore && coreKeys.has(key)) continue;
-       if (onlyDeclaredKeys && !(key in scopes) && !overrideKeys.has(key)) continue;
+       const isDeclared = Object.prototype.hasOwnProperty.call(scopes, key);
+       if (onlyDeclaredKeys && !isDeclared && !overrideKeys.has(key)) continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/core/manifest-generator.js` around lines 491 - 494, The
filtering currently uses the prototype-sensitive check "key in scopes" which
treats inherited properties (e.g., constructor) as declared; update the
declared-key test in the loop that iterates over Object.entries(cfg) to use an
own-property check such as Object.prototype.hasOwnProperty.call(scopes, key) (or
scopes.hasOwnProperty if preferred) instead of "key in scopes", leaving the
surrounding logic with onlyDeclaredKeys, overrideKeys, isCore, and coreKeys
unchanged; ensure you reference the variables overrideKeys, setOverrideKeys,
moduleName, cfg, isCore, coreKeys, onlyDeclaredKeys, and scopes when making the
change.

Comment on lines +121 to +136
let count = 0;
for (const [key, item] of Object.entries(parsed)) {
if (!item || typeof item !== 'object' || !('prompt' in item)) continue;
count++;
const type = inferType(item);
const scope = item.scope === 'user' ? ' [user-scope]' : '';
const defaultStr = item.default === undefined || item.default === null ? '(none)' : String(item.default);
lines.push(` ${code}.${key} (${type}${scope}) default: ${defaultStr}`);
const promptText = formatPromptText(item);
if (promptText) lines.push(` ${promptText}`);
if (Array.isArray(item['single-select'])) {
const values = item['single-select'].map((v) => (typeof v === 'object' ? v.value : v)).filter((v) => v !== undefined);
if (values.length > 0) lines.push(` values: ${values.join(' | ')}`);
}
lines.push('');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include result-only schema keys in --list-options.

OfficialModules already treats both prompted keys and declaredResultKeys as valid --set targets, but Line 123 drops every schema entry that lacks prompt. That means a result-only key can work during install and still never show up in the discovery output.

Suggested fix
   let count = 0;
   for (const [key, item] of Object.entries(parsed)) {
-    if (!item || typeof item !== 'object' || !('prompt' in item)) continue;
+    if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
+    const isSettable = 'prompt' in item || 'result' in item;
+    if (!isSettable) continue;
     count++;
     const type = inferType(item);
     const scope = item.scope === 'user' ? ' [user-scope]' : '';
     const defaultStr = item.default === undefined || item.default === null ? '(none)' : String(item.default);
     lines.push(`  ${code}.${key}    (${type}${scope})  default: ${defaultStr}`);
     const promptText = formatPromptText(item);
-    if (promptText) lines.push(`    ${promptText}`);
+    if (promptText) {
+      lines.push(`    ${promptText}`);
+    } else if ('result' in item) {
+      lines.push('    result-only key');
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/list-options.js` around lines 121 - 136, The loop currently
skips any schema entries that lack a 'prompt', which omits result-only keys;
change the guard so entries are kept if they have a prompt OR their key is
listed in the module's declared result keys (e.g. check
OfficialModules.declaredResultKeys or the appropriate declaredResultKeys set) —
replace the current check (if (!item || typeof item !== 'object' || !('prompt'
in item)) continue;) with a condition that only continues when item is
missing/invalid OR the key is not in declaredResultKeys and has no prompt, so
result-only keys (by key membership in declaredResultKeys) are included while
preserving the existing formatting logic (inferType, formatPromptText,
single-select handling).

Comment on lines +68 to +73
if (await fs.pathExists(schemaPath)) {
try {
schema = yaml.parse(await fs.readFile(schemaPath, 'utf8'));
} catch {
// schema unparseable — fall through to no-schema behavior
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Surface schema parse failures before falling back.

If module.yaml exists but yaml.parse() fails here, the installer silently drops into no-schema mode and then mislabels valid --set keys as undeclared. A warning before the fallback would make the failure diagnosable instead of looking like user error.

As per coding guidelines, tools/**: Build script/tooling. Check error handling and proper exit codes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/modules/official-modules.js` around lines 68 - 73, When
parsing module.yaml (the schemaPath block) swallow the parse exception into a
named error (catch (err)) and surface it before falling back: log a clear
warning including schemaPath and err.message/stack (e.g., via console.warn or
the repo's logger) so users see the parse failure, and set process.exitCode = 1
to mark the tool run as degraded while still allowing the installer to continue
with no-schema behavior; update the try/catch around yaml.parse(await
fs.readFile(schemaPath, 'utf8')) and assignment to schema accordingly.

Comment on lines +75 to +88
const declaredKeys = new Set();
if (schema && typeof schema === 'object') {
for (const [key, decl] of Object.entries(schema)) {
if (decl && typeof decl === 'object' && 'prompt' in decl) declaredKeys.add(key);
}
}

// Warn + track unknown keys from this run's --set entries.
for (const key of Object.keys(overrides)) {
if (!declaredKeys.has(key)) {
await prompts.log.warn(
`--set ${moduleName}.${key} — '${key}' is not a declared config key for module '${moduleName}'; persisted but unused by current install.`,
);
this.setOverrideKeys[moduleName].add(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize result-only --set handling across both paths.

Line 78 only treats prompt entries as declared, while Lines 1650-1652 treat every result-only entry as seedable. The render path only consumes the seeded value when the template contains {value}, so derived result-only keys can still be ignored during normal collection, and the seeded-core path can still warn on them as if they were unknown. Please share one prompt/result-only classifier between these paths and bypass templating when a result-only override has no {value} placeholder.

Also applies to: 1602-1623, 1649-1655

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/modules/official-modules.js` around lines 75 - 88, Create a
single prompt/result-only classifier function (e.g.,
isResultOnlyOrPrompt(schemaEntry) used by both the render and seeded-core paths)
and replace the ad-hoc checks that currently only check for 'prompt' in the
schema (the declaredKeys logic) and the seeded-core logic that treats every
non-prompt entry as seedable; use this shared helper to determine whether a key
is declared/result-only. In the render path, when applying an override from
overrides[moduleName] and the classifier marks it as result-only, skip
templating and only inject the override if the string template contains the
"{value}" placeholder; otherwise treat it as a plain seeded-only result (do not
warn or attempt to render). Also update the warning path that calls
prompts.log.warn and this.setOverrideKeys[moduleName].add(key) to rely on the
same classifier so warnings are only emitted for truly undeclared keys (not for
result-only keys).

Comment thread tools/installer/ui.js
Comment on lines +729 to +735
for (const moduleCode of Object.keys(setOverrides)) {
if (!selectedModuleSet.has(moduleCode)) {
await prompts.log.warn(
`--set ${moduleCode}.* — module '${moduleCode}' is not in the install set; values will be ignored. Add it to --modules to apply.`,
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Actually drop ignored --set modules before returning setOverrides.

This warns that the values will be ignored, but Line 819 still returns the untouched setOverrides map and promptInstall() now threads it downstream in both flows. That leaves rejected overrides in the config state even after the UI says they were dropped.

Suggested fix
     for (const moduleCode of Object.keys(setOverrides)) {
       if (!selectedModuleSet.has(moduleCode)) {
         await prompts.log.warn(
           `--set ${moduleCode}.* — module '${moduleCode}' is not in the install set; values will be ignored. Add it to --modules to apply.`,
         );
+        delete setOverrides[moduleCode];
       }
     }

Also applies to: 819-819

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/ui.js` around lines 729 - 735, The loop that warns about
ignored --set entries (iterating Object.keys(setOverrides)) currently does not
remove those keys, so setOverrides still contains values for modules not in
selectedModuleSet; update the logic inside that loop (or immediately after it)
to delete each ignored module key from the setOverrides map/object (e.g., delete
setOverrides[moduleCode] or setOverrides.delete(moduleCode) depending on type)
before returning from promptInstall(), ensuring promptInstall() and downstream
code receive the cleaned setOverrides without rejected overrides.

…alls

Found via end-to-end smoke test, not flagged by either bot review:
`--set bmm.future_thing=x` was persisted to config.toml on install #1
but silently dropped on the next quick-update reinstall, even though
the per-module _bmad/bmm/config.yaml retained it. The central
manifest's schema-strict partition stripped it because
collectModuleConfigQuick (the quick-update helper) never populated
setOverrideKeys for carried-forward unknown keys, and quickUpdate's
installConfig didn't thread setOverrideKeys into the install call.

This is the same bug class as the round-1 fix to collectModuleConfig
(CodeRabbit major #3155145084) but for the quick-update code path,
which has a separate collection helper.

Fix:
- Add OfficialModules._trackUnknownKeysAsOverrides(moduleName, schema)
  helper that walks collectedConfig[moduleName] and adds any non-schema
  key to setOverrideKeys[moduleName]. Without a schema, every key is
  treated as unknown (safe fallback for modules with no module.yaml).
- Call it from all four return paths in collectModuleConfigQuick:
  no-schema, parse-failed, hasNoConfig+subheader, silent+no-new-keys,
  and the regular end-of-method.
- Mirror ui.collectModuleConfigs's setOverrideKeys conversion in
  installer.quickUpdate so the Set→array round-trip lands in
  Config.build, and writeCentralConfig sees the exemption list.

Tests: +4 cases — collectModuleConfigQuick carry-forward of unknown
key, declared-key non-tracking under quick-update, and
_trackUnknownKeysAsOverrides no-schema fallback. Total 351 passing.

E2E smoke verified: --set <unknown>=x survives install→quick-update,
install→regular-update, and install→quick-update→regular-update with
a new --set added.
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 48a7ec8).
- `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.
@bmadcode bmadcode closed this Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add command line flag for non-interactive BMM project knowledge directory selection

1 participant