bmad-method 6.2.0 → 6.2.1-next.1

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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/bmm/agents/bmad-agent-analyst/SKILL.md +58 -0
  3. package/src/bmm/agents/bmad-agent-analyst/bmad-manifest.json +44 -0
  4. package/src/bmm/agents/bmad-agent-analyst/bmad-skill-manifest.yaml +12 -0
  5. package/src/bmm/agents/bmad-agent-architect/SKILL.md +58 -0
  6. package/src/bmm/agents/bmad-agent-architect/bmad-manifest.json +20 -0
  7. package/src/bmm/agents/bmad-agent-architect/bmad-skill-manifest.yaml +12 -0
  8. package/src/bmm/agents/bmad-agent-dev/SKILL.md +68 -0
  9. package/src/bmm/agents/bmad-agent-dev/bmad-manifest.json +20 -0
  10. package/src/bmm/agents/bmad-agent-dev/bmad-skill-manifest.yaml +12 -0
  11. package/src/bmm/agents/bmad-agent-pm/SKILL.md +59 -0
  12. package/src/bmm/agents/bmad-agent-pm/bmad-manifest.json +44 -0
  13. package/src/bmm/agents/bmad-agent-pm/bmad-skill-manifest.yaml +12 -0
  14. package/src/bmm/agents/bmad-agent-qa/SKILL.md +66 -0
  15. package/src/bmm/agents/bmad-agent-qa/bmad-manifest.json +14 -0
  16. package/src/bmm/agents/bmad-agent-qa/bmad-skill-manifest.yaml +12 -0
  17. package/src/bmm/agents/bmad-agent-quick-flow-solo-dev/SKILL.md +57 -0
  18. package/src/bmm/agents/bmad-agent-quick-flow-solo-dev/bmad-manifest.json +32 -0
  19. package/src/bmm/agents/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +12 -0
  20. package/src/bmm/agents/bmad-agent-sm/SKILL.md +57 -0
  21. package/src/bmm/agents/bmad-agent-sm/bmad-manifest.json +32 -0
  22. package/src/bmm/agents/bmad-agent-sm/bmad-skill-manifest.yaml +12 -0
  23. package/src/bmm/agents/bmad-agent-tech-writer/SKILL.md +58 -0
  24. package/src/bmm/agents/bmad-agent-tech-writer/bmad-manifest.json +38 -0
  25. package/src/bmm/agents/bmad-agent-tech-writer/bmad-skill-manifest.yaml +12 -0
  26. package/src/bmm/agents/bmad-agent-tech-writer/explain-concept.md +20 -0
  27. package/src/bmm/agents/bmad-agent-tech-writer/mermaid-gen.md +20 -0
  28. package/src/bmm/agents/bmad-agent-tech-writer/validate-doc.md +19 -0
  29. package/src/bmm/agents/bmad-agent-tech-writer/write-document.md +20 -0
  30. package/src/bmm/agents/bmad-agent-ux-designer/SKILL.md +60 -0
  31. package/src/bmm/agents/bmad-agent-ux-designer/bmad-manifest.json +14 -0
  32. package/src/bmm/agents/bmad-agent-ux-designer/bmad-skill-manifest.yaml +12 -0
  33. package/src/bmm/module-help.csv +5 -5
  34. package/src/core/skills/bmad-init/SKILL.md +100 -0
  35. package/src/core/skills/bmad-init/bmad-skill-manifest.yaml +1 -0
  36. package/src/core/skills/bmad-init/resources/core-module.yaml +25 -0
  37. package/src/core/skills/bmad-init/scripts/bmad_init.py +593 -0
  38. package/src/core/skills/bmad-init/scripts/tests/test_bmad_init.py +329 -0
  39. package/tools/cli/installers/lib/core/manifest-generator.js +43 -5
  40. package/tools/cli/installers/lib/ide/platform-codes.yaml +10 -0
  41. package/tools/skill-validator.md +34 -0
  42. package/src/bmm/agents/analyst.agent.yaml +0 -43
  43. package/src/bmm/agents/architect.agent.yaml +0 -29
  44. package/src/bmm/agents/dev.agent.yaml +0 -38
  45. package/src/bmm/agents/pm.agent.yaml +0 -44
  46. package/src/bmm/agents/qa.agent.yaml +0 -58
  47. package/src/bmm/agents/quick-flow-solo-dev.agent.yaml +0 -36
  48. package/src/bmm/agents/sm.agent.yaml +0 -37
  49. package/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml +0 -3
  50. package/src/bmm/agents/tech-writer/tech-writer-sidecar/documentation-standards.md +0 -224
  51. package/src/bmm/agents/tech-writer/tech-writer.agent.yaml +0 -46
  52. package/src/bmm/agents/ux-designer.agent.yaml +0 -27
@@ -0,0 +1,593 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+
6
+ #!/usr/bin/env python3
7
+ """
8
+ BMad Init — Project configuration bootstrap and config loader.
9
+
10
+ Config files (flat YAML per module):
11
+ - _bmad/core/config.yaml (core settings — user_name, language, output_folder, etc.)
12
+ - _bmad/{module}/config.yaml (module settings + core values merged in)
13
+
14
+ Usage:
15
+ # Fast path — load all vars for a module (includes core vars)
16
+ python bmad_init.py load --module bmb --all --project-root /path
17
+
18
+ # Load specific vars with optional defaults
19
+ python bmad_init.py load --module bmb --vars var1:default1,var2 --project-root /path
20
+
21
+ # Load core only
22
+ python bmad_init.py load --all --project-root /path
23
+
24
+ # Check if init is needed
25
+ python bmad_init.py check --project-root /path
26
+ python bmad_init.py check --module bmb --skill-path /path/to/skill --project-root /path
27
+
28
+ # Resolve module defaults given core answers
29
+ python bmad_init.py resolve-defaults --module bmb --core-answers '{"output_folder":"..."}' --project-root /path
30
+
31
+ # Write config from answered questions
32
+ python bmad_init.py write --answers '{"core": {...}, "bmb": {...}}' --project-root /path
33
+ """
34
+
35
+ import argparse
36
+ import json
37
+ import os
38
+ import sys
39
+ from pathlib import Path
40
+
41
+ import yaml
42
+
43
+
44
+ # =============================================================================
45
+ # Project Root Detection
46
+ # =============================================================================
47
+
48
+ def find_project_root(llm_provided=None):
49
+ """
50
+ Find project root by looking for _bmad folder.
51
+
52
+ Args:
53
+ llm_provided: Path explicitly provided via --project-root.
54
+
55
+ Returns:
56
+ Path to project root, or None if not found.
57
+ """
58
+ if llm_provided:
59
+ candidate = Path(llm_provided)
60
+ if (candidate / '_bmad').exists():
61
+ return candidate
62
+ # First run — _bmad won't exist yet but LLM path is still valid
63
+ if candidate.is_dir():
64
+ return candidate
65
+
66
+ for start_dir in [Path.cwd(), Path(__file__).resolve().parent]:
67
+ current_dir = start_dir
68
+ while current_dir != current_dir.parent:
69
+ if (current_dir / '_bmad').exists():
70
+ return current_dir
71
+ current_dir = current_dir.parent
72
+
73
+ return None
74
+
75
+
76
+ # =============================================================================
77
+ # Module YAML Loading
78
+ # =============================================================================
79
+
80
+ def load_module_yaml(path):
81
+ """
82
+ Load and parse a module.yaml file, separating metadata from variable definitions.
83
+
84
+ Returns:
85
+ Dict with 'meta' (code, name, etc.) and 'variables' (var definitions)
86
+ and 'directories' (list of dir templates), or None on failure.
87
+ """
88
+ try:
89
+ with open(path, 'r', encoding='utf-8') as f:
90
+ raw = yaml.safe_load(f)
91
+ except Exception:
92
+ return None
93
+
94
+ if not raw or not isinstance(raw, dict):
95
+ return None
96
+
97
+ meta_keys = {'code', 'name', 'description', 'default_selected', 'header', 'subheader'}
98
+ meta = {}
99
+ variables = {}
100
+ directories = []
101
+
102
+ for key, value in raw.items():
103
+ if key == 'directories':
104
+ directories = value if isinstance(value, list) else []
105
+ elif key in meta_keys:
106
+ meta[key] = value
107
+ elif isinstance(value, dict) and 'prompt' in value:
108
+ variables[key] = value
109
+ # Skip comment-only entries (## var_name lines become None values)
110
+
111
+ return {'meta': meta, 'variables': variables, 'directories': directories}
112
+
113
+
114
+ def find_core_module_yaml():
115
+ """Find the core module.yaml bundled with this skill."""
116
+ return Path(__file__).resolve().parent.parent / 'resources' / 'core-module.yaml'
117
+
118
+
119
+ def find_target_module_yaml(module_code, project_root, skill_path=None):
120
+ """
121
+ Find module.yaml for a given module code.
122
+
123
+ Search order:
124
+ 1. skill_path/assets/module.yaml (calling skill's assets)
125
+ 2. skill_path/module.yaml (calling skill's root)
126
+ 3. _bmad/{module_code}/module.yaml (installed module location)
127
+ """
128
+ search_paths = []
129
+
130
+ if skill_path:
131
+ sp = Path(skill_path)
132
+ search_paths.append(sp / 'assets' / 'module.yaml')
133
+ search_paths.append(sp / 'module.yaml')
134
+
135
+ if project_root and module_code:
136
+ search_paths.append(Path(project_root) / '_bmad' / module_code / 'module.yaml')
137
+
138
+ for path in search_paths:
139
+ if path.exists():
140
+ return path
141
+
142
+ return None
143
+
144
+
145
+ # =============================================================================
146
+ # Config Loading (Flat per-module files)
147
+ # =============================================================================
148
+
149
+ def load_config_file(path):
150
+ """Load a flat YAML config file. Returns dict or None."""
151
+ try:
152
+ with open(path, 'r', encoding='utf-8') as f:
153
+ data = yaml.safe_load(f)
154
+ return data if isinstance(data, dict) else None
155
+ except Exception:
156
+ return None
157
+
158
+
159
+ def load_module_config(module_code, project_root):
160
+ """Load config for a specific module from _bmad/{module}/config.yaml."""
161
+ config_path = Path(project_root) / '_bmad' / module_code / 'config.yaml'
162
+ return load_config_file(config_path)
163
+
164
+
165
+ def resolve_project_root_placeholder(value, project_root):
166
+ """Replace {project-root} placeholder with actual path."""
167
+ if not value or not isinstance(value, str):
168
+ return value
169
+ if '{project-root}' in value:
170
+ return value.replace('{project-root}', str(project_root))
171
+ return value
172
+
173
+
174
+ def parse_var_specs(vars_string):
175
+ """
176
+ Parse variable specs: var_name:default_value,var_name2:default_value2
177
+ No default = returns null if missing.
178
+ """
179
+ if not vars_string:
180
+ return []
181
+ specs = []
182
+ for spec in vars_string.split(','):
183
+ spec = spec.strip()
184
+ if not spec:
185
+ continue
186
+ if ':' in spec:
187
+ parts = spec.split(':', 1)
188
+ specs.append({'name': parts[0].strip(), 'default': parts[1].strip()})
189
+ else:
190
+ specs.append({'name': spec, 'default': None})
191
+ return specs
192
+
193
+
194
+ # =============================================================================
195
+ # Template Expansion
196
+ # =============================================================================
197
+
198
+ def expand_template(value, context):
199
+ """
200
+ Expand {placeholder} references in a string using context dict.
201
+
202
+ Supports: {project-root}, {value}, {output_folder}, {directory_name}, etc.
203
+ """
204
+ if not value or not isinstance(value, str):
205
+ return value
206
+ result = value
207
+ for key, val in context.items():
208
+ placeholder = '{' + key + '}'
209
+ if placeholder in result and val is not None:
210
+ result = result.replace(placeholder, str(val))
211
+ return result
212
+
213
+
214
+ def apply_result_template(var_def, raw_value, context):
215
+ """
216
+ Apply a variable's result template to transform the raw user answer.
217
+
218
+ E.g., result: "{project-root}/{value}" with value="_bmad-output"
219
+ becomes "/Users/foo/project/_bmad-output"
220
+ """
221
+ result_template = var_def.get('result')
222
+ if not result_template:
223
+ return raw_value
224
+
225
+ ctx = dict(context)
226
+ ctx['value'] = raw_value
227
+ return expand_template(result_template, ctx)
228
+
229
+
230
+ # =============================================================================
231
+ # Load Command (Fast Path)
232
+ # =============================================================================
233
+
234
+ def cmd_load(args):
235
+ """Load config vars — the fast path."""
236
+ project_root = find_project_root(llm_provided=args.project_root)
237
+ if not project_root:
238
+ print(json.dumps({'error': 'Project root not found (_bmad folder not detected)'}),
239
+ file=sys.stderr)
240
+ sys.exit(1)
241
+
242
+ module_code = args.module or 'core'
243
+
244
+ # Load the module's config (which includes core vars)
245
+ config = load_module_config(module_code, project_root)
246
+ if config is None:
247
+ print(json.dumps({
248
+ 'init_required': True,
249
+ 'missing_module': module_code,
250
+ }), file=sys.stderr)
251
+ sys.exit(1)
252
+
253
+ # Resolve {project-root} in all values
254
+ for key in config:
255
+ config[key] = resolve_project_root_placeholder(config[key], project_root)
256
+
257
+ if args.all:
258
+ print(json.dumps(config, indent=2))
259
+ else:
260
+ var_specs = parse_var_specs(args.vars)
261
+ if not var_specs:
262
+ print(json.dumps({'error': 'Either --vars or --all must be specified'}),
263
+ file=sys.stderr)
264
+ sys.exit(1)
265
+ result = {}
266
+ for spec in var_specs:
267
+ val = config.get(spec['name'])
268
+ if val is not None and val != '':
269
+ result[spec['name']] = val
270
+ elif spec['default'] is not None:
271
+ result[spec['name']] = spec['default']
272
+ else:
273
+ result[spec['name']] = None
274
+ print(json.dumps(result, indent=2))
275
+
276
+
277
+ # =============================================================================
278
+ # Check Command
279
+ # =============================================================================
280
+
281
+ def cmd_check(args):
282
+ """Check if config exists and return status with module.yaml questions if needed."""
283
+ project_root = find_project_root(llm_provided=args.project_root)
284
+ if not project_root:
285
+ print(json.dumps({
286
+ 'status': 'no_project',
287
+ 'message': 'No project root found. Provide --project-root to bootstrap.',
288
+ }, indent=2))
289
+ return
290
+
291
+ project_root = Path(project_root)
292
+ module_code = args.module
293
+
294
+ # Check core config
295
+ core_config = load_module_config('core', project_root)
296
+ core_exists = core_config is not None
297
+
298
+ # If no module requested, just check core
299
+ if not module_code or module_code == 'core':
300
+ if core_exists:
301
+ print(json.dumps({'status': 'ready', 'project_root': str(project_root)}, indent=2))
302
+ else:
303
+ core_yaml_path = find_core_module_yaml()
304
+ core_module = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None
305
+ print(json.dumps({
306
+ 'status': 'core_missing',
307
+ 'project_root': str(project_root),
308
+ 'core_module': core_module,
309
+ }, indent=2))
310
+ return
311
+
312
+ # Module requested — check if its config exists
313
+ module_config = load_module_config(module_code, project_root)
314
+ if module_config is not None:
315
+ print(json.dumps({'status': 'ready', 'project_root': str(project_root)}, indent=2))
316
+ return
317
+
318
+ # Module config missing — find its module.yaml for questions
319
+ target_yaml_path = find_target_module_yaml(
320
+ module_code, project_root, skill_path=args.skill_path
321
+ )
322
+ target_module = load_module_yaml(target_yaml_path) if target_yaml_path else None
323
+
324
+ result = {
325
+ 'project_root': str(project_root),
326
+ }
327
+
328
+ if not core_exists:
329
+ result['status'] = 'core_missing'
330
+ core_yaml_path = find_core_module_yaml()
331
+ result['core_module'] = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None
332
+ else:
333
+ result['status'] = 'module_missing'
334
+ result['core_vars'] = core_config
335
+
336
+ result['target_module'] = target_module
337
+ if target_yaml_path:
338
+ result['target_module_yaml_path'] = str(target_yaml_path)
339
+
340
+ print(json.dumps(result, indent=2))
341
+
342
+
343
+ # =============================================================================
344
+ # Resolve Defaults Command
345
+ # =============================================================================
346
+
347
+ def cmd_resolve_defaults(args):
348
+ """Given core answers, resolve a module's variable defaults."""
349
+ project_root = find_project_root(llm_provided=args.project_root)
350
+ if not project_root:
351
+ print(json.dumps({'error': 'Project root not found'}), file=sys.stderr)
352
+ sys.exit(1)
353
+
354
+ try:
355
+ core_answers = json.loads(args.core_answers)
356
+ except json.JSONDecodeError as e:
357
+ print(json.dumps({'error': f'Invalid JSON in --core-answers: {e}'}),
358
+ file=sys.stderr)
359
+ sys.exit(1)
360
+
361
+ # Build context for template expansion
362
+ context = {
363
+ 'project-root': str(project_root),
364
+ 'directory_name': Path(project_root).name,
365
+ }
366
+ context.update(core_answers)
367
+
368
+ # Find and load the module's module.yaml
369
+ module_code = args.module
370
+ target_yaml_path = find_target_module_yaml(
371
+ module_code, project_root, skill_path=args.skill_path
372
+ )
373
+ if not target_yaml_path:
374
+ print(json.dumps({'error': f'No module.yaml found for module: {module_code}'}),
375
+ file=sys.stderr)
376
+ sys.exit(1)
377
+
378
+ module_def = load_module_yaml(target_yaml_path)
379
+ if not module_def:
380
+ print(json.dumps({'error': f'Failed to parse module.yaml at: {target_yaml_path}'}),
381
+ file=sys.stderr)
382
+ sys.exit(1)
383
+
384
+ # Resolve defaults in each variable
385
+ resolved_vars = {}
386
+ for var_name, var_def in module_def['variables'].items():
387
+ default = var_def.get('default', '')
388
+ resolved_default = expand_template(str(default), context)
389
+ resolved_vars[var_name] = dict(var_def)
390
+ resolved_vars[var_name]['default'] = resolved_default
391
+
392
+ result = {
393
+ 'module_code': module_code,
394
+ 'meta': module_def['meta'],
395
+ 'variables': resolved_vars,
396
+ 'directories': module_def['directories'],
397
+ }
398
+ print(json.dumps(result, indent=2))
399
+
400
+
401
+ # =============================================================================
402
+ # Write Command
403
+ # =============================================================================
404
+
405
+ def cmd_write(args):
406
+ """Write config files from answered questions."""
407
+ project_root = find_project_root(llm_provided=args.project_root)
408
+ if not project_root:
409
+ if args.project_root:
410
+ project_root = Path(args.project_root)
411
+ else:
412
+ print(json.dumps({'error': 'Project root not found and --project-root not provided'}),
413
+ file=sys.stderr)
414
+ sys.exit(1)
415
+
416
+ project_root = Path(project_root)
417
+
418
+ try:
419
+ answers = json.loads(args.answers)
420
+ except json.JSONDecodeError as e:
421
+ print(json.dumps({'error': f'Invalid JSON in --answers: {e}'}),
422
+ file=sys.stderr)
423
+ sys.exit(1)
424
+
425
+ context = {
426
+ 'project-root': str(project_root),
427
+ 'directory_name': project_root.name,
428
+ }
429
+
430
+ # Load module.yaml definitions to get result templates
431
+ core_yaml_path = find_core_module_yaml()
432
+ core_def = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None
433
+
434
+ files_written = []
435
+ dirs_created = []
436
+
437
+ # Process core answers first (needed for module config expansion)
438
+ core_answers_raw = answers.get('core', {})
439
+ core_config = {}
440
+
441
+ if core_answers_raw and core_def:
442
+ for var_name, raw_value in core_answers_raw.items():
443
+ var_def = core_def['variables'].get(var_name, {})
444
+ expanded = apply_result_template(var_def, raw_value, context)
445
+ core_config[var_name] = expanded
446
+
447
+ # Write core config
448
+ core_dir = project_root / '_bmad' / 'core'
449
+ core_dir.mkdir(parents=True, exist_ok=True)
450
+ core_config_path = core_dir / 'config.yaml'
451
+
452
+ # Merge with existing if present
453
+ existing = load_config_file(core_config_path) or {}
454
+ existing.update(core_config)
455
+
456
+ _write_config_file(core_config_path, existing, 'CORE')
457
+ files_written.append(str(core_config_path))
458
+ elif core_answers_raw:
459
+ # No core_def available — write raw values
460
+ core_config = dict(core_answers_raw)
461
+ core_dir = project_root / '_bmad' / 'core'
462
+ core_dir.mkdir(parents=True, exist_ok=True)
463
+ core_config_path = core_dir / 'config.yaml'
464
+ existing = load_config_file(core_config_path) or {}
465
+ existing.update(core_config)
466
+ _write_config_file(core_config_path, existing, 'CORE')
467
+ files_written.append(str(core_config_path))
468
+
469
+ # Update context with resolved core values for module expansion
470
+ context.update(core_config)
471
+
472
+ # Process module answers
473
+ for module_code, module_answers_raw in answers.items():
474
+ if module_code == 'core':
475
+ continue
476
+
477
+ # Find module.yaml for result templates
478
+ target_yaml_path = find_target_module_yaml(
479
+ module_code, project_root, skill_path=args.skill_path
480
+ )
481
+ module_def = load_module_yaml(target_yaml_path) if target_yaml_path else None
482
+
483
+ # Build module config: start with core values, then add module values
484
+ # Re-read core config to get the latest (may have been updated above)
485
+ latest_core = load_module_config('core', project_root) or core_config
486
+ module_config = dict(latest_core)
487
+
488
+ for var_name, raw_value in module_answers_raw.items():
489
+ if module_def:
490
+ var_def = module_def['variables'].get(var_name, {})
491
+ expanded = apply_result_template(var_def, raw_value, context)
492
+ else:
493
+ expanded = raw_value
494
+ module_config[var_name] = expanded
495
+ context[var_name] = expanded # Available for subsequent template expansion
496
+
497
+ # Write module config
498
+ module_dir = project_root / '_bmad' / module_code
499
+ module_dir.mkdir(parents=True, exist_ok=True)
500
+ module_config_path = module_dir / 'config.yaml'
501
+
502
+ existing = load_config_file(module_config_path) or {}
503
+ existing.update(module_config)
504
+
505
+ module_name = module_def['meta'].get('name', module_code.upper()) if module_def else module_code.upper()
506
+ _write_config_file(module_config_path, existing, module_name)
507
+ files_written.append(str(module_config_path))
508
+
509
+ # Create directories declared in module.yaml
510
+ if module_def and module_def.get('directories'):
511
+ for dir_template in module_def['directories']:
512
+ dir_path = expand_template(dir_template, context)
513
+ if dir_path:
514
+ Path(dir_path).mkdir(parents=True, exist_ok=True)
515
+ dirs_created.append(dir_path)
516
+
517
+ result = {
518
+ 'status': 'written',
519
+ 'files_written': files_written,
520
+ 'dirs_created': dirs_created,
521
+ }
522
+ print(json.dumps(result, indent=2))
523
+
524
+
525
+ def _write_config_file(path, data, module_label):
526
+ """Write a config YAML file with a header comment."""
527
+ from datetime import datetime, timezone
528
+ with open(path, 'w', encoding='utf-8') as f:
529
+ f.write(f'# {module_label} Module Configuration\n')
530
+ f.write(f'# Generated by bmad-init\n')
531
+ f.write(f'# Date: {datetime.now(timezone.utc).isoformat()}\n\n')
532
+ yaml.safe_dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
533
+
534
+
535
+ # =============================================================================
536
+ # CLI Entry Point
537
+ # =============================================================================
538
+
539
+ def main():
540
+ parser = argparse.ArgumentParser(
541
+ description='BMad Init — Project configuration bootstrap and config loader.'
542
+ )
543
+ subparsers = parser.add_subparsers(dest='command')
544
+
545
+ # --- load ---
546
+ load_parser = subparsers.add_parser('load', help='Load config vars (fast path)')
547
+ load_parser.add_argument('--module', help='Module code (omit for core only)')
548
+ load_parser.add_argument('--vars', help='Comma-separated vars with optional defaults')
549
+ load_parser.add_argument('--all', action='store_true', help='Return all config vars')
550
+ load_parser.add_argument('--project-root', help='Project root path')
551
+
552
+ # --- check ---
553
+ check_parser = subparsers.add_parser('check', help='Check if init is needed')
554
+ check_parser.add_argument('--module', help='Module code to check (optional)')
555
+ check_parser.add_argument('--skill-path', help='Path to the calling skill folder')
556
+ check_parser.add_argument('--project-root', help='Project root path')
557
+
558
+ # --- resolve-defaults ---
559
+ resolve_parser = subparsers.add_parser('resolve-defaults',
560
+ help='Resolve module defaults given core answers')
561
+ resolve_parser.add_argument('--module', required=True, help='Module code')
562
+ resolve_parser.add_argument('--core-answers', required=True, help='JSON string of core answers')
563
+ resolve_parser.add_argument('--skill-path', help='Path to calling skill folder')
564
+ resolve_parser.add_argument('--project-root', help='Project root path')
565
+
566
+ # --- write ---
567
+ write_parser = subparsers.add_parser('write', help='Write config files')
568
+ write_parser.add_argument('--answers', required=True, help='JSON string of all answers')
569
+ write_parser.add_argument('--skill-path', help='Path to calling skill (for module.yaml lookup)')
570
+ write_parser.add_argument('--project-root', help='Project root path')
571
+
572
+ args = parser.parse_args()
573
+ if args.command is None:
574
+ parser.print_help()
575
+ sys.exit(1)
576
+
577
+ commands = {
578
+ 'load': cmd_load,
579
+ 'check': cmd_check,
580
+ 'resolve-defaults': cmd_resolve_defaults,
581
+ 'write': cmd_write,
582
+ }
583
+
584
+ handler = commands.get(args.command)
585
+ if handler:
586
+ handler(args)
587
+ else:
588
+ parser.print_help()
589
+ sys.exit(1)
590
+
591
+
592
+ if __name__ == '__main__':
593
+ main()