convoke-agents 3.1.0 → 3.2.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 (78) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +37 -10
  3. package/_bmad/bme/_artifacts/config.yaml +15 -0
  4. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
  5. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
  6. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
  7. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
  8. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
  9. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
  10. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
  11. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
  12. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
  13. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
  14. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
  15. package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
  16. package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
  17. package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
  18. package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
  19. package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
  20. package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
  21. package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
  22. package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
  23. package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
  24. package/_bmad/bme/_team-factory/config.yaml +13 -0
  25. package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
  26. package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
  27. package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
  28. package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
  29. package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
  30. package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
  31. package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
  32. package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
  33. package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
  34. package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
  35. package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
  36. package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
  37. package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
  38. package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
  39. package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
  40. package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
  41. package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
  42. package/_bmad/bme/_team-factory/module-help.csv +3 -0
  43. package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
  44. package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
  45. package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
  46. package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
  47. package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
  48. package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
  49. package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
  50. package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
  51. package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
  52. package/_bmad/bme/_vortex/config.yaml +4 -4
  53. package/package.json +13 -7
  54. package/scripts/convoke-doctor.js +172 -1
  55. package/scripts/install-gyre-agents.js +0 -0
  56. package/scripts/lib/artifact-utils.js +521 -13
  57. package/scripts/lib/portfolio/portfolio-engine.js +301 -34
  58. package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
  59. package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
  60. package/scripts/migrate-artifacts.js +69 -10
  61. package/scripts/portability/catalog-generator.js +353 -0
  62. package/scripts/portability/classify-skills.js +646 -0
  63. package/scripts/portability/convoke-export.js +522 -0
  64. package/scripts/portability/export-engine.js +1156 -0
  65. package/scripts/portability/generate-adapters.js +79 -0
  66. package/scripts/portability/manifest-csv.js +147 -0
  67. package/scripts/portability/seed-catalog-repo.js +427 -0
  68. package/scripts/portability/templates/canonical-example.md +102 -0
  69. package/scripts/portability/templates/canonical-format.md +218 -0
  70. package/scripts/portability/templates/readme-template.md +72 -0
  71. package/scripts/portability/test-constants.js +42 -0
  72. package/scripts/portability/validate-classification.js +529 -0
  73. package/scripts/portability/validate-exports.js +348 -0
  74. package/scripts/update/lib/agent-registry.js +35 -0
  75. package/scripts/update/lib/config-merger.js +140 -10
  76. package/scripts/update/lib/refresh-installation.js +293 -8
  77. package/scripts/update/lib/utils.js +27 -1
  78. package/scripts/update/lib/validator.js +114 -4
@@ -3,9 +3,10 @@
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const yaml = require('js-yaml');
6
- const { getPackageVersion } = require('./utils');
6
+ const YAML = require('yaml'); // Comment-preserving YAML library (ag-7-1: I29). Use for WRITE sites that need to preserve comments. js-yaml stays for read-only consumers.
7
+ const { getPackageVersion, assertVersion } = require('./utils');
7
8
  const configMerger = require('./config-merger');
8
- const { AGENTS, AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, USER_GUIDES, GYRE_AGENTS, GYRE_AGENT_FILES, GYRE_AGENT_IDS, GYRE_WORKFLOW_NAMES } = require('./agent-registry');
9
+ const { AGENTS, AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, USER_GUIDES, GYRE_AGENTS, GYRE_AGENT_FILES, GYRE_AGENT_IDS, GYRE_WORKFLOW_NAMES, EXTRA_BME_AGENTS } = require('./agent-registry');
9
10
 
10
11
  /**
11
12
  * Refresh Installation for Convoke
@@ -91,6 +92,34 @@ async function refreshInstallation(projectRoot, options = {}) {
91
92
  if (verbose) console.log(' Skipped workflow copy (dev environment)');
92
93
  }
93
94
 
95
+ // 2b1. Standalone bme submodule trees (e.g., _team-factory)
96
+ // Each EXTRA_BME_AGENTS entry references a submodule directory under _bmad/bme/
97
+ // that must be copied wholesale so the agent file, workflows, lib code, and config travel together.
98
+ // Mirrors the workflow loop pattern (2-step remove-then-copy) so renamed/deleted files
99
+ // in the package don't survive in the user install as stale leftovers.
100
+ const copiedExtraSubmodules = new Set();
101
+ if (!isSameRoot) {
102
+ for (const agent of EXTRA_BME_AGENTS) {
103
+ if (copiedExtraSubmodules.has(agent.submodule)) continue;
104
+ copiedExtraSubmodules.add(agent.submodule);
105
+ const srcDir = path.join(packageRoot, '_bmad', 'bme', agent.submodule);
106
+ const destDir = path.join(projectRoot, '_bmad', 'bme', agent.submodule);
107
+ if (fs.existsSync(srcDir)) {
108
+ // Remove existing destination first to clear stale files
109
+ // (e.g., renamed/deleted workflow steps from previous versions)
110
+ if (fs.existsSync(destDir)) {
111
+ await fs.remove(destDir);
112
+ }
113
+ await fs.copy(srcDir, destDir, { overwrite: true });
114
+ changes.push(`Refreshed standalone bme submodule: ${agent.submodule}`);
115
+ if (verbose) console.log(` Refreshed standalone bme submodule: ${agent.submodule}`);
116
+ }
117
+ }
118
+ } else {
119
+ changes.push('Skipped standalone bme submodule copy (dev environment — files already in place)');
120
+ if (verbose) console.log(' Skipped standalone bme submodule copy (dev environment)');
121
+ }
122
+
94
123
  // 2a. Enhance module — read config, copy directory tree, patch target agent menu
95
124
  const packageEnhance = path.join(packageRoot, '_bmad', 'bme', '_enhance');
96
125
  const enhanceConfigPath = path.join(packageEnhance, 'config.yaml');
@@ -115,12 +144,17 @@ async function refreshInstallation(projectRoot, options = {}) {
115
144
 
116
145
  if (!isSameRoot) {
117
146
  await fs.copy(packageEnhance, targetEnhance, { overwrite: true });
118
- // Stamp enhance config version to match package version
147
+ // Stamp enhance config version to match package version (ag-7-1: I30 + I29).
148
+ // Uses comment-preserving YAML.parseDocument so the doc comments survive.
119
149
  const targetEnhanceConfig = path.join(targetEnhance, 'config.yaml');
120
150
  if (fs.existsSync(targetEnhanceConfig)) {
121
- const ecContent = yaml.load(fs.readFileSync(targetEnhanceConfig, 'utf8'));
122
- ecContent.version = version;
123
- fs.writeFileSync(targetEnhanceConfig, yaml.dump(ecContent, { lineWidth: -1 }), 'utf8');
151
+ assertVersion(version, 'enhance');
152
+ const ecDoc = YAML.parseDocument(fs.readFileSync(targetEnhanceConfig, 'utf8'));
153
+ if (ecDoc.errors && ecDoc.errors.length > 0) {
154
+ throw new Error(`Refresh: cannot parse Enhance config.yaml: ${ecDoc.errors[0].message}`);
155
+ }
156
+ ecDoc.set('version', version);
157
+ fs.writeFileSync(targetEnhanceConfig, ecDoc.toString({ lineWidth: 0 }), 'utf8');
124
158
  }
125
159
  changes.push('Refreshed Enhance module: _bmad/bme/_enhance/');
126
160
  if (verbose) console.log(' Refreshed Enhance module: _bmad/bme/_enhance/');
@@ -189,6 +223,61 @@ async function refreshInstallation(projectRoot, options = {}) {
189
223
  }
190
224
  }
191
225
 
226
+ // 2c. Artifacts module — read config, copy directory tree, generate skill wrappers
227
+ // Workflow-only submodule (no agents). Workflows are STANDALONE: each gets a Claude Code
228
+ // skill wrapper but NO menu patch. The `standalone: true` flag in the workflow entry is
229
+ // the discriminator — workflows without it are NOT supported in this module today (Story 6.6).
230
+ const packageArtifacts = path.join(packageRoot, '_bmad', 'bme', '_artifacts');
231
+ const artifactsConfigPath = path.join(packageArtifacts, 'config.yaml');
232
+
233
+ let artifactsConfig = null;
234
+ if (fs.existsSync(artifactsConfigPath)) {
235
+ try {
236
+ artifactsConfig = yaml.load(fs.readFileSync(artifactsConfigPath, 'utf8'));
237
+ } catch (err) {
238
+ const msg = `Artifacts config.yaml parse error: ${err.message} — skipping Artifacts installation`;
239
+ changes.push(msg);
240
+ if (verbose) console.log(` ⚠ ${msg}`);
241
+ }
242
+ } else {
243
+ changes.push('Artifacts config.yaml not found — skipping Artifacts installation');
244
+ if (verbose) console.log(' ⚠ Artifacts config.yaml not found — skipping Artifacts installation');
245
+ }
246
+
247
+ if (artifactsConfig) {
248
+ // Copy _artifacts/ directory tree
249
+ const targetArtifacts = path.join(projectRoot, '_bmad', 'bme', '_artifacts');
250
+
251
+ if (!isSameRoot) {
252
+ // Remove existing destination first to clear stale files
253
+ if (fs.existsSync(targetArtifacts)) {
254
+ await fs.remove(targetArtifacts);
255
+ }
256
+ await fs.copy(packageArtifacts, targetArtifacts, { overwrite: true });
257
+ // Stamp artifacts config version to match package version (ag-7-1: I30 + I29).
258
+ // Uses comment-preserving YAML.parseDocument so the standalone:true doc comments survive.
259
+ const targetArtifactsConfig = path.join(targetArtifacts, 'config.yaml');
260
+ if (fs.existsSync(targetArtifactsConfig)) {
261
+ assertVersion(version, 'artifacts');
262
+ const acDoc = YAML.parseDocument(fs.readFileSync(targetArtifactsConfig, 'utf8'));
263
+ if (acDoc.errors && acDoc.errors.length > 0) {
264
+ throw new Error(`Refresh: cannot parse Artifacts config.yaml: ${acDoc.errors[0].message}`);
265
+ }
266
+ acDoc.set('version', version);
267
+ fs.writeFileSync(targetArtifactsConfig, acDoc.toString({ lineWidth: 0 }), 'utf8');
268
+ }
269
+ changes.push('Refreshed Artifacts module: _bmad/bme/_artifacts/');
270
+ if (verbose) console.log(' Refreshed Artifacts module: _bmad/bme/_artifacts/');
271
+ } else {
272
+ changes.push('Skipped Artifacts copy (dev environment — files already in place)');
273
+ if (verbose) console.log(' Skipped Artifacts copy (dev environment)');
274
+ }
275
+
276
+ // Skill wrapper generation for each workflow happens later in section 6d,
277
+ // after skillsDir is defined (mirrors Enhance pattern: config/copy here, skill
278
+ // wrappers in section 6c after agent skills are generated).
279
+ }
280
+
192
281
  // 2d. Gyre module — copy agents, workflows, contracts, config
193
282
  const packageGyre = path.join(packageRoot, '_bmad', 'bme', '_gyre');
194
283
  const targetGyre = path.join(projectRoot, '_bmad', 'bme', '_gyre');
@@ -257,6 +346,7 @@ async function refreshInstallation(projectRoot, options = {}) {
257
346
  agents: GYRE_AGENT_IDS,
258
347
  workflows: GYRE_WORKFLOW_NAMES
259
348
  };
349
+ assertVersion(version, 'config-merger:gyre'); // ag-7-1: defense-in-depth before mergeConfig
260
350
  const gyreConfigMerged = await configMerger.mergeConfig(gyreConfigTarget, version, gyreUpdates);
261
351
  await configMerger.writeConfig(gyreConfigTarget, gyreConfigMerged);
262
352
  changes.push(`Updated Gyre config.yaml to v${version}`);
@@ -282,6 +372,7 @@ async function refreshInstallation(projectRoot, options = {}) {
282
372
  workflows: WORKFLOW_NAMES
283
373
  };
284
374
 
375
+ assertVersion(version, 'config-merger:vortex'); // ag-7-1: defense-in-depth before mergeConfig
285
376
  const merged = await configMerger.mergeConfig(configPath, version, updates);
286
377
  await configMerger.writeConfig(configPath, merged);
287
378
  changes.push(`Updated config.yaml to v${version}`);
@@ -383,16 +474,46 @@ async function refreshInstallation(projectRoot, options = {}) {
383
474
  ].map(csvEscape).join(',');
384
475
  }
385
476
 
477
+ // Row builder for standalone bme agents (e.g., team-factory) — submodule path differs from team agents
478
+ function buildExtraBmeAgentRow610(a) {
479
+ const p = a.persona;
480
+ return [
481
+ csvEscape(a.name),
482
+ csvEscape(''),
483
+ csvEscape(a.title),
484
+ csvEscape(a.icon),
485
+ csvEscape(''),
486
+ csvEscape(p.role),
487
+ csvEscape(p.identity),
488
+ csvEscape(p.communication_style),
489
+ csvEscape(p.expertise),
490
+ csvEscape('bme'),
491
+ csvEscape(`_bmad/bme/${a.submodule}/agents/${a.id}.md`),
492
+ csvEscape(`bmad-agent-bme-${a.id}`),
493
+ ].join(',');
494
+ }
495
+
496
+ function buildExtraBmeAgentRowLegacy(a) {
497
+ const p = a.persona;
498
+ return [
499
+ a.id, a.name, a.title, a.icon,
500
+ p.role, p.identity, p.communication_style, p.expertise,
501
+ 'bme', `_bmad/bme/${a.submodule}/agents/${a.id}.md`,
502
+ ].map(csvEscape).join(',');
503
+ }
504
+
386
505
  let bmeRows;
387
506
  if (isV610) {
388
507
  bmeRows = [
389
508
  ...AGENTS.map(a => buildAgentRow610(a, '_vortex')),
390
509
  ...GYRE_AGENTS.map(a => buildAgentRow610(a, '_gyre')),
510
+ ...EXTRA_BME_AGENTS.map(buildExtraBmeAgentRow610),
391
511
  ];
392
512
  } else {
393
513
  bmeRows = [
394
514
  ...AGENTS.map(a => buildAgentRowLegacy(a, '_vortex')),
395
515
  ...GYRE_AGENTS.map(a => buildAgentRowLegacy(a, '_gyre')),
516
+ ...EXTRA_BME_AGENTS.map(buildExtraBmeAgentRowLegacy),
396
517
  ];
397
518
  }
398
519
 
@@ -446,6 +567,7 @@ async function refreshInstallation(projectRoot, options = {}) {
446
567
  const currentSkillDirs = new Set([
447
568
  ...AGENTS.map(a => `bmad-agent-bme-${a.id}`),
448
569
  ...GYRE_AGENTS.map(a => `bmad-agent-bme-${a.id}`),
570
+ ...EXTRA_BME_AGENTS.map(a => `bmad-agent-bme-${a.id}`),
449
571
  ]);
450
572
  if (fs.existsSync(skillsDir)) {
451
573
  const existingSkills = (await fs.readdir(skillsDir)).filter(d => d.startsWith('bmad-agent-bme-'));
@@ -507,6 +629,31 @@ You must fully embody this agent's persona and follow all activation instruction
507
629
  if (verbose) console.log(` Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
508
630
  }
509
631
 
632
+ // 6b1. Generate .claude/skills/ for standalone bme agents (e.g., team-factory)
633
+ for (const agent of EXTRA_BME_AGENTS) {
634
+ const skillDir = path.join(skillsDir, `bmad-agent-bme-${agent.id}`);
635
+ await fs.ensureDir(skillDir);
636
+ const content = `---
637
+ name: bmad-agent-bme-${agent.id}
638
+ description: ${agent.id} agent
639
+ ---
640
+
641
+ You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
642
+
643
+ <agent-activation CRITICAL="TRUE">
644
+ 1. LOAD the FULL agent file from {project-root}/_bmad/bme/${agent.submodule}/agents/${agent.id}.md
645
+ 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
646
+ 3. FOLLOW every step in the <activation> section precisely
647
+ 4. DISPLAY the welcome/greeting as instructed
648
+ 5. PRESENT the numbered menu
649
+ 6. WAIT for user input before proceeding
650
+ </agent-activation>
651
+ `;
652
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf8');
653
+ changes.push(`Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
654
+ if (verbose) console.log(` Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
655
+ }
656
+
510
657
  // 6c. Copy Enhance workflow skill wrappers and register in manifests
511
658
  if (enhanceConfig && !isSameRoot) {
512
659
  for (const workflow of enhanceConfig.workflows || []) {
@@ -540,7 +687,7 @@ You must fully embody this agent's persona and follow all activation instruction
540
687
  if (fs.existsSync(skManifestPath)) {
541
688
  const skCsv = fs.readFileSync(skManifestPath, 'utf8');
542
689
  if (!skCsv.includes(`"${canonicalId}"`)) {
543
- const skRow = `\n"${canonicalId}","${canonicalId}","Manage RICE initiatives backlog — triage review findings, rescore existing items, or bootstrap new backlogs.","bme","_bmad/bme/_enhance/workflows/${workflow.name}/SKILL.md","true"`;
690
+ const skRow = `\n"${canonicalId}","${canonicalId}","Manage RICE initiatives backlog — triage review findings, rescore existing items, or bootstrap new backlogs.","bme","_bmad/bme/_enhance/workflows/${workflow.name}/SKILL.md","true",,,`;
544
691
  fs.appendFileSync(skManifestPath, skRow, 'utf8');
545
692
  changes.push(`Added ${canonicalId} to skill-manifest.csv`);
546
693
  if (verbose) console.log(` Added ${canonicalId} to skill-manifest.csv`);
@@ -554,6 +701,80 @@ You must fully embody this agent's persona and follow all activation instruction
554
701
  if (verbose) console.log(' Skipped Enhance skill registration (dev environment)');
555
702
  }
556
703
 
704
+ // 6d. Copy Artifacts workflow skill wrappers (Story 6.6)
705
+ // Each standalone:true workflow gets a skill wrapper at .claude/skills/{workflow.name}/SKILL.md.
706
+ // workflow.name already carries the bmad- prefix, so we use it verbatim (unlike Enhance which
707
+ // synthesizes bmad-enhance-${workflow.name}). The remove-then-copy pattern clears any leftover
708
+ // files from prior installs (e.g., the obsolete bmad-portfolio-status/workflow.md thin wrapper).
709
+ if (artifactsConfig && !isSameRoot) {
710
+ for (const workflow of artifactsConfig.workflows || []) {
711
+ if (workflow.standalone !== true) {
712
+ const msg = `Artifacts: workflow ${workflow.name} has no standalone:true flag — only standalone workflows are supported, skipping`;
713
+ changes.push(msg);
714
+ if (verbose) console.log(` ⚠ ${msg}`);
715
+ continue;
716
+ }
717
+
718
+ const destSkillDir = path.join(skillsDir, workflow.name);
719
+
720
+ // Remove the destination directory first to clear leftover files from prior installs
721
+ if (fs.existsSync(destSkillDir)) {
722
+ await fs.remove(destSkillDir);
723
+ }
724
+ await fs.ensureDir(destSkillDir);
725
+
726
+ // Copy source SKILL.md from the package (the SKILL.md uses an absolute {project-root}
727
+ // path to load workflow.md, so workflow.md does NOT need to be co-located).
728
+ const sourceSkillPath = path.join(packageRoot, '_bmad', 'bme', '_artifacts', 'workflows', workflow.name, 'SKILL.md');
729
+ const targetSkillPath = path.join(destSkillDir, 'SKILL.md');
730
+ if (fs.existsSync(sourceSkillPath)) {
731
+ await fs.copy(sourceSkillPath, targetSkillPath, { overwrite: true });
732
+ changes.push(`Generated skill wrapper: ${workflow.name}`);
733
+ if (verbose) console.log(` Generated skill wrapper: ${workflow.name}`);
734
+ } else {
735
+ const msg = `Artifacts: source SKILL.md not found for ${workflow.name} at ${sourceSkillPath}`;
736
+ changes.push(msg);
737
+ if (verbose) console.log(` ⚠ ${msg}`);
738
+ }
739
+ }
740
+ } else if (artifactsConfig && isSameRoot) {
741
+ changes.push('Skipped Artifacts skill wrapper generation (dev environment — source files unchanged)');
742
+ if (verbose) console.log(' Skipped Artifacts skill wrapper generation (dev environment)');
743
+ }
744
+
745
+ // 6e. Orphan workflow-wrapper cleanup (Story 7.4, I32)
746
+ // Removes stale .claude/skills/ directories for workflow wrappers that are no longer
747
+ // declared in the module configs. Uses a two-strategy matching approach:
748
+ // Strategy 1 (Enhance): any bmad-enhance-* dir not in the current union → orphan
749
+ // Strategy 2 (Artifacts): any dir whose name exactly matches a known Artifacts
750
+ // workflow name but is not in the current union → orphan
751
+ // All other directories (agent wrappers, upstream BMAD skills, third-party) are ignored.
752
+ if (!isSameRoot) {
753
+ const currentWorkflowWrappers = new Set();
754
+ // Enhance wrappers: bmad-enhance-${workflow.name}
755
+ if (enhanceConfig && Array.isArray(enhanceConfig.workflows)) {
756
+ for (const wf of enhanceConfig.workflows) {
757
+ if (wf && wf.name) currentWorkflowWrappers.add(`bmad-enhance-${wf.name}`);
758
+ }
759
+ }
760
+ // Artifacts wrappers: workflow.name verbatim (only standalone:true are installed,
761
+ // but we track ALL names so a removed standalone workflow is still recognized as an orphan)
762
+ const knownArtifactsNames = new Set();
763
+ if (artifactsConfig && Array.isArray(artifactsConfig.workflows)) {
764
+ for (const wf of artifactsConfig.workflows) {
765
+ if (wf && wf.name) {
766
+ knownArtifactsNames.add(wf.name);
767
+ if (wf.standalone === true) currentWorkflowWrappers.add(wf.name);
768
+ }
769
+ }
770
+ }
771
+ const orphanChanges = cleanupOrphanWorkflowWrappers(skillsDir, currentWorkflowWrappers, knownArtifactsNames, { verbose });
772
+ changes.push(...orphanChanges);
773
+ } else {
774
+ changes.push('Skipped orphan workflow-wrapper cleanup (dev environment)');
775
+ if (verbose) console.log(' Skipped orphan workflow-wrapper cleanup (dev environment)');
776
+ }
777
+
557
778
  // 7. Generate agent customize files (only if they don't already exist)
558
779
  const customizeDir = path.join(projectRoot, '_bmad', '_config', 'agents');
559
780
  await fs.ensureDir(customizeDir);
@@ -599,4 +820,68 @@ prompts: []
599
820
  return changes;
600
821
  }
601
822
 
602
- module.exports = { refreshInstallation };
823
+ /**
824
+ * Remove orphan workflow-wrapper directories from .claude/skills/.
825
+ *
826
+ * Two-strategy matching (Story 7.4, I32):
827
+ * Strategy 1: Enhance prefix — any dir starting with `bmad-enhance-` that is
828
+ * not in `currentWrappers` is an orphan.
829
+ * Strategy 2: Artifacts exact-name — any dir whose name is in `knownArtifactsNames`
830
+ * but not in `currentWrappers` is an orphan.
831
+ * Everything else (agent wrappers, upstream BMAD skills, third-party) is ignored.
832
+ *
833
+ * @param {string} skillsDir - Absolute path to .claude/skills/
834
+ * @param {Set<string>} currentWrappers - Union of live workflow wrapper names
835
+ * @param {Set<string>} knownArtifactsNames - ALL Artifacts workflow names (including non-standalone)
836
+ * @param {object} [options]
837
+ * @param {boolean} [options.verbose] - Log each action
838
+ * @returns {Array<string>} Changes array entries for removed orphans
839
+ */
840
+ function cleanupOrphanWorkflowWrappers(skillsDir, currentWrappers, knownArtifactsNames, options = {}) {
841
+ // Deliberately synchronous (fs.removeSync / fs.readdirSync) — the function returns
842
+ // Array<string>, not a Promise. The sync pattern keeps the contract simple for both
843
+ // the caller (section 6e spreads the result into changes[]) and the test file (which
844
+ // imports the function directly without async scaffolding). The existing agent
845
+ // stale-skill sweep at section 6 uses async fs.remove because it runs inline in the
846
+ // async refreshInstallation body; this function is extracted to be testable standalone.
847
+ const { verbose = false } = options;
848
+ const changes = [];
849
+
850
+ if (!fs.existsSync(skillsDir)) return changes;
851
+
852
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
853
+
854
+ for (const entry of entries) {
855
+ if (!entry.isDirectory()) continue;
856
+ const name = entry.name;
857
+
858
+ // Skip agent wrappers (handled by existing stale-skill sweep)
859
+ if (name.startsWith('bmad-agent-bme-')) continue;
860
+
861
+ // Strategy 1: Enhance prefix (unambiguous — no upstream module uses bmad-enhance-)
862
+ if (name.startsWith('bmad-enhance-')) {
863
+ if (!currentWrappers.has(name)) {
864
+ fs.removeSync(path.join(skillsDir, name));
865
+ changes.push(`Removed orphan skill wrapper: ${name}`);
866
+ if (verbose) console.log(` Removed orphan skill wrapper: ${name}`);
867
+ }
868
+ continue;
869
+ }
870
+
871
+ // Strategy 2: Artifacts exact-name match
872
+ if (knownArtifactsNames.has(name)) {
873
+ if (!currentWrappers.has(name)) {
874
+ fs.removeSync(path.join(skillsDir, name));
875
+ changes.push(`Removed orphan skill wrapper: ${name}`);
876
+ if (verbose) console.log(` Removed orphan skill wrapper: ${name}`);
877
+ }
878
+ continue;
879
+ }
880
+
881
+ // Everything else: not a workflow wrapper we own — leave alone
882
+ }
883
+
884
+ return changes;
885
+ }
886
+
887
+ module.exports = { refreshInstallation, cleanupOrphanWorkflowWrappers };
@@ -88,9 +88,35 @@ function findProjectRoot() {
88
88
  return null;
89
89
  }
90
90
 
91
+ /**
92
+ * Assert that a version string is valid before stamping a config file.
93
+ * Throws a clear error if version is undefined, null, or empty.
94
+ * Used by refresh-installation.js, config-merger.js, and any future config writer
95
+ * that stamps a version field — closes I30 (ag-7-1).
96
+ *
97
+ * @param {string} version - The version string to validate
98
+ * @param {string} callSite - Identifier for the call site (e.g., 'enhance', 'artifacts', 'config-merger')
99
+ * @throws {Error} if version is not a non-empty string
100
+ */
101
+ function assertVersion(version, callSite) {
102
+ // Reject all non-string types (numeric 0, boolean false, etc.) — version
103
+ // must be a non-empty string. Closes Blind Hunter finding #9 (ag-7-1 review).
104
+ if (typeof version !== 'string' || version === '') {
105
+ let displayed;
106
+ if (version === null) displayed = 'null';
107
+ else if (version === undefined) displayed = 'undefined';
108
+ else if (version === '') displayed = "''";
109
+ else displayed = `${typeof version} (${String(version)})`;
110
+ throw new Error(
111
+ `Refresh: cannot stamp config — getPackageVersion() returned ${displayed}; check package.json (call site: ${callSite})`
112
+ );
113
+ }
114
+ }
115
+
91
116
  module.exports = {
92
117
  getPackageVersion,
93
118
  compareVersions,
94
119
  countUserDataFiles,
95
- findProjectRoot
120
+ findProjectRoot,
121
+ assertVersion
96
122
  };
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const yaml = require('js-yaml');
6
6
  const configMerger = require('./config-merger');
7
7
  const { countUserDataFiles } = require('./utils');
8
- const { AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, WAVE3_WORKFLOW_NAMES } = require('./agent-registry');
8
+ const { AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, WAVE3_WORKFLOW_NAMES, EXTRA_BME_AGENTS, EXTRA_BME_AGENT_IDS } = require('./agent-registry');
9
9
 
10
10
  /**
11
11
  * Validator for Convoke
@@ -47,6 +47,9 @@ async function validateInstallation(preMigrationData = {}, projectRoot) {
47
47
  // 8. Enhance module validation (optional — passes if not installed)
48
48
  checks.push(await validateEnhanceModule(projectRoot));
49
49
 
50
+ // 9. Artifacts module validation (optional — passes if not installed)
51
+ checks.push(await validateArtifactsModule(projectRoot));
52
+
50
53
  const allPassed = checks.every(c => c.passed);
51
54
 
52
55
  return {
@@ -202,11 +205,22 @@ async function validateManifest(projectRoot) {
202
205
 
203
206
  const manifestContent = fs.readFileSync(manifestPath, 'utf8');
204
207
 
205
- // Check for all Convoke agents
208
+ // Check for all Convoke agents (Vortex/Gyre IDs and standalone bme agents)
206
209
  const missingFromManifest = AGENT_IDS.filter(id => !manifestContent.includes(id));
210
+ const missingExtras = EXTRA_BME_AGENT_IDS.filter(id => !manifestContent.includes(`bmad-agent-bme-${id}`));
211
+
212
+ const allMissing = [...missingFromManifest, ...missingExtras];
213
+ if (allMissing.length > 0) {
214
+ check.error = `Agent manifest missing: ${allMissing.join(', ')}`;
215
+ return check;
216
+ }
207
217
 
208
- if (missingFromManifest.length > 0) {
209
- check.error = `Agent manifest missing: ${missingFromManifest.join(', ')}`;
218
+ // Confirm standalone bme agent files exist on disk
219
+ const missingExtraFiles = EXTRA_BME_AGENTS
220
+ .filter(a => !fs.existsSync(path.join(projectRoot, '_bmad', 'bme', a.submodule, 'agents', `${a.id}.md`)))
221
+ .map(a => a.id);
222
+ if (missingExtraFiles.length > 0) {
223
+ check.error = `Standalone bme agent files missing: ${missingExtraFiles.join(', ')}`;
210
224
  return check;
211
225
  }
212
226
 
@@ -468,6 +482,101 @@ async function validateEnhanceModule(projectRoot) {
468
482
  return check;
469
483
  }
470
484
 
485
+ /**
486
+ * Validate Artifacts module installation (optional — passes if not installed)
487
+ * Performs 5-point verification: directory, config, workflows array, per-workflow entry, per-workflow skill wrapper
488
+ * @param {string} projectRoot - Absolute path to project root
489
+ * @returns {Promise<object>} Validation check result
490
+ */
491
+ async function validateArtifactsModule(projectRoot) {
492
+ const check = {
493
+ name: 'Artifacts module',
494
+ passed: false,
495
+ error: null
496
+ };
497
+
498
+ try {
499
+ const artifactsDir = path.join(projectRoot, '_bmad/bme/_artifacts');
500
+
501
+ // Check 1: Directory exists — if not, Artifacts is simply not installed (optional)
502
+ if (!fs.existsSync(artifactsDir)) {
503
+ check.passed = true;
504
+ check.info = 'not installed';
505
+ return check;
506
+ }
507
+
508
+ const failures = [];
509
+
510
+ // Check 2: Config parse — bail early if config is unreadable, since later checks
511
+ // depend on a parsed workflows array.
512
+ const configPath = path.join(artifactsDir, 'config.yaml');
513
+ if (!fs.existsSync(configPath)) {
514
+ check.error = 'Artifacts: config.yaml not found';
515
+ return check;
516
+ }
517
+
518
+ let config = null;
519
+ try {
520
+ config = yaml.load(fs.readFileSync(configPath, 'utf8'));
521
+ } catch (err) {
522
+ check.error = `Artifacts: config.yaml parse error: ${err.message}`;
523
+ return check;
524
+ }
525
+
526
+ if (!config || typeof config !== 'object') {
527
+ check.error = 'Artifacts: config.yaml is empty or invalid';
528
+ return check;
529
+ }
530
+
531
+ // Check 3: Workflows array non-empty
532
+ if (!Array.isArray(config.workflows) || config.workflows.length === 0) {
533
+ check.error = 'Artifacts: config.yaml has no workflows array';
534
+ return check;
535
+ }
536
+
537
+ // Checks 4 & 5: Per-workflow entry point and skill wrapper.
538
+ // Aggregate failures across all workflows so a single doctor run reports every
539
+ // problem at once (mirrors validateEnhanceModule).
540
+ // Non-standalone workflows are skipped from wrapper/entry checks because
541
+ // refresh-installation.js section 6d does NOT install them — validating their
542
+ // wrapper would be a contract mismatch with the refresh logic.
543
+ for (const wf of config.workflows) {
544
+ if (!wf || !wf.name || !wf.entry) {
545
+ failures.push('workflow entry missing name or entry field');
546
+ continue;
547
+ }
548
+
549
+ if (wf.standalone !== true) {
550
+ // Refresh skips non-standalone workflows; nothing to validate.
551
+ continue;
552
+ }
553
+
554
+ // Check 4: Workflow entry point file exists
555
+ const entryPath = path.join(artifactsDir, wf.entry);
556
+ if (!fs.existsSync(entryPath)) {
557
+ failures.push(`workflow entry missing for ${wf.name}: ${wf.entry}`);
558
+ }
559
+
560
+ // Check 5: Skill wrapper exists at .claude/skills/{workflow.name}/SKILL.md
561
+ // (workflow.name already carries the bmad- prefix; do NOT synthesize bmad-${wf.name})
562
+ const skillWrapperPath = path.join(projectRoot, '.claude', 'skills', wf.name, 'SKILL.md');
563
+ if (!fs.existsSync(skillWrapperPath)) {
564
+ failures.push(`skill wrapper missing for ${wf.name}`);
565
+ }
566
+ }
567
+
568
+ if (failures.length > 0) {
569
+ check.error = `Artifacts: ${failures.join('; ')}`;
570
+ } else {
571
+ check.passed = true;
572
+ }
573
+ } catch (error) {
574
+ check.error = error.message;
575
+ }
576
+
577
+ return check;
578
+ }
579
+
471
580
  /**
472
581
  * Validate a SKILL.md file has required frontmatter fields
473
582
  * @param {string} skillMdPath - Absolute path to SKILL.md file
@@ -633,6 +742,7 @@ module.exports = {
633
742
  validateDeprecatedWorkflows,
634
743
  validateWorkflowStepStructure,
635
744
  validateEnhanceModule,
745
+ validateArtifactsModule,
636
746
  validateSkillMd,
637
747
  validateStepFiles,
638
748
  validateSkillCohesion,