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
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-exports.js — Story sp-4-2
4
+ *
5
+ * Structural validation of a staging directory produced by seed-catalog-repo.js.
6
+ * Checks every exported skill for markdown validity, forbidden strings, persona
7
+ * sections, platform install instructions, and generates a VALIDATION-REPORT.md
8
+ * with automated results + manual test checklists.
9
+ *
10
+ * Usage:
11
+ * node scripts/portability/validate-exports.js --input <path>
12
+ * node scripts/portability/validate-exports.js --input <path> --report <path>
13
+ * node scripts/portability/validate-exports.js --help
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // =============================================================================
22
+ // CONSTANTS
23
+ // =============================================================================
24
+
25
+ const { FORBIDDEN_STRINGS } = require('./test-constants');
26
+
27
+ const XML_TAGS = [
28
+ '<workflow>', '</workflow>', '<step ', '</step>', '<action>', '</action>',
29
+ '<check ', '</check>', '<critical>', '</critical>', '<output>', '</output>',
30
+ '<ask>', '</ask>',
31
+ ];
32
+
33
+ const MANUAL_SMOKE_SKILLS = [
34
+ { name: 'bmad-brainstorming', persona: 'Carson', role: 'brainstorming facilitator' },
35
+ { name: 'bmad-agent-architect', persona: 'Winston', role: 'system architect' },
36
+ { name: 'bmad-tea', persona: 'Murat', role: 'test architect' },
37
+ ];
38
+
39
+ // =============================================================================
40
+ // VALIDATION CHECKS
41
+ // =============================================================================
42
+
43
+ function validateInstructions(filePath, skillName) {
44
+ const issues = [];
45
+ if (!fs.existsSync(filePath)) {
46
+ issues.push({ skill: skillName, file: 'instructions.md', issue: 'file missing' });
47
+ return issues;
48
+ }
49
+
50
+ const content = fs.readFileSync(filePath, 'utf8');
51
+
52
+ // Starts with # heading
53
+ if (!content.startsWith('# ')) {
54
+ issues.push({ skill: skillName, file: 'instructions.md', issue: 'does not start with # heading' });
55
+ }
56
+
57
+ // Persona section
58
+ if (!content.includes('## You are')) {
59
+ issues.push({ skill: skillName, file: 'instructions.md', issue: 'missing ## You are section' });
60
+ }
61
+
62
+ // Workflow section
63
+ if (!content.includes('## How to proceed')) {
64
+ issues.push({ skill: skillName, file: 'instructions.md', issue: 'missing ## How to proceed section' });
65
+ }
66
+
67
+ // Forbidden strings
68
+ for (const forbidden of FORBIDDEN_STRINGS) {
69
+ if (content.includes(forbidden)) {
70
+ issues.push({ skill: skillName, file: 'instructions.md', issue: `contains forbidden: "${forbidden}"` });
71
+ }
72
+ }
73
+
74
+ // XML tags remaining
75
+ for (const tag of XML_TAGS) {
76
+ if (content.includes(tag)) {
77
+ issues.push({ skill: skillName, file: 'instructions.md', issue: `contains XML tag: ${tag}` });
78
+ }
79
+ }
80
+
81
+ // Frontmatter blocks
82
+ // Check for YAML frontmatter at the START of the file (no m flag — ^ = string start)
83
+ if (/^---\n[\s\S]*?\n---\n/.test(content)) {
84
+ issues.push({ skill: skillName, file: 'instructions.md', issue: 'contains frontmatter block' });
85
+ }
86
+
87
+ // Balanced code fences
88
+ const fenceCount = (content.match(/^```/gm) || []).length;
89
+ if (fenceCount % 2 !== 0) {
90
+ issues.push({ skill: skillName, file: 'instructions.md', issue: `unbalanced code fences (${fenceCount} found)` });
91
+ }
92
+
93
+ // Broken markdown links (links to local files that aren't URLs or anchors)
94
+ const localLinks = [...content.matchAll(/\[([^\]]*)\]\((?!https?:\/\/|#|mailto:)([^)]+)\)/g)];
95
+ for (const match of localLinks) {
96
+ const linkTarget = match[2];
97
+ // Skip links containing template placeholders or non-path content
98
+ if (linkTarget.includes('[') || linkTarget.includes('{')) continue;
99
+ if (linkTarget.includes(' ')) continue; // URLs/paths don't have spaces; this is prose like "(none found)"
100
+ const resolved = path.resolve(path.dirname(filePath), linkTarget);
101
+ if (!fs.existsSync(resolved)) {
102
+ issues.push({ skill: skillName, file: 'instructions.md', issue: `broken link: [${match[1]}](${linkTarget})` });
103
+ }
104
+ }
105
+
106
+ return issues;
107
+ }
108
+
109
+ function validateReadme(filePath, skillName) {
110
+ const issues = [];
111
+ if (!fs.existsSync(filePath)) {
112
+ issues.push({ skill: skillName, file: 'README.md', issue: 'file missing' });
113
+ return issues;
114
+ }
115
+
116
+ const content = fs.readFileSync(filePath, 'utf8');
117
+
118
+ // 3 platform sections
119
+ if (!content.includes('Claude Code')) {
120
+ issues.push({ skill: skillName, file: 'README.md', issue: 'missing Claude Code section' });
121
+ }
122
+ if (!content.includes('Copilot')) {
123
+ issues.push({ skill: skillName, file: 'README.md', issue: 'missing Copilot section' });
124
+ }
125
+ if (!content.includes('Cursor')) {
126
+ issues.push({ skill: skillName, file: 'README.md', issue: 'missing Cursor section' });
127
+ }
128
+
129
+ // Line count
130
+ const lineCount = content.split('\n').length;
131
+ if (lineCount > 80) {
132
+ issues.push({ skill: skillName, file: 'README.md', issue: `${lineCount} lines (exceeds 80)` });
133
+ }
134
+
135
+ // No HTML comments in output
136
+ if (content.includes('<!--')) {
137
+ issues.push({ skill: skillName, file: 'README.md', issue: 'contains HTML comments' });
138
+ }
139
+
140
+ return issues;
141
+ }
142
+
143
+ function validateStagingDir(inputDir) {
144
+ const allIssues = [];
145
+
146
+ // Check root files
147
+ if (!fs.existsSync(path.join(inputDir, 'README.md'))) {
148
+ allIssues.push({ skill: 'ROOT', file: 'README.md', issue: 'missing catalog README' });
149
+ }
150
+ if (!fs.existsSync(path.join(inputDir, 'LICENSE'))) {
151
+ allIssues.push({ skill: 'ROOT', file: 'LICENSE', issue: 'missing' });
152
+ }
153
+ if (!fs.existsSync(path.join(inputDir, 'CONTRIBUTING.md'))) {
154
+ allIssues.push({ skill: 'ROOT', file: 'CONTRIBUTING.md', issue: 'missing' });
155
+ }
156
+
157
+ // Validate each skill directory
158
+ const entries = fs.readdirSync(inputDir, { withFileTypes: true });
159
+ const skillDirs = entries
160
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
161
+ .map((e) => e.name)
162
+ .sort();
163
+
164
+ for (const skillName of skillDirs) {
165
+ const skillDir = path.join(inputDir, skillName);
166
+ allIssues.push(
167
+ ...validateInstructions(path.join(skillDir, 'instructions.md'), skillName),
168
+ ...validateReadme(path.join(skillDir, 'README.md'), skillName)
169
+ );
170
+ }
171
+
172
+ return { issues: allIssues, skillCount: skillDirs.length, skillDirs };
173
+ }
174
+
175
+ // =============================================================================
176
+ // REPORT GENERATION
177
+ // =============================================================================
178
+
179
+ function generateReport(inputDir, validationResult) {
180
+ const { issues, skillCount, skillDirs } = validationResult;
181
+ const date = new Date().toISOString().slice(0, 10);
182
+ const passCount = skillDirs.filter(
183
+ (d) => !issues.some((i) => i.skill === d)
184
+ ).length;
185
+ const failCount = skillCount - passCount;
186
+
187
+ const lines = [];
188
+
189
+ lines.push('# Export Validation Report');
190
+ lines.push('');
191
+ lines.push(`**Date:** ${date}`);
192
+ lines.push(`**Skills validated:** ${skillCount}`);
193
+ lines.push(`**Automated checks:** ${issues.length === 0 ? 'ALL PASSED' : `${issues.length} issue(s) found`}`);
194
+ lines.push(`**Pass/Fail:** ${passCount} passed, ${failCount} failed`);
195
+ lines.push('');
196
+ lines.push('---');
197
+ lines.push('');
198
+
199
+ // Automated results
200
+ lines.push('## Automated Structural Checks');
201
+ lines.push('');
202
+ if (issues.length === 0) {
203
+ lines.push('All checks passed across all exported skills.');
204
+ } else {
205
+ lines.push('| Skill | File | Issue |');
206
+ lines.push('|---|---|---|');
207
+ for (const issue of issues) {
208
+ lines.push(`| ${issue.skill} | ${issue.file} | ${issue.issue} |`);
209
+ }
210
+ }
211
+ lines.push('');
212
+ lines.push('---');
213
+ lines.push('');
214
+
215
+ // Manual Claude Code smoke tests
216
+ lines.push('## Manual Smoke Tests — Claude Code');
217
+ lines.push('');
218
+ lines.push('Copy each skill into a clean project and invoke it. Verify persona behavior.');
219
+ lines.push('');
220
+ for (const skill of MANUAL_SMOKE_SKILLS) {
221
+ lines.push(`### ${skill.persona} (${skill.name})`);
222
+ lines.push('');
223
+ lines.push(`- [ ] [PENDING] Copied \`${skill.name}/instructions.md\` to \`.claude/skills/${skill.name}/SKILL.md\``);
224
+ lines.push(`- [ ] [PENDING] Invoked skill — ${skill.persona} introduces self as ${skill.role}`);
225
+ lines.push(`- [ ] [PENDING] No references to \`_bmad/\`, \`bmad-init\`, or missing files`);
226
+ lines.push(`- [ ] [PENDING] Skill produces usable output`);
227
+ lines.push('');
228
+ }
229
+ lines.push('---');
230
+ lines.push('');
231
+
232
+ // Manual Catalog walkthrough
233
+ lines.push('## Manual Catalog Walkthrough');
234
+ lines.push('');
235
+ lines.push('Have a teammate unfamiliar with BMAD read the catalog README.');
236
+ lines.push('');
237
+ lines.push('- [ ] [PENDING] Reader can identify what each intent category means');
238
+ lines.push('- [ ] [PENDING] Reader finds a skill matching "I want to brainstorm" within 60 seconds');
239
+ lines.push('- [ ] [PENDING] Tier badges are clear — "Ready to use" vs "Framework only" understood');
240
+ lines.push('- [ ] [PENDING] "How to use a skill" section gives enough context to install');
241
+ lines.push('');
242
+ lines.push('---');
243
+ lines.push('');
244
+ lines.push(`*Generated by validate-exports.js on ${date}.*`);
245
+ lines.push('');
246
+
247
+ return lines.join('\n');
248
+ }
249
+
250
+ // =============================================================================
251
+ // CLI
252
+ // =============================================================================
253
+
254
+ function printHelp() {
255
+ process.stdout.write(
256
+ [
257
+ 'Usage: validate-exports --input <path> [--report <path>]',
258
+ '',
259
+ 'Validate a staging directory produced by seed-catalog-repo.js.',
260
+ '',
261
+ 'Options:',
262
+ ' --input <path> Staging directory to validate (required)',
263
+ ' --report <path> Write VALIDATION-REPORT.md to this path (optional)',
264
+ ' --help, -h Show this help message',
265
+ '',
266
+ 'Exit codes:',
267
+ ' 0 All checks passed',
268
+ ' 1 Validation failures found',
269
+ ' 2 Usage error',
270
+ '',
271
+ ].join('\n')
272
+ );
273
+ }
274
+
275
+ function main() {
276
+ const argv = process.argv.slice(2);
277
+
278
+ if (argv.includes('--help') || argv.includes('-h')) {
279
+ printHelp();
280
+ return 0;
281
+ }
282
+
283
+ let inputDir = null;
284
+ let reportPath = null;
285
+
286
+ for (let i = 0; i < argv.length; i++) {
287
+ if (argv[i] === '--input') {
288
+ const next = argv[i + 1];
289
+ if (!next || next.startsWith('--')) {
290
+ process.stderr.write('Error: --input requires a path argument\n');
291
+ return 2;
292
+ }
293
+ inputDir = argv[++i];
294
+ } else if (argv[i] === '--report') {
295
+ const next = argv[i + 1];
296
+ if (!next || next.startsWith('--')) {
297
+ process.stderr.write('Error: --report requires a path argument\n');
298
+ return 2;
299
+ }
300
+ reportPath = argv[++i];
301
+ } else if (argv[i].startsWith('--')) {
302
+ process.stderr.write(`Unknown flag: ${argv[i]}. Run --help for usage.\n`);
303
+ return 2;
304
+ }
305
+ }
306
+
307
+ if (!inputDir) {
308
+ process.stderr.write('Error: --input <path> is required. Run --help for usage.\n');
309
+ return 2;
310
+ }
311
+
312
+ if (!fs.existsSync(inputDir)) {
313
+ process.stderr.write(`Error: input directory "${inputDir}" does not exist\n`);
314
+ return 2;
315
+ }
316
+ if (!fs.statSync(inputDir).isDirectory()) {
317
+ process.stderr.write(`Error: "${inputDir}" is not a directory\n`);
318
+ return 2;
319
+ }
320
+
321
+ console.log(`Validating exports in ${inputDir}...`);
322
+ const result = validateStagingDir(inputDir);
323
+
324
+ if (result.issues.length === 0) {
325
+ console.log(`All checks passed (${result.skillCount} skills validated)`);
326
+ } else {
327
+ console.log(`${result.issues.length} issue(s) found across ${result.skillCount} skills:`);
328
+ for (const issue of result.issues) {
329
+ console.log(` ${issue.skill}/${issue.file}: ${issue.issue}`);
330
+ }
331
+ }
332
+
333
+ if (reportPath) {
334
+ const report = generateReport(inputDir, result);
335
+ const reportDir = path.dirname(reportPath);
336
+ fs.mkdirSync(reportDir, { recursive: true });
337
+ fs.writeFileSync(reportPath, report);
338
+ console.log(`Validation report written to ${reportPath}`);
339
+ }
340
+
341
+ return result.issues.length === 0 ? 0 : 1;
342
+ }
343
+
344
+ if (require.main === module) {
345
+ process.exit(main());
346
+ }
347
+
348
+ module.exports = { validateStagingDir, generateReport, main };
@@ -198,6 +198,39 @@ const GYRE_AGENT_FILES = GYRE_AGENTS.map(a => `${a.id}.md`);
198
198
  const GYRE_AGENT_IDS = GYRE_AGENTS.map(a => a.id);
199
199
  const GYRE_WORKFLOW_NAMES = GYRE_WORKFLOWS.map(w => w.name);
200
200
 
201
+ // Standalone bme agents that don't fit the Vortex/Gyre team pattern.
202
+ // These agents live in their own submodule (not _vortex or _gyre) and are
203
+ // individually registered. refresh-installation.js and validator.js both
204
+ // consume this list to preserve and validate them.
205
+ //
206
+ // Each entry must include:
207
+ // - id: kebab-case identifier (becomes bmad-agent-bme-{id})
208
+ // - submodule: directory under _bmad/bme/ (e.g., '_team-factory')
209
+ // - name: displayName for manifest
210
+ // - title: persona title
211
+ // - icon: emoji
212
+ // - role: persona role string
213
+ // - identity: persona identity description
214
+ // - communication_style: persona voice
215
+ // - expertise: principles/expertise bullets
216
+ const EXTRA_BME_AGENTS = [
217
+ {
218
+ id: 'team-factory',
219
+ submodule: '_team-factory',
220
+ name: 'Loom Master',
221
+ title: 'Team Factory',
222
+ icon: '🏭',
223
+ persona: {
224
+ role: 'Team Architecture Specialist + BMAD Compliance Expert',
225
+ identity: 'Master team architect who guides framework contributors through creating fully-wired, BMAD-compliant teams. Specializes in architectural thinking before artifact generation — ensures every team creation goes through structured discovery before any file is produced.',
226
+ communication_style: 'Methodical yet encouraging — like a senior architect pair-programming with a colleague. Asks focused questions, explains trade-offs clearly, and celebrates good decisions. Uses concrete examples from Vortex and Gyre to illustrate patterns. Never dumps all decisions at once — progressive disclosure, one step at a time.',
227
+ expertise: "- Thinking before files: every team creation goes through discovery before generation - BMAD compliance is non-negotiable: output must be indistinguishable from native teams - No orphaned artifacts: if a file is created, it must be registered, wired, and discoverable - Delegate to BMB for artifact generation: factory owns integration wiring only - Validate continuously: don't wait until the end to check"
228
+ }
229
+ }
230
+ ];
231
+
232
+ const EXTRA_BME_AGENT_IDS = EXTRA_BME_AGENTS.map(a => a.id);
233
+
201
234
  module.exports = {
202
235
  AGENTS,
203
236
  WORKFLOWS,
@@ -211,4 +244,6 @@ module.exports = {
211
244
  GYRE_AGENT_FILES,
212
245
  GYRE_AGENT_IDS,
213
246
  GYRE_WORKFLOW_NAMES,
247
+ EXTRA_BME_AGENTS,
248
+ EXTRA_BME_AGENT_IDS,
214
249
  };
@@ -2,13 +2,23 @@
2
2
 
3
3
  const fs = require('fs-extra');
4
4
  const yaml = require('js-yaml');
5
+ const YAML = require('yaml'); // Comment-preserving YAML library (ag-7-1: I29). Used by mergeConfig + writeConfig to preserve comments across the merge round-trip.
5
6
  const { AGENT_IDS, WORKFLOW_NAMES } = require('./agent-registry');
7
+ const { assertVersion } = require('./utils');
6
8
 
7
9
  /**
8
10
  * Config Merger for Convoke
9
11
  * Smart YAML merging preserving user settings
12
+ *
13
+ * ag-7-1 (I29): mergeConfig now returns a sentinel-tagged structure that carries
14
+ * the parsed YAML.Document alongside the merged plain-object form. writeConfig
15
+ * detects the sentinel and writes via the Document API (preserving comments) when
16
+ * possible, falling back to js-yaml.dump for backwards compatibility with any
17
+ * caller that passes a bare object.
10
18
  */
11
19
 
20
+ const MERGED_DOC_SENTINEL = Symbol.for('convoke.config-merger.docMerged');
21
+
12
22
  /**
13
23
  * Merge current config with new template while preserving user preferences.
14
24
  * Agents and workflows use smart-merge: canonical entries in registry order
@@ -18,20 +28,32 @@ const { AGENT_IDS, WORKFLOW_NAMES } = require('./agent-registry');
18
28
  * @param {string} currentConfigPath - Path to current config.yaml
19
29
  * @param {string} newVersion - New version to set
20
30
  * @param {object} updates - Updates to apply (agents, workflows, etc.)
21
- * @returns {Promise<object>} Merged config object
31
+ * @returns {Promise<object>} Merged config object (with hidden Document sentinel for comment preservation)
22
32
  */
23
33
  async function mergeConfig(currentConfigPath, newVersion, updates = {}) {
34
+ assertVersion(newVersion, 'config-merger'); // ag-7-1: I30 — fail fast on undefined/null/empty version
35
+
24
36
  let current = {};
37
+ let doc = null; // YAML.Document for comment preservation
25
38
 
26
39
  // Read current config if it exists
27
40
  if (fs.existsSync(currentConfigPath)) {
28
41
  try {
29
42
  const currentContent = fs.readFileSync(currentConfigPath, 'utf8');
30
- const parsed = yaml.load(currentContent);
31
- current = (parsed && typeof parsed === 'object') ? parsed : {};
43
+ doc = YAML.parseDocument(currentContent);
44
+ // Note: YAML.parseDocument does not throw on syntax errors check doc.errors
45
+ if (doc.errors && doc.errors.length > 0) {
46
+ console.warn(`Warning: Could not parse current config.yaml (${doc.errors[0].message}), using defaults`);
47
+ current = {};
48
+ doc = null;
49
+ } else {
50
+ const parsed = doc.toJSON();
51
+ current = (parsed && typeof parsed === 'object') ? parsed : {};
52
+ }
32
53
  } catch (_error) {
33
54
  console.warn('Warning: Could not parse current config.yaml, using defaults');
34
55
  current = {};
56
+ doc = null;
35
57
  }
36
58
  }
37
59
 
@@ -80,6 +102,18 @@ async function mergeConfig(currentConfigPath, newVersion, updates = {}) {
80
102
  merged.migration_history = [];
81
103
  }
82
104
 
105
+ // ag-7-1 (I29): attach the Document for comment-preserving writes.
106
+ // writeConfig detects the sentinel and writes via doc.toString() when set;
107
+ // otherwise it falls back to js-yaml.dump (backwards compat).
108
+ if (doc) {
109
+ Object.defineProperty(merged, MERGED_DOC_SENTINEL, {
110
+ value: doc,
111
+ enumerable: false,
112
+ writable: false,
113
+ configurable: false
114
+ });
115
+ }
116
+
83
117
  return merged;
84
118
  }
85
119
 
@@ -194,21 +228,117 @@ function validateConfig(config) {
194
228
  }
195
229
 
196
230
  /**
197
- * Write config to file
231
+ * Write config to file.
232
+ * If the config object carries the merged-doc sentinel from mergeConfig (ag-7-1),
233
+ * write via the Document API to preserve comments. Otherwise fall back to
234
+ * js-yaml.dump for backwards compatibility.
235
+ *
198
236
  * @param {string} configPath - Path to write config
199
- * @param {object} config - Config object
237
+ * @param {object} config - Config object (optionally carrying a Document sentinel)
200
238
  * @returns {Promise<void>}
201
239
  */
202
240
  async function writeConfig(configPath, config) {
203
- const yamlContent = yaml.dump(config, {
204
- indent: 2,
205
- lineWidth: -1, // Don't wrap long lines
206
- noRefs: true
207
- });
241
+ // ag-7-1 (I29) — Comment preservation paths, in order of preference:
242
+ //
243
+ // 1. SENTINEL PATH: caller went through `mergeConfig` which attached the parsed
244
+ // Document via the MERGED_DOC_SENTINEL symbol. Use it directly. The sentinel
245
+ // contract guarantees `merged` is a complete config (mergeConfig produces a
246
+ // full structure), so it's safe to delete keys from the Document that aren't
247
+ // in `merged` (e.g., a removed user field).
248
+ // 2. SELF-HEAL PATH: caller passed a bare object (e.g., `migration-runner.js`'s
249
+ // `updateMigrationHistory` calling `addMigrationHistory` then `writeConfig`)
250
+ // AND the destination file exists. Re-parse the existing file as a Document
251
+ // so any comments inside it survive the rewrite. CRITICAL: in the self-heal
252
+ // path, we ONLY apply additive/update operations (doc.set for each key the
253
+ // caller knows about) — we do NOT delete keys the caller doesn't mention,
254
+ // because the bare-object caller may not know about every field on disk
255
+ // (e.g., a future caller passing `{ version: '4.0.0' }` to update only the
256
+ // version would otherwise wipe out every other top-level field).
257
+ // 3. FALLBACK PATH: bare object + no existing destination file (fresh install).
258
+ // No comments to preserve. Use js-yaml.dump for backwards compatibility.
259
+ //
260
+ // CONTRACT NOTE: callers should not reuse the same `merged` object across multiple
261
+ // `writeConfig` calls. The Document reference inside the sentinel is mutated on
262
+ // write, so a second call would see an already-mutated Document instead of the
263
+ // originally parsed state. This is fine for current callers (refresh-installation
264
+ // calls writeConfig once per merged result) but document the constraint for
265
+ // future maintainers.
266
+ const sentinelDoc = config[MERGED_DOC_SENTINEL];
267
+ let doc = sentinelDoc;
268
+ const isSentinelPath = !!sentinelDoc;
269
+
270
+ if (!doc && fs.existsSync(configPath)) {
271
+ // Self-heal: re-parse the existing file so its comments survive the rewrite.
272
+ try {
273
+ const existingContent = fs.readFileSync(configPath, 'utf8');
274
+ const reparsed = YAML.parseDocument(existingContent);
275
+ if (!reparsed.errors || reparsed.errors.length === 0) {
276
+ doc = reparsed;
277
+ }
278
+ // If parse fails, fall through to the bare-object path silently —
279
+ // the caller is writing a known-good structure either way.
280
+ } catch (_err) {
281
+ // Silent — fall through to bare-object path.
282
+ }
283
+ }
284
+
285
+ let yamlContent;
286
+ if (doc) {
287
+ // Comment-preserving path: sync the merged structure into the Document via per-field
288
+ // doc.set() calls. Replacing doc.contents wholesale would blow away comments attached
289
+ // to the top-level mapping (e.g., header comments above the first key).
290
+ // Per-field updates preserve all comment metadata anchored to the document or to fields.
291
+ const merged = stripSentinel(config);
292
+
293
+ // Update existing fields and add new ones
294
+ for (const key of Object.keys(merged)) {
295
+ doc.set(key, merged[key]);
296
+ }
297
+
298
+ // Remove keys that were in the original doc but are no longer in the merged structure.
299
+ // ONLY do this on the SENTINEL path — `mergeConfig` produces a complete config so any
300
+ // missing key was intentionally removed. On the SELF-HEAL path the caller is a legacy
301
+ // bare-object caller that may not know about every on-disk field, so deleting unknown
302
+ // keys would silently destroy user data.
303
+ if (isSentinelPath && doc.contents && typeof doc.contents.items !== 'undefined') {
304
+ const mergedKeys = new Set(Object.keys(merged));
305
+ const docKeys = doc.contents.items.map(item => String(item.key.value));
306
+ for (const docKey of docKeys) {
307
+ if (!mergedKeys.has(docKey)) {
308
+ doc.delete(docKey);
309
+ }
310
+ }
311
+ }
312
+
313
+ yamlContent = doc.toString({ lineWidth: 0 });
314
+ } else {
315
+ // Backwards-compat path: bare object, no existing file, no comments to preserve.
316
+ yamlContent = yaml.dump(config, {
317
+ indent: 2,
318
+ lineWidth: -1, // Don't wrap long lines
319
+ noRefs: true
320
+ });
321
+ }
208
322
 
209
323
  await fs.writeFile(configPath, yamlContent, 'utf8');
210
324
  }
211
325
 
326
+ /**
327
+ * Return a plain-object copy of config with the sentinel symbol stripped,
328
+ * so doc.createNode doesn't try to serialize it.
329
+ * @param {object} config
330
+ * @returns {object}
331
+ */
332
+ function stripSentinel(config) {
333
+ // Symbol-keyed properties are not enumerable in our case (defineProperty above),
334
+ // but as a safety net we explicitly clone only string-keyed enumerable fields.
335
+ const plain = {};
336
+ for (const key of Object.keys(config)) {
337
+ plain[key] = config[key];
338
+ }
339
+ return plain;
340
+ }
341
+
212
342
  /**
213
343
  * Add migration history entry
214
344
  * @param {object} config - Config object
@@ -466,7 +466,7 @@ async function runRefreshOnly(fromVersion, options = {}) {
466
466
  try {
467
467
  await backupManager.restoreBackup(backupMetadata, projectRoot);
468
468
  console.log(chalk.green('✓ Installation restored from backup'));
469
- } catch (restoreError) {
469
+ } catch (_restoreError) {
470
470
  console.error(chalk.red('✗ Restore failed!'));
471
471
  console.error(chalk.yellow(`Manual restore may be needed from: ${backupMetadata.backup_dir}`));
472
472
  }