convoke-agents 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +37 -10
  3. package/_bmad/bme/_artifacts/config.yaml +15 -0
  4. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
  5. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
  6. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
  7. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
  8. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
  9. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
  10. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
  11. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
  12. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
  13. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
  14. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
  15. package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
  16. package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
  17. package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
  18. package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
  19. package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
  20. package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
  21. package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
  22. package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
  23. package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
  24. package/_bmad/bme/_team-factory/config.yaml +13 -0
  25. package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
  26. package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
  27. package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
  28. package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
  29. package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
  30. package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
  31. package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
  32. package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
  33. package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
  34. package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
  35. package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
  36. package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
  37. package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
  38. package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
  39. package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
  40. package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
  41. package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
  42. package/_bmad/bme/_team-factory/module-help.csv +3 -0
  43. package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
  44. package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
  45. package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
  46. package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
  47. package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
  48. package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
  49. package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
  50. package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
  51. package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
  52. package/_bmad/bme/_vortex/config.yaml +4 -4
  53. package/package.json +13 -7
  54. package/scripts/convoke-doctor.js +172 -1
  55. package/scripts/install-gyre-agents.js +0 -0
  56. package/scripts/lib/artifact-utils.js +521 -13
  57. package/scripts/lib/portfolio/portfolio-engine.js +301 -34
  58. package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
  59. package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
  60. package/scripts/migrate-artifacts.js +69 -10
  61. package/scripts/portability/catalog-generator.js +353 -0
  62. package/scripts/portability/classify-skills.js +646 -0
  63. package/scripts/portability/convoke-export.js +522 -0
  64. package/scripts/portability/export-engine.js +1156 -0
  65. package/scripts/portability/generate-adapters.js +79 -0
  66. package/scripts/portability/manifest-csv.js +147 -0
  67. package/scripts/portability/seed-catalog-repo.js +427 -0
  68. package/scripts/portability/templates/canonical-example.md +102 -0
  69. package/scripts/portability/templates/canonical-format.md +218 -0
  70. package/scripts/portability/templates/readme-template.md +72 -0
  71. package/scripts/portability/test-constants.js +42 -0
  72. package/scripts/portability/validate-classification.js +529 -0
  73. package/scripts/portability/validate-exports.js +348 -0
  74. package/scripts/update/lib/agent-registry.js +35 -0
  75. package/scripts/update/lib/config-merger.js +140 -10
  76. package/scripts/update/lib/refresh-installation.js +293 -8
  77. package/scripts/update/lib/utils.js +27 -1
  78. package/scripts/update/lib/validator.js +114 -4
@@ -0,0 +1,79 @@
1
+ /**
2
+ * generate-adapters.js — Story sp-5-2
3
+ *
4
+ * Generates per-platform adapter files for exported skills.
5
+ * Each adapter wraps the canonical instructions.md with platform-specific
6
+ * metadata/formatting. Thin adapter principle: adapters add packaging,
7
+ * NOT content modifications.
8
+ *
9
+ * This is a module (no CLI entry point). Called from convoke-export.js's
10
+ * runSingle() after writing instructions.md and README.md.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /**
19
+ * Truncate description to first sentence (period + space) or 120 chars.
20
+ * Same logic as catalog-generator.js's truncateDescription.
21
+ */
22
+ function truncateDescription(desc) {
23
+ if (!desc) return '';
24
+ const periodIdx = desc.indexOf('. ');
25
+ if (periodIdx > 0 && periodIdx < 120) {
26
+ return desc.slice(0, periodIdx + 1);
27
+ }
28
+ if (desc.length <= 120) return desc;
29
+ return desc.slice(0, 120) + '...';
30
+ }
31
+
32
+ /**
33
+ * Generate all platform adapters for a single exported skill.
34
+ *
35
+ * @param {string} skillName - manifest skill name (e.g., 'bmad-brainstorming')
36
+ * @param {object} skillRow - manifest row as keyed object (.name, .description, etc.)
37
+ * @param {string} instructionsContent - the canonical instructions.md content
38
+ * @param {string} skillOutputDir - path to the skill's export directory (e.g., <output>/bmad-brainstorming/)
39
+ */
40
+ function generateAdapters(skillName, skillRow, instructionsContent, skillOutputDir) {
41
+ const adaptersDir = path.join(skillOutputDir, 'adapters');
42
+
43
+ // Claude Code adapter
44
+ const claudeDir = path.join(adaptersDir, 'claude-code');
45
+ fs.mkdirSync(claudeDir, { recursive: true });
46
+ const description = truncateDescription(skillRow.description || '');
47
+ // Escape single quotes in description for YAML safety
48
+ const safeDesc = description.replace(/'/g, "''");
49
+ const claudeContent = [
50
+ '---',
51
+ `name: ${skillName}`,
52
+ `description: '${safeDesc}'`,
53
+ '---',
54
+ '',
55
+ instructionsContent,
56
+ ].join('\n');
57
+ fs.writeFileSync(path.join(claudeDir, 'SKILL.md'), claudeContent);
58
+
59
+ // GitHub Copilot adapter
60
+ const copilotDir = path.join(adaptersDir, 'copilot');
61
+ fs.mkdirSync(copilotDir, { recursive: true });
62
+ const displayName = (skillRow.name || skillName)
63
+ .replace(/^bmad-cis-agent-/, '')
64
+ .replace(/^bmad-agent-/, '')
65
+ .replace(/^bmad-cis-/, '')
66
+ .replace(/^bmad-/, '')
67
+ .split('-')
68
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
69
+ .join(' ');
70
+ const copilotContent = `<!-- Skill: ${displayName} — append to .github/copilot-instructions.md -->\n${instructionsContent}`;
71
+ fs.writeFileSync(path.join(copilotDir, 'copilot-instructions.md'), copilotContent);
72
+
73
+ // Cursor adapter
74
+ const cursorDir = path.join(adaptersDir, 'cursor');
75
+ fs.mkdirSync(cursorDir, { recursive: true });
76
+ fs.writeFileSync(path.join(cursorDir, `${skillName}.md`), instructionsContent);
77
+ }
78
+
79
+ module.exports = { generateAdapters };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * RFC-4180-aware CSV parser for skill-manifest.csv and friends.
3
+ *
4
+ * This is the manifest *parser* used by tests and the classification script.
5
+ * It is intentionally separate from `scripts/lib/csv-utils.js` (Team Factory's
6
+ * CSV writer utility) — different concerns, different consumers.
7
+ *
8
+ * Handles:
9
+ * - Quoted fields containing commas
10
+ * - Escaped quotes (`""` inside a quoted field → literal `"`)
11
+ * - Unquoted fields
12
+ * - Trailing CR (CRLF line endings)
13
+ * - UTF-8 BOM stripping
14
+ * - Whitespace-only line filtering
15
+ *
16
+ * Story: sp-1-1 (introduced parser inline in test), sp-1-2 (extracted to shared module).
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+
23
+ /**
24
+ * Parse a single CSV row into an array of fields.
25
+ *
26
+ * Fields are trimmed of leading/trailing whitespace AFTER quote handling.
27
+ * This protects idempotency: hand-edited CSVs commonly accumulate stray
28
+ * spaces around values, and untrimmed comparisons would mark every such
29
+ * row as a manual-override conflict on every classifier run.
30
+ *
31
+ * @param {string} line
32
+ * @returns {string[]}
33
+ */
34
+ function parseCsvRow(line) {
35
+ // Strip trailing CR for CRLF tolerance
36
+ if (line.endsWith('\r')) line = line.slice(0, -1);
37
+
38
+ const fields = [];
39
+ let current = '';
40
+ let inQuotes = false;
41
+ for (let i = 0; i < line.length; i++) {
42
+ const ch = line[i];
43
+ if (ch === '"') {
44
+ // RFC-4180: doubled quote inside a quoted field is a literal "
45
+ if (inQuotes && line[i + 1] === '"') {
46
+ current += '"';
47
+ i++;
48
+ } else {
49
+ inQuotes = !inQuotes;
50
+ }
51
+ } else if (ch === ',' && !inQuotes) {
52
+ fields.push(current);
53
+ current = '';
54
+ } else {
55
+ current += ch;
56
+ }
57
+ }
58
+ fields.push(current);
59
+ // P2 (sp-1-2 review): trim whitespace from every field. Fields that were
60
+ // originally quoted are unaffected by this since the quotes are already
61
+ // stripped during parse and any internal padding is preserved unless it
62
+ // was actually outside the quotes.
63
+ return fields.map((f) => f.trim());
64
+ }
65
+
66
+ /**
67
+ * Count the number of CSV columns in a row (RFC-4180-aware).
68
+ *
69
+ * @param {string} line
70
+ * @returns {number}
71
+ */
72
+ function countCsvColumns(line) {
73
+ return parseCsvRow(line).length;
74
+ }
75
+
76
+ /**
77
+ * Format a single field for CSV output. Quotes the field if it contains
78
+ * a comma, double-quote, or newline. Escapes embedded quotes by doubling.
79
+ *
80
+ * @param {string} field
81
+ * @returns {string}
82
+ */
83
+ function formatCsvField(field) {
84
+ if (field == null) return '';
85
+ const s = String(field);
86
+ if (s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r')) {
87
+ return '"' + s.replace(/"/g, '""') + '"';
88
+ }
89
+ return s;
90
+ }
91
+
92
+ /**
93
+ * Format an array of fields as a CSV row (no trailing newline).
94
+ *
95
+ * @param {string[]} fields
96
+ * @returns {string}
97
+ */
98
+ function formatCsvRow(fields) {
99
+ return fields.map(formatCsvField).join(',');
100
+ }
101
+
102
+ /**
103
+ * Read a CSV file from disk and return its parsed contents.
104
+ * Strips UTF-8 BOM and ignores blank/whitespace-only lines.
105
+ *
106
+ * @param {string} filePath
107
+ * @returns {{ header: string[], rows: string[][], rawLines: string[] }}
108
+ * - `header`: array of column names
109
+ * - `rows`: array of data rows (each an array of fields)
110
+ * - `rawLines`: original (post-BOM-strip, post-filter) line strings — useful
111
+ * when callers need to preserve exact whitespace/quoting on rows they
112
+ * don't intend to modify
113
+ */
114
+ function readManifest(filePath) {
115
+ let content = fs.readFileSync(filePath, 'utf8');
116
+ // Strip UTF-8 BOM if present
117
+ if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
118
+
119
+ const rawLines = content.split('\n').filter((l) => /\S/.test(l));
120
+ if (rawLines.length === 0) {
121
+ return { header: [], rows: [], rawLines: [] };
122
+ }
123
+ const header = parseCsvRow(rawLines[0]);
124
+ const rows = rawLines.slice(1).map(parseCsvRow);
125
+ return { header, rows, rawLines };
126
+ }
127
+
128
+ /**
129
+ * Write a manifest back to disk. Joins rows with `\n` and adds a trailing newline.
130
+ *
131
+ * @param {string} filePath
132
+ * @param {string[]} header
133
+ * @param {string[][]} rows
134
+ */
135
+ function writeManifest(filePath, header, rows) {
136
+ const lines = [formatCsvRow(header), ...rows.map(formatCsvRow)];
137
+ fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf8');
138
+ }
139
+
140
+ module.exports = {
141
+ parseCsvRow,
142
+ countCsvColumns,
143
+ formatCsvField,
144
+ formatCsvRow,
145
+ readManifest,
146
+ writeManifest,
147
+ };
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * seed-catalog-repo.js — Story sp-4-1
4
+ *
5
+ * Generates the complete convoke-skills-catalog repo content into a staging
6
+ * directory. Orchestrates the export engine, catalog generator, and README
7
+ * builder into a single pipeline with built-in self-verification.
8
+ *
9
+ * Usage:
10
+ * node scripts/portability/seed-catalog-repo.js --output <path>
11
+ * node scripts/portability/seed-catalog-repo.js --help
12
+ *
13
+ * The script does NOT create a git repo or interact with GitHub.
14
+ * It only produces a directory tree. The user handles git init + push.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { findProjectRoot } = require('../update/lib/utils');
22
+ const { readManifest } = require('./manifest-csv');
23
+ const { exportSkill, loadSkillRow } = require('./export-engine');
24
+ const { generateCatalog } = require('./catalog-generator');
25
+ const { buildReadme } = require('./convoke-export');
26
+ const { generateAdapters } = require('./generate-adapters');
27
+
28
+ // =============================================================================
29
+ // CONSTANTS
30
+ // =============================================================================
31
+
32
+ const { FORBIDDEN_STRINGS } = require('./test-constants');
33
+
34
+ const MIT_LICENSE = `MIT License
35
+
36
+ Copyright (c) 2026 Convoke Contributors
37
+
38
+ Permission is hereby granted, free of charge, to any person obtaining a copy
39
+ of this software and associated documentation files (the "Software"), to deal
40
+ in the Software without restriction, including without limitation the rights
41
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
42
+ copies of the Software, and to permit persons to whom the Software is
43
+ furnished to do so, subject to the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be included in all
46
+ copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
51
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
52
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
53
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
54
+ SOFTWARE.
55
+ `;
56
+
57
+ const CONTRIBUTING_MD = `# Contributing
58
+
59
+ The skills in this repository are **auto-generated** from the main [Convoke Agents](https://github.com/amalik/convoke-agents) repository. They are regenerated on each release.
60
+
61
+ ## Important
62
+
63
+ - **Do not edit skill files directly.** Your changes will be overwritten on the next regeneration.
64
+ - To improve a skill's content, contribute to the source skill in the main Convoke repo.
65
+ - To request a new skill or report an issue, [open an issue](https://github.com/amalik/convoke-agents/issues) on the main repo.
66
+
67
+ ## How skills are generated
68
+
69
+ Each skill is exported from the Convoke framework using the \`convoke-export\` tool, which:
70
+
71
+ 1. Reads the skill's source files (agent definition, workflow, step files)
72
+ 2. Transforms them into an LLM-agnostic \`instructions.md\` format
73
+ 3. Generates a per-skill \`README.md\` with install instructions for Claude Code, GitHub Copilot, and Cursor
74
+ 4. Produces this catalog \`README.md\` organized by user intent
75
+
76
+ ## License
77
+
78
+ This repository is licensed under the MIT License. See [LICENSE](LICENSE) for details.
79
+ `;
80
+
81
+ // =============================================================================
82
+ // GENERATION PIPELINE
83
+ // =============================================================================
84
+
85
+ function generate(outputDir, projectRoot) {
86
+ const manifestPath = path.join(projectRoot, '_bmad', '_config', 'skill-manifest.csv');
87
+ const { header, rows } = readManifest(manifestPath);
88
+ const nameIdx = header.indexOf('name');
89
+ const tierIdx = header.indexOf('tier');
90
+
91
+ // Get unique exportable skill names (standalone + light-deps)
92
+ const seen = new Set();
93
+ const exportableNames = [];
94
+ for (const row of rows) {
95
+ const name = row[nameIdx];
96
+ if (seen.has(name)) continue;
97
+ seen.add(name);
98
+ if (row[tierIdx] === 'standalone' || row[tierIdx] === 'light-deps') {
99
+ exportableNames.push(name);
100
+ }
101
+ }
102
+ exportableNames.sort();
103
+
104
+ console.log(`Exporting ${exportableNames.length} skills (standalone + light-deps)...`);
105
+
106
+ // Create output directory
107
+ fs.mkdirSync(outputDir, { recursive: true });
108
+
109
+ // Export each skill
110
+ let exportedCount = 0;
111
+ const failures = [];
112
+ for (const skillName of exportableNames) {
113
+ try {
114
+ const result = exportSkill(skillName, projectRoot);
115
+ const skillRow = loadSkillRow(skillName, projectRoot);
116
+ const readme = buildReadme(skillRow, result, projectRoot);
117
+
118
+ const skillDir = path.join(outputDir, skillName);
119
+ fs.mkdirSync(skillDir, { recursive: true });
120
+ fs.writeFileSync(path.join(skillDir, 'instructions.md'), result.instructions);
121
+ fs.writeFileSync(path.join(skillDir, 'README.md'), readme);
122
+ generateAdapters(skillName, skillRow, result.instructions, skillDir);
123
+ exportedCount++;
124
+ } catch (err) {
125
+ failures.push({ skill: skillName, error: err.message.split('\n')[0] });
126
+ }
127
+ }
128
+
129
+ if (failures.length > 0) {
130
+ console.error(`${failures.length} skill(s) failed to export:`);
131
+ for (const f of failures) {
132
+ console.error(` ${f.skill}: ${f.error}`);
133
+ }
134
+ throw new Error(`${failures.length} skill export(s) failed`);
135
+ }
136
+
137
+ console.log(`Exported ${exportedCount} skills successfully.`);
138
+
139
+ // Generate catalog README
140
+ console.log('Generating catalog README...');
141
+ const catalogReadme = generateCatalog(projectRoot);
142
+ fs.writeFileSync(path.join(outputDir, 'README.md'), catalogReadme);
143
+
144
+ // Write LICENSE
145
+ fs.writeFileSync(path.join(outputDir, 'LICENSE'), MIT_LICENSE);
146
+
147
+ // Write CONTRIBUTING.md
148
+ fs.writeFileSync(path.join(outputDir, 'CONTRIBUTING.md'), CONTRIBUTING_MD);
149
+
150
+ console.log('Generation complete.');
151
+ return { skillCount: exportedCount, expectedCount: exportableNames.length };
152
+ }
153
+
154
+ // =============================================================================
155
+ // SELF-VERIFICATION
156
+ // =============================================================================
157
+
158
+ function verify(outputDir, expectedSkillCount) {
159
+ const failures = [];
160
+
161
+ // 1. Directory count
162
+ const entries = fs.readdirSync(outputDir, { withFileTypes: true });
163
+ const skillDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
164
+ if (skillDirs.length !== expectedSkillCount) {
165
+ failures.push(`Directory count: expected ${expectedSkillCount}, got ${skillDirs.length}`);
166
+ }
167
+
168
+ // 2. Every skill dir has both files
169
+ for (const dir of skillDirs) {
170
+ const instrPath = path.join(outputDir, dir, 'instructions.md');
171
+ const readmePath = path.join(outputDir, dir, 'README.md');
172
+ if (!fs.existsSync(instrPath)) {
173
+ failures.push(`${dir}: missing instructions.md`);
174
+ }
175
+ if (!fs.existsSync(readmePath)) {
176
+ failures.push(`${dir}: missing README.md`);
177
+ }
178
+ }
179
+
180
+ // 3. Zero forbidden strings in all instructions.md
181
+ for (const dir of skillDirs) {
182
+ const instrPath = path.join(outputDir, dir, 'instructions.md');
183
+ if (!fs.existsSync(instrPath)) continue;
184
+ const content = fs.readFileSync(instrPath, 'utf8');
185
+ for (const forbidden of FORBIDDEN_STRINGS) {
186
+ if (content.includes(forbidden)) {
187
+ failures.push(`${dir}/instructions.md: contains "${forbidden}"`);
188
+ }
189
+ }
190
+ }
191
+
192
+ // 4. README validity (How to use, 3 platforms, under 80 lines)
193
+ for (const dir of skillDirs) {
194
+ const readmePath = path.join(outputDir, dir, 'README.md');
195
+ if (!fs.existsSync(readmePath)) continue;
196
+ const content = fs.readFileSync(readmePath, 'utf8');
197
+ if (!content.includes('How to use')) {
198
+ failures.push(`${dir}/README.md: missing "How to use"`);
199
+ }
200
+ if (!content.includes('Claude Code')) {
201
+ failures.push(`${dir}/README.md: missing Claude Code section`);
202
+ }
203
+ if (!content.includes('Copilot')) {
204
+ failures.push(`${dir}/README.md: missing Copilot section`);
205
+ }
206
+ if (!content.includes('Cursor')) {
207
+ failures.push(`${dir}/README.md: missing Cursor section`);
208
+ }
209
+ const lineCount = content.split('\n').length;
210
+ if (lineCount > 80) {
211
+ failures.push(`${dir}/README.md: ${lineCount} lines (exceeds 80)`);
212
+ }
213
+ }
214
+
215
+ // 4b. Adapters exist for each skill
216
+ for (const dir of skillDirs) {
217
+ const adaptersBase = path.join(outputDir, dir, 'adapters');
218
+ if (!fs.existsSync(path.join(adaptersBase, 'claude-code', 'SKILL.md'))) {
219
+ failures.push(`${dir}: missing adapters/claude-code/SKILL.md`);
220
+ }
221
+ if (!fs.existsSync(path.join(adaptersBase, 'copilot', 'copilot-instructions.md'))) {
222
+ failures.push(`${dir}: missing adapters/copilot/copilot-instructions.md`);
223
+ }
224
+ if (!fs.existsSync(path.join(adaptersBase, 'cursor', `${dir}.md`))) {
225
+ failures.push(`${dir}: missing adapters/cursor/${dir}.md`);
226
+ }
227
+ }
228
+
229
+ // 5. Zero _bmad/ or .claude/hooks in ANY file
230
+ const allFiles = [];
231
+ function collectMdFiles(dir) {
232
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
233
+ const full = path.join(dir, entry.name);
234
+ if (entry.isDirectory()) {
235
+ collectMdFiles(full);
236
+ } else if (entry.name.endsWith('.md')) {
237
+ allFiles.push(full);
238
+ }
239
+ }
240
+ }
241
+ collectMdFiles(outputDir);
242
+ for (const filePath of allFiles) {
243
+ const content = fs.readFileSync(filePath, 'utf8');
244
+ const rel = path.relative(outputDir, filePath);
245
+ if (content.includes('_bmad/')) {
246
+ failures.push(`${rel}: contains "_bmad/"`);
247
+ }
248
+ if (content.includes('.claude/hooks')) {
249
+ failures.push(`${rel}: contains ".claude/hooks"`);
250
+ }
251
+ }
252
+
253
+ // 6. Root README
254
+ const rootReadme = path.join(outputDir, 'README.md');
255
+ if (!fs.existsSync(rootReadme)) {
256
+ failures.push('Root README.md missing');
257
+ } else {
258
+ const content = fs.readFileSync(rootReadme, 'utf8');
259
+ if (!content.includes('# Convoke Skills Catalog')) {
260
+ failures.push('Root README.md: missing "# Convoke Skills Catalog" heading');
261
+ }
262
+ }
263
+
264
+ // 7. LICENSE
265
+ const licensePath = path.join(outputDir, 'LICENSE');
266
+ if (!fs.existsSync(licensePath)) {
267
+ failures.push('LICENSE missing');
268
+ } else {
269
+ const content = fs.readFileSync(licensePath, 'utf8');
270
+ if (!content.includes('MIT')) {
271
+ failures.push('LICENSE: does not contain "MIT"');
272
+ }
273
+ }
274
+
275
+ // 8. CONTRIBUTING.md
276
+ const contribPath = path.join(outputDir, 'CONTRIBUTING.md');
277
+ if (!fs.existsSync(contribPath)) {
278
+ failures.push('CONTRIBUTING.md missing');
279
+ } else {
280
+ const content = fs.readFileSync(contribPath, 'utf8');
281
+ if (!content.includes('auto-generated')) {
282
+ failures.push('CONTRIBUTING.md: does not contain "auto-generated"');
283
+ }
284
+ }
285
+
286
+ return failures;
287
+ }
288
+
289
+ // =============================================================================
290
+ // CLI
291
+ // =============================================================================
292
+
293
+ function printHelp() {
294
+ process.stdout.write(
295
+ [
296
+ 'Usage: seed-catalog-repo --output <path>',
297
+ '',
298
+ 'Generate the complete convoke-skills-catalog repo content into a staging directory.',
299
+ 'Does NOT create a git repo or interact with GitHub.',
300
+ '',
301
+ 'Options:',
302
+ ' --output <path> Staging directory path (required)',
303
+ ' --help, -h Show this help message',
304
+ '',
305
+ 'Exit codes:',
306
+ ' 0 Success (all skills exported, verification passed)',
307
+ ' 1 Usage error',
308
+ ' 2 Generation failure',
309
+ ' 3 Verification failure',
310
+ '',
311
+ 'After the staging directory is generated, you can create the repo manually:',
312
+ '',
313
+ ' cd <staging-dir>',
314
+ ' git init',
315
+ ' git add -A',
316
+ ' git commit -m "Initial catalog seed"',
317
+ ' gh repo create convoke-skills-catalog --public --source=. --push',
318
+ '',
319
+ ].join('\n')
320
+ );
321
+ }
322
+
323
+ function main() {
324
+ const argv = process.argv.slice(2);
325
+
326
+ if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
327
+ printHelp();
328
+ return argv.length === 0 ? 1 : 0;
329
+ }
330
+
331
+ let outputDir = null;
332
+ for (let i = 0; i < argv.length; i++) {
333
+ if (argv[i] === '--output') {
334
+ const next = argv[i + 1];
335
+ if (!next || next.startsWith('--')) {
336
+ process.stderr.write('Error: --output requires a path argument\n');
337
+ return 1;
338
+ }
339
+ outputDir = argv[++i];
340
+ } else if (argv[i].startsWith('--')) {
341
+ process.stderr.write(`Unknown flag: ${argv[i]}. Run --help for usage.\n`);
342
+ return 1;
343
+ }
344
+ }
345
+
346
+ if (!outputDir) {
347
+ process.stderr.write('Error: --output <path> is required. Run --help for usage.\n');
348
+ return 1;
349
+ }
350
+
351
+ // Resolve output path
352
+ outputDir = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
353
+
354
+ let projectRoot;
355
+ try {
356
+ projectRoot = findProjectRoot();
357
+ } catch (e) {
358
+ process.stderr.write(`Error: could not find project root — ${e.message}\n`);
359
+ return 2;
360
+ }
361
+
362
+ // Safety: refuse to write into a non-empty pre-existing directory.
363
+ // Prevents destructive cleanup from deleting user content on failure.
364
+ if (fs.existsSync(outputDir)) {
365
+ const existing = fs.readdirSync(outputDir);
366
+ if (existing.length > 0) {
367
+ process.stderr.write(`Error: output directory "${outputDir}" already exists and is non-empty.\nUse an empty or non-existent path.\n`);
368
+ return 1;
369
+ }
370
+ }
371
+ const dirCreatedByUs = !fs.existsSync(outputDir);
372
+
373
+ // Generate
374
+ let genResult;
375
+ try {
376
+ genResult = generate(outputDir, projectRoot);
377
+ } catch (e) {
378
+ process.stderr.write(`Generation failed: ${e.message}\n`);
379
+ // Only clean up if WE created the directory
380
+ if (dirCreatedByUs && fs.existsSync(outputDir)) {
381
+ fs.rmSync(outputDir, { recursive: true, force: true });
382
+ }
383
+ return 2;
384
+ }
385
+
386
+ // Verify
387
+ let failures;
388
+ try {
389
+ console.log('Running verification...');
390
+ failures = verify(outputDir, genResult.expectedCount);
391
+ } catch (e) {
392
+ process.stderr.write(`Verification crashed: ${e.message}\n`);
393
+ if (dirCreatedByUs && fs.existsSync(outputDir)) {
394
+ fs.rmSync(outputDir, { recursive: true, force: true });
395
+ }
396
+ return 3;
397
+ }
398
+
399
+ if (failures.length > 0) {
400
+ process.stderr.write(`Verification failed with ${failures.length} issue(s):\n`);
401
+ for (const f of failures) {
402
+ process.stderr.write(` - ${f}\n`);
403
+ }
404
+ if (dirCreatedByUs) {
405
+ fs.rmSync(outputDir, { recursive: true, force: true });
406
+ }
407
+ return 3;
408
+ }
409
+
410
+ console.log(`\nCatalog staging complete!`);
411
+ console.log(` Skills: ${genResult.skillCount}`);
412
+ console.log(` Directory: ${outputDir}`);
413
+ console.log(` Files: ${genResult.skillCount * 2 + 3} (${genResult.skillCount} x instructions.md + README.md + catalog + LICENSE + CONTRIBUTING.md)`);
414
+ console.log(` Verification: PASSED (zero violations)`);
415
+ console.log(`\nNext steps:`);
416
+ console.log(` cd ${outputDir}`);
417
+ console.log(` git init && git add -A && git commit -m "Initial catalog seed"`);
418
+ console.log(` gh repo create convoke-skills-catalog --public --source=. --push`);
419
+
420
+ return 0;
421
+ }
422
+
423
+ if (require.main === module) {
424
+ process.exit(main());
425
+ }
426
+
427
+ module.exports = { generate, verify, main };