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,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
+ };