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,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* classify-skills.js — Story sp-1-2
|
|
4
|
+
*
|
|
5
|
+
* Reads `_bmad/_config/skill-manifest.csv`, classifies each skill with tier,
|
|
6
|
+
* intent, and exporter-essential dependencies, and writes the manifest back.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/portability/classify-skills.js # write
|
|
10
|
+
* node scripts/portability/classify-skills.js --dry-run # preview only
|
|
11
|
+
* node scripts/portability/classify-skills.js --force # override conflicts
|
|
12
|
+
*
|
|
13
|
+
* Idempotent: re-running with no source changes produces zero diff.
|
|
14
|
+
* Non-destructive: preserves manual classification overrides unless --force.
|
|
15
|
+
*
|
|
16
|
+
* See _bmad-output/implementation-artifacts/sp-1-2-classify-all-skills.md
|
|
17
|
+
* for the full classification policy and heuristics.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { findProjectRoot } = require('../update/lib/utils');
|
|
25
|
+
const { readManifest, writeManifest } = require('./manifest-csv');
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// CONSTANTS — locked classification rules from sp-1-2 spec (AC #7, Task 3)
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
const VALID_TIERS = ['standalone', 'light-deps', 'pipeline'];
|
|
32
|
+
const VALID_INTENTS = [
|
|
33
|
+
'think-through-problem',
|
|
34
|
+
'define-what-to-build',
|
|
35
|
+
'review-something',
|
|
36
|
+
'write-documentation',
|
|
37
|
+
'plan-your-work',
|
|
38
|
+
'test-your-code',
|
|
39
|
+
'discover-product-fit',
|
|
40
|
+
'assess-readiness',
|
|
41
|
+
'meta-platform',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Canonical meta-platform skills (AC #7) — framework-internal, NOT portable.
|
|
45
|
+
//
|
|
46
|
+
// Note: AC #7 originally listed `bmad-agent-bme-team-factory` (Loom Master) as
|
|
47
|
+
// the 6th meta-platform skill, but that name lives in the AGENT manifest, not
|
|
48
|
+
// the skill manifest. There is no team-factory entry in skill-manifest.csv to
|
|
49
|
+
// classify, so the meta-platform set has 5 skills, not 6. Loom Master is
|
|
50
|
+
// invoked via party-mode and the team-factory agent file directly.
|
|
51
|
+
const META_PLATFORM_SKILLS = new Set([
|
|
52
|
+
'bmad-init',
|
|
53
|
+
'bmad-help',
|
|
54
|
+
'bmad-party-mode',
|
|
55
|
+
'bmad-builder-setup',
|
|
56
|
+
'bmad-agent-builder',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Standalone utilities explicitly carved out from meta-platform (AC #7).
|
|
60
|
+
const STANDALONE_UTILITY_INTENTS = {
|
|
61
|
+
'bmad-distillator': 'write-documentation',
|
|
62
|
+
'bmad-advanced-elicitation': 'think-through-problem',
|
|
63
|
+
'bmad-shard-doc': 'write-documentation',
|
|
64
|
+
'bmad-index-docs': 'write-documentation',
|
|
65
|
+
// bmad-generate-project-context handled separately — Tier 2, write-documentation
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Pipeline skills by name (Task 2 priority 1).
|
|
69
|
+
const PIPELINE_BY_NAME = new Set([
|
|
70
|
+
'bmad-dev-story',
|
|
71
|
+
'bmad-sprint-planning',
|
|
72
|
+
'bmad-sprint-status',
|
|
73
|
+
'bmad-create-story',
|
|
74
|
+
'bmad-correct-course',
|
|
75
|
+
'bmad-retrospective',
|
|
76
|
+
'bmad-code-review',
|
|
77
|
+
'bmad-check-implementation-readiness',
|
|
78
|
+
'bmad-validate-prd',
|
|
79
|
+
'bmad-edit-prd',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Persona-only bmad-agent-* skills, enumerated (Task 3, no judgment calls).
|
|
83
|
+
const PERSONA_AGENT_INTENTS = {
|
|
84
|
+
'bmad-agent-analyst': 'define-what-to-build',
|
|
85
|
+
'bmad-agent-pm': 'define-what-to-build',
|
|
86
|
+
'bmad-agent-architect': 'define-what-to-build',
|
|
87
|
+
'bmad-agent-ux-designer': 'define-what-to-build',
|
|
88
|
+
'bmad-agent-tech-writer': 'write-documentation',
|
|
89
|
+
'bmad-agent-sm': 'plan-your-work',
|
|
90
|
+
'bmad-agent-dev': 'plan-your-work',
|
|
91
|
+
'bmad-agent-quick-flow-solo-dev': 'plan-your-work',
|
|
92
|
+
'bmad-agent-qa': 'test-your-code',
|
|
93
|
+
// bmad-agent-builder and bmad-agent-bme-team-factory are in META_PLATFORM_SKILLS above.
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Vortex 7 stream agents (discover-product-fit).
|
|
97
|
+
const VORTEX_AGENTS = new Set([
|
|
98
|
+
'bmad-agent-bme-contextualization-expert',
|
|
99
|
+
'bmad-agent-bme-discovery-empathy-expert',
|
|
100
|
+
'bmad-agent-bme-research-convergence-specialist',
|
|
101
|
+
'bmad-agent-bme-hypothesis-engineer',
|
|
102
|
+
'bmad-agent-bme-lean-experiments-specialist',
|
|
103
|
+
'bmad-agent-bme-production-intelligence-specialist',
|
|
104
|
+
'bmad-agent-bme-learning-decision-expert',
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// Gyre 4 readiness agents (assess-readiness).
|
|
108
|
+
const GYRE_AGENTS = new Set([
|
|
109
|
+
'bmad-agent-bme-stack-detective',
|
|
110
|
+
'bmad-agent-bme-model-curator',
|
|
111
|
+
'bmad-agent-bme-readiness-analyst',
|
|
112
|
+
'bmad-agent-bme-review-coach',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// Note: universal config vars (user_name, output_folder, etc.) are stripped
|
|
116
|
+
// implicitly by extractDependencies — we only emit deps for templates,
|
|
117
|
+
// sidecars, and chained skills. Config vars are never extracted.
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// CLASSIFICATION HEURISTICS
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Determine intent from skill name + path. Returns null if no match.
|
|
125
|
+
*
|
|
126
|
+
* Order matters — more specific matches first.
|
|
127
|
+
*/
|
|
128
|
+
function classifyIntent(name, _modulePath) {
|
|
129
|
+
// 1. Meta-platform (locked set)
|
|
130
|
+
if (META_PLATFORM_SKILLS.has(name)) return 'meta-platform';
|
|
131
|
+
|
|
132
|
+
// 2. Standalone utilities carved out from meta-platform
|
|
133
|
+
if (name in STANDALONE_UTILITY_INTENTS) return STANDALONE_UTILITY_INTENTS[name];
|
|
134
|
+
if (name === 'bmad-generate-project-context') return 'write-documentation';
|
|
135
|
+
|
|
136
|
+
// 3. Persona-only agent skills (enumerated)
|
|
137
|
+
if (name in PERSONA_AGENT_INTENTS) return PERSONA_AGENT_INTENTS[name];
|
|
138
|
+
|
|
139
|
+
// 4. Vortex / Gyre agent skills
|
|
140
|
+
if (VORTEX_AGENTS.has(name)) return 'discover-product-fit';
|
|
141
|
+
if (GYRE_AGENTS.has(name)) return 'assess-readiness';
|
|
142
|
+
|
|
143
|
+
// 5. Pattern-matched intent groups
|
|
144
|
+
if (
|
|
145
|
+
name.startsWith('bmad-cis-agent-') ||
|
|
146
|
+
name === 'bmad-brainstorming' ||
|
|
147
|
+
name === 'bmad-cis-problem-solving' ||
|
|
148
|
+
name === 'bmad-cis-design-thinking' ||
|
|
149
|
+
name === 'bmad-cis-storytelling' ||
|
|
150
|
+
name === 'bmad-cis-innovation-strategy'
|
|
151
|
+
) {
|
|
152
|
+
return 'think-through-problem';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
[
|
|
157
|
+
'bmad-create-prd',
|
|
158
|
+
'bmad-edit-prd',
|
|
159
|
+
'bmad-validate-prd',
|
|
160
|
+
'bmad-product-brief',
|
|
161
|
+
'bmad-create-ux-design',
|
|
162
|
+
'bmad-create-architecture',
|
|
163
|
+
'bmad-create-epics-and-stories',
|
|
164
|
+
'bmad-domain-research',
|
|
165
|
+
'bmad-market-research',
|
|
166
|
+
'bmad-technical-research',
|
|
167
|
+
].includes(name)
|
|
168
|
+
) {
|
|
169
|
+
return 'define-what-to-build';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (
|
|
173
|
+
name === 'bmad-code-review' ||
|
|
174
|
+
name.startsWith('bmad-review-') ||
|
|
175
|
+
name.startsWith('bmad-editorial-review-')
|
|
176
|
+
) {
|
|
177
|
+
return 'review-something';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
[
|
|
182
|
+
'bmad-document-project',
|
|
183
|
+
'bmad-generate-project-context',
|
|
184
|
+
'bmad-index-docs',
|
|
185
|
+
].includes(name) ||
|
|
186
|
+
name.startsWith('bmad-tech-writer')
|
|
187
|
+
) {
|
|
188
|
+
return 'write-documentation';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
name === 'bmad-create-story' ||
|
|
193
|
+
name === 'bmad-correct-course' ||
|
|
194
|
+
name === 'bmad-retrospective' ||
|
|
195
|
+
name === 'bmad-dev-story' ||
|
|
196
|
+
name === 'bmad-quick-dev' ||
|
|
197
|
+
name.startsWith('bmad-sprint-')
|
|
198
|
+
) {
|
|
199
|
+
return 'plan-your-work';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
name.startsWith('bmad-testarch-') ||
|
|
204
|
+
name === 'bmad-qa-generate-e2e-tests' ||
|
|
205
|
+
name === 'bmad-teach-me-testing'
|
|
206
|
+
) {
|
|
207
|
+
return 'test-your-code';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (name === 'bmad-check-implementation-readiness') return 'assess-readiness';
|
|
211
|
+
|
|
212
|
+
// 6. WDS skills — all phases produce specs/scenarios → define-what-to-build
|
|
213
|
+
if (name.startsWith('wds-')) return 'define-what-to-build';
|
|
214
|
+
|
|
215
|
+
// 7. tea / workflow-builder edge cases
|
|
216
|
+
if (name === 'bmad-tea') return 'test-your-code';
|
|
217
|
+
if (name === 'bmad-workflow-builder') return 'meta-platform';
|
|
218
|
+
|
|
219
|
+
// 8. AG Epic 6 governance skills — framework-internal artifact governance tooling
|
|
220
|
+
if (name === 'bmad-migrate-artifacts' || name === 'bmad-portfolio-status') {
|
|
221
|
+
return 'meta-platform';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 9. Enhance module skills — initiatives-backlog manages a planning artifact
|
|
225
|
+
if (name === 'bmad-enhance-initiatives-backlog') return 'plan-your-work';
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Read a skill's source files (SKILL.md + referenced workflow + step files)
|
|
232
|
+
* up to a recursion depth limit. Returns concatenated text content.
|
|
233
|
+
*/
|
|
234
|
+
function readSkillContent(projectRoot, relativePath, maxDepth = 3) {
|
|
235
|
+
const visited = new Set();
|
|
236
|
+
const chunks = [];
|
|
237
|
+
// Project root must end with separator so the prefix check is unambiguous
|
|
238
|
+
const rootWithSep = projectRoot.endsWith(path.sep)
|
|
239
|
+
? projectRoot
|
|
240
|
+
: projectRoot + path.sep;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Containment guard: only follow refs that resolve inside projectRoot.
|
|
244
|
+
* Defends against malicious/buggy markdown refs with `../../../etc/passwd`.
|
|
245
|
+
*/
|
|
246
|
+
function isInsideProjectRoot(absPath) {
|
|
247
|
+
return absPath === projectRoot || absPath.startsWith(rootWithSep);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function readOne(absPath, depth) {
|
|
251
|
+
if (depth > maxDepth) return;
|
|
252
|
+
if (!isInsideProjectRoot(absPath)) return; // P1: containment guard
|
|
253
|
+
if (visited.has(absPath)) return;
|
|
254
|
+
visited.add(absPath);
|
|
255
|
+
if (!fs.existsSync(absPath)) return;
|
|
256
|
+
let content;
|
|
257
|
+
try {
|
|
258
|
+
content = fs.readFileSync(absPath, 'utf8');
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
chunks.push(content);
|
|
263
|
+
|
|
264
|
+
// Find referenced files: workflow.md, steps/*.md, ./templates/*.md, etc.
|
|
265
|
+
const dir = path.dirname(absPath);
|
|
266
|
+
const refs = [];
|
|
267
|
+
// Relative refs (./...md or ../...md or steps/...md)
|
|
268
|
+
const relRegex = /(?:\.\/|\.\.\/|steps\/)[\w./-]+\.md/g;
|
|
269
|
+
let m;
|
|
270
|
+
while ((m = relRegex.exec(content)) !== null) {
|
|
271
|
+
refs.push(path.resolve(dir, m[0]));
|
|
272
|
+
}
|
|
273
|
+
// Project-root refs (support both {project-root} and {project_root})
|
|
274
|
+
const projRegex = /\{project[-_]root\}\/([\w./-]+\.md)/g;
|
|
275
|
+
while ((m = projRegex.exec(content)) !== null) {
|
|
276
|
+
refs.push(path.join(projectRoot, m[1]));
|
|
277
|
+
}
|
|
278
|
+
// Bare _bmad/ refs in load directives
|
|
279
|
+
const bmadRegex = /(?:Load[^:]*:|read fully and follow:?)[^\n]*?(_bmad\/[\w./-]+\.md)/gi;
|
|
280
|
+
while ((m = bmadRegex.exec(content)) !== null) {
|
|
281
|
+
refs.push(path.join(projectRoot, m[1]));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const ref of refs) {
|
|
285
|
+
// Each readOne call re-checks containment; this loop just enqueues.
|
|
286
|
+
readOne(ref, depth + 1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
readOne(path.join(projectRoot, relativePath), 0);
|
|
291
|
+
return chunks.join('\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Determine tier from skill name + workflow content.
|
|
296
|
+
*/
|
|
297
|
+
function classifyTier(name, modulePath, content, intent) {
|
|
298
|
+
// Persona-only agent skills (bmad-agent-pm, bmad-agent-sm, etc.) are
|
|
299
|
+
// menu wrappers around their persona — they LIST other skills as menu
|
|
300
|
+
// options but don't actively chain them. They are standalone personas.
|
|
301
|
+
// Override applies BEFORE pipeline detection to prevent the menu list
|
|
302
|
+
// from triggering "invokes another skill" pipeline detection.
|
|
303
|
+
if (name in PERSONA_AGENT_INTENTS) {
|
|
304
|
+
return 'standalone';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Pipeline indicators (Tier 3) — first match wins.
|
|
308
|
+
if (PIPELINE_BY_NAME.has(name)) return 'pipeline';
|
|
309
|
+
if (intent === 'meta-platform') return 'pipeline';
|
|
310
|
+
if (modulePath.startsWith('_bmad/wds/') || name.startsWith('wds-')) return 'pipeline';
|
|
311
|
+
if (modulePath.includes('_bmad/bme/_vortex/')) return 'pipeline';
|
|
312
|
+
if (modulePath.includes('_bmad/bme/_gyre/')) return 'pipeline';
|
|
313
|
+
|
|
314
|
+
if (content) {
|
|
315
|
+
// Consumes prior artifacts
|
|
316
|
+
if (/\{implementation_artifacts\}\/[^}\s]*\.md/.test(content)) return 'pipeline';
|
|
317
|
+
// P4 (sp-1-2 review): tightened "invokes another skill" detection.
|
|
318
|
+
// Old regex matched any prose mentioning "Skill tool" — too broad.
|
|
319
|
+
// New regex requires:
|
|
320
|
+
// - An XML-style <Skill ...> activation tag, OR
|
|
321
|
+
// - A YAML/frontmatter `skill:` key with bmad- value, OR
|
|
322
|
+
// - An explicit `Skill tool` invocation phrase paired with a tool action verb
|
|
323
|
+
// (use|invoke|call|launch) within the same line
|
|
324
|
+
if (
|
|
325
|
+
/<Skill\b/.test(content) ||
|
|
326
|
+
/^\s*skill:\s*['"]?bmad-/im.test(content) ||
|
|
327
|
+
/(?:use|invoke|call|launch)[^.\n]{0,40}\bSkill tool\b/i.test(content)
|
|
328
|
+
) {
|
|
329
|
+
return 'pipeline';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Light-deps indicators (Tier 2)
|
|
334
|
+
if (content) {
|
|
335
|
+
if (/^templateFile:/m.test(content)) return 'light-deps';
|
|
336
|
+
// Match both absolute (_bmad/foo/templates/) and relative (../templates/, ./templates/) forms
|
|
337
|
+
if (/(?:^|[\s`'"(/])(?:\.{1,2}\/)?[\w/-]*templates\/[\w./-]+\.(?:md|yaml|json)/.test(content)) {
|
|
338
|
+
return 'light-deps';
|
|
339
|
+
}
|
|
340
|
+
if (/_bmad\/_memory\/[\w./-]+/.test(content)) return 'light-deps';
|
|
341
|
+
// Explicit "Load template:" directive
|
|
342
|
+
if (/Load template:/i.test(content)) return 'light-deps';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Tier 2 carve-out: bmad-generate-project-context
|
|
346
|
+
if (name === 'bmad-generate-project-context') return 'light-deps';
|
|
347
|
+
|
|
348
|
+
// Default: Tier 1
|
|
349
|
+
return 'standalone';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Extract exporter-essential dependencies from a skill's content.
|
|
354
|
+
* Returns a sorted, deduplicated array of dependency strings.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} name Skill name (excluded from its own deps)
|
|
357
|
+
* @param {string} content Concatenated skill source
|
|
358
|
+
* @param {Set<string>} validSkillNames Whitelist of canonical skill IDs (filters out hook scripts, link text, etc.)
|
|
359
|
+
*/
|
|
360
|
+
function extractDependencies(name, content, validSkillNames) {
|
|
361
|
+
if (!content) return [];
|
|
362
|
+
const deps = new Set();
|
|
363
|
+
|
|
364
|
+
// 1. Templates — match both absolute (_bmad/foo/templates/) and relative (../templates/) forms.
|
|
365
|
+
// We normalize relative paths to absolute repo paths only when we can — for now, just record
|
|
366
|
+
// the matched path as-is and let Story 1.3 validate resolution.
|
|
367
|
+
const absTemplateRegex = /(_bmad\/[^/\s]+\/templates\/[\w./-]+\.(?:md|yaml|json))/g;
|
|
368
|
+
let m;
|
|
369
|
+
while ((m = absTemplateRegex.exec(content)) !== null) {
|
|
370
|
+
deps.add(m[1]);
|
|
371
|
+
}
|
|
372
|
+
// Relative templates — we can't reliably resolve to absolute without knowing the source dir,
|
|
373
|
+
// so we record a relative-template marker. This is intentionally lossy until Story 1.3.
|
|
374
|
+
const relTemplateRegex = /(?:^|[\s`'"(])(\.{1,2}\/[\w/-]*templates\/[\w./-]+\.(?:md|yaml|json))/gm;
|
|
375
|
+
while ((m = relTemplateRegex.exec(content)) !== null) {
|
|
376
|
+
deps.add(m[1]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 2. Sidecars (_bmad/_memory/...)
|
|
380
|
+
const sidecarRegex = /(_bmad\/_memory\/[\w./-]+)/g;
|
|
381
|
+
while ((m = sidecarRegex.exec(content)) !== null) {
|
|
382
|
+
const cleaned = m[1].replace(/[.,;:`'")\]]+$/, '');
|
|
383
|
+
if (cleaned) deps.add(cleaned);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 3. Chained skills — match candidate patterns, then filter against the canonical
|
|
387
|
+
// set of valid skill names. This eliminates false positives from hook scripts
|
|
388
|
+
// (`bmad-speak`), markdown link text (`WDS-SPECIFICATION-PATTERN`), and
|
|
389
|
+
// hyphenation false-matches (`bmad-quick-dev-new-preview`).
|
|
390
|
+
//
|
|
391
|
+
// P4 (sp-1-2 review): also strip URL-context matches. The whitelist filter
|
|
392
|
+
// catches unknown names, but a URL like https://github.com/.../bmad-dev-story
|
|
393
|
+
// contains a real skill name. Remove URLs from the search space first.
|
|
394
|
+
const contentNoUrls = content.replace(/https?:\/\/\S+/g, ' ');
|
|
395
|
+
const skillRegex = /\b((?:bmad|wds)-[a-z][a-z0-9-]+)\b/g;
|
|
396
|
+
while ((m = skillRegex.exec(contentNoUrls)) !== null) {
|
|
397
|
+
const skillName = m[1];
|
|
398
|
+
if (skillName === name) continue; // self-reference
|
|
399
|
+
if (skillName === 'bmad-init') continue; // universal — handled centrally
|
|
400
|
+
if (validSkillNames && !validSkillNames.has(skillName)) continue; // not a real skill
|
|
401
|
+
deps.add(skillName);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return Array.from(deps).sort();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Classify a single skill row. Returns { tier, intent, dependencies } proposal.
|
|
409
|
+
*
|
|
410
|
+
* @param {string[]} row Row fields
|
|
411
|
+
* @param {string[]} header Column names
|
|
412
|
+
* @param {string} projectRoot Absolute project root
|
|
413
|
+
* @param {Set<string>} validSkillNames Whitelist of canonical skill IDs for dep filtering
|
|
414
|
+
*/
|
|
415
|
+
function classifyRow(row, header, projectRoot, validSkillNames) {
|
|
416
|
+
const idx = (col) => header.indexOf(col);
|
|
417
|
+
const name = row[idx('name')];
|
|
418
|
+
const modulePath = row[idx('path')];
|
|
419
|
+
|
|
420
|
+
const intent = classifyIntent(name, modulePath);
|
|
421
|
+
const content = readSkillContent(projectRoot, modulePath);
|
|
422
|
+
const tier = classifyTier(name, modulePath, content, intent);
|
|
423
|
+
|
|
424
|
+
// Persona-only agent skills are menu wrappers — they list other skills as
|
|
425
|
+
// menu options, but those are not exporter dependencies. The exporter only
|
|
426
|
+
// needs the persona file itself. Skip dep extraction for them.
|
|
427
|
+
const dependencies =
|
|
428
|
+
name in PERSONA_AGENT_INTENTS
|
|
429
|
+
? []
|
|
430
|
+
: extractDependencies(name, content, validSkillNames);
|
|
431
|
+
|
|
432
|
+
// P3 (sp-1-2 review): unknown intent must NOT silently default to
|
|
433
|
+
// meta-platform — that would mark new/custom skills as framework-internal
|
|
434
|
+
// without warning. Return null to signal "no proposal" so main() can
|
|
435
|
+
// preserve the existing value (if any) and route the row to BORDERLINE.md
|
|
436
|
+
// as a heuristic miss for human review.
|
|
437
|
+
return {
|
|
438
|
+
tier,
|
|
439
|
+
intent, // null when no heuristic matched — main() handles the miss
|
|
440
|
+
dependencies: dependencies.join(';'),
|
|
441
|
+
intentMatched: intent !== null,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// =============================================================================
|
|
446
|
+
// BORDERLINE.md GENERATION
|
|
447
|
+
// =============================================================================
|
|
448
|
+
|
|
449
|
+
function renderBorderlineMd(date, conflicts, ambiguous, misses) {
|
|
450
|
+
const lines = [
|
|
451
|
+
'# Portability Classification — Borderline Cases',
|
|
452
|
+
'',
|
|
453
|
+
`Generated by \`scripts/portability/classify-skills.js\` on ${date}.`,
|
|
454
|
+
'',
|
|
455
|
+
'## Manual Override Protected',
|
|
456
|
+
'',
|
|
457
|
+
];
|
|
458
|
+
if (conflicts.length === 0) {
|
|
459
|
+
lines.push('_None._', '');
|
|
460
|
+
} else {
|
|
461
|
+
lines.push('| Skill | Existing | Proposed | Reason |');
|
|
462
|
+
lines.push('|-------|----------|----------|--------|');
|
|
463
|
+
for (const c of conflicts) {
|
|
464
|
+
const existing = `tier=${c.existing.tier} intent=${c.existing.intent} deps=${c.existing.dependencies || '(empty)'}`;
|
|
465
|
+
const proposed = `tier=${c.proposed.tier} intent=${c.proposed.intent} deps=${c.proposed.dependencies || '(empty)'}`;
|
|
466
|
+
lines.push(`| \`${c.name}\` | ${existing} | ${proposed} | ${c.reason} |`);
|
|
467
|
+
}
|
|
468
|
+
lines.push('');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
lines.push('## Ambiguous (multiple plausible classifications)', '');
|
|
472
|
+
if (ambiguous.length === 0) {
|
|
473
|
+
lines.push('_None._', '');
|
|
474
|
+
} else {
|
|
475
|
+
lines.push('| Skill | Tier | Intent | Reason |');
|
|
476
|
+
lines.push('|-------|------|--------|--------|');
|
|
477
|
+
for (const a of ambiguous) {
|
|
478
|
+
lines.push(`| \`${a.name}\` | ${a.tier} | ${a.intent} | ${a.reason} |`);
|
|
479
|
+
}
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
lines.push('## Heuristic Misses (no clear match)', '');
|
|
484
|
+
if (misses.length === 0) {
|
|
485
|
+
lines.push('_None._', '');
|
|
486
|
+
} else {
|
|
487
|
+
lines.push('| Skill | Best guess | Confidence | Recommendation |');
|
|
488
|
+
lines.push('|-------|-----------|------------|----------------|');
|
|
489
|
+
for (const ms of misses) {
|
|
490
|
+
lines.push(`| \`${ms.name}\` | tier=${ms.tier} intent=${ms.intent} | low | Manual review needed |`);
|
|
491
|
+
}
|
|
492
|
+
lines.push('');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return lines.join('\n');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// =============================================================================
|
|
499
|
+
// MAIN
|
|
500
|
+
// =============================================================================
|
|
501
|
+
|
|
502
|
+
function main() {
|
|
503
|
+
const args = process.argv.slice(2);
|
|
504
|
+
const dryRun = args.includes('--dry-run');
|
|
505
|
+
const force = args.includes('--force');
|
|
506
|
+
|
|
507
|
+
const projectRoot = findProjectRoot();
|
|
508
|
+
const manifestPath = path.join(projectRoot, '_bmad', '_config', 'skill-manifest.csv');
|
|
509
|
+
const borderlinePath = path.join(
|
|
510
|
+
projectRoot,
|
|
511
|
+
'_bmad-output',
|
|
512
|
+
'planning-artifacts',
|
|
513
|
+
'portability-borderline.md'
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
console.log(`Reading manifest from ${path.relative(projectRoot, manifestPath)}`);
|
|
517
|
+
const { header, rows } = readManifest(manifestPath);
|
|
518
|
+
|
|
519
|
+
// Build the set of canonical skill names for dependency-extraction filtering.
|
|
520
|
+
const nameIdx = header.indexOf('name');
|
|
521
|
+
const validSkillNames = new Set(rows.map((r) => r[nameIdx]));
|
|
522
|
+
|
|
523
|
+
const tierIdx = header.indexOf('tier');
|
|
524
|
+
const intentIdx = header.indexOf('intent');
|
|
525
|
+
const depsIdx = header.indexOf('dependencies');
|
|
526
|
+
|
|
527
|
+
if (tierIdx < 0 || intentIdx < 0 || depsIdx < 0) {
|
|
528
|
+
console.error(
|
|
529
|
+
'ERROR: skill-manifest.csv is missing tier/intent/dependencies columns. Run sp-1-1 first.'
|
|
530
|
+
);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const conflicts = [];
|
|
535
|
+
const ambiguous = [];
|
|
536
|
+
const misses = [];
|
|
537
|
+
let writes = 0;
|
|
538
|
+
let skips = 0;
|
|
539
|
+
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
const proposal = classifyRow(row, header, projectRoot, validSkillNames);
|
|
542
|
+
const existing = {
|
|
543
|
+
tier: row[tierIdx] || '',
|
|
544
|
+
intent: row[intentIdx] || '',
|
|
545
|
+
dependencies: row[depsIdx] || '',
|
|
546
|
+
};
|
|
547
|
+
const rowName = row[header.indexOf('name')];
|
|
548
|
+
|
|
549
|
+
// P3 (sp-1-2 review): if heuristics couldn't determine intent, route to
|
|
550
|
+
// misses and preserve existing values (do not clobber with `meta-platform`).
|
|
551
|
+
if (!proposal.intentMatched) {
|
|
552
|
+
misses.push({
|
|
553
|
+
name: rowName,
|
|
554
|
+
tier: proposal.tier,
|
|
555
|
+
intent: existing.intent || '(none)',
|
|
556
|
+
dependencies: proposal.dependencies,
|
|
557
|
+
});
|
|
558
|
+
// Skip the write so the row keeps whatever value was there before.
|
|
559
|
+
// If the row was previously unclassified, it stays unclassified —
|
|
560
|
+
// visible in BORDERLINE.md and caught by the test that requires
|
|
561
|
+
// every row to have non-empty tier+intent.
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Conflict detection
|
|
566
|
+
const hasExistingClassification =
|
|
567
|
+
existing.tier !== '' && existing.intent !== '';
|
|
568
|
+
const differs =
|
|
569
|
+
existing.tier !== proposal.tier ||
|
|
570
|
+
existing.intent !== proposal.intent ||
|
|
571
|
+
existing.dependencies !== proposal.dependencies;
|
|
572
|
+
|
|
573
|
+
if (hasExistingClassification && differs && !force) {
|
|
574
|
+
// Manual override protected
|
|
575
|
+
conflicts.push({
|
|
576
|
+
name: rowName,
|
|
577
|
+
existing,
|
|
578
|
+
proposed: proposal,
|
|
579
|
+
reason: 'Existing values differ from heuristics',
|
|
580
|
+
});
|
|
581
|
+
skips++;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Apply
|
|
586
|
+
row[tierIdx] = proposal.tier;
|
|
587
|
+
row[intentIdx] = proposal.intent;
|
|
588
|
+
row[depsIdx] = proposal.dependencies;
|
|
589
|
+
|
|
590
|
+
if (differs) writes++;
|
|
591
|
+
|
|
592
|
+
if (force && hasExistingClassification && differs) {
|
|
593
|
+
conflicts.push({
|
|
594
|
+
name: rowName,
|
|
595
|
+
existing,
|
|
596
|
+
proposed: proposal,
|
|
597
|
+
reason: 'FORCED — manual override clobbered',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log(`Processed ${rows.length} rows: ${writes} updated, ${skips} preserved (manual overrides)`);
|
|
603
|
+
|
|
604
|
+
if (dryRun) {
|
|
605
|
+
console.log('\n--- DRY RUN — preview ---');
|
|
606
|
+
for (const row of rows) {
|
|
607
|
+
const name = row[header.indexOf('name')];
|
|
608
|
+
console.log(
|
|
609
|
+
` ${name.padEnd(50)} tier=${row[tierIdx].padEnd(11)} intent=${row[intentIdx].padEnd(22)} deps=${row[depsIdx] || '(empty)'}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
console.log(`\nDry run complete. ${conflicts.length} conflicts, ${misses.length} heuristic misses.`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Write manifest
|
|
617
|
+
writeManifest(manifestPath, header, rows);
|
|
618
|
+
console.log(`Wrote ${path.relative(projectRoot, manifestPath)}`);
|
|
619
|
+
|
|
620
|
+
// Write borderline file
|
|
621
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
622
|
+
const md = renderBorderlineMd(today, conflicts, ambiguous, misses);
|
|
623
|
+
fs.mkdirSync(path.dirname(borderlinePath), { recursive: true });
|
|
624
|
+
fs.writeFileSync(borderlinePath, md, 'utf8');
|
|
625
|
+
console.log(`Wrote ${path.relative(projectRoot, borderlinePath)}`);
|
|
626
|
+
|
|
627
|
+
console.log(
|
|
628
|
+
`\nSummary: ${writes} updated, ${skips} preserved, ${conflicts.length} conflicts, ${misses.length} heuristic misses.`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (require.main === module) {
|
|
633
|
+
main();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
module.exports = {
|
|
637
|
+
classifyIntent,
|
|
638
|
+
classifyTier,
|
|
639
|
+
extractDependencies,
|
|
640
|
+
classifyRow,
|
|
641
|
+
readSkillContent,
|
|
642
|
+
// Constants exported for tests
|
|
643
|
+
META_PLATFORM_SKILLS,
|
|
644
|
+
VALID_TIERS,
|
|
645
|
+
VALID_INTENTS,
|
|
646
|
+
};
|