convoke-agents 3.0.4 → 3.2.0

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 (92) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +14 -13
  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/_gyre/guides/GYRE-TEAM-GUIDE.md +506 -0
  16. package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
  17. package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
  18. package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
  19. package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
  20. package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
  21. package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
  22. package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
  23. package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
  24. package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
  25. package/_bmad/bme/_team-factory/config.yaml +13 -0
  26. package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
  27. package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
  28. package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
  29. package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
  30. package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
  31. package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
  32. package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
  33. package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
  34. package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
  35. package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
  36. package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
  37. package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
  38. package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
  39. package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
  40. package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
  41. package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
  42. package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
  43. package/_bmad/bme/_team-factory/module-help.csv +3 -0
  44. package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
  45. package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
  46. package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
  47. package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
  48. package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
  49. package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
  50. package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
  51. package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
  52. package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
  53. package/_bmad/bme/_vortex/config.yaml +4 -4
  54. package/_bmad/bme/_vortex/guides/VORTEX-TEAM-GUIDE.md +441 -0
  55. package/package.json +17 -8
  56. package/scripts/archive.js +26 -45
  57. package/scripts/convoke-check.js +88 -0
  58. package/scripts/convoke-doctor.js +303 -4
  59. package/scripts/install-gyre-agents.js +0 -0
  60. package/scripts/lib/artifact-utils.js +2182 -0
  61. package/scripts/lib/portfolio/formatters/markdown-formatter.js +40 -0
  62. package/scripts/lib/portfolio/formatters/terminal-formatter.js +56 -0
  63. package/scripts/lib/portfolio/portfolio-engine.js +572 -0
  64. package/scripts/lib/portfolio/rules/artifact-chain-rule.js +156 -0
  65. package/scripts/lib/portfolio/rules/conflict-resolver.js +99 -0
  66. package/scripts/lib/portfolio/rules/frontmatter-rule.js +42 -0
  67. package/scripts/lib/portfolio/rules/git-recency-rule.js +69 -0
  68. package/scripts/lib/types.js +122 -0
  69. package/scripts/migrate-artifacts.js +439 -0
  70. package/scripts/portability/catalog-generator.js +353 -0
  71. package/scripts/portability/classify-skills.js +646 -0
  72. package/scripts/portability/convoke-export.js +522 -0
  73. package/scripts/portability/export-engine.js +1133 -0
  74. package/scripts/portability/generate-adapters.js +79 -0
  75. package/scripts/portability/manifest-csv.js +147 -0
  76. package/scripts/portability/seed-catalog-repo.js +427 -0
  77. package/scripts/portability/templates/canonical-example.md +102 -0
  78. package/scripts/portability/templates/canonical-format.md +218 -0
  79. package/scripts/portability/templates/readme-template.md +72 -0
  80. package/scripts/portability/test-constants.js +42 -0
  81. package/scripts/portability/validate-classification.js +529 -0
  82. package/scripts/portability/validate-exports.js +348 -0
  83. package/scripts/update/lib/agent-registry.js +35 -0
  84. package/scripts/update/lib/config-merger.js +140 -10
  85. package/scripts/update/lib/migration-runner.js +1 -1
  86. package/scripts/update/lib/refresh-installation.js +293 -8
  87. package/scripts/update/lib/taxonomy-merger.js +138 -0
  88. package/scripts/update/lib/utils.js +27 -1
  89. package/scripts/update/lib/validator.js +114 -4
  90. package/scripts/update/migrations/2.0.x-to-3.1.0.js +50 -0
  91. package/scripts/update/migrations/3.0.x-to-3.1.0.js +41 -0
  92. package/scripts/update/migrations/registry.js +14 -0
@@ -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 };
@@ -0,0 +1,138 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ /**
6
+ * Platform-canonical taxonomy defaults.
7
+ * Mirrors migrate-artifacts.js constants (separate module boundary).
8
+ */
9
+ const PLATFORM_INITIATIVES = ['vortex', 'gyre', 'bmm', 'forge', 'helm', 'enhance', 'loom', 'convoke'];
10
+
11
+ const DEFAULT_ARTIFACT_TYPES = [
12
+ 'prd', 'epic', 'arch', 'adr', 'persona', 'lean-persona', 'empathy-map',
13
+ 'problem-def', 'hypothesis', 'experiment', 'signal', 'decision', 'scope',
14
+ 'pre-reg', 'sprint', 'brief', 'vision', 'report', 'research', 'story', 'spec'
15
+ ];
16
+
17
+ const DEFAULT_ALIASES = {
18
+ 'strategy-perimeter': 'helm',
19
+ 'strategy': 'helm',
20
+ 'strategic': 'helm',
21
+ 'strategic-navigator': 'helm',
22
+ 'strategic-practitioner': 'helm',
23
+ 'team-factory': 'loom'
24
+ };
25
+
26
+ const TAXONOMY_HEADER = [
27
+ '# Artifact Governance Taxonomy Configuration',
28
+ '# Schema version: 1',
29
+ '# Managed by: convoke-update taxonomy merger',
30
+ '#',
31
+ '# This file is the single source of truth for initiative IDs, artifact types,',
32
+ '# and historical name aliases used by the governance system.',
33
+ ''
34
+ ].join('\n');
35
+
36
+ /**
37
+ * Create or merge taxonomy.yaml with platform defaults.
38
+ * Idempotent: safe to run multiple times.
39
+ *
40
+ * - If absent: creates with platform defaults
41
+ * - If present: merges platform entries (adds missing, preserves user additions)
42
+ * - Promotes user initiative IDs to platform when they match (FR42)
43
+ *
44
+ * @param {string} projectRoot - Absolute path to project root
45
+ * @returns {Promise<{created: boolean, merged: boolean, promoted: string[]}>}
46
+ */
47
+ async function mergeTaxonomy(projectRoot) {
48
+ const configDir = path.join(projectRoot, '_bmad', '_config');
49
+ const configPath = path.join(configDir, 'taxonomy.yaml');
50
+
51
+ await fs.ensureDir(configDir);
52
+
53
+ // If no taxonomy exists, create from scratch
54
+ if (!await fs.pathExists(configPath)) {
55
+ const defaults = {
56
+ initiatives: { platform: [...PLATFORM_INITIATIVES], user: [] },
57
+ artifact_types: [...DEFAULT_ARTIFACT_TYPES],
58
+ aliases: { ...DEFAULT_ALIASES }
59
+ };
60
+ await fs.writeFile(configPath, TAXONOMY_HEADER + yaml.dump(defaults, { lineWidth: -1 }), 'utf8');
61
+ return { created: true, merged: false, promoted: [] };
62
+ }
63
+
64
+ // Read existing taxonomy (handle corrupt YAML gracefully, matching config-merger pattern)
65
+ const content = await fs.readFile(configPath, 'utf8');
66
+ let existing;
67
+ try {
68
+ existing = yaml.load(content) || {};
69
+ } catch {
70
+ console.warn('Warning: taxonomy.yaml contains invalid YAML. Treating as empty and merging defaults.');
71
+ existing = {};
72
+ }
73
+
74
+ // Ensure structure
75
+ if (!existing.initiatives) existing.initiatives = {};
76
+ if (!Array.isArray(existing.initiatives.platform)) existing.initiatives.platform = [];
77
+ if (!Array.isArray(existing.initiatives.user)) existing.initiatives.user = [];
78
+ if (!Array.isArray(existing.artifact_types)) existing.artifact_types = [];
79
+ if (!existing.aliases || typeof existing.aliases !== 'object') existing.aliases = {};
80
+
81
+ let merged = false;
82
+ const promoted = [];
83
+
84
+ // Merge platform initiatives (add missing)
85
+ const platformSet = new Set(existing.initiatives.platform);
86
+ for (const id of PLATFORM_INITIATIVES) {
87
+ if (!platformSet.has(id)) {
88
+ existing.initiatives.platform.push(id);
89
+ merged = true;
90
+ }
91
+ }
92
+
93
+ // Promote user IDs that match platform (FR42)
94
+ const date = new Date().toISOString().split('T')[0];
95
+ const newPlatformSet = new Set(existing.initiatives.platform);
96
+ existing.initiatives.user = existing.initiatives.user.filter(userId => {
97
+ if (newPlatformSet.has(userId)) {
98
+ promoted.push(userId);
99
+ return false; // Remove from user (already in platform)
100
+ }
101
+ return true;
102
+ });
103
+
104
+ // Merge artifact types (add missing)
105
+ const typeSet = new Set(existing.artifact_types);
106
+ for (const type of DEFAULT_ARTIFACT_TYPES) {
107
+ if (!typeSet.has(type)) {
108
+ existing.artifact_types.push(type);
109
+ merged = true;
110
+ }
111
+ }
112
+
113
+ // Merge aliases (add missing, don't overwrite existing)
114
+ for (const [key, value] of Object.entries(DEFAULT_ALIASES)) {
115
+ if (!(key in existing.aliases)) {
116
+ existing.aliases[key] = value;
117
+ merged = true;
118
+ }
119
+ }
120
+
121
+ // Write back if changes were made
122
+ if (merged || promoted.length > 0) {
123
+ let output = TAXONOMY_HEADER + yaml.dump(existing, { lineWidth: -1 });
124
+
125
+ // Add promotion comments
126
+ if (promoted.length > 0) {
127
+ for (const id of promoted) {
128
+ output += `# ${id}: promoted from user section on ${date}\n`;
129
+ }
130
+ }
131
+
132
+ await fs.writeFile(configPath, output, 'utf8');
133
+ }
134
+
135
+ return { created: false, merged: merged || promoted.length > 0, promoted };
136
+ }
137
+
138
+ module.exports = { mergeTaxonomy, PLATFORM_INITIATIVES, DEFAULT_ARTIFACT_TYPES, DEFAULT_ALIASES };
@@ -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
  };