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.
- package/CHANGELOG.md +60 -0
- package/README.md +14 -13
- package/_bmad/bme/_artifacts/config.yaml +15 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
- package/_bmad/bme/_gyre/guides/GYRE-TEAM-GUIDE.md +506 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
- package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
- package/_bmad/bme/_team-factory/config.yaml +13 -0
- package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
- package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
- package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
- package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
- package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
- package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
- package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
- package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
- package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
- package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
- package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
- package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
- package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
- package/_bmad/bme/_team-factory/module-help.csv +3 -0
- package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
- package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
- package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
- package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
- package/_bmad/bme/_vortex/config.yaml +4 -4
- package/_bmad/bme/_vortex/guides/VORTEX-TEAM-GUIDE.md +441 -0
- package/package.json +17 -8
- package/scripts/archive.js +26 -45
- package/scripts/convoke-check.js +88 -0
- package/scripts/convoke-doctor.js +303 -4
- package/scripts/install-gyre-agents.js +0 -0
- package/scripts/lib/artifact-utils.js +2182 -0
- package/scripts/lib/portfolio/formatters/markdown-formatter.js +40 -0
- package/scripts/lib/portfolio/formatters/terminal-formatter.js +56 -0
- package/scripts/lib/portfolio/portfolio-engine.js +572 -0
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +156 -0
- package/scripts/lib/portfolio/rules/conflict-resolver.js +99 -0
- package/scripts/lib/portfolio/rules/frontmatter-rule.js +42 -0
- package/scripts/lib/portfolio/rules/git-recency-rule.js +69 -0
- package/scripts/lib/types.js +122 -0
- package/scripts/migrate-artifacts.js +439 -0
- package/scripts/portability/catalog-generator.js +353 -0
- package/scripts/portability/classify-skills.js +646 -0
- package/scripts/portability/convoke-export.js +522 -0
- package/scripts/portability/export-engine.js +1133 -0
- package/scripts/portability/generate-adapters.js +79 -0
- package/scripts/portability/manifest-csv.js +147 -0
- package/scripts/portability/seed-catalog-repo.js +427 -0
- package/scripts/portability/templates/canonical-example.md +102 -0
- package/scripts/portability/templates/canonical-format.md +218 -0
- package/scripts/portability/templates/readme-template.md +72 -0
- package/scripts/portability/test-constants.js +42 -0
- package/scripts/portability/validate-classification.js +529 -0
- package/scripts/portability/validate-exports.js +348 -0
- package/scripts/update/lib/agent-registry.js +35 -0
- package/scripts/update/lib/config-merger.js +140 -10
- package/scripts/update/lib/migration-runner.js +1 -1
- package/scripts/update/lib/refresh-installation.js +293 -8
- package/scripts/update/lib/taxonomy-merger.js +138 -0
- package/scripts/update/lib/utils.js +27 -1
- package/scripts/update/lib/validator.js +114 -4
- package/scripts/update/migrations/2.0.x-to-3.1.0.js +50 -0
- package/scripts/update/migrations/3.0.x-to-3.1.0.js +41 -0
- 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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 (
|
|
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
|
}
|