Compare commits

...

14 Commits

Author SHA1 Message Date
Can Tecim 6173066791
Merge 084e602f94 into 1da6bf80df 2026-05-18 19:37:23 -04:00
github-actions[bot] 1da6bf80df chore(release): v6.7.1 [skip ci] 2026-05-18 13:59:29 +00:00
Brian b4f47e1f05
docs(changelog): add v6.7.1 entry for installer stale-module fix (#2393)
Documents the fix landed in #2391 and the manual step required for users
who installed the experimental BMad Automator module under its previous
`baut` code.
2026-05-18 08:56:41 -05:00
Dicky Moore a08522631b
fix(installer): preserve stale installed modules during update (#2391)
* fix(installer): preserve stale installed modules on update

* test: drop stale baut regression case

* fix(installer): preserve source-backed modules and configs

* fix(installer): retain preserved module config in quick update

* fix(installer): preserve module config blocks for retained modules

* fix(installer): preserve user-scope blocks for retained modules

* fix(installer): retain stale modules during updates
2026-05-18 08:39:11 -05:00
Murat K Ozcan 084e602f94
Merge branch 'main' into patch-1 2026-05-09 19:09:18 -05:00
Can Tecim 2a38c060e3 fix(): fix post-merge issues 2026-05-09 22:21:09 +03:00
Can Tecim c695af84c3 Merge branch 'main' into patch-1
# Conflicts:
#	src/bmm-skills/4-implementation/bmad-dev-story/workflow.md
2026-05-09 22:18:11 +03:00
Can Tecim f70a9e0569
Merge branch 'main' into patch-1 2026-04-19 19:54:13 +03:00
Can Tecim 9794507384
fix: update allowed modifications 2026-04-19 22:07:27 +07:00
Can Tecim 18c6fb6b7c
Merge branch 'main' into patch-1 2026-04-19 17:54:02 +03:00
Can Tecim 84bf1b7bba
refactor: allow 'Marking Review Follow-ups'
Updated instructions to include 'Marking Review Follow-ups' in the modification areas.
2026-04-19 21:53:23 +07:00
Can Tecim e67672ae38
refactor: update dev-story workflow instructions 2026-04-19 21:49:23 +07:00
Can Tecim 02a8c203c6
refactor: fix findings section in step-04-present.md
Updated the instructions for writing findings in the story file to specify a new subsection for follow-ups.
2026-04-19 21:48:20 +07:00
Can Tecim c90bdce306
refactor: allow dev-story skill to modify Review Findings todo items 2026-04-03 04:48:01 +07:00
8 changed files with 165 additions and 31 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## v6.7.1 - 2026-05-18
### 🐛 Fixes
* **Installer no longer errors when a previously installed module's source can no longer be found** — In v6.7.0 the experimental BMad Automator module's installer code (the value used for its `_bmad/<code>/` folder and manifest entry) was renamed from `baut` to `automator`. Anyone who had installed it under the old `baut` code saw `quick-update` fail with `Source for module 'baut' is not available` and risked having the existing install removed. The installer now detects installed modules that can no longer be resolved from any source, leaves them in place untouched, and continues the update. If you previously installed it as `baut` and want the renamed `automator` version, run `npx bmad-method install`, choose **Modify BMAD Installation**, and reselect **BMad Automator**; the old `_bmad/baut/` directory can then be deleted manually
## v6.7.0 - 2026-05-17
### ✨ Headline

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "bmad-method",
"version": "6.7.0",
"version": "6.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bmad-method",
"version": "6.7.0",
"version": "6.7.1",
"license": "MIT",
"dependencies": {
"@clack/core": "^1.3.1",

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method",
"version": "6.7.0",
"version": "6.7.1",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",

View File

@ -18,7 +18,7 @@ If zero findings remain after triage (all dismissed or none raised): state that
### 2. Write findings to the story file
If `{spec_file}` exists and contains a Tasks/Subtasks section, append a `### Review Findings` subsection. Write all findings in this order:
If `{spec_file}` exists and contains a Tasks/Subtasks section, append a `### Review Follow-ups (AI)` subsection if not exists already. Append all findings in this order:
1. **`decision-needed`** findings (unchecked):
`- [ ] [Review][Decision] <Title> — <Detail>`

View File

@ -10,7 +10,7 @@ description: 'Execute story implementation following a context filled story spec
**Your Role:** Developer implementing the story.
- Communicate all responses in {communication_language} and language MUST be tailored to {user_skill_level}
- Generate all documents in {document_output_language}
- Only modify the story file in these areas: Tasks/Subtasks checkboxes, Dev Agent Record (Debug Log, Completion Notes), File List, Change Log, and Status
- Only modify the story file in these areas: Tasks/Subtasks checkboxes (including Review Follow-ups (AI)), Dev Agent Record (Debug Log, Completion Notes), File List, Change Log, and Status
- Execute ALL steps in exact order; do NOT skip steps
- Absolutely DO NOT stop because of "milestones", "significant progress", or "session boundaries". Continue in a single execution until the story is COMPLETE (all ACs satisfied and all tasks/subtasks checked) UNLESS a HALT condition is triggered or the USER gives other instruction.
- Do NOT schedule a "next session" or request review pauses unless a HALT condition applies. Only Step 9 decides completion.
@ -75,7 +75,7 @@ Activation is complete. Begin the workflow below.
<workflow>
<critical>Communicate all responses in {communication_language} and language MUST be tailored to {user_skill_level}</critical>
<critical>Generate all documents in {document_output_language}</critical>
<critical>Only modify the story file in these areas: Tasks/Subtasks checkboxes, Dev Agent Record (Debug Log, Completion Notes), File List,
<critical>Only modify the story file in these areas: Tasks/Subtasks checkboxes (including Review Follow-ups (AI)), Dev Agent Record (Debug Log, Completion Notes), File List,
Change Log, and Status</critical>
<critical>Execute ALL steps in exact order; do NOT skip steps</critical>
<critical>Absolutely DO NOT stop because of "milestones", "significant progress", or "session boundaries". Continue in a single execution

View File

@ -54,7 +54,7 @@ class Installer {
}
if (existingInstall.installed) {
await this._removeDeselectedModules(existingInstall, config, paths);
await this._removeDeselectedModules(existingInstall, config, paths, originalConfig._preserveModules || []);
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
await this._removeDeselectedIdes(existingInstall, config, paths);
}
@ -76,25 +76,23 @@ class Installer {
const results = [];
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
// Capture previously installed skill IDs before they get overwritten
const previousSkillIds = new Set();
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
if (await fs.pathExists(prevCsvPath)) {
try {
const csvParse = require('csv-parse/sync');
const content = await fs.readFile(prevCsvPath, 'utf8');
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
for (const r of records) {
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
}
} catch (error) {
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
}
}
// Capture previously installed skill rows before they get overwritten
const preservedModules = originalConfig._preserveModules || [];
const previousSkillManifestRows = await this._readSkillManifestRows(paths.bmadDir);
const previousSkillIds = this._getPreviousSkillIdsForCleanup(previousSkillManifestRows, preservedModules);
const allModules = config.modules || [];
await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
await this._installAndConfigure(
config,
originalConfig,
paths,
allModules,
allModules,
addResult,
officialModules,
previousSkillManifestRows,
);
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
@ -144,10 +142,11 @@ class Installer {
* Remove modules that were previously installed but are no longer selected.
* No confirmation the user's module selection is the decision.
*/
async _removeDeselectedModules(existingInstall, config, paths) {
async _removeDeselectedModules(existingInstall, config, paths, preservedModules = []) {
const previouslyInstalled = new Set(existingInstall.moduleIds);
const newlySelected = new Set(config.modules || []);
const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core');
const preserved = new Set(preservedModules);
const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core' && !preserved.has(m));
for (const moduleId of toRemove) {
const modulePath = paths.moduleDir(moduleId);
@ -212,7 +211,16 @@ class Installer {
/**
* Install modules, create directories, generate configs and manifests.
*/
async _installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules) {
async _installAndConfigure(
config,
originalConfig,
paths,
officialModuleIds,
allModules,
addResult,
officialModules,
previousSkillManifestRows = [],
) {
const isQuickUpdate = config.isQuickUpdate();
const moduleConfigs = officialModules.moduleConfigs;
@ -291,25 +299,29 @@ class Installer {
message('Generating manifests...');
const manifestGen = new ManifestGenerator();
const preservedModules = originalConfig._preserveModules || [];
const allModulesForManifest = config.isQuickUpdate()
? originalConfig._existingModules || allModules || []
: originalConfig._preserveModules
? [...allModules, ...originalConfig._preserveModules]
: preservedModules.length > 0
? [...allModules, ...preservedModules]
: allModules || [];
let modulesForCsvPreserve;
if (config.isQuickUpdate()) {
modulesForCsvPreserve = originalConfig._existingModules || allModules || [];
} else {
modulesForCsvPreserve = originalConfig._preserveModules ? [...allModules, ...originalConfig._preserveModules] : allModules;
modulesForCsvPreserve = preservedModules.length > 0 ? [...allModules, ...preservedModules] : allModules;
}
await this._trackPreservedModuleFiles(paths.bmadDir, preservedModules);
await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [],
preservedModules: modulesForCsvPreserve,
moduleConfigs,
});
await this._appendPreservedSkillManifestRows(paths.bmadDir, previousSkillManifestRows, preservedModules);
// Apply post-install --set TOML patches. Runs after writeCentralConfig
// (inside generateManifests above) so the patch operates on the
@ -411,6 +423,62 @@ class Installer {
}
}
async _readSkillManifestRows(bmadDir) {
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return [];
try {
const csvParse = require('csv-parse/sync');
const content = await fs.readFile(csvPath, 'utf8');
return csvParse.parse(content, { columns: true, skip_empty_lines: true });
} catch (error) {
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
return [];
}
}
_getPreviousSkillIdsForCleanup(previousRows, preservedModules = []) {
const preservedModuleSet = new Set(preservedModules || []);
const ids = new Set();
for (const row of previousRows || []) {
if (row.canonicalId && !preservedModuleSet.has(row.module)) {
ids.add(row.canonicalId);
}
}
return ids;
}
async _appendPreservedSkillManifestRows(bmadDir, previousRows, preservedModules = []) {
if (!previousRows || previousRows.length === 0 || preservedModules.length === 0) return;
const preservedModuleSet = new Set(preservedModules);
const rowsToPreserve = previousRows.filter((row) => row.canonicalId && row.module && preservedModuleSet.has(row.module));
if (rowsToPreserve.length === 0) return;
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return;
const currentRows = await this._readSkillManifestRows(bmadDir);
const activeIds = new Set(currentRows.map((row) => row.canonicalId).filter(Boolean));
const appendedRows = [];
for (const row of rowsToPreserve) {
if (activeIds.has(row.canonicalId)) continue;
activeIds.add(row.canonicalId);
appendedRows.push(
[row.canonicalId, row.name || row.canonicalId, row.description || '', row.module, row.path || '']
.map((field) => this.escapeCSVField(field))
.join(','),
);
}
if (appendedRows.length === 0) return;
const currentContent = await fs.readFile(csvPath, 'utf8');
const prefix = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
await fs.writeFile(csvPath, prefix + appendedRows.join('\n') + '\n', 'utf8');
}
/**
* Restore custom and modified files that were backed up before the update.
* No-op for fresh installs (updateState is null).
@ -597,6 +665,15 @@ class Installer {
}
}
async _trackPreservedModuleFiles(bmadDir, preservedModules = []) {
for (const moduleName of preservedModules) {
const modulePath = path.join(bmadDir, moduleName);
if (await fs.pathExists(modulePath)) {
await this._trackFilesRecursive(modulePath);
}
}
}
/**
* Install official (non-custom) modules.
* @param {Object} config - Installation configuration

View File

@ -501,7 +501,7 @@ class ConfigDrivenIdeSetup {
// Build removal set: previously installed skills + removals.txt entries
let removalSet;
if (options.previousSkillIds && options.previousSkillIds.size > 0) {
if (options.previousSkillIds) {
// Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
removalSet = new Set(options.previousSkillIds);
if (resolvedBmadDir) {
@ -547,7 +547,7 @@ class ConfigDrivenIdeSetup {
// previousSkillIds — full uninstall or per-IDE removal via
// cleanupByList), don't spare anything; the IDE itself is going away,
// so its pointers should go with it.
const isInstallFlow = options.previousSkillIds && options.previousSkillIds.size > 0;
const isInstallFlow = !!options.previousSkillIds;
const activeSkillIds = isInstallFlow ? await this._readActiveSkillIds(resolvedBmadDir) : new Set();
const extension = this.installerConfig.commands_extension || '.md';
await this.cleanupCommandPointers(

View File

@ -110,6 +110,44 @@ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault =
* UI utilities for the installer
*/
class UI {
async _retainUnavailableInstalledModules(selectedModules, installedModuleIds, bmadDir, options = {}) {
const { OfficialModules } = require('./modules/official-modules');
const officialCodes = new Set(['core']);
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
for (const mod of builtInModules) {
officialCodes.add(mod.id);
}
const externalManager = new ExternalModuleManager();
const registryModules = await externalManager.listAvailable();
for (const mod of registryModules) {
officialCodes.add(mod.code);
}
const { CustomModuleManager } = require('./modules/custom-module-manager');
const customMgr = new CustomModuleManager();
const selectedSet = new Set(selectedModules);
const preserveModules = [];
for (const moduleId of installedModuleIds) {
if (moduleId === 'core') continue;
if (!selectedSet.has(moduleId) && !options.preserveUnselected) continue;
if (officialCodes.has(moduleId)) continue;
const customSource = await customMgr.findModuleSourceByCode(moduleId, { bmadDir });
if (!customSource) {
preserveModules.push(moduleId);
}
}
const preservedSet = new Set(preserveModules);
return {
selectedModules: selectedModules.filter((moduleId) => !preservedSet.has(moduleId)),
preserveModules,
};
}
/**
* Prompt for installation configuration
* @param {Object} options - Command-line options from install command
@ -273,6 +311,18 @@ class UI {
selectedModules.unshift('core');
}
const retainedModuleResult = await this._retainUnavailableInstalledModules(selectedModules, installedModuleIds, bmadDir, {
preserveUnselected: options.yes && !options.modules,
});
selectedModules = retainedModuleResult.selectedModules;
const preservedModules = retainedModuleResult.preserveModules;
if (preservedModules.length > 0) {
await prompts.log.warn(
`Retaining ${preservedModules.length} installed module(s) with no available source: ${preservedModules.join(', ')}`,
);
}
// For existing installs, resolve per-module update decisions BEFORE
// we clone anything. Reads the existing manifest's recorded channel
// per module and prompts the user on available upgrades (patch/minor
@ -317,6 +367,7 @@ class UI {
setOverrides,
skipPrompts: options.yes || false,
channelOptions,
_preserveModules: preservedModules,
};
}
}