bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.3b00cb36

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.
@@ -2,16 +2,17 @@ const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
3
  const yaml = require('yaml');
4
4
  const { ScopeValidator } = require('./scope-validator');
5
+ const { ScopeInitializer } = require('./scope-initializer');
5
6
 
6
7
  /**
7
8
  * Manages scope lifecycle and CRUD operations
8
9
  * Handles scope configuration in scopes.yaml file
9
- *
10
+ *
10
11
  * @class ScopeManager
11
12
  * @requires fs-extra
12
13
  * @requires yaml
13
14
  * @requires ScopeValidator
14
- *
15
+ *
15
16
  * @example
16
17
  * const manager = new ScopeManager({ projectRoot: '/path/to/project' });
17
18
  * await manager.initialize();
@@ -23,8 +24,9 @@ class ScopeManager {
23
24
  this.bmadPath = options.bmadPath || path.join(this.projectRoot, '_bmad');
24
25
  this.configPath = options.configPath || path.join(this.bmadPath, '_config');
25
26
  this.scopesFilePath = options.scopesFilePath || path.join(this.configPath, 'scopes.yaml');
26
-
27
+
27
28
  this.validator = new ScopeValidator();
29
+ this.initializer = new ScopeInitializer({ projectRoot: this.projectRoot });
28
30
  this._config = null; // Cached configuration
29
31
  }
30
32
 
@@ -38,6 +40,7 @@ class ScopeManager {
38
40
  this.configPath = path.join(this.bmadPath, '_config');
39
41
  this.scopesFilePath = path.join(this.configPath, 'scopes.yaml');
40
42
  this._config = null; // Clear cache
43
+ this.initializer.setProjectRoot(projectRoot);
41
44
  }
42
45
 
43
46
  /**
@@ -52,13 +55,16 @@ class ScopeManager {
52
55
 
53
56
  // Check if scopes.yaml exists
54
57
  const exists = await fs.pathExists(this.scopesFilePath);
55
-
58
+
56
59
  if (!exists) {
57
60
  // Create default configuration
58
61
  const defaultConfig = this.validator.createDefaultConfig();
59
62
  await this.saveConfig(defaultConfig);
60
63
  }
61
64
 
65
+ // Initialize scope system directories (_shared, _events)
66
+ await this.initializer.initializeScopeSystem();
67
+
62
68
  // Load and validate configuration
63
69
  const config = await this.loadConfig();
64
70
  return config !== null;
@@ -122,7 +128,7 @@ class ScopeManager {
122
128
  // Write to file
123
129
  const yamlContent = yaml.stringify(config, {
124
130
  indent: 2,
125
- lineWidth: 100
131
+ lineWidth: 100,
126
132
  });
127
133
  await fs.writeFile(this.scopesFilePath, yamlContent, 'utf8');
128
134
 
@@ -146,7 +152,7 @@ class ScopeManager {
146
152
 
147
153
  // Apply filters
148
154
  if (filters.status) {
149
- scopes = scopes.filter(scope => scope.status === filters.status);
155
+ scopes = scopes.filter((scope) => scope.status === filters.status);
150
156
  }
151
157
 
152
158
  // Sort by created date (newest first)
@@ -223,8 +229,8 @@ class ScopeManager {
223
229
  created: new Date().toISOString(),
224
230
  _meta: {
225
231
  last_activity: new Date().toISOString(),
226
- artifact_count: 0
227
- }
232
+ artifact_count: 0,
233
+ },
228
234
  };
229
235
 
230
236
  // Validate scope with existing scopes
@@ -239,6 +245,9 @@ class ScopeManager {
239
245
  // Save configuration
240
246
  await this.saveConfig(config);
241
247
 
248
+ // Create scope directory structure
249
+ await this.initializer.initializeScope(scopeId, options);
250
+
242
251
  return scope;
243
252
  } catch (error) {
244
253
  throw new Error(`Failed to create scope '${scopeId}': ${error.message}`);
@@ -272,8 +281,8 @@ class ScopeManager {
272
281
  _meta: {
273
282
  ...currentScope._meta,
274
283
  ...updates._meta,
275
- last_activity: new Date().toISOString()
276
- }
284
+ last_activity: new Date().toISOString(),
285
+ },
277
286
  };
278
287
 
279
288
  // Validate updated scope
@@ -311,10 +320,10 @@ class ScopeManager {
311
320
  }
312
321
 
313
322
  // Check if other scopes depend on this one
314
- const dependentScopes = this.findDependentScopes(scopeId, config.scopes);
323
+ const dependentScopes = this.findDependentScopesSync(scopeId, config.scopes);
315
324
  if (dependentScopes.length > 0 && !options.force) {
316
325
  throw new Error(
317
- `Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway.`
326
+ `Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway.`,
318
327
  );
319
328
  }
320
329
 
@@ -325,7 +334,7 @@ class ScopeManager {
325
334
  if (options.force && dependentScopes.length > 0) {
326
335
  for (const depScopeId of dependentScopes) {
327
336
  const depScope = config.scopes[depScopeId];
328
- depScope.dependencies = depScope.dependencies.filter(dep => dep !== scopeId);
337
+ depScope.dependencies = depScope.dependencies.filter((dep) => dep !== scopeId);
329
338
  }
330
339
  }
331
340
 
@@ -347,7 +356,7 @@ class ScopeManager {
347
356
  try {
348
357
  const config = await this.loadConfig();
349
358
  const scope = config.scopes[scopeId];
350
-
359
+
351
360
  if (!scope) {
352
361
  throw new Error(`Scope '${scopeId}' does not exist`);
353
362
  }
@@ -360,7 +369,7 @@ class ScopeManager {
360
369
  planning: path.join(scopePath, 'planning-artifacts'),
361
370
  implementation: path.join(scopePath, 'implementation-artifacts'),
362
371
  tests: path.join(scopePath, 'tests'),
363
- meta: path.join(scopePath, '.scope-meta.yaml')
372
+ meta: path.join(scopePath, '.scope-meta.yaml'),
364
373
  };
365
374
  } catch (error) {
366
375
  throw new Error(`Failed to get scope paths for '${scopeId}': ${error.message}`);
@@ -388,7 +397,7 @@ class ScopeManager {
388
397
  try {
389
398
  const config = await this.loadConfig();
390
399
  const scope = config.scopes[scopeId];
391
-
400
+
392
401
  if (!scope) {
393
402
  throw new Error(`Scope '${scopeId}' does not exist`);
394
403
  }
@@ -396,7 +405,7 @@ class ScopeManager {
396
405
  const tree = {
397
406
  scope: scopeId,
398
407
  dependencies: [],
399
- dependents: this.findDependentScopes(scopeId, config.scopes)
408
+ dependents: this.findDependentScopesSync(scopeId, config.scopes),
400
409
  };
401
410
 
402
411
  // Build dependency tree recursively
@@ -407,7 +416,7 @@ class ScopeManager {
407
416
  tree.dependencies.push({
408
417
  scope: depId,
409
418
  name: depScope.name,
410
- status: depScope.status
419
+ status: depScope.status,
411
420
  });
412
421
  }
413
422
  }
@@ -422,12 +431,36 @@ class ScopeManager {
422
431
  /**
423
432
  * Find scopes that depend on a given scope
424
433
  * @param {string} scopeId - The scope ID
425
- * @param {object} allScopes - All scopes object
434
+ * @param {object} allScopes - All scopes object (optional, will load if not provided)
435
+ * @returns {Promise<string[]>|string[]} Array of dependent scope IDs
436
+ */
437
+ async findDependentScopes(scopeId, allScopes = null) {
438
+ // If allScopes not provided, load from config
439
+ if (!allScopes) {
440
+ const config = await this.loadConfig();
441
+ allScopes = config.scopes || {};
442
+ }
443
+
444
+ const dependents = [];
445
+
446
+ for (const [sid, scope] of Object.entries(allScopes)) {
447
+ if (scope.dependencies && scope.dependencies.includes(scopeId)) {
448
+ dependents.push(sid);
449
+ }
450
+ }
451
+
452
+ return dependents;
453
+ }
454
+
455
+ /**
456
+ * Find scopes that depend on a given scope (synchronous version)
457
+ * @param {string} scopeId - The scope ID
458
+ * @param {object} allScopes - All scopes object (required)
426
459
  * @returns {string[]} Array of dependent scope IDs
427
460
  */
428
- findDependentScopes(scopeId, allScopes) {
461
+ findDependentScopesSync(scopeId, allScopes) {
429
462
  const dependents = [];
430
-
463
+
431
464
  for (const [sid, scope] of Object.entries(allScopes)) {
432
465
  if (scope.dependencies && scope.dependencies.includes(scopeId)) {
433
466
  dependents.push(sid);
@@ -462,7 +495,7 @@ class ScopeManager {
462
495
  */
463
496
  async touchScope(scopeId) {
464
497
  return this.updateScope(scopeId, {
465
- _meta: { last_activity: new Date().toISOString() }
498
+ _meta: { last_activity: new Date().toISOString() },
466
499
  });
467
500
  }
468
501
 
@@ -480,7 +513,7 @@ class ScopeManager {
480
513
 
481
514
  const currentCount = scope._meta?.artifact_count || 0;
482
515
  return this.updateScope(scopeId, {
483
- _meta: { artifact_count: currentCount + increment }
516
+ _meta: { artifact_count: currentCount + increment },
484
517
  });
485
518
  }
486
519
 
@@ -502,7 +535,7 @@ class ScopeManager {
502
535
  const config = await this.loadConfig();
503
536
  config.settings = {
504
537
  ...config.settings,
505
- ...settings
538
+ ...settings,
506
539
  };
507
540
  await this.saveConfig(config);
508
541
  return config.settings;
@@ -5,11 +5,11 @@ const yaml = require('yaml');
5
5
  /**
6
6
  * Migrates existing artifacts to scoped structure
7
7
  * Handles migration of legacy non-scoped installations
8
- *
8
+ *
9
9
  * @class ScopeMigrator
10
10
  * @requires fs-extra
11
11
  * @requires yaml
12
- *
12
+ *
13
13
  * @example
14
14
  * const migrator = new ScopeMigrator({ projectRoot: '/path/to/project' });
15
15
  * await migrator.migrate();
@@ -41,18 +41,14 @@ class ScopeMigrator {
41
41
  async needsMigration() {
42
42
  try {
43
43
  // Check if output directory exists
44
- if (!await fs.pathExists(this.outputPath)) {
44
+ if (!(await fs.pathExists(this.outputPath))) {
45
45
  return false;
46
46
  }
47
47
 
48
48
  // Check for legacy structure indicators
49
- const hasLegacyPlanning = await fs.pathExists(
50
- path.join(this.outputPath, 'planning-artifacts')
51
- );
52
- const hasLegacyImplementation = await fs.pathExists(
53
- path.join(this.outputPath, 'implementation-artifacts')
54
- );
55
-
49
+ const hasLegacyPlanning = await fs.pathExists(path.join(this.outputPath, 'planning-artifacts'));
50
+ const hasLegacyImplementation = await fs.pathExists(path.join(this.outputPath, 'implementation-artifacts'));
51
+
56
52
  // Check if already migrated (scopes.yaml exists and has scopes)
57
53
  const scopesYamlPath = path.join(this.bmadPath, '_config', 'scopes.yaml');
58
54
  if (await fs.pathExists(scopesYamlPath)) {
@@ -80,23 +76,19 @@ class ScopeMigrator {
80
76
  directories: [],
81
77
  files: [],
82
78
  totalSize: 0,
83
- suggestedScope: this.defaultScopeId
79
+ suggestedScope: this.defaultScopeId,
84
80
  };
85
81
 
86
82
  try {
87
83
  // Check for legacy directories
88
- const legacyDirs = [
89
- 'planning-artifacts',
90
- 'implementation-artifacts',
91
- 'tests'
92
- ];
84
+ const legacyDirs = ['planning-artifacts', 'implementation-artifacts', 'tests'];
93
85
 
94
86
  for (const dir of legacyDirs) {
95
87
  const dirPath = path.join(this.outputPath, dir);
96
88
  if (await fs.pathExists(dirPath)) {
97
89
  analysis.hasLegacyArtifacts = true;
98
90
  analysis.directories.push(dir);
99
-
91
+
100
92
  // Count files and size
101
93
  const stats = await this.getDirStats(dirPath);
102
94
  analysis.files.push(...stats.files);
@@ -115,7 +107,6 @@ class ScopeMigrator {
115
107
  analysis.totalSize += stat.size;
116
108
  }
117
109
  }
118
-
119
110
  } catch (error) {
120
111
  throw new Error(`Failed to analyze existing artifacts: ${error.message}`);
121
112
  }
@@ -130,16 +121,16 @@ class ScopeMigrator {
130
121
  */
131
122
  async getDirStats(dirPath) {
132
123
  const stats = { files: [], size: 0 };
133
-
124
+
134
125
  try {
135
126
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
136
-
127
+
137
128
  for (const entry of entries) {
138
129
  const fullPath = path.join(dirPath, entry.name);
139
-
130
+
140
131
  if (entry.isDirectory()) {
141
132
  const subStats = await this.getDirStats(fullPath);
142
- stats.files.push(...subStats.files.map(f => path.join(entry.name, f)));
133
+ stats.files.push(...subStats.files.map((f) => path.join(entry.name, f)));
143
134
  stats.size += subStats.size;
144
135
  } else {
145
136
  stats.files.push(entry.name);
@@ -150,7 +141,7 @@ class ScopeMigrator {
150
141
  } catch {
151
142
  // Ignore permission errors
152
143
  }
153
-
144
+
154
145
  return stats;
155
146
  }
156
147
 
@@ -161,10 +152,10 @@ class ScopeMigrator {
161
152
  async createBackup() {
162
153
  const backupName = `_backup_migration_${Date.now()}`;
163
154
  const backupPath = path.join(this.outputPath, backupName);
164
-
155
+
165
156
  try {
166
157
  await fs.ensureDir(backupPath);
167
-
158
+
168
159
  // Copy legacy directories
169
160
  const legacyDirs = ['planning-artifacts', 'implementation-artifacts', 'tests'];
170
161
  for (const dir of legacyDirs) {
@@ -173,7 +164,7 @@ class ScopeMigrator {
173
164
  await fs.copy(sourcePath, path.join(backupPath, dir));
174
165
  }
175
166
  }
176
-
167
+
177
168
  // Copy root-level files
178
169
  const rootFiles = ['project-context.md', 'sprint-status.yaml', 'bmm-workflow-status.yaml'];
179
170
  for (const file of rootFiles) {
@@ -182,7 +173,7 @@ class ScopeMigrator {
182
173
  await fs.copy(sourcePath, path.join(backupPath, file));
183
174
  }
184
175
  }
185
-
176
+
186
177
  return backupPath;
187
178
  } catch (error) {
188
179
  throw new Error(`Failed to create backup: ${error.message}`);
@@ -197,13 +188,13 @@ class ScopeMigrator {
197
188
  async migrate(options = {}) {
198
189
  const scopeId = options.scopeId || this.defaultScopeId;
199
190
  const createBackup = options.backup !== false;
200
-
191
+
201
192
  const result = {
202
193
  success: false,
203
194
  scopeId,
204
195
  backupPath: null,
205
196
  migratedFiles: [],
206
- errors: []
197
+ errors: [],
207
198
  };
208
199
 
209
200
  try {
@@ -225,7 +216,7 @@ class ScopeMigrator {
225
216
  const scopeDirs = {
226
217
  planning: path.join(scopePath, 'planning-artifacts'),
227
218
  implementation: path.join(scopePath, 'implementation-artifacts'),
228
- tests: path.join(scopePath, 'tests')
219
+ tests: path.join(scopePath, 'tests'),
229
220
  };
230
221
 
231
222
  for (const dir of Object.values(scopeDirs)) {
@@ -236,7 +227,7 @@ class ScopeMigrator {
236
227
  const migrations = [
237
228
  { from: 'planning-artifacts', to: scopeDirs.planning },
238
229
  { from: 'implementation-artifacts', to: scopeDirs.implementation },
239
- { from: 'tests', to: scopeDirs.tests }
230
+ { from: 'tests', to: scopeDirs.tests },
240
231
  ];
241
232
 
242
233
  for (const migration of migrations) {
@@ -247,17 +238,17 @@ class ScopeMigrator {
247
238
  for (const entry of entries) {
248
239
  const sourceFile = path.join(sourcePath, entry.name);
249
240
  const targetFile = path.join(migration.to, entry.name);
250
-
241
+
251
242
  // Skip if target already exists
252
243
  if (await fs.pathExists(targetFile)) {
253
244
  result.errors.push(`Skipped ${entry.name}: already exists in target`);
254
245
  continue;
255
246
  }
256
-
247
+
257
248
  await fs.copy(sourceFile, targetFile);
258
249
  result.migratedFiles.push(path.join(migration.from, entry.name));
259
250
  }
260
-
251
+
261
252
  // Remove original directory
262
253
  await fs.remove(sourcePath);
263
254
  }
@@ -267,7 +258,7 @@ class ScopeMigrator {
267
258
  const rootFileMigrations = [
268
259
  { from: 'project-context.md', to: path.join(scopePath, 'project-context.md') },
269
260
  { from: 'sprint-status.yaml', to: path.join(scopeDirs.implementation, 'sprint-status.yaml') },
270
- { from: 'bmm-workflow-status.yaml', to: path.join(scopeDirs.planning, 'bmm-workflow-status.yaml') }
261
+ { from: 'bmm-workflow-status.yaml', to: path.join(scopeDirs.planning, 'bmm-workflow-status.yaml') },
271
262
  ];
272
263
 
273
264
  for (const migration of rootFileMigrations) {
@@ -290,20 +281,19 @@ class ScopeMigrator {
290
281
  migrated: true,
291
282
  migrated_at: new Date().toISOString(),
292
283
  original_backup: result.backupPath,
293
- version: 1
284
+ version: 1,
294
285
  };
295
286
  await fs.writeFile(metaPath, yaml.stringify(metadata), 'utf8');
296
287
 
297
288
  // Create scope README
298
289
  const readmePath = path.join(scopePath, 'README.md');
299
- if (!await fs.pathExists(readmePath)) {
290
+ if (!(await fs.pathExists(readmePath))) {
300
291
  const readme = this.generateMigrationReadme(scopeId, result.migratedFiles.length);
301
292
  await fs.writeFile(readmePath, readme, 'utf8');
302
293
  }
303
294
 
304
295
  result.success = true;
305
296
  result.message = `Migrated ${result.migratedFiles.length} items to scope '${scopeId}'`;
306
-
307
297
  } catch (error) {
308
298
  result.success = false;
309
299
  result.errors.push(error.message);
@@ -359,22 +349,22 @@ bmad scope info ${scopeId}
359
349
  */
360
350
  async rollback(backupPath) {
361
351
  try {
362
- if (!await fs.pathExists(backupPath)) {
352
+ if (!(await fs.pathExists(backupPath))) {
363
353
  throw new Error(`Backup not found at: ${backupPath}`);
364
354
  }
365
355
 
366
356
  // Restore backed up directories
367
357
  const entries = await fs.readdir(backupPath, { withFileTypes: true });
368
-
358
+
369
359
  for (const entry of entries) {
370
360
  const sourcePath = path.join(backupPath, entry.name);
371
361
  const targetPath = path.join(this.outputPath, entry.name);
372
-
362
+
373
363
  // Remove current version if exists
374
364
  if (await fs.pathExists(targetPath)) {
375
365
  await fs.remove(targetPath);
376
366
  }
377
-
367
+
378
368
  // Restore from backup
379
369
  await fs.copy(sourcePath, targetPath);
380
370
  }
@@ -395,34 +385,36 @@ bmad scope info ${scopeId}
395
385
  */
396
386
  async updateReferences(scopeId) {
397
387
  const result = { updated: [], errors: [] };
398
-
388
+
399
389
  const scopePath = path.join(this.outputPath, scopeId);
400
-
390
+
401
391
  // Files that might contain path references
402
392
  const filesToUpdate = [
403
393
  path.join(scopePath, 'implementation-artifacts', 'sprint-status.yaml'),
404
- path.join(scopePath, 'planning-artifacts', 'bmm-workflow-status.yaml')
394
+ path.join(scopePath, 'planning-artifacts', 'bmm-workflow-status.yaml'),
405
395
  ];
406
396
 
407
397
  for (const filePath of filesToUpdate) {
408
398
  if (await fs.pathExists(filePath)) {
409
399
  try {
410
400
  let content = await fs.readFile(filePath, 'utf8');
411
-
401
+
412
402
  // Update common path patterns
413
403
  const patterns = [
414
404
  { from: /planning-artifacts\//g, to: `${scopeId}/planning-artifacts/` },
415
405
  { from: /implementation-artifacts\//g, to: `${scopeId}/implementation-artifacts/` },
416
- { from: /tests\//g, to: `${scopeId}/tests/` }
406
+ { from: /tests\//g, to: `${scopeId}/tests/` },
417
407
  ];
418
408
 
419
409
  let modified = false;
420
410
  for (const pattern of patterns) {
421
- if (pattern.from.test(content) && // Only update if not already scoped
422
- !content.includes(`${scopeId}/`)) {
423
- content = content.replace(pattern.from, pattern.to);
424
- modified = true;
425
- }
411
+ if (
412
+ pattern.from.test(content) && // Only update if not already scoped
413
+ !content.includes(`${scopeId}/`)
414
+ ) {
415
+ content = content.replace(pattern.from, pattern.to);
416
+ modified = true;
417
+ }
426
418
  }
427
419
 
428
420
  if (modified) {