bmad-method 6.2.3-next.10 → 6.2.3-next.12

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.10",
4
+ "version": "6.2.3-next.12",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  wipFile: '{implementation_artifacts}/spec-wip.md'
3
3
  deferred_work_file: '{implementation_artifacts}/deferred-work.md'
4
- spec_file: '' # set at runtime for plan-code-review before leaving this step
4
+ spec_file: '' # set at runtime for both routes before leaving this step
5
5
  ---
6
6
 
7
7
  # Step 1: Clarify and Route
@@ -52,11 +52,13 @@ Never ask extra questions if you already understand what the user intends.
52
52
  - On **K**: Proceed as-is.
53
53
  5. Route — choose exactly one:
54
54
 
55
+ 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`.
56
+
55
57
  **a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
58
+
56
59
  **EARLY EXIT** → `./step-oneshot.md`
57
60
 
58
61
  **b) Plan-code-review** — everything else. When uncertain whether blast radius is truly zero, choose this path.
59
- 1. 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`.
60
62
 
61
63
 
62
64
  ## NEXT
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  deferred_work_file: '{implementation_artifacts}/deferred-work.md'
3
+ spec_file: '' # set by step-01 before entering this step
3
4
  ---
4
5
 
5
6
  # Step One-Shot: Implement, Review, Present
@@ -29,19 +30,31 @@ Deduplicate all review findings. Three categories only:
29
30
 
30
31
  If a finding is caused by this change but too significant for a trivial patch, HALT and present it to the human for decision before proceeding.
31
32
 
33
+ ### Generate Spec Trace
34
+
35
+ Set `{title}` = a concise title derived from the clarified intent.
36
+
37
+ Write `{spec_file}` using `./spec-template.md`. Fill only these sections — delete all others:
38
+
39
+ 1. **Frontmatter** — set `title: '{title}'`, `type`, `created`, `status: 'done'`. Add `route: 'one-shot'`.
40
+ 2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
41
+ 3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
42
+
32
43
  ### Commit
33
44
 
34
45
  If version control is available and the tree is dirty, create a local commit with a conventional message derived from the intent. If VCS is unavailable, skip.
35
46
 
36
47
  ### Present
37
48
 
38
- 1. Open all changed files in the user's editor so they can review the code directly:
39
- - Resolve two sets of absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) each changed file. Run `code -r "{absolute-root}" <absolute-changed-file-paths>` — the root first so VS Code opens in the right context, then each changed file. Always double-quote paths to handle spaces and special characters.
40
- - If `code` is not available (command fails), skip gracefully and list the file paths instead.
49
+ 1. Open the spec in the user's editor so they can click through the Suggested Review Order:
50
+ - Resolve two absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) `{spec_file}`. Run `code -r "{absolute-root}" "{absolute-spec-file}"` — the root first so VS Code opens in the right context, then the spec file. Always double-quote paths to handle spaces and special characters.
51
+ - If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
41
52
  2. Display a summary in conversation output, including:
42
53
  - The commit hash (if one was created).
43
- - List of files changed with one-line descriptions. Use CWD-relative paths with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/`.
54
+ - List of files changed with one-line descriptions. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability this differs from spec-file links which use spec-file-relative paths.
44
55
  - Review findings breakdown: patches applied, items deferred, items rejected. If all findings were rejected, say so.
56
+ - A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
57
+ - **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
45
58
  3. Offer to push and/or create a pull request.
46
59
 
47
60
  HALT and wait for human input.
@@ -1144,59 +1144,12 @@ class Installer {
1144
1144
  const configuredIdes = existingInstall.ides;
1145
1145
  const projectRoot = path.dirname(bmadDir);
1146
1146
 
1147
- // Get custom module sources: first from --custom-content (re-cache from source), then from cache
1148
- const customModuleSources = new Map();
1149
- if (config.customContent?.sources?.length > 0) {
1150
- for (const source of config.customContent.sources) {
1151
- if (source.id && source.path && (await fs.pathExists(source.path))) {
1152
- customModuleSources.set(source.id, {
1153
- id: source.id,
1154
- name: source.name || source.id,
1155
- sourcePath: source.path,
1156
- cached: false, // From CLI, will be re-cached
1157
- });
1158
- }
1159
- }
1160
- }
1161
- const cacheDir = path.join(bmadDir, '_config', 'custom');
1162
- if (await fs.pathExists(cacheDir)) {
1163
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
1164
-
1165
- for (const cachedModule of cachedModules) {
1166
- const moduleId = cachedModule.name;
1167
- const cachedPath = path.join(cacheDir, moduleId);
1168
-
1169
- // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
1170
- if (!(await fs.pathExists(cachedPath))) {
1171
- continue;
1172
- }
1173
- if (!cachedModule.isDirectory()) {
1174
- continue;
1175
- }
1176
-
1177
- // Skip if we already have this module from manifest
1178
- if (customModuleSources.has(moduleId)) {
1179
- continue;
1180
- }
1181
-
1182
- // Check if this is an external official module - skip cache for those
1183
- const isExternal = await this.externalModuleManager.hasModule(moduleId);
1184
- if (isExternal) {
1185
- continue;
1186
- }
1187
-
1188
- // Check if this is actually a custom module (has module.yaml)
1189
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
1190
- if (await fs.pathExists(moduleYamlPath)) {
1191
- customModuleSources.set(moduleId, {
1192
- id: moduleId,
1193
- name: moduleId,
1194
- sourcePath: cachedPath,
1195
- cached: true,
1196
- });
1197
- }
1198
- }
1199
- }
1147
+ const customModuleSources = await this.customModules.assembleQuickUpdateSources(
1148
+ config,
1149
+ existingInstall,
1150
+ bmadDir,
1151
+ this.externalModuleManager,
1152
+ );
1200
1153
 
1201
1154
  // Get available modules (what we have source for)
1202
1155
  const availableModulesData = await new OfficialModules().listAvailable();
@@ -377,10 +377,12 @@ class ManifestGenerator {
377
377
  */
378
378
  async writeMainManifest(cfgDir) {
379
379
  const manifestPath = path.join(cfgDir, 'manifest.yaml');
380
+ const installedModuleSet = new Set(this.modules);
380
381
 
381
382
  // Read existing manifest to preserve install date
382
383
  let existingInstallDate = null;
383
384
  const existingModulesMap = new Map();
385
+ let existingCustomModules = [];
384
386
 
385
387
  if (await fs.pathExists(manifestPath)) {
386
388
  try {
@@ -402,6 +404,12 @@ class ManifestGenerator {
402
404
  }
403
405
  }
404
406
  }
407
+
408
+ if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) {
409
+ // We filter here so manifest regeneration preserves source metadata only for custom modules that
410
+ // are still installed. Without that, customModules can retain stale entries for modules that were removed.
411
+ existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id));
412
+ }
405
413
  } catch {
406
414
  // If we can't read existing manifest, continue with defaults
407
415
  }
@@ -437,6 +445,7 @@ class ManifestGenerator {
437
445
  lastUpdated: new Date().toISOString(),
438
446
  },
439
447
  modules: updatedModules,
448
+ customModules: existingCustomModules,
440
449
  ides: this.selectedIdes,
441
450
  };
442
451
 
@@ -192,6 +192,111 @@ class CustomModules {
192
192
 
193
193
  return this.paths;
194
194
  }
195
+
196
+ /**
197
+ * Assemble quick-update source candidates before install() hands them to discoverPaths().
198
+ * This exists because discoverPaths() consumes already-prepared quick-update sources,
199
+ * while quickUpdate() still has to build that source map from manifest, explicit inputs,
200
+ * and cache conventions.
201
+ * Precedence: manifest-backed paths, explicit sources override them, then cached modules.
202
+ * @param {Object} config - Quick update configuration
203
+ * @param {Object} existingInstall - Existing installation snapshot
204
+ * @param {string} bmadDir - BMAD directory
205
+ * @param {Object} externalModuleManager - External module manager
206
+ * @returns {Promise<Map<string, Object>>} Map of custom module ID to source info
207
+ */
208
+ async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) {
209
+ const projectRoot = path.dirname(bmadDir);
210
+ const customModuleSources = new Map();
211
+
212
+ if (existingInstall.customModules) {
213
+ for (const customModule of existingInstall.customModules) {
214
+ // Skip if no ID - can't reliably track or re-cache without it
215
+ if (!customModule?.id) continue;
216
+
217
+ let sourcePath = customModule.sourcePath;
218
+ if (sourcePath && sourcePath.startsWith('_config')) {
219
+ // Paths are relative to BMAD dir, but we want absolute paths for install
220
+ sourcePath = path.join(bmadDir, sourcePath);
221
+ } else if (!sourcePath && customModule.relativePath) {
222
+ // Fall back to relativePath
223
+ sourcePath = path.resolve(projectRoot, customModule.relativePath);
224
+ } else if (sourcePath && !path.isAbsolute(sourcePath)) {
225
+ // If we have a sourcePath but it's not absolute, resolve it relative to project root
226
+ sourcePath = path.resolve(projectRoot, sourcePath);
227
+ }
228
+
229
+ // If we still don't have a valid source path, skip this module
230
+ if (!sourcePath || !(await fs.pathExists(sourcePath))) {
231
+ continue;
232
+ }
233
+
234
+ customModuleSources.set(customModule.id, {
235
+ id: customModule.id,
236
+ name: customModule.name || customModule.id,
237
+ sourcePath,
238
+ relativePath: customModule.relativePath,
239
+ cached: false,
240
+ });
241
+ }
242
+ }
243
+
244
+ if (config.customContent?.sources?.length > 0) {
245
+ for (const source of config.customContent.sources) {
246
+ if (source.id && source.path) {
247
+ customModuleSources.set(source.id, {
248
+ id: source.id,
249
+ name: source.name || source.id,
250
+ sourcePath: source.path,
251
+ cached: false, // From CLI, will be re-cached
252
+ });
253
+ }
254
+ }
255
+ }
256
+
257
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
258
+ if (!(await fs.pathExists(cacheDir))) {
259
+ return customModuleSources;
260
+ }
261
+
262
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
263
+ for (const cachedModule of cachedModules) {
264
+ const moduleId = cachedModule.name;
265
+ const cachedPath = path.join(cacheDir, moduleId);
266
+
267
+ // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
268
+ if (!(await fs.pathExists(cachedPath))) {
269
+ continue;
270
+ }
271
+ if (!cachedModule.isDirectory()) {
272
+ continue;
273
+ }
274
+
275
+ // Skip if we already have this module from manifest
276
+ if (customModuleSources.has(moduleId)) {
277
+ continue;
278
+ }
279
+
280
+ // Check if this is an external official module - skip cache for those
281
+ const isExternal = await externalModuleManager.hasModule(moduleId);
282
+ if (isExternal) {
283
+ continue;
284
+ }
285
+
286
+ // Check if this is actually a custom module (has module.yaml)
287
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
288
+ if (await fs.pathExists(moduleYamlPath)) {
289
+ customModuleSources.set(moduleId, {
290
+ id: moduleId,
291
+ name: moduleId,
292
+ sourcePath: cachedPath,
293
+ cached: true,
294
+ });
295
+ }
296
+ }
297
+
298
+ return customModuleSources;
299
+ }
195
300
  }
196
301
 
197
302
  module.exports = { CustomModules };