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.
- package/CHANGELOG.md +31 -0
- package/README.md +37 -10
- 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/_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/package.json +13 -7
- package/scripts/convoke-doctor.js +172 -1
- package/scripts/install-gyre-agents.js +0 -0
- package/scripts/lib/artifact-utils.js +521 -13
- package/scripts/lib/portfolio/portfolio-engine.js +301 -34
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
- package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
- package/scripts/migrate-artifacts.js +69 -10
- 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 +1156 -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/refresh-installation.js +293 -8
- package/scripts/update/lib/utils.js +27 -1
- package/scripts/update/lib/validator.js +114 -4
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-classification.js — Story sp-1-3
|
|
4
|
+
*
|
|
5
|
+
* Read-only validator for the classified skill manifest. Checks completeness,
|
|
6
|
+
* vocabulary correctness, and dependency integrity. Writes a markdown report.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/portability/validate-classification.js
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — PASS (no hard errors; warnings allowed)
|
|
13
|
+
* 1 — FAIL (one or more hard findings)
|
|
14
|
+
*
|
|
15
|
+
* Hard findings (exit 1):
|
|
16
|
+
* [MISSING] empty tier or intent
|
|
17
|
+
* [INVALID] out-of-vocabulary tier or intent
|
|
18
|
+
* [BROKEN-DEP] file-path dep doesn't resolve (or escapes project root)
|
|
19
|
+
* [BAD-CONFIG-DEP] malformed config: dep
|
|
20
|
+
* [ORPHAN-DEP] bare skill-name dep not in manifest
|
|
21
|
+
*
|
|
22
|
+
* Warnings (exit 0):
|
|
23
|
+
* [MISSING-PREREQS] pipeline skill (non-meta) with empty deps
|
|
24
|
+
*
|
|
25
|
+
* The script is read-only EXCEPT for the report file. It does not modify
|
|
26
|
+
* skill-manifest.csv, the schema doc, or any source files.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { findProjectRoot } = require('../update/lib/utils');
|
|
34
|
+
const { readManifest } = require('./manifest-csv');
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// CONSTANTS — must match scripts/portability/classify-skills.js
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
const VALID_TIERS = ['standalone', 'light-deps', 'pipeline'];
|
|
41
|
+
const VALID_INTENTS = [
|
|
42
|
+
'think-through-problem',
|
|
43
|
+
'define-what-to-build',
|
|
44
|
+
'review-something',
|
|
45
|
+
'write-documentation',
|
|
46
|
+
'plan-your-work',
|
|
47
|
+
'test-your-code',
|
|
48
|
+
'discover-product-fit',
|
|
49
|
+
'assess-readiness',
|
|
50
|
+
'meta-platform',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const CONFIG_KEY_PATTERN = /^config:[a-z_][a-z0-9_]*$/;
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// VALIDATION CHECKS
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check completeness + vocabulary for a single row.
|
|
61
|
+
* Returns an array of findings (may be empty).
|
|
62
|
+
*/
|
|
63
|
+
function checkRowVocabulary(row, header) {
|
|
64
|
+
const findings = [];
|
|
65
|
+
const name = row[header.indexOf('name')];
|
|
66
|
+
const tier = row[header.indexOf('tier')];
|
|
67
|
+
const intent = row[header.indexOf('intent')];
|
|
68
|
+
|
|
69
|
+
if (!tier) {
|
|
70
|
+
findings.push({
|
|
71
|
+
type: '[MISSING]',
|
|
72
|
+
skill: name,
|
|
73
|
+
detail: 'tier is empty',
|
|
74
|
+
recommendation: 'Run classify-skills.js to populate, or hand-edit if classifier produces a heuristic miss',
|
|
75
|
+
});
|
|
76
|
+
} else if (!VALID_TIERS.includes(tier)) {
|
|
77
|
+
findings.push({
|
|
78
|
+
type: '[INVALID]',
|
|
79
|
+
skill: name,
|
|
80
|
+
detail: `tier="${tier}" (not in canonical set: ${VALID_TIERS.join(', ')})`,
|
|
81
|
+
recommendation: 'Fix the tier value to one of the canonical values',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!intent) {
|
|
86
|
+
findings.push({
|
|
87
|
+
type: '[MISSING]',
|
|
88
|
+
skill: name,
|
|
89
|
+
detail: 'intent is empty',
|
|
90
|
+
recommendation: 'Run classify-skills.js to populate, or hand-edit if classifier produces a heuristic miss',
|
|
91
|
+
});
|
|
92
|
+
} else if (!VALID_INTENTS.includes(intent)) {
|
|
93
|
+
findings.push({
|
|
94
|
+
type: '[INVALID]',
|
|
95
|
+
skill: name,
|
|
96
|
+
detail: `intent="${intent}" (not in canonical set)`,
|
|
97
|
+
recommendation: 'Fix the intent value to one of the 9 canonical categories from portability-schema.md',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check that a project-relative path stays inside projectRoot.
|
|
106
|
+
*/
|
|
107
|
+
function isInsideProjectRoot(absPath, projectRoot) {
|
|
108
|
+
const rootWithSep = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
|
|
109
|
+
return absPath === projectRoot || absPath.startsWith(rootWithSep);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a relative dep against a skill's source tree.
|
|
114
|
+
*
|
|
115
|
+
* Why this is non-trivial: sp-1-2's classifier flattens content from
|
|
116
|
+
* SKILL.md + workflow.md + step files into one blob, then extracts
|
|
117
|
+
* relative-template references. The references are correct relative to
|
|
118
|
+
* the file they appeared in (e.g., a step file in `steps-c/`), but the
|
|
119
|
+
* validator only knows the skill's SKILL.md path. Naïvely resolving
|
|
120
|
+
* `../templates/X` against `path.dirname(SKILL.md)` gives the wrong
|
|
121
|
+
* answer when the reference originated in a step subdirectory.
|
|
122
|
+
*
|
|
123
|
+
* Strategy: try the SKILL.md directory first (the simple case), then
|
|
124
|
+
* search the skill's subtree by basename. The first existing match wins.
|
|
125
|
+
*
|
|
126
|
+
* Returns one of:
|
|
127
|
+
* - {string} absolute resolved path (success)
|
|
128
|
+
* - {{ error: 'escapes project root' | 'not found', resolved: string }} (failure)
|
|
129
|
+
*
|
|
130
|
+
* Note: never returns null. The caller discriminates with
|
|
131
|
+
* `typeof result === 'string'`.
|
|
132
|
+
*/
|
|
133
|
+
function resolveRelativeDep(dep, skillDir, projectRoot) {
|
|
134
|
+
// Attempt 1: resolve against the skill's own directory (handles `./templates/X`)
|
|
135
|
+
const direct = path.resolve(skillDir, dep);
|
|
136
|
+
if (isInsideProjectRoot(direct, projectRoot) && fs.existsSync(direct)) {
|
|
137
|
+
return direct;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Attempt 2: search the skill subtree for a file with the same basename.
|
|
141
|
+
// sp-1-2's classifier stripped the originating step-file directory context,
|
|
142
|
+
// so we walk the skill dir to recover it.
|
|
143
|
+
const basename = path.basename(dep);
|
|
144
|
+
try {
|
|
145
|
+
const found = findFileInSubtree(skillDir, basename, projectRoot);
|
|
146
|
+
if (found) return found;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// ignore stat errors during subtree walk
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Attempt 3 (escape check): if the direct resolution escaped the project
|
|
152
|
+
// root, surface that as the failure reason; otherwise fall through to "missing".
|
|
153
|
+
if (!isInsideProjectRoot(direct, projectRoot)) {
|
|
154
|
+
return { error: 'escapes project root', resolved: direct };
|
|
155
|
+
}
|
|
156
|
+
return { error: 'not found', resolved: direct };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Walk a directory subtree (bounded to projectRoot) looking for a file
|
|
161
|
+
* matching the target basename. Returns the first absolute match, or null.
|
|
162
|
+
*
|
|
163
|
+
* P3 (sp-1-3 review): symlinks are followed via `fs.statSync` (rather than
|
|
164
|
+
* Dirent.isFile/isDirectory which return false for symlinks). Cycles are
|
|
165
|
+
* prevented with a realpath visited-set.
|
|
166
|
+
*
|
|
167
|
+
* Stops descending at common skip directories to avoid runaway walks.
|
|
168
|
+
*/
|
|
169
|
+
function findFileInSubtree(dir, targetBasename, projectRoot) {
|
|
170
|
+
const SKIP = new Set(['node_modules', '.git', '_archive']);
|
|
171
|
+
const MAX_DEPTH = 6;
|
|
172
|
+
const visited = new Set();
|
|
173
|
+
|
|
174
|
+
function walk(currentDir, depth) {
|
|
175
|
+
if (depth > MAX_DEPTH) return null;
|
|
176
|
+
if (!isInsideProjectRoot(currentDir, projectRoot)) return null;
|
|
177
|
+
|
|
178
|
+
// Cycle protection via realpath (follows symlinks)
|
|
179
|
+
let realDir;
|
|
180
|
+
try {
|
|
181
|
+
realDir = fs.realpathSync(currentDir);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (visited.has(realDir)) return null;
|
|
186
|
+
visited.add(realDir);
|
|
187
|
+
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = fs.readdirSync(currentDir);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Files first (cheap match) — use statSync to follow symlinks
|
|
196
|
+
for (const name of entries) {
|
|
197
|
+
if (name !== targetBasename) continue;
|
|
198
|
+
const fullPath = path.join(currentDir, name);
|
|
199
|
+
try {
|
|
200
|
+
const stat = fs.statSync(fullPath); // follows symlinks
|
|
201
|
+
if (stat.isFile()) return fullPath;
|
|
202
|
+
} catch (e) {
|
|
203
|
+
// broken symlink or stat failure — skip
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Then recurse into subdirectories (also follows symlinks via statSync)
|
|
208
|
+
for (const name of entries) {
|
|
209
|
+
if (SKIP.has(name)) continue;
|
|
210
|
+
const fullPath = path.join(currentDir, name);
|
|
211
|
+
try {
|
|
212
|
+
const stat = fs.statSync(fullPath);
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
const found = walk(fullPath, depth + 1);
|
|
215
|
+
if (found) return found;
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// broken symlink or stat failure — skip
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return walk(dir, 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check dependency entries for one row. Returns an array of findings.
|
|
229
|
+
*
|
|
230
|
+
* @param {string[]} row
|
|
231
|
+
* @param {string[]} header
|
|
232
|
+
* @param {string} projectRoot
|
|
233
|
+
* @param {Set<string>} validSkillNames
|
|
234
|
+
*/
|
|
235
|
+
function checkRowDependencies(row, header, projectRoot, validSkillNames) {
|
|
236
|
+
const findings = [];
|
|
237
|
+
const name = row[header.indexOf('name')];
|
|
238
|
+
const skillPath = row[header.indexOf('path')];
|
|
239
|
+
const depsStr = row[header.indexOf('dependencies')];
|
|
240
|
+
|
|
241
|
+
if (!depsStr) return findings;
|
|
242
|
+
|
|
243
|
+
const deps = depsStr.split(';').filter((d) => d.length > 0);
|
|
244
|
+
const skillDir = path.dirname(path.join(projectRoot, skillPath));
|
|
245
|
+
|
|
246
|
+
for (const dep of deps) {
|
|
247
|
+
// 1. Self-reference: skip silently
|
|
248
|
+
if (dep === name) continue;
|
|
249
|
+
|
|
250
|
+
// 2. Absolute project-relative file path
|
|
251
|
+
if (dep.startsWith('_bmad/')) {
|
|
252
|
+
const absPath = path.join(projectRoot, dep);
|
|
253
|
+
if (!fs.existsSync(absPath)) {
|
|
254
|
+
findings.push({
|
|
255
|
+
type: '[BROKEN-DEP]',
|
|
256
|
+
skill: name,
|
|
257
|
+
detail: `dependency "${dep}" → ${path.relative(projectRoot, absPath)} (does not exist)`,
|
|
258
|
+
recommendation: 'Verify the dep path is correct, or remove the entry if the file was deleted',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 3. Relative path. sp-1-2's classifier flattens content from SKILL.md +
|
|
265
|
+
// workflow + step files into one blob, so a `../templates/X` reference
|
|
266
|
+
// may have originated in a step subdirectory rather than the SKILL.md
|
|
267
|
+
// directory. resolveRelativeDep handles both: tries SKILL.md first, then
|
|
268
|
+
// searches the skill subtree by basename.
|
|
269
|
+
if (dep.startsWith('./') || dep.startsWith('../')) {
|
|
270
|
+
const result = resolveRelativeDep(dep, skillDir, projectRoot);
|
|
271
|
+
if (typeof result === 'string') {
|
|
272
|
+
// Resolved cleanly to an existing file — no finding
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// Failure case: result is { error, resolved }
|
|
276
|
+
const escapesRoot = result && result.error === 'escapes project root';
|
|
277
|
+
findings.push({
|
|
278
|
+
type: '[BROKEN-DEP]',
|
|
279
|
+
skill: name,
|
|
280
|
+
detail: escapesRoot
|
|
281
|
+
? `relative dep "${dep}" escapes project root (resolves to ${result.resolved})`
|
|
282
|
+
: `relative dep "${dep}" not found in skill subtree (searched ${path.relative(projectRoot, skillDir)} for ${path.basename(dep)})`,
|
|
283
|
+
recommendation: escapesRoot
|
|
284
|
+
? 'Use a project-relative `_bmad/...` path instead'
|
|
285
|
+
: 'Verify the file exists somewhere in the skill directory tree, or remove the entry',
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 4. Config key
|
|
291
|
+
if (dep.startsWith('config:')) {
|
|
292
|
+
if (!CONFIG_KEY_PATTERN.test(dep)) {
|
|
293
|
+
findings.push({
|
|
294
|
+
type: '[BAD-CONFIG-DEP]',
|
|
295
|
+
skill: name,
|
|
296
|
+
detail: `malformed config dep "${dep}" (must match config:[a-z_][a-z0-9_]*)`,
|
|
297
|
+
recommendation: 'Fix the config key format — lowercase, alphanumeric + underscore only',
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 5. Bare skill name → check membership in manifest
|
|
304
|
+
if (!validSkillNames.has(dep)) {
|
|
305
|
+
findings.push({
|
|
306
|
+
type: '[ORPHAN-DEP]',
|
|
307
|
+
skill: name,
|
|
308
|
+
detail: `skill-name dep "${dep}" not found in manifest`,
|
|
309
|
+
recommendation: 'Dependency may have been removed; re-run classify-skills.js or remove the entry',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return findings;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check Tier 3 prerequisite documentation. Returns warning findings.
|
|
319
|
+
*/
|
|
320
|
+
function checkTier3Prereqs(row, header) {
|
|
321
|
+
const tier = row[header.indexOf('tier')];
|
|
322
|
+
const intent = row[header.indexOf('intent')];
|
|
323
|
+
const deps = row[header.indexOf('dependencies')];
|
|
324
|
+
const name = row[header.indexOf('name')];
|
|
325
|
+
|
|
326
|
+
if (tier !== 'pipeline') return [];
|
|
327
|
+
if (intent === 'meta-platform') return []; // framework-internal exemption
|
|
328
|
+
if (deps && deps.length > 0) return [];
|
|
329
|
+
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
type: '[MISSING-PREREQS]',
|
|
333
|
+
skill: name,
|
|
334
|
+
detail: 'pipeline skill has empty dependencies column',
|
|
335
|
+
recommendation: 'Known limitation of sp-1-2 classifier (does not extract artifact-consumption patterns) — not a fix in this story',
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =============================================================================
|
|
341
|
+
// REPORT GENERATION
|
|
342
|
+
// =============================================================================
|
|
343
|
+
|
|
344
|
+
const FINDING_TYPES = [
|
|
345
|
+
'[MISSING]',
|
|
346
|
+
'[INVALID]',
|
|
347
|
+
'[BROKEN-DEP]',
|
|
348
|
+
'[BAD-CONFIG-DEP]',
|
|
349
|
+
'[ORPHAN-DEP]',
|
|
350
|
+
'[MISSING-PREREQS]',
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const HARD_FINDING_TYPES = new Set([
|
|
354
|
+
'[MISSING]',
|
|
355
|
+
'[INVALID]',
|
|
356
|
+
'[BROKEN-DEP]',
|
|
357
|
+
'[BAD-CONFIG-DEP]',
|
|
358
|
+
'[ORPHAN-DEP]',
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
function escapeMarkdownTableCell(s) {
|
|
362
|
+
if (s == null) return '';
|
|
363
|
+
return String(s).replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderReport(date, totalSkills, status, findings) {
|
|
367
|
+
const counts = Object.create(null);
|
|
368
|
+
for (const t of FINDING_TYPES) counts[t] = 0;
|
|
369
|
+
for (const f of findings) counts[f.type]++;
|
|
370
|
+
|
|
371
|
+
const lines = [
|
|
372
|
+
'# Portability Classification — Validation Report',
|
|
373
|
+
'',
|
|
374
|
+
`Generated by \`scripts/portability/validate-classification.js\` on ${date}.`,
|
|
375
|
+
'',
|
|
376
|
+
`**Total skills checked:** ${totalSkills}`,
|
|
377
|
+
`**Status:** ${status}`,
|
|
378
|
+
'',
|
|
379
|
+
'## Summary',
|
|
380
|
+
'',
|
|
381
|
+
'| Finding type | Count |',
|
|
382
|
+
'|--------------|-------|',
|
|
383
|
+
`| [MISSING] | ${counts['[MISSING]']} |`,
|
|
384
|
+
`| [INVALID] | ${counts['[INVALID]']} |`,
|
|
385
|
+
`| [BROKEN-DEP] | ${counts['[BROKEN-DEP]']} |`,
|
|
386
|
+
`| [BAD-CONFIG-DEP] | ${counts['[BAD-CONFIG-DEP]']} |`,
|
|
387
|
+
`| [ORPHAN-DEP] | ${counts['[ORPHAN-DEP]']} |`,
|
|
388
|
+
`| [MISSING-PREREQS] | ${counts['[MISSING-PREREQS]']} (warning) |`,
|
|
389
|
+
'',
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const sectionTitles = {
|
|
393
|
+
'[MISSING]': '[MISSING] — Unclassified rows',
|
|
394
|
+
'[INVALID]': '[INVALID] — Out-of-vocabulary values',
|
|
395
|
+
'[BROKEN-DEP]': "[BROKEN-DEP] — File-path dependencies that don't resolve",
|
|
396
|
+
'[BAD-CONFIG-DEP]': '[BAD-CONFIG-DEP] — Malformed config: dependencies',
|
|
397
|
+
'[ORPHAN-DEP]': "[ORPHAN-DEP] — Skill-name dependencies that don't exist in the manifest",
|
|
398
|
+
'[MISSING-PREREQS]': '[MISSING-PREREQS] — Pipeline skills with empty dependencies (warning)',
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
for (const t of FINDING_TYPES) {
|
|
402
|
+
lines.push(`## ${sectionTitles[t]}`);
|
|
403
|
+
lines.push('');
|
|
404
|
+
const rows = findings.filter((f) => f.type === t);
|
|
405
|
+
if (rows.length === 0) {
|
|
406
|
+
lines.push('_None._');
|
|
407
|
+
lines.push('');
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
lines.push('| Skill | Finding detail | Recommendation |');
|
|
411
|
+
lines.push('|-------|----------------|----------------|');
|
|
412
|
+
for (const r of rows) {
|
|
413
|
+
lines.push(
|
|
414
|
+
`| \`${escapeMarkdownTableCell(r.skill)}\` | ${escapeMarkdownTableCell(r.detail)} | ${escapeMarkdownTableCell(r.recommendation)} |`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
lines.push('');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return lines.join('\n');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// =============================================================================
|
|
424
|
+
// MAIN
|
|
425
|
+
// =============================================================================
|
|
426
|
+
|
|
427
|
+
function validate(projectRoot) {
|
|
428
|
+
const manifestPath = path.join(projectRoot, '_bmad', '_config', 'skill-manifest.csv');
|
|
429
|
+
const { header, rows } = readManifest(manifestPath);
|
|
430
|
+
|
|
431
|
+
const tierIdx = header.indexOf('tier');
|
|
432
|
+
const intentIdx = header.indexOf('intent');
|
|
433
|
+
const depsIdx = header.indexOf('dependencies');
|
|
434
|
+
if (tierIdx < 0 || intentIdx < 0 || depsIdx < 0) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
'skill-manifest.csv is missing tier/intent/dependencies columns. Run sp-1-1 first.'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const nameIdx = header.indexOf('name');
|
|
441
|
+
const validSkillNames = new Set(rows.map((r) => r[nameIdx]));
|
|
442
|
+
|
|
443
|
+
const findings = [];
|
|
444
|
+
for (const row of rows) {
|
|
445
|
+
// P2 (sp-1-3 review): a malformed row (missing path column, undefined
|
|
446
|
+
// fields) shouldn't crash the entire validator. Catch per-row errors
|
|
447
|
+
// and surface them as a [MISSING] finding so the rest of the manifest
|
|
448
|
+
// still gets validated.
|
|
449
|
+
try {
|
|
450
|
+
findings.push(...checkRowVocabulary(row, header));
|
|
451
|
+
findings.push(...checkRowDependencies(row, header, projectRoot, validSkillNames));
|
|
452
|
+
findings.push(...checkTier3Prereqs(row, header));
|
|
453
|
+
} catch (e) {
|
|
454
|
+
const skillName = row[nameIdx] || '(unknown row)';
|
|
455
|
+
findings.push({
|
|
456
|
+
type: '[MISSING]',
|
|
457
|
+
skill: skillName,
|
|
458
|
+
detail: `row processing failed: ${e.message}`,
|
|
459
|
+
recommendation: 'Inspect this row in skill-manifest.csv for malformed fields (missing columns, undefined path, etc.)',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { totalSkills: rows.length, findings };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function main() {
|
|
468
|
+
const projectRoot = findProjectRoot();
|
|
469
|
+
let result;
|
|
470
|
+
try {
|
|
471
|
+
result = validate(projectRoot);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
console.error(`ERROR: ${e.message}`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const { totalSkills, findings } = result;
|
|
478
|
+
const errorCount = findings.filter((f) => HARD_FINDING_TYPES.has(f.type)).length;
|
|
479
|
+
const warningCount = findings.length - errorCount;
|
|
480
|
+
const status = errorCount === 0 ? 'PASS' : 'FAIL';
|
|
481
|
+
|
|
482
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
483
|
+
const report = renderReport(date, totalSkills, status, findings);
|
|
484
|
+
|
|
485
|
+
const reportPath = path.join(
|
|
486
|
+
projectRoot,
|
|
487
|
+
'_bmad-output',
|
|
488
|
+
'planning-artifacts',
|
|
489
|
+
'portability-validation-report.md'
|
|
490
|
+
);
|
|
491
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
492
|
+
fs.writeFileSync(reportPath, report, 'utf8');
|
|
493
|
+
|
|
494
|
+
// Single-line stdout summary
|
|
495
|
+
if (errorCount === 0) {
|
|
496
|
+
console.log(`PASS: ${totalSkills} skills validated, 0 errors, ${warningCount} warnings`);
|
|
497
|
+
} else {
|
|
498
|
+
const breakdown = {};
|
|
499
|
+
for (const f of findings) {
|
|
500
|
+
if (HARD_FINDING_TYPES.has(f.type)) {
|
|
501
|
+
breakdown[f.type] = (breakdown[f.type] || 0) + 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const breakdownStr = Object.entries(breakdown)
|
|
505
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
506
|
+
.join(', ');
|
|
507
|
+
console.log(
|
|
508
|
+
`FAIL: ${totalSkills} skills checked, ${errorCount} errors (${breakdownStr}), ${warningCount} warnings`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(`Report written to ${path.relative(projectRoot, reportPath)}`);
|
|
513
|
+
process.exit(errorCount === 0 ? 0 : 1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (require.main === module) {
|
|
517
|
+
main();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
module.exports = {
|
|
521
|
+
validate,
|
|
522
|
+
checkRowVocabulary,
|
|
523
|
+
checkRowDependencies,
|
|
524
|
+
checkTier3Prereqs,
|
|
525
|
+
renderReport,
|
|
526
|
+
VALID_TIERS,
|
|
527
|
+
VALID_INTENTS,
|
|
528
|
+
HARD_FINDING_TYPES,
|
|
529
|
+
};
|