convoke-agents 2.4.0 → 3.0.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 (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/INSTALLATION.md +109 -86
  3. package/README.md +220 -163
  4. package/UPDATE-GUIDE.md +63 -23
  5. package/_bmad/bme/_gyre/README.md +100 -0
  6. package/_bmad/bme/_gyre/agents/.gitkeep +0 -0
  7. package/_bmad/bme/_gyre/agents/model-curator.md +128 -0
  8. package/_bmad/bme/_gyre/agents/readiness-analyst.md +127 -0
  9. package/_bmad/bme/_gyre/agents/review-coach.md +130 -0
  10. package/_bmad/bme/_gyre/agents/stack-detective.md +125 -0
  11. package/_bmad/bme/_gyre/compass-routing-reference.md +168 -0
  12. package/_bmad/bme/_gyre/config.yaml +22 -0
  13. package/_bmad/bme/_gyre/contracts/.gitkeep +0 -0
  14. package/_bmad/bme/_gyre/contracts/gc1-stack-profile.md +152 -0
  15. package/_bmad/bme/_gyre/contracts/gc2-capabilities-manifest.md +189 -0
  16. package/_bmad/bme/_gyre/contracts/gc3-findings-report.md +197 -0
  17. package/_bmad/bme/_gyre/contracts/gc4-feedback-loop.md +209 -0
  18. package/_bmad/bme/_gyre/guides/ATLAS-USER-GUIDE.md +177 -0
  19. package/_bmad/bme/_gyre/guides/COACH-USER-GUIDE.md +172 -0
  20. package/_bmad/bme/_gyre/guides/LENS-USER-GUIDE.md +181 -0
  21. package/_bmad/bme/_gyre/guides/SCOUT-USER-GUIDE.md +158 -0
  22. package/_bmad/bme/_gyre/workflows/.gitkeep +0 -0
  23. package/_bmad/bme/_gyre/workflows/accuracy-validation/steps/step-01-select-repos.md +55 -0
  24. package/_bmad/bme/_gyre/workflows/accuracy-validation/steps/step-02-run-validation.md +78 -0
  25. package/_bmad/bme/_gyre/workflows/accuracy-validation/steps/step-03-score-results.md +143 -0
  26. package/_bmad/bme/_gyre/workflows/accuracy-validation/workflow.md +41 -0
  27. package/_bmad/bme/_gyre/workflows/delta-report/steps/step-01-load-history.md +63 -0
  28. package/_bmad/bme/_gyre/workflows/delta-report/steps/step-02-compute-delta.md +72 -0
  29. package/_bmad/bme/_gyre/workflows/delta-report/steps/step-03-present-delta.md +143 -0
  30. package/_bmad/bme/_gyre/workflows/delta-report/workflow.md +34 -0
  31. package/_bmad/bme/_gyre/workflows/full-analysis/steps/step-01-initialize.md +68 -0
  32. package/_bmad/bme/_gyre/workflows/full-analysis/steps/step-02-detect-stack.md +49 -0
  33. package/_bmad/bme/_gyre/workflows/full-analysis/steps/step-03-generate-model.md +52 -0
  34. package/_bmad/bme/_gyre/workflows/full-analysis/steps/step-04-analyze-gaps.md +42 -0
  35. package/_bmad/bme/_gyre/workflows/full-analysis/steps/step-05-review-findings.md +128 -0
  36. package/_bmad/bme/_gyre/workflows/full-analysis/workflow.md +39 -0
  37. package/_bmad/bme/_gyre/workflows/gap-analysis/steps/step-01-load-manifest.md +70 -0
  38. package/_bmad/bme/_gyre/workflows/gap-analysis/steps/step-02-observability-analysis.md +110 -0
  39. package/_bmad/bme/_gyre/workflows/gap-analysis/steps/step-03-deployment-analysis.md +87 -0
  40. package/_bmad/bme/_gyre/workflows/gap-analysis/steps/step-04-cross-domain-correlation.md +105 -0
  41. package/_bmad/bme/_gyre/workflows/gap-analysis/steps/step-05-present-findings.md +172 -0
  42. package/_bmad/bme/_gyre/workflows/gap-analysis/workflow.md +38 -0
  43. package/_bmad/bme/_gyre/workflows/model-generation/steps/step-01-load-profile.md +74 -0
  44. package/_bmad/bme/_gyre/workflows/model-generation/steps/step-02-generate-capabilities.md +116 -0
  45. package/_bmad/bme/_gyre/workflows/model-generation/steps/step-03-web-enrichment.md +89 -0
  46. package/_bmad/bme/_gyre/workflows/model-generation/steps/step-04-write-manifest.md +122 -0
  47. package/_bmad/bme/_gyre/workflows/model-generation/workflow.md +40 -0
  48. package/_bmad/bme/_gyre/workflows/model-review/steps/step-01-load-context.md +86 -0
  49. package/_bmad/bme/_gyre/workflows/model-review/steps/step-02-walkthrough.md +116 -0
  50. package/_bmad/bme/_gyre/workflows/model-review/steps/step-03-apply-amendments.md +92 -0
  51. package/_bmad/bme/_gyre/workflows/model-review/steps/step-04-capture-feedback.md +107 -0
  52. package/_bmad/bme/_gyre/workflows/model-review/steps/step-05-summary.md +60 -0
  53. package/_bmad/bme/_gyre/workflows/model-review/workflow.md +41 -0
  54. package/_bmad/bme/_gyre/workflows/stack-detection/steps/step-01-scan-filesystem.md +176 -0
  55. package/_bmad/bme/_gyre/workflows/stack-detection/steps/step-02-classify-stack.md +111 -0
  56. package/_bmad/bme/_gyre/workflows/stack-detection/steps/step-03-guard-questions.md +117 -0
  57. package/_bmad/bme/_gyre/workflows/stack-detection/workflow.md +42 -0
  58. package/_bmad/bme/_vortex/config.yaml +1 -1
  59. package/package.json +5 -2
  60. package/scripts/archive.js +304 -0
  61. package/scripts/convoke-doctor.js +146 -132
  62. package/scripts/docs-audit.js +21 -5
  63. package/scripts/install-gyre-agents.js +140 -0
  64. package/scripts/install-vortex-agents.js +0 -0
  65. package/scripts/update/lib/agent-registry.js +70 -0
  66. package/scripts/update/lib/refresh-installation.js +152 -30
  67. package/scripts/update/lib/validator.js +160 -1
@@ -133,6 +133,71 @@ const WAVE3_STREAMS = new Set(['Synthesize', 'Hypothesize', 'Sensitize']);
133
133
  const _wave3AgentIds = new Set(AGENTS.filter(a => WAVE3_STREAMS.has(a.stream)).map(a => a.id));
134
134
  const WAVE3_WORKFLOW_NAMES = new Set(WORKFLOWS.filter(w => _wave3AgentIds.has(w.agent)).map(w => w.name));
135
135
 
136
+ // ── Gyre Module ──────────────────────────────────────────────────────
137
+ const GYRE_AGENTS = [
138
+ {
139
+ id: 'stack-detective', name: 'Scout', icon: '\u{1F50E}',
140
+ title: 'Stack Detective', stream: 'Detect',
141
+ persona: {
142
+ role: 'Technology Stack Detective + Architecture Classification Specialist',
143
+ identity: 'Methodical investigator who detects project technology stacks by analyzing filesystem artifacts. Reads manifests, configs, and IaC files. Never guesses — reports what evidence supports. Asks targeted guard questions derived from detection results to confirm architecture intent. Produces the Stack Profile (GC1) that downstream agents use to generate contextual models.',
144
+ communication_style: 'Methodical and evidence-driven. Reports findings with source references. Says things like \'I found evidence of...\' and \'Based on the manifests, this appears to be...\' Never speculates — distinguishes confirmed detections from inferences.',
145
+ expertise: '- Evidence over inference — every detection claim cites a specific file or pattern - Guard questions clarify ambiguity, not confirm the obvious - Privacy boundary: Stack Profile carries categories, never file contents or secrets - Report secondary stacks as warnings, not errors - Detection is the foundation — get it right and everything downstream improves',
146
+ },
147
+ },
148
+ {
149
+ id: 'model-curator', name: 'Atlas', icon: '\u{1F4D0}',
150
+ title: 'Model Curator', stream: 'Model',
151
+ persona: {
152
+ role: 'Contextual Model Generation + Capabilities Curation Specialist',
153
+ identity: 'Knowledgeable curator who generates capabilities manifests unique to each detected stack. Balances industry standards (DORA, OpenTelemetry, Google PRR) with practical relevance. Explains why each capability matters. Transparent about confidence levels — distinguishes well-known patterns from emerging practices.',
154
+ communication_style: 'Knowledgeable and transparent — explains reasoning behind each capability. Says things like \'This capability matters for your stack because...\' and \'I\'m less confident about this one — it\'s an emerging practice.\' Respects team ownership of the model.',
155
+ expertise: '- Industry standards inform but don\'t dictate — every capability must be relevant to THIS stack - Web search for current best practices keeps the model fresh - Model is team-owned — amendments from Coach (GC4) are respected on regeneration - Transparency about sources and confidence builds trust - Generate ≥20 capabilities for supported archetypes',
156
+ },
157
+ },
158
+ {
159
+ id: 'readiness-analyst', name: 'Lens', icon: '\u{1F52C}',
160
+ title: 'Readiness Analyst', stream: 'Analyze',
161
+ persona: {
162
+ role: 'Absence Detection + Cross-Domain Correlation Specialist',
163
+ identity: 'Thorough analyst who compares the capabilities manifest against what actually exists in the project. Identifies absences — what\'s missing, not just what\'s misconfigured. Runs observability and deployment domain analyses with cross-domain correlation for compound findings.',
164
+ communication_style: 'Thorough and honest — presents findings with evidence and confidence levels. Says things like \'I found no evidence of...\' and \'These two gaps amplify each other.\' Never inflates severity — a nice-to-have stays a nice-to-have.',
165
+ expertise: '- Absence detection finds what\'s missing, not just what\'s broken - Source-tag every finding (static analysis vs contextual model) - Cross-domain correlation reveals compound gaps that single-domain analysis misses - Confidence levels must reflect actual evidence strength - Never inflate severity — accuracy builds credibility',
166
+ },
167
+ },
168
+ {
169
+ id: 'review-coach', name: 'Coach', icon: '\u{1F3CB}',
170
+ title: 'Review Coach', stream: 'Review',
171
+ persona: {
172
+ role: 'Findings Review + Model Amendment + Feedback Capture Specialist',
173
+ identity: 'Patient guide who respects the user\'s expertise. Presents findings clearly — severity-first summary, then walkthrough. For model review, presents each capability one at a time with keep/remove/edit options. Captures feedback on missed gaps to improve the model over time.',
174
+ communication_style: 'Patient and respectful — lets the user decide what\'s relevant. Says things like \'Here\'s what we found — let me walk you through it\' and \'Did Gyre miss anything you know about?\' Explains why feedback matters for model improvement.',
175
+ expertise: '- Severity-first presentation respects the user\'s time - Model review is a conversation, not a checklist - Feedback capture improves the model for the whole team - Amendments persist — the model becomes team-owned through review - Never push — the user knows their system best',
176
+ },
177
+ },
178
+ ];
179
+
180
+ const GYRE_WORKFLOWS = [
181
+ // Scout — Detect
182
+ { name: 'stack-detection', agent: 'stack-detective' },
183
+ // Atlas — Model
184
+ { name: 'model-generation', agent: 'model-curator' },
185
+ // Lens — Analyze
186
+ { name: 'gap-analysis', agent: 'readiness-analyst' },
187
+ { name: 'delta-report', agent: 'readiness-analyst' },
188
+ // Coach — Review
189
+ { name: 'model-review', agent: 'review-coach' },
190
+ // Orchestration
191
+ { name: 'full-analysis', agent: 'stack-detective' },
192
+ // Validation
193
+ { name: 'accuracy-validation', agent: 'model-curator' },
194
+ ];
195
+
196
+ // Derived lists for Gyre
197
+ const GYRE_AGENT_FILES = GYRE_AGENTS.map(a => `${a.id}.md`);
198
+ const GYRE_AGENT_IDS = GYRE_AGENTS.map(a => a.id);
199
+ const GYRE_WORKFLOW_NAMES = GYRE_WORKFLOWS.map(w => w.name);
200
+
136
201
  module.exports = {
137
202
  AGENTS,
138
203
  WORKFLOWS,
@@ -141,4 +206,9 @@ module.exports = {
141
206
  WORKFLOW_NAMES,
142
207
  USER_GUIDES,
143
208
  WAVE3_WORKFLOW_NAMES,
209
+ GYRE_AGENTS,
210
+ GYRE_WORKFLOWS,
211
+ GYRE_AGENT_FILES,
212
+ GYRE_AGENT_IDS,
213
+ GYRE_WORKFLOW_NAMES,
144
214
  };
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const yaml = require('js-yaml');
6
6
  const { getPackageVersion } = require('./utils');
7
7
  const configMerger = require('./config-merger');
8
- const { AGENTS, AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, USER_GUIDES } = require('./agent-registry');
8
+ const { AGENTS, AGENT_FILES, AGENT_IDS, WORKFLOW_NAMES, USER_GUIDES, GYRE_AGENTS, GYRE_AGENT_FILES, GYRE_AGENT_IDS, GYRE_WORKFLOW_NAMES } = require('./agent-registry');
9
9
 
10
10
  /**
11
11
  * Refresh Installation for Convoke
@@ -182,6 +182,90 @@ async function refreshInstallation(projectRoot, options = {}) {
182
182
  }
183
183
  }
184
184
 
185
+ // 2d. Gyre module — copy agents, workflows, contracts, config
186
+ const packageGyre = path.join(packageRoot, '_bmad', 'bme', '_gyre');
187
+ const targetGyre = path.join(projectRoot, '_bmad', 'bme', '_gyre');
188
+
189
+ if (fs.existsSync(packageGyre)) {
190
+ // Copy Gyre agents
191
+ const gyreAgentsSource = path.join(packageGyre, 'agents');
192
+ const gyreAgentsTarget = path.join(targetGyre, 'agents');
193
+ await fs.ensureDir(gyreAgentsTarget);
194
+
195
+ if (!isSameRoot) {
196
+ for (const file of GYRE_AGENT_FILES) {
197
+ const src = path.join(gyreAgentsSource, file);
198
+ if (fs.existsSync(src)) {
199
+ await fs.copy(src, path.join(gyreAgentsTarget, file), { overwrite: true });
200
+ changes.push(`Refreshed Gyre agent: ${file}`);
201
+ if (verbose) console.log(` Refreshed Gyre agent: ${file}`);
202
+ }
203
+ }
204
+ } else {
205
+ changes.push('Skipped Gyre agent copy (dev environment)');
206
+ if (verbose) console.log(' Skipped Gyre agent copy (dev environment)');
207
+ }
208
+
209
+ // Copy Gyre workflows
210
+ const gyreWorkflowsSource = path.join(packageGyre, 'workflows');
211
+ const gyreWorkflowsTarget = path.join(targetGyre, 'workflows');
212
+ await fs.ensureDir(gyreWorkflowsTarget);
213
+
214
+ if (!isSameRoot) {
215
+ for (const wf of GYRE_WORKFLOW_NAMES) {
216
+ const src = path.join(gyreWorkflowsSource, wf);
217
+ const dest = path.join(gyreWorkflowsTarget, wf);
218
+ if (fs.existsSync(src)) {
219
+ if (fs.existsSync(dest)) {
220
+ await fs.remove(dest);
221
+ }
222
+ await fs.copy(src, dest, { overwrite: true });
223
+ changes.push(`Refreshed Gyre workflow: ${wf}`);
224
+ if (verbose) console.log(` Refreshed Gyre workflow: ${wf}`);
225
+ }
226
+ }
227
+ } else {
228
+ changes.push('Skipped Gyre workflow copy (dev environment)');
229
+ if (verbose) console.log(' Skipped Gyre workflow copy (dev environment)');
230
+ }
231
+
232
+ // Copy Gyre contracts
233
+ const gyreContractsSource = path.join(packageGyre, 'contracts');
234
+ const gyreContractsTarget = path.join(targetGyre, 'contracts');
235
+ if (fs.existsSync(gyreContractsSource)) {
236
+ await fs.ensureDir(gyreContractsTarget);
237
+ if (!isSameRoot) {
238
+ await fs.copy(gyreContractsSource, gyreContractsTarget, { overwrite: true });
239
+ changes.push('Refreshed Gyre contracts');
240
+ if (verbose) console.log(' Refreshed Gyre contracts');
241
+ }
242
+ }
243
+
244
+ // Copy Gyre config.yaml
245
+ const gyreConfigSource = path.join(packageGyre, 'config.yaml');
246
+ const gyreConfigTarget = path.join(targetGyre, 'config.yaml');
247
+ if (!isSameRoot && fs.existsSync(gyreConfigSource)) {
248
+ // Merge Gyre config preserving user prefs, same as Vortex
249
+ const gyreUpdates = {
250
+ agents: GYRE_AGENT_IDS,
251
+ workflows: GYRE_WORKFLOW_NAMES
252
+ };
253
+ const gyreConfigMerged = await configMerger.mergeConfig(gyreConfigTarget, version, gyreUpdates);
254
+ await configMerger.writeConfig(gyreConfigTarget, gyreConfigMerged);
255
+ changes.push(`Updated Gyre config.yaml to v${version}`);
256
+ if (verbose) console.log(` Updated Gyre config.yaml to v${version}`);
257
+ }
258
+
259
+ // Copy Gyre README
260
+ const gyreReadmeSource = path.join(packageGyre, 'README.md');
261
+ const gyreReadmeTarget = path.join(targetGyre, 'README.md');
262
+ if (!isSameRoot && fs.existsSync(gyreReadmeSource)) {
263
+ await fs.copy(gyreReadmeSource, gyreReadmeTarget, { overwrite: true });
264
+ changes.push('Refreshed Gyre README.md');
265
+ if (verbose) console.log(' Refreshed Gyre README.md');
266
+ }
267
+ }
268
+
185
269
  // 3. Update config.yaml (merge, preserving user prefs)
186
270
  const configPath = path.join(targetVortex, 'config.yaml');
187
271
  await fs.ensureDir(path.dirname(configPath));
@@ -264,35 +348,45 @@ async function refreshInstallation(projectRoot, options = {}) {
264
348
  isV610 = true;
265
349
  }
266
350
 
267
- // Build fresh bme rows matching the detected schema
351
+ // Build fresh bme rows matching the detected schema (Vortex + Gyre agents)
352
+ function buildAgentRow610(a, submodule) {
353
+ const p = a.persona;
354
+ return [
355
+ csvEscape(a.name), // name
356
+ csvEscape(''), // displayName
357
+ csvEscape(a.title), // title
358
+ csvEscape(a.icon), // icon
359
+ csvEscape(''), // capabilities
360
+ csvEscape(p.role), // role
361
+ csvEscape(p.identity), // identity
362
+ csvEscape(p.communication_style), // communicationStyle
363
+ csvEscape(p.expertise), // principles
364
+ csvEscape('bme'), // module
365
+ csvEscape(`_bmad/bme/${submodule}/agents/${a.id}.md`), // path
366
+ csvEscape(`bmad-agent-bme-${a.id}`), // canonicalId
367
+ ].join(',');
368
+ }
369
+
370
+ function buildAgentRowLegacy(a, submodule) {
371
+ const p = a.persona;
372
+ return [
373
+ a.id, a.name, a.title, a.icon,
374
+ p.role, p.identity, p.communication_style, p.expertise,
375
+ 'bme', `_bmad/bme/${submodule}/agents/${a.id}.md`,
376
+ ].map(csvEscape).join(',');
377
+ }
378
+
268
379
  let bmeRows;
269
380
  if (isV610) {
270
- bmeRows = AGENTS.map(a => {
271
- const p = a.persona;
272
- return [
273
- csvEscape(a.name), // name
274
- csvEscape(''), // displayName
275
- csvEscape(a.title), // title
276
- csvEscape(a.icon), // icon
277
- csvEscape(''), // capabilities
278
- csvEscape(p.role), // role
279
- csvEscape(p.identity), // identity
280
- csvEscape(p.communication_style), // communicationStyle
281
- csvEscape(p.expertise), // principles
282
- csvEscape('bme'), // module
283
- csvEscape(`_bmad/bme/_vortex/agents/${a.id}.md`), // path
284
- csvEscape(`bmad-agent-bme-${a.id}`), // canonicalId
285
- ].join(',');
286
- });
381
+ bmeRows = [
382
+ ...AGENTS.map(a => buildAgentRow610(a, '_vortex')),
383
+ ...GYRE_AGENTS.map(a => buildAgentRow610(a, '_gyre')),
384
+ ];
287
385
  } else {
288
- bmeRows = AGENTS.map(a => {
289
- const p = a.persona;
290
- return [
291
- a.id, a.name, a.title, a.icon,
292
- p.role, p.identity, p.communication_style, p.expertise,
293
- 'bme', `_bmad/bme/_vortex/agents/${a.id}.md`,
294
- ].map(csvEscape).join(',');
295
- });
386
+ bmeRows = [
387
+ ...AGENTS.map(a => buildAgentRowLegacy(a, '_vortex')),
388
+ ...GYRE_AGENTS.map(a => buildAgentRowLegacy(a, '_gyre')),
389
+ ];
296
390
  }
297
391
 
298
392
  const allRows = [...preservedRows, ...bmeRows].join('\n') + '\n';
@@ -342,7 +436,10 @@ async function refreshInstallation(projectRoot, options = {}) {
342
436
  const skillsDir = path.join(projectRoot, '.claude', 'skills');
343
437
 
344
438
  // Remove stale skill directories (agents no longer in registry)
345
- const currentSkillDirs = new Set(AGENTS.map(a => `bmad-agent-bme-${a.id}`));
439
+ const currentSkillDirs = new Set([
440
+ ...AGENTS.map(a => `bmad-agent-bme-${a.id}`),
441
+ ...GYRE_AGENTS.map(a => `bmad-agent-bme-${a.id}`),
442
+ ]);
346
443
  if (fs.existsSync(skillsDir)) {
347
444
  const existingSkills = (await fs.readdir(skillsDir)).filter(d => d.startsWith('bmad-agent-bme-'));
348
445
  for (const dir of existingSkills) {
@@ -378,7 +475,32 @@ You must fully embody this agent's persona and follow all activation instruction
378
475
  if (verbose) console.log(` Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
379
476
  }
380
477
 
381
- // 6a. Copy Enhance workflow skill wrappers and register in manifests
478
+ // 6b. Generate .claude/skills/ for Gyre agents
479
+ for (const agent of GYRE_AGENTS) {
480
+ const skillDir = path.join(skillsDir, `bmad-agent-bme-${agent.id}`);
481
+ await fs.ensureDir(skillDir);
482
+ const content = `---
483
+ name: bmad-agent-bme-${agent.id}
484
+ description: ${agent.id} agent
485
+ ---
486
+
487
+ You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
488
+
489
+ <agent-activation CRITICAL="TRUE">
490
+ 1. LOAD the FULL agent file from {project-root}/_bmad/bme/_gyre/agents/${agent.id}.md
491
+ 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
492
+ 3. FOLLOW every step in the <activation> section precisely
493
+ 4. DISPLAY the welcome/greeting as instructed
494
+ 5. PRESENT the numbered menu
495
+ 6. WAIT for user input before proceeding
496
+ </agent-activation>
497
+ `;
498
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf8');
499
+ changes.push(`Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
500
+ if (verbose) console.log(` Refreshed skill: bmad-agent-bme-${agent.id}/SKILL.md`);
501
+ }
502
+
503
+ // 6c. Copy Enhance workflow skill wrappers and register in manifests
382
504
  if (enhanceConfig && !isSameRoot) {
383
505
  for (const workflow of enhanceConfig.workflows || []) {
384
506
  const canonicalId = `bmad-enhance-${workflow.name}`;
@@ -457,7 +579,7 @@ menu: []
457
579
  prompts: []
458
580
  `;
459
581
 
460
- for (const agent of AGENTS) {
582
+ for (const agent of [...AGENTS, ...GYRE_AGENTS]) {
461
583
  const filename = `bme-${agent.name.toLowerCase()}.customize.yaml`;
462
584
  const filePath = path.join(customizeDir, filename);
463
585
  if (!fs.existsSync(filePath)) {
@@ -468,6 +468,161 @@ async function validateEnhanceModule(projectRoot) {
468
468
  return check;
469
469
  }
470
470
 
471
+ /**
472
+ * Validate a SKILL.md file has required frontmatter fields
473
+ * @param {string} skillMdPath - Absolute path to SKILL.md file
474
+ * @returns {Promise<object>} Validation result { valid, errors }
475
+ */
476
+ async function validateSkillMd(skillMdPath) {
477
+ const errors = [];
478
+
479
+ if (!fs.existsSync(skillMdPath)) {
480
+ errors.push(`SKILL.md not found: ${skillMdPath}`);
481
+ return { valid: false, errors };
482
+ }
483
+
484
+ const content = fs.readFileSync(skillMdPath, 'utf8');
485
+
486
+ // Extract YAML frontmatter between --- delimiters
487
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
488
+ if (!fmMatch) {
489
+ errors.push('SKILL.md missing YAML frontmatter (--- delimiters)');
490
+ return { valid: false, errors };
491
+ }
492
+
493
+ let frontmatter;
494
+ try {
495
+ frontmatter = yaml.load(fmMatch[1]);
496
+ } catch (err) {
497
+ errors.push(`SKILL.md frontmatter parse error: ${err.message}`);
498
+ return { valid: false, errors };
499
+ }
500
+
501
+ if (!frontmatter || typeof frontmatter !== 'object') {
502
+ errors.push('SKILL.md frontmatter is empty or not an object');
503
+ return { valid: false, errors };
504
+ }
505
+
506
+ if (!frontmatter.name || typeof frontmatter.name !== 'string') {
507
+ errors.push('SKILL.md missing required field: name');
508
+ }
509
+
510
+ if (!frontmatter.description || typeof frontmatter.description !== 'string') {
511
+ errors.push('SKILL.md missing required field: description');
512
+ }
513
+
514
+ return { valid: errors.length === 0, errors };
515
+ }
516
+
517
+ /**
518
+ * Validate step files in a skill directory follow sequential numbering
519
+ * @param {string} skillDir - Absolute path to skill directory
520
+ * @returns {Promise<object>} Validation result { valid, errors }
521
+ */
522
+ async function validateStepFiles(skillDir) {
523
+ const errors = [];
524
+
525
+ if (!fs.existsSync(skillDir)) {
526
+ errors.push(`Skill directory not found: ${skillDir}`);
527
+ return { valid: false, errors };
528
+ }
529
+
530
+ // Collect step files from skill dir and steps/ subdirectory
531
+ const stepFiles = [];
532
+ const entries = fs.readdirSync(skillDir);
533
+
534
+ for (const entry of entries) {
535
+ if (/^step-\d+-/.test(entry) && entry.endsWith('.md')) {
536
+ stepFiles.push(entry);
537
+ }
538
+ }
539
+
540
+ const stepsSubdir = path.join(skillDir, 'steps');
541
+ if (fs.existsSync(stepsSubdir)) {
542
+ const subEntries = fs.readdirSync(stepsSubdir);
543
+ for (const entry of subEntries) {
544
+ if (/^step-\d+-/.test(entry) && entry.endsWith('.md')) {
545
+ stepFiles.push(entry);
546
+ }
547
+ }
548
+ }
549
+
550
+ if (stepFiles.length === 0) {
551
+ // No step files is valid — some skills are single-file (agent-activation type)
552
+ return { valid: true, errors };
553
+ }
554
+
555
+ // Extract step numbers and check for sequential numbering
556
+ const stepNumbers = stepFiles
557
+ .map(f => {
558
+ const match = f.match(/^step-(\d+)-/);
559
+ return match ? parseInt(match[1], 10) : null;
560
+ })
561
+ .filter(n => n !== null)
562
+ .sort((a, b) => a - b);
563
+
564
+ // Remove duplicates (same step number in root and steps/)
565
+ const uniqueSteps = [...new Set(stepNumbers)];
566
+
567
+ // Check for gaps in sequence
568
+ for (let i = 0; i < uniqueSteps.length - 1; i++) {
569
+ if (uniqueSteps[i + 1] - uniqueSteps[i] > 1) {
570
+ errors.push(
571
+ `Step numbering gap: step-${String(uniqueSteps[i]).padStart(2, '0')} to step-${String(uniqueSteps[i + 1]).padStart(2, '0')} (missing step-${String(uniqueSteps[i] + 1).padStart(2, '0')})`
572
+ );
573
+ }
574
+ }
575
+
576
+ return { valid: errors.length === 0, errors };
577
+ }
578
+
579
+ /**
580
+ * Validate skill cohesion — workflow.md exists if step files exist
581
+ * @param {string} skillDir - Absolute path to skill directory
582
+ * @returns {Promise<object>} Validation result { valid, errors }
583
+ */
584
+ async function validateSkillCohesion(skillDir) {
585
+ const errors = [];
586
+
587
+ if (!fs.existsSync(skillDir)) {
588
+ errors.push(`Skill directory not found: ${skillDir}`);
589
+ return { valid: false, errors };
590
+ }
591
+
592
+ const entries = fs.readdirSync(skillDir);
593
+ const hasStepFiles = entries.some(e => /^step-\d+-/.test(e) && e.endsWith('.md'));
594
+ const hasStepsSubdir = fs.existsSync(path.join(skillDir, 'steps'));
595
+ const hasWorkflow = entries.includes('workflow.md');
596
+
597
+ // If step files or steps/ subdirectory exist, workflow.md should too
598
+ if ((hasStepFiles || hasStepsSubdir) && !hasWorkflow) {
599
+ errors.push('Skill has step files but no workflow.md');
600
+ }
601
+
602
+ return { valid: errors.length === 0, errors };
603
+ }
604
+
605
+ /**
606
+ * Validate a complete skill package (SKILL.md + step files + cohesion)
607
+ * @param {string} skillDir - Absolute path to skill directory
608
+ * @returns {Promise<object>} Validation result { valid, errors }
609
+ */
610
+ async function validateSkill(skillDir) {
611
+ const allErrors = [];
612
+
613
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
614
+ const skillMdResult = await validateSkillMd(skillMdPath);
615
+ allErrors.push(...skillMdResult.errors);
616
+
617
+ const stepResult = await validateStepFiles(skillDir);
618
+ allErrors.push(...stepResult.errors);
619
+
620
+ const cohesionResult = await validateSkillCohesion(skillDir);
621
+ allErrors.push(...cohesionResult.errors);
622
+
623
+ return { valid: allErrors.length === 0, errors: allErrors };
624
+ }
625
+
471
626
  module.exports = {
472
627
  validateInstallation,
473
628
  validateConfigStructure,
@@ -477,5 +632,9 @@ module.exports = {
477
632
  validateUserDataIntegrity,
478
633
  validateDeprecatedWorkflows,
479
634
  validateWorkflowStepStructure,
480
- validateEnhanceModule
635
+ validateEnhanceModule,
636
+ validateSkillMd,
637
+ validateStepFiles,
638
+ validateSkillCohesion,
639
+ validateSkill
481
640
  };