bmad-method 6.2.3-next.24 → 6.2.3-next.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.2.3-next.24",
4
+ "version": "6.2.3-next.26",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -1,5 +1,4 @@
1
1
  ---
2
- wipFile: '{implementation_artifacts}/spec-wip.md'
3
2
  deferred_work_file: '{implementation_artifacts}/deferred-work.md'
4
3
  spec_file: '' # set at runtime for both routes before leaving this step
5
4
  ---
@@ -21,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
21
20
 
22
21
  1. Explicit argument
23
22
  Did the user pass a specific file path, spec name, or clear instruction this message?
24
- - If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: ready-for-dev, in-progress, or in-review) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-03 for ready/in-progress, step-04 for review).
23
+ - If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
25
24
  - Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
26
25
 
27
26
  2. Recent conversation
@@ -29,8 +28,8 @@ Before listing artifacts or prompting the user, check whether you already know t
29
28
  Use the same routing as above.
30
29
 
31
30
  3. Otherwise — scan artifacts and ask
32
- - `{wipFile}` exists? Offer resume or archive.
33
- - Active specs (`ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? List them and HALT. Ask user which to resume (or `[N]` for new).
31
+ - Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`?List them and HALT. Ask user which to resume (or `[N]` for new).
32
+ - If `draft` selected: Set `spec_file`. **EARLY EXIT** `./step-02-plan.md` (resume planning from the draft)
34
33
  - If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT** → `./step-03-implement.md`
35
34
  - If `in-review` selected: Set `spec_file`. **EARLY EXIT** → `./step-04-review.md`
36
35
  - Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
@@ -65,7 +64,7 @@ Never ask extra questions if you already understand what the user intends.
65
64
  - On **K**: Proceed as-is.
66
65
  5. Route — choose exactly one:
67
66
 
68
- Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists, append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
67
+ Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
69
68
 
70
69
  **a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
71
70
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- wipFile: '{implementation_artifacts}/spec-wip.md'
3
2
  deferred_work_file: '{implementation_artifacts}/deferred-work.md'
4
3
  ---
5
4
 
@@ -12,11 +11,12 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
12
11
 
13
12
  ## INSTRUCTIONS
14
13
 
15
- 1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
16
- 2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{wipFile}`.
17
- 3. Self-review against READY FOR DEVELOPMENT standard.
18
- 4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
19
- 5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
14
+ 1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
15
+ 2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
16
+ 3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent}` is non-empty, substitute it for the `<frozen-after-approval>` block in your filled spec before writing. Write the result to `{spec_file}`.
17
+ 4. Self-review against READY FOR DEVELOPMENT standard.
18
+ 5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
19
+ 6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
20
20
  - Show user the token count.
21
21
  - HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
22
22
  - On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
@@ -26,7 +26,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
26
26
 
27
27
  Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit`
28
28
 
29
- - **A**: Rename `{wipFile}` to `{spec_file}`, set status `ready-for-dev`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
29
+ - **A**: Set status `ready-for-dev` in `{spec_file}`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
30
30
  - **E**: Apply changes, then return to CHECKPOINT 1.
31
31
 
32
32
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  deferred_work_file: '{implementation_artifacts}/deferred-work.md'
3
- spec_file: '' # set by step-01 before entering this step
4
3
  ---
5
4
 
6
5
  # Step One-Shot: Implement, Review, Present
@@ -70,10 +70,6 @@ Load and read full config from `{main_config}` and resolve:
70
70
 
71
71
  YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`.
72
72
 
73
- ### 2. Paths
74
-
75
- - `wipFile` = `{implementation_artifacts}/spec-wip.md`
76
-
77
- ### 3. First Step Execution
73
+ ### 2. First Step Execution
78
74
 
79
75
  Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
@@ -17,7 +17,6 @@ module.exports = {
17
17
  '--tools <tools>',
18
18
  'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
19
19
  ],
20
- ['--custom-content <paths>', 'Comma-separated list of paths to custom modules/agents/workflows'],
21
20
  ['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
22
21
  ['--user-name <name>', 'Name for agents to use (default: system username)'],
23
22
  ['--communication-language <lang>', 'Language for agent communication (default: English)'],
@@ -10,14 +10,13 @@ const { Manifest } = require('./manifest');
10
10
  class ExistingInstall {
11
11
  #version;
12
12
 
13
- constructor({ installed, version, hasCore, modules, ides, customModules }) {
13
+ constructor({ installed, version, hasCore, modules, ides }) {
14
14
  this.installed = installed;
15
15
  this.#version = version;
16
16
  this.hasCore = hasCore;
17
17
  this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
18
18
  this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
19
19
  this.ides = Object.freeze([...ides]);
20
- this.customModules = Object.freeze([...customModules]);
21
20
  Object.freeze(this);
22
21
  }
23
22
 
@@ -35,7 +34,6 @@ class ExistingInstall {
35
34
  hasCore: false,
36
35
  modules: [],
37
36
  ides: [],
38
- customModules: [],
39
37
  });
40
38
  }
41
39
 
@@ -53,15 +51,11 @@ class ExistingInstall {
53
51
  let hasCore = false;
54
52
  const modules = [];
55
53
  let ides = [];
56
- let customModules = [];
57
54
 
58
55
  const manifest = new Manifest();
59
56
  const manifestData = await manifest.read(bmadDir);
60
57
  if (manifestData) {
61
58
  version = manifestData.version;
62
- if (manifestData.customModules) {
63
- customModules = manifestData.customModules;
64
- }
65
59
  if (manifestData.ides) {
66
60
  ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
67
61
  }
@@ -120,7 +114,7 @@ class ExistingInstall {
120
114
  return ExistingInstall.empty();
121
115
  }
122
116
 
123
- return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules });
117
+ return new ExistingInstall({ installed, version, hasCore, modules, ides });
124
118
  }
125
119
  }
126
120
 
@@ -20,14 +20,12 @@ class InstallPaths {
20
20
 
21
21
  const configDir = path.join(bmadDir, '_config');
22
22
  const agentsDir = path.join(configDir, 'agents');
23
- const customCacheDir = path.join(configDir, 'custom');
24
23
  const coreDir = path.join(bmadDir, 'core');
25
24
 
26
25
  for (const [dir, label] of [
27
26
  [bmadDir, 'bmad directory'],
28
27
  [configDir, 'config directory'],
29
28
  [agentsDir, 'agents config directory'],
30
- [customCacheDir, 'custom modules cache'],
31
29
  [coreDir, 'core module directory'],
32
30
  ]) {
33
31
  await ensureWritableDir(dir, label);
@@ -40,7 +38,6 @@ class InstallPaths {
40
38
  bmadDir,
41
39
  configDir,
42
40
  agentsDir,
43
- customCacheDir,
44
41
  coreDir,
45
42
  isUpdate,
46
43
  });
@@ -2,7 +2,6 @@ const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
3
  const { Manifest } = require('./manifest');
4
4
  const { OfficialModules } = require('../modules/official-modules');
5
- const { CustomModules } = require('../modules/custom-modules');
6
5
  const { IdeManager } = require('../ide/manager');
7
6
  const { FileOps } = require('../file-ops');
8
7
  const { Config } = require('./config');
@@ -19,7 +18,6 @@ class Installer {
19
18
  constructor() {
20
19
  this.externalModuleManager = new ExternalModuleManager();
21
20
  this.manifest = new Manifest();
22
- this.customModules = new CustomModules();
23
21
  this.ideManager = new IdeManager();
24
22
  this.fileOps = new FileOps();
25
23
  this.installedFiles = new Set(); // Track all installed files
@@ -80,8 +78,6 @@ class Installer {
80
78
  const officialModules = await OfficialModules.build(config, paths);
81
79
  const existingInstall = await ExistingInstall.detect(paths.bmadDir);
82
80
 
83
- await this.customModules.discoverPaths(originalConfig, paths);
84
-
85
81
  if (existingInstall.installed) {
86
82
  await this._removeDeselectedModules(existingInstall, config, paths);
87
83
  updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@@ -121,14 +117,9 @@ class Installer {
121
117
  }
122
118
  }
123
119
 
124
- await this._cacheCustomModules(paths, addResult);
125
-
126
- // Compute module lists: official = selected minus custom, all = both
127
- const customModuleIds = new Set(this.customModules.paths.keys());
128
- const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
129
- const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
120
+ const allModules = config.modules || [];
130
121
 
131
- await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
122
+ await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
132
123
 
133
124
  await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
134
125
 
@@ -242,26 +233,6 @@ class Installer {
242
233
  }
243
234
  }
244
235
 
245
- /**
246
- * Cache custom modules into the local cache directory.
247
- * Updates this.customModules.paths in place with cached locations.
248
- */
249
- async _cacheCustomModules(paths, addResult) {
250
- if (!this.customModules.paths || this.customModules.paths.size === 0) return;
251
-
252
- const { CustomModuleCache } = require('./custom-module-cache');
253
- const customCache = new CustomModuleCache(paths.bmadDir);
254
-
255
- for (const [moduleId, sourcePath] of this.customModules.paths) {
256
- const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
257
- sourcePath: sourcePath,
258
- });
259
- this.customModules.paths.set(moduleId, cachedInfo.cachePath);
260
- }
261
-
262
- addResult('Custom modules cached', 'ok');
263
- }
264
-
265
236
  /**
266
237
  * Install modules, create directories, generate configs and manifests.
267
238
  */
@@ -284,11 +255,6 @@ class Installer {
284
255
  installedModuleNames,
285
256
  });
286
257
 
287
- await this._installCustomModules(config, paths, addResult, officialModules, {
288
- message,
289
- installedModuleNames,
290
- });
291
-
292
258
  return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
293
259
  },
294
260
  });
@@ -515,48 +481,7 @@ class Installer {
515
481
  }
516
482
 
517
483
  /**
518
- * Scan the custom module cache directory and register any cached custom modules
519
- * that aren't already known from the manifest or external module list.
520
- * @param {Object} paths - InstallPaths instance
521
- */
522
- async _scanCachedCustomModules(paths) {
523
- const cacheDir = paths.customCacheDir;
524
- if (!(await fs.pathExists(cacheDir))) {
525
- return;
526
- }
527
-
528
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
529
-
530
- for (const cachedModule of cachedModules) {
531
- const moduleId = cachedModule.name;
532
- const cachedPath = path.join(cacheDir, moduleId);
533
-
534
- // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
535
- if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
536
- continue;
537
- }
538
-
539
- // Skip if we already have this module from manifest
540
- if (this.customModules.paths.has(moduleId)) {
541
- continue;
542
- }
543
-
544
- // Check if this is an external official module - skip cache for those
545
- const isExternal = await this.externalModuleManager.hasModule(moduleId);
546
- if (isExternal) {
547
- continue;
548
- }
549
-
550
- // Check if this is actually a custom module (has module.yaml)
551
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
552
- if (await fs.pathExists(moduleYamlPath)) {
553
- this.customModules.paths.set(moduleId, cachedPath);
554
- }
555
- }
556
- }
557
-
558
- /**
559
- * Common update preparation: detect files, preserve core config, scan cache, back up.
484
+ * Common update preparation: detect files, preserve core config, back up.
560
485
  * @param {Object} paths - InstallPaths instance
561
486
  * @param {Object} config - Clean config (may have coreConfig updated)
562
487
  * @param {Object} existingInstall - Detection result
@@ -584,8 +509,6 @@ class Installer {
584
509
  }
585
510
  }
586
511
 
587
- await this._scanCachedCustomModules(paths);
588
-
589
512
  const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
590
513
 
591
514
  return {
@@ -677,38 +600,6 @@ class Installer {
677
600
  }
678
601
  }
679
602
 
680
- /**
681
- * Install custom modules using CustomModules.install().
682
- * Source paths come from this.customModules.paths (populated by discoverPaths).
683
- */
684
- async _installCustomModules(config, paths, addResult, officialModules, ctx) {
685
- const { message, installedModuleNames } = ctx;
686
- const isQuickUpdate = config.isQuickUpdate();
687
-
688
- for (const [moduleName, sourcePath] of this.customModules.paths) {
689
- if (installedModuleNames.has(moduleName)) continue;
690
- installedModuleNames.add(moduleName);
691
-
692
- message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
693
-
694
- const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
695
- const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
696
- moduleConfig: collectedModuleConfig,
697
- });
698
-
699
- // Generate runtime config.yaml with merged values
700
- await this.generateModuleConfigs(paths.bmadDir, {
701
- [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
702
- });
703
-
704
- // Get display name from source module.yaml; version from marketplace.json
705
- const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
706
- const displayName = moduleInfo?.name || moduleName;
707
- const version = await this._getMarketplaceVersion(sourcePath);
708
- addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
709
- }
710
- }
711
-
712
603
  /**
713
604
  * Read files-manifest.csv
714
605
  * @param {string} bmadDir - BMAD installation directory
@@ -1253,16 +1144,9 @@ class Installer {
1253
1144
  const configuredIdes = existingInstall.ides;
1254
1145
  const projectRoot = path.dirname(bmadDir);
1255
1146
 
1256
- const customModuleSources = await this.customModules.assembleQuickUpdateSources(
1257
- config,
1258
- existingInstall,
1259
- bmadDir,
1260
- this.externalModuleManager,
1261
- );
1262
-
1263
1147
  // Get available modules (what we have source for)
1264
1148
  const availableModulesData = await new OfficialModules().listAvailable();
1265
- const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
1149
+ const availableModules = [...availableModulesData.modules];
1266
1150
 
1267
1151
  // Add external official modules to available modules
1268
1152
  const externalModules = await this.externalModuleManager.listAvailable();
@@ -1277,52 +1161,12 @@ class Installer {
1277
1161
  }
1278
1162
  }
1279
1163
 
1280
- // Add custom modules from manifest if their sources exist
1281
- for (const [moduleId, customModule] of customModuleSources) {
1282
- const sourcePath = customModule.sourcePath;
1283
- if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
1284
- availableModules.push({
1285
- id: moduleId,
1286
- name: customModule.name || moduleId,
1287
- path: sourcePath,
1288
- isCustom: true,
1289
- fromManifest: true,
1290
- });
1291
- }
1292
- }
1293
-
1294
- // Handle missing custom module sources
1295
- const customModuleResult = await this.handleMissingCustomSources(
1296
- customModuleSources,
1297
- bmadDir,
1298
- projectRoot,
1299
- 'update',
1300
- installedModules,
1301
- config.skipPrompts || false,
1302
- );
1303
-
1304
- const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
1305
-
1306
- const customModulesFromManifest = validCustomModules.map((m) => ({
1307
- ...m,
1308
- isCustom: true,
1309
- hasUpdate: true,
1310
- }));
1311
-
1312
- const allAvailableModules = [...availableModules, ...customModulesFromManifest];
1313
- const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
1164
+ const availableModuleIds = new Set(availableModules.map((m) => m.id));
1314
1165
 
1315
1166
  // Only update modules that are BOTH installed AND available (we have source for)
1316
1167
  const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
1317
1168
  const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
1318
1169
 
1319
- // Add custom modules that were kept without sources to the skipped modules
1320
- for (const keptModule of keptModulesWithoutSources) {
1321
- if (!skippedModules.includes(keptModule)) {
1322
- skippedModules.push(keptModule);
1323
- }
1324
- }
1325
-
1326
1170
  if (skippedModules.length > 0) {
1327
1171
  await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
1328
1172
  }
@@ -1367,9 +1211,7 @@ class Installer {
1367
1211
  actionType: 'install',
1368
1212
  _quickUpdate: true,
1369
1213
  _preserveModules: skippedModules,
1370
- _customModuleSources: customModuleSources,
1371
1214
  _existingModules: installedModules,
1372
- customContent: config.customContent,
1373
1215
  };
1374
1216
 
1375
1217
  await this.install(installConfig);
@@ -1504,239 +1346,6 @@ class Installer {
1504
1346
  return this._readOutputFolder(bmadDir);
1505
1347
  }
1506
1348
 
1507
- /**
1508
- * Handle missing custom module sources interactively
1509
- * @param {Map} customModuleSources - Map of custom module ID to info
1510
- * @param {string} bmadDir - BMAD directory
1511
- * @param {string} projectRoot - Project root directory
1512
- * @param {string} operation - Current operation ('update', 'compile', etc.)
1513
- * @param {Array} installedModules - Array of installed module IDs (will be modified)
1514
- * @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
1515
- * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
1516
- */
1517
- async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
1518
- const validCustomModules = [];
1519
- const keptModulesWithoutSources = []; // Track modules kept without sources
1520
- const customModulesWithMissingSources = [];
1521
-
1522
- // Check which sources exist
1523
- for (const [moduleId, customInfo] of customModuleSources) {
1524
- if (await fs.pathExists(customInfo.sourcePath)) {
1525
- validCustomModules.push({
1526
- id: moduleId,
1527
- name: customInfo.name,
1528
- path: customInfo.sourcePath,
1529
- info: customInfo,
1530
- });
1531
- } else {
1532
- // For cached modules that are missing, we just skip them without prompting
1533
- if (customInfo.cached) {
1534
- // Skip cached modules without prompting
1535
- keptModulesWithoutSources.push({
1536
- id: moduleId,
1537
- name: customInfo.name,
1538
- cached: true,
1539
- });
1540
- } else {
1541
- customModulesWithMissingSources.push({
1542
- id: moduleId,
1543
- name: customInfo.name,
1544
- sourcePath: customInfo.sourcePath,
1545
- relativePath: customInfo.relativePath,
1546
- info: customInfo,
1547
- });
1548
- }
1549
- }
1550
- }
1551
-
1552
- // If no missing sources, return immediately
1553
- if (customModulesWithMissingSources.length === 0) {
1554
- return {
1555
- validCustomModules,
1556
- keptModulesWithoutSources: [],
1557
- };
1558
- }
1559
-
1560
- // Non-interactive mode: keep all modules with missing sources
1561
- if (skipPrompts) {
1562
- for (const missing of customModulesWithMissingSources) {
1563
- keptModulesWithoutSources.push(missing.id);
1564
- }
1565
- return { validCustomModules, keptModulesWithoutSources };
1566
- }
1567
-
1568
- await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
1569
-
1570
- let keptCount = 0;
1571
- let updatedCount = 0;
1572
- let removedCount = 0;
1573
-
1574
- for (const missing of customModulesWithMissingSources) {
1575
- await prompts.log.message(
1576
- `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
1577
- );
1578
-
1579
- const choices = [
1580
- {
1581
- name: 'Keep installed (will not be processed)',
1582
- value: 'keep',
1583
- hint: 'Keep',
1584
- },
1585
- {
1586
- name: 'Specify new source location',
1587
- value: 'update',
1588
- hint: 'Update',
1589
- },
1590
- ];
1591
-
1592
- // Only add remove option if not just compiling agents
1593
- if (operation !== 'compile-agents') {
1594
- choices.push({
1595
- name: '⚠️ REMOVE module completely (destructive!)',
1596
- value: 'remove',
1597
- hint: 'Remove',
1598
- });
1599
- }
1600
-
1601
- const action = await prompts.select({
1602
- message: `How would you like to handle "${missing.name}"?`,
1603
- choices,
1604
- });
1605
-
1606
- switch (action) {
1607
- case 'update': {
1608
- // Use sync validation because @clack/prompts doesn't support async validate
1609
- const newSourcePath = await prompts.text({
1610
- message: 'Enter the new path to the custom module:',
1611
- default: missing.sourcePath,
1612
- validate: (input) => {
1613
- if (!input || input.trim() === '') {
1614
- return 'Please enter a path';
1615
- }
1616
- const expandedPath = path.resolve(input.trim());
1617
- if (!fs.pathExistsSync(expandedPath)) {
1618
- return 'Path does not exist';
1619
- }
1620
- // Check if it looks like a valid module
1621
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
1622
- const agentsPath = path.join(expandedPath, 'agents');
1623
- const workflowsPath = path.join(expandedPath, 'workflows');
1624
-
1625
- if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
1626
- return 'Path does not appear to contain a valid custom module';
1627
- }
1628
- return; // clack expects undefined for valid input
1629
- },
1630
- });
1631
-
1632
- // Defensive: handleCancel should have exited, but guard against symbol propagation
1633
- if (typeof newSourcePath !== 'string') {
1634
- keptCount++;
1635
- keptModulesWithoutSources.push(missing.id);
1636
- continue;
1637
- }
1638
-
1639
- // Update the source in manifest
1640
- const resolvedPath = path.resolve(newSourcePath.trim());
1641
- missing.info.sourcePath = resolvedPath;
1642
- // Remove relativePath - we only store absolute sourcePath now
1643
- delete missing.info.relativePath;
1644
- await this.manifest.addCustomModule(bmadDir, missing.info);
1645
-
1646
- validCustomModules.push({
1647
- id: missing.id,
1648
- name: missing.name,
1649
- path: resolvedPath,
1650
- info: missing.info,
1651
- });
1652
-
1653
- updatedCount++;
1654
- await prompts.log.success('Updated source location');
1655
-
1656
- break;
1657
- }
1658
- case 'remove': {
1659
- // Extra confirmation for destructive remove
1660
- await prompts.log.error(
1661
- `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
1662
- );
1663
-
1664
- const confirmDelete = await prompts.confirm({
1665
- message: 'Are you absolutely sure you want to delete this module?',
1666
- default: false,
1667
- });
1668
-
1669
- if (confirmDelete) {
1670
- const typedConfirm = await prompts.text({
1671
- message: 'Type "DELETE" to confirm permanent deletion:',
1672
- validate: (input) => {
1673
- if (input !== 'DELETE') {
1674
- return 'You must type "DELETE" exactly to proceed';
1675
- }
1676
- return; // clack expects undefined for valid input
1677
- },
1678
- });
1679
-
1680
- if (typedConfirm === 'DELETE') {
1681
- // Remove the module from filesystem and manifest
1682
- const modulePath = path.join(bmadDir, missing.id);
1683
- if (await fs.pathExists(modulePath)) {
1684
- const fsExtra = require('fs-extra');
1685
- await fsExtra.remove(modulePath);
1686
- await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
1687
- }
1688
-
1689
- await this.manifest.removeModule(bmadDir, missing.id);
1690
- await this.manifest.removeCustomModule(bmadDir, missing.id);
1691
- await prompts.log.warn('Removed from manifest');
1692
-
1693
- // Also remove from installedModules list
1694
- if (installedModules && installedModules.includes(missing.id)) {
1695
- const index = installedModules.indexOf(missing.id);
1696
- if (index !== -1) {
1697
- installedModules.splice(index, 1);
1698
- }
1699
- }
1700
-
1701
- removedCount++;
1702
- await prompts.log.error(`"${missing.name}" has been permanently removed`);
1703
- } else {
1704
- await prompts.log.message('Removal cancelled - module will be kept');
1705
- keptCount++;
1706
- }
1707
- } else {
1708
- await prompts.log.message('Removal cancelled - module will be kept');
1709
- keptCount++;
1710
- }
1711
-
1712
- break;
1713
- }
1714
- case 'keep': {
1715
- keptCount++;
1716
- keptModulesWithoutSources.push(missing.id);
1717
- await prompts.log.message('Module will be kept as-is');
1718
-
1719
- break;
1720
- }
1721
- // No default
1722
- }
1723
- }
1724
-
1725
- // Show summary
1726
- if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
1727
- let summary = 'Summary for custom modules with missing sources:';
1728
- if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
1729
- if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
1730
- if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
1731
- await prompts.log.message(summary);
1732
- }
1733
-
1734
- return {
1735
- validCustomModules,
1736
- keptModulesWithoutSources,
1737
- };
1738
- }
1739
-
1740
1349
  /**
1741
1350
  * Find the bmad installation directory in a project
1742
1351
  * Always uses the standard _bmad folder name