convoke-agents 3.0.4 → 3.2.0

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 (92) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +14 -13
  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/_gyre/guides/GYRE-TEAM-GUIDE.md +506 -0
  16. package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
  17. package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
  18. package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
  19. package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
  20. package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
  21. package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
  22. package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
  23. package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
  24. package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
  25. package/_bmad/bme/_team-factory/config.yaml +13 -0
  26. package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
  27. package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
  28. package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
  29. package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
  30. package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
  31. package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
  32. package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
  33. package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
  34. package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
  35. package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
  36. package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
  37. package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
  38. package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
  39. package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
  40. package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
  41. package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
  42. package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
  43. package/_bmad/bme/_team-factory/module-help.csv +3 -0
  44. package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
  45. package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
  46. package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
  47. package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
  48. package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
  49. package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
  50. package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
  51. package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
  52. package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
  53. package/_bmad/bme/_vortex/config.yaml +4 -4
  54. package/_bmad/bme/_vortex/guides/VORTEX-TEAM-GUIDE.md +441 -0
  55. package/package.json +17 -8
  56. package/scripts/archive.js +26 -45
  57. package/scripts/convoke-check.js +88 -0
  58. package/scripts/convoke-doctor.js +303 -4
  59. package/scripts/install-gyre-agents.js +0 -0
  60. package/scripts/lib/artifact-utils.js +2182 -0
  61. package/scripts/lib/portfolio/formatters/markdown-formatter.js +40 -0
  62. package/scripts/lib/portfolio/formatters/terminal-formatter.js +56 -0
  63. package/scripts/lib/portfolio/portfolio-engine.js +572 -0
  64. package/scripts/lib/portfolio/rules/artifact-chain-rule.js +156 -0
  65. package/scripts/lib/portfolio/rules/conflict-resolver.js +99 -0
  66. package/scripts/lib/portfolio/rules/frontmatter-rule.js +42 -0
  67. package/scripts/lib/portfolio/rules/git-recency-rule.js +69 -0
  68. package/scripts/lib/types.js +122 -0
  69. package/scripts/migrate-artifacts.js +439 -0
  70. package/scripts/portability/catalog-generator.js +353 -0
  71. package/scripts/portability/classify-skills.js +646 -0
  72. package/scripts/portability/convoke-export.js +522 -0
  73. package/scripts/portability/export-engine.js +1133 -0
  74. package/scripts/portability/generate-adapters.js +79 -0
  75. package/scripts/portability/manifest-csv.js +147 -0
  76. package/scripts/portability/seed-catalog-repo.js +427 -0
  77. package/scripts/portability/templates/canonical-example.md +102 -0
  78. package/scripts/portability/templates/canonical-format.md +218 -0
  79. package/scripts/portability/templates/readme-template.md +72 -0
  80. package/scripts/portability/test-constants.js +42 -0
  81. package/scripts/portability/validate-classification.js +529 -0
  82. package/scripts/portability/validate-exports.js +348 -0
  83. package/scripts/update/lib/agent-registry.js +35 -0
  84. package/scripts/update/lib/config-merger.js +140 -10
  85. package/scripts/update/lib/migration-runner.js +1 -1
  86. package/scripts/update/lib/refresh-installation.js +293 -8
  87. package/scripts/update/lib/taxonomy-merger.js +138 -0
  88. package/scripts/update/lib/utils.js +27 -1
  89. package/scripts/update/lib/validator.js +114 -4
  90. package/scripts/update/migrations/2.0.x-to-3.1.0.js +50 -0
  91. package/scripts/update/migrations/3.0.x-to-3.1.0.js +41 -0
  92. package/scripts/update/migrations/registry.js +14 -0
@@ -0,0 +1,898 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+ const { verifyRequire, buildExportNames } = require('../writers/registry-writer');
7
+ const { parseCsvRow } = require('../utils/csv-utils');
8
+
9
+ /** @typedef {import('../types/factory-types').E2EValidationResult} E2EValidationResult */
10
+ /** @typedef {import('../types/factory-types').E2ECheck} E2ECheck */
11
+
12
+ /**
13
+ * Run end-to-end validation on a factory-created team.
14
+ *
15
+ * Checks: structural (config, csv, agents, workflows, contracts),
16
+ * wiring (registry, activation), and regression (registry require, Vortex validation).
17
+ *
18
+ * @param {Object} specData - Parsed team spec
19
+ * @param {Object} generationContext - Context from Step 4
20
+ * @param {string} projectRoot - Absolute path to project root
21
+ * @returns {Promise<E2EValidationResult>}
22
+ */
23
+ async function validateTeam(specData, generationContext, projectRoot) {
24
+ const checks = [];
25
+ const errors = [];
26
+
27
+ // --- Structural checks ---
28
+ checks.push(...checkConfig(generationContext));
29
+ checks.push(...checkCsv(generationContext, specData));
30
+ checks.push(...checkAgentFiles(generationContext));
31
+ checks.push(...checkWorkflowDirs(generationContext));
32
+ checks.push(...checkContractFiles(generationContext));
33
+
34
+ // --- Wiring checks ---
35
+ checks.push(checkRegistryWiring(generationContext));
36
+ checks.push(checkActivation(generationContext));
37
+
38
+ // --- Regression checks ---
39
+ checks.push(checkRegistryRegression(projectRoot));
40
+ checks.push(await checkVortexRegression(projectRoot));
41
+
42
+ const valid = checks.every(c => c.passed);
43
+ if (!valid) {
44
+ for (const c of checks) {
45
+ if (!c.passed) {
46
+ errors.push(`${c.name}: expected ${c.expected || '(pass)'}, got ${c.actual || '(fail)'}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ return { valid, checks, errors };
52
+ }
53
+
54
+ // ── Structural checks ────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Check config.yaml: exists, parseable, has required fields.
58
+ * @param {Object} ctx - Generation context
59
+ * @returns {E2ECheck[]}
60
+ */
61
+ function checkConfig(ctx) {
62
+ const checks = [];
63
+ const configPath = ctx.config_yaml_path;
64
+
65
+ // EXISTS
66
+ const exists = configPath && fs.existsSync(configPath);
67
+ checks.push({
68
+ name: 'CONFIG-EXISTS',
69
+ stepName: 'structural',
70
+ passed: !!exists,
71
+ expected: 'file exists',
72
+ actual: exists ? 'file exists' : 'file not found',
73
+ detail: configPath || '(no path provided)',
74
+ });
75
+
76
+ if (!exists) return checks;
77
+
78
+ // PARSEABLE
79
+ let config;
80
+ try {
81
+ config = yaml.load(fs.readFileSync(configPath, 'utf8'));
82
+ checks.push({
83
+ name: 'CONFIG-PARSEABLE',
84
+ stepName: 'structural',
85
+ passed: true,
86
+ detail: configPath,
87
+ });
88
+ } catch (err) {
89
+ checks.push({
90
+ name: 'CONFIG-PARSEABLE',
91
+ stepName: 'structural',
92
+ passed: false,
93
+ expected: 'valid YAML',
94
+ actual: `parse error: ${err.message}`,
95
+ detail: configPath,
96
+ });
97
+ return checks;
98
+ }
99
+
100
+ // REQUIRED FIELDS
101
+ const required = ['submodule_name', 'module', 'agents', 'workflows'];
102
+ const missing = required.filter(f => !config[f]);
103
+ checks.push({
104
+ name: 'CONFIG-REQUIRED-FIELDS',
105
+ stepName: 'structural',
106
+ passed: missing.length === 0,
107
+ expected: required.join(', '),
108
+ actual: missing.length === 0 ? 'all present' : `missing: ${missing.join(', ')}`,
109
+ detail: configPath,
110
+ });
111
+
112
+ return checks;
113
+ }
114
+
115
+ /**
116
+ * Check module-help.csv: exists, correct header, correct row count.
117
+ * @param {Object} ctx - Generation context
118
+ * @param {Object} specData - Team spec
119
+ * @returns {E2ECheck[]}
120
+ */
121
+ function checkCsv(ctx, specData) {
122
+ const checks = [];
123
+ const csvPath = ctx.module_help_csv_path;
124
+
125
+ // EXISTS
126
+ const exists = csvPath && fs.existsSync(csvPath);
127
+ checks.push({
128
+ name: 'CSV-EXISTS',
129
+ stepName: 'structural',
130
+ passed: !!exists,
131
+ expected: 'file exists',
132
+ actual: exists ? 'file exists' : 'file not found',
133
+ detail: csvPath || '(no path provided)',
134
+ });
135
+
136
+ if (!exists) return checks;
137
+
138
+ const content = fs.readFileSync(csvPath, 'utf8');
139
+ const lines = content.trim().split('\n');
140
+
141
+ // HEADER
142
+ const expectedHeader = 'module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,';
143
+ const actualHeader = lines[0] || '';
144
+ checks.push({
145
+ name: 'CSV-HEADER',
146
+ stepName: 'structural',
147
+ passed: actualHeader.trim() === expectedHeader,
148
+ expected: expectedHeader,
149
+ actual: actualHeader.trim(),
150
+ detail: csvPath,
151
+ });
152
+
153
+ // ROW COUNT (data rows = agent count)
154
+ const dataRows = lines.length - 1; // subtract header
155
+ const agentCount = (specData.agents || []).length;
156
+ checks.push({
157
+ name: 'CSV-ROW-COUNT',
158
+ stepName: 'structural',
159
+ passed: dataRows === agentCount,
160
+ expected: `${agentCount} rows`,
161
+ actual: `${dataRows} rows`,
162
+ detail: csvPath,
163
+ });
164
+
165
+ return checks;
166
+ }
167
+
168
+ /**
169
+ * Check all agent files exist.
170
+ * @param {Object} ctx
171
+ * @returns {E2ECheck[]}
172
+ */
173
+ function checkAgentFiles(ctx) {
174
+ const checks = [];
175
+ for (const agentFile of (ctx.agent_files || [])) {
176
+ const exists = fs.existsSync(agentFile);
177
+ checks.push({
178
+ name: 'AGENT-FILE-EXISTS',
179
+ stepName: 'structural',
180
+ passed: exists,
181
+ expected: 'file exists',
182
+ actual: exists ? 'file exists' : 'file not found',
183
+ detail: agentFile,
184
+ });
185
+ }
186
+ return checks;
187
+ }
188
+
189
+ /**
190
+ * Check all workflow directories exist.
191
+ * @param {Object} ctx
192
+ * @returns {E2ECheck[]}
193
+ */
194
+ function checkWorkflowDirs(ctx) {
195
+ const checks = [];
196
+ for (const wfDir of (ctx.workflow_dirs || [])) {
197
+ const exists = fs.existsSync(wfDir);
198
+ checks.push({
199
+ name: 'WORKFLOW-DIR-EXISTS',
200
+ stepName: 'structural',
201
+ passed: exists,
202
+ expected: 'directory exists',
203
+ actual: exists ? 'directory exists' : 'directory not found',
204
+ detail: wfDir,
205
+ });
206
+ }
207
+ return checks;
208
+ }
209
+
210
+ /**
211
+ * Check all contract files exist.
212
+ * @param {Object} ctx
213
+ * @returns {E2ECheck[]}
214
+ */
215
+ function checkContractFiles(ctx) {
216
+ const checks = [];
217
+ for (const contractFile of (ctx.contract_files || [])) {
218
+ const exists = fs.existsSync(contractFile);
219
+ checks.push({
220
+ name: 'CONTRACT-FILE-EXISTS',
221
+ stepName: 'structural',
222
+ passed: exists,
223
+ expected: 'file exists',
224
+ actual: exists ? 'file exists' : 'file not found',
225
+ detail: contractFile,
226
+ });
227
+ }
228
+ return checks;
229
+ }
230
+
231
+ // ── Wiring checks ────────────────────────────────────────────────────
232
+
233
+ /**
234
+ * Check registry wiring result from Step 4.
235
+ * @param {Object} ctx
236
+ * @returns {E2ECheck}
237
+ */
238
+ function checkRegistryWiring(ctx) {
239
+ const result = ctx.registry_wiring_result || {};
240
+ const expectedCount = buildExportNames('X').length; // derive from canonical source
241
+ const passed = result.success === true && Array.isArray(result.written) && result.written.length === expectedCount;
242
+ return {
243
+ name: 'REGISTRY-WIRING',
244
+ stepName: 'wiring',
245
+ passed,
246
+ expected: `success with ${expectedCount} exports`,
247
+ actual: result.success ? `success with ${(result.written || []).length} exports` : `failed: ${(result.errors || []).join(', ')}`,
248
+ detail: result.skipped && result.skipped.length > 0 ? `skipped: ${result.skipped.join(', ')}` : undefined,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Check activation validation result from Step 4.
254
+ * @param {Object} ctx
255
+ * @returns {E2ECheck}
256
+ */
257
+ function checkActivation(ctx) {
258
+ const result = ctx.activation_validation_results || {};
259
+ return {
260
+ name: 'ACTIVATION-VALID',
261
+ stepName: 'wiring',
262
+ passed: result.valid === true,
263
+ expected: 'valid',
264
+ actual: result.valid === true ? 'valid' : 'invalid',
265
+ detail: result.valid === true ? undefined : JSON.stringify(result.results || []),
266
+ };
267
+ }
268
+
269
+ // ── Regression checks ────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Verify agent-registry.js still loads via require().
273
+ * Reuses verifyRequire from registry-writer.js.
274
+ * @param {string} projectRoot
275
+ * @returns {E2ECheck}
276
+ */
277
+ function checkRegistryRegression(projectRoot) {
278
+ const registryPath = path.join(projectRoot, 'scripts/update/lib/agent-registry.js');
279
+ const error = verifyRequire(registryPath);
280
+ return {
281
+ name: 'REGISTRY-REGRESSION',
282
+ stepName: 'regression',
283
+ passed: error === null,
284
+ expected: 'require() succeeds',
285
+ actual: error === null ? 'require() succeeds' : error,
286
+ detail: registryPath,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Run existing Vortex validateInstallation() to confirm native team still passes.
292
+ * @param {string} projectRoot
293
+ * @returns {Promise<E2ECheck>}
294
+ */
295
+ async function checkVortexRegression(projectRoot) {
296
+ const validatorPath = path.join(projectRoot, 'scripts/update/lib/validator.js');
297
+ if (!fs.existsSync(validatorPath)) {
298
+ return {
299
+ name: 'VORTEX-REGRESSION',
300
+ stepName: 'regression',
301
+ passed: false,
302
+ expected: 'validator.js exists',
303
+ actual: `validator.js not found at ${validatorPath}`,
304
+ detail: validatorPath,
305
+ };
306
+ }
307
+ try {
308
+ const { validateInstallation } = require(validatorPath);
309
+ const result = await validateInstallation({}, projectRoot);
310
+ const failedChecks = (result.checks || []).filter(c => !c.passed);
311
+ return {
312
+ name: 'VORTEX-REGRESSION',
313
+ stepName: 'regression',
314
+ passed: result.valid === true,
315
+ expected: 'all Vortex checks pass',
316
+ actual: result.valid ? 'all Vortex checks pass' : `${failedChecks.length} check(s) failed: ${failedChecks.map(c => c.name).join(', ')}`,
317
+ detail: validatorPath,
318
+ };
319
+ } catch (err) {
320
+ return {
321
+ name: 'VORTEX-REGRESSION',
322
+ stepName: 'regression',
323
+ passed: false,
324
+ expected: 'validateInstallation() succeeds',
325
+ actual: `error: ${err.message}`,
326
+ detail: path.join(projectRoot, 'scripts/update/lib/validator.js'),
327
+ };
328
+ }
329
+ }
330
+
331
+ // ── Extension validation ──────────────────────────────────────────────
332
+
333
+ /**
334
+ * Run end-to-end validation on an agent extension (add-agent-to-existing-team).
335
+ *
336
+ * Checks: append results (registry, config, CSV), structural (new agent files,
337
+ * new workflow dirs), regression (existing agents unchanged, registry require).
338
+ *
339
+ * @param {Object} extensionContext - Context from the add-agent workflow
340
+ * @param {string} extensionContext.new_agent_id - New agent ID
341
+ * @param {string[]} extensionContext.existing_agent_ids - Existing agent IDs to verify unchanged
342
+ * @param {string[]} extensionContext.new_agent_files - New agent .md file paths
343
+ * @param {string[]} extensionContext.new_workflow_dirs - New workflow directory paths
344
+ * @param {string[]} [extensionContext.new_contract_files] - New contract file paths
345
+ * @param {string} extensionContext.config_yaml_path - Path to config.yaml
346
+ * @param {string} extensionContext.module_help_csv_path - Path to module-help.csv
347
+ * @param {Object} extensionContext.registry_append_result - Result from appendAgentToBlock
348
+ * @param {Object} extensionContext.config_append_result - Result from appendConfigAgent
349
+ * @param {Object} extensionContext.csv_append_result - Result from appendCsvRow
350
+ * @param {string} projectRoot - Absolute path to project root
351
+ * @returns {Promise<E2EValidationResult>}
352
+ */
353
+ async function validateExtension(extensionContext, projectRoot) {
354
+ const checks = [];
355
+ const errors = [];
356
+
357
+ // --- Append result checks ---
358
+ checks.push(checkRegistryAppend(extensionContext));
359
+ checks.push(checkConfigAppend(extensionContext));
360
+ checks.push(checkCsvAppend(extensionContext));
361
+
362
+ // --- Structural checks for new files ---
363
+ checks.push(...checkNewAgentFiles(extensionContext));
364
+ checks.push(...checkNewWorkflowDirs(extensionContext));
365
+ checks.push(...checkNewContractFiles(extensionContext));
366
+
367
+ // --- Extension regression: existing agents unchanged ---
368
+ checks.push(checkExistingAgentsRegistry(extensionContext, projectRoot));
369
+ checks.push(checkExistingAgentsConfig(extensionContext));
370
+ checks.push(checkExistingAgentsCsv(extensionContext));
371
+
372
+ // --- Standard regression ---
373
+ checks.push(checkRegistryRegression(projectRoot));
374
+ checks.push(await checkVortexRegression(projectRoot));
375
+
376
+ const valid = checks.every(c => c.passed);
377
+ if (!valid) {
378
+ for (const c of checks) {
379
+ if (!c.passed) {
380
+ errors.push(`${c.name}: expected ${c.expected || '(pass)'}, got ${c.actual || '(fail)'}`);
381
+ }
382
+ }
383
+ }
384
+
385
+ return { valid, checks, errors };
386
+ }
387
+
388
+ /**
389
+ * Check registry append result.
390
+ * @param {Object} ctx
391
+ * @returns {E2ECheck}
392
+ */
393
+ function checkRegistryAppend(ctx) {
394
+ const result = ctx.registry_append_result || {};
395
+ return {
396
+ name: 'AGENT-REGISTRY-APPEND',
397
+ stepName: 'extension',
398
+ passed: result.success === true && Array.isArray(result.written) && result.written.includes(ctx.new_agent_id),
399
+ expected: `success with ${ctx.new_agent_id} written`,
400
+ actual: result.success ? `written: ${(result.written || []).join(', ')}` : `failed: ${(result.errors || []).join(', ')}`,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Check config append result.
406
+ * @param {Object} ctx
407
+ * @returns {E2ECheck}
408
+ */
409
+ function checkConfigAppend(ctx) {
410
+ const result = ctx.config_append_result || {};
411
+ return {
412
+ name: 'CONFIG-APPEND',
413
+ stepName: 'extension',
414
+ passed: result.success === true,
415
+ expected: 'success',
416
+ actual: result.success ? 'success' : `failed: ${(result.errors || []).join(', ')}`,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Check CSV append result.
422
+ * @param {Object} ctx
423
+ * @returns {E2ECheck}
424
+ */
425
+ function checkCsvAppend(ctx) {
426
+ const result = ctx.csv_append_result || {};
427
+ return {
428
+ name: 'CSV-APPEND',
429
+ stepName: 'extension',
430
+ passed: result.success === true,
431
+ expected: 'success',
432
+ actual: result.success ? `success (${result.rowCount} rows)` : `failed: ${(result.errors || []).join(', ')}`,
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Check new agent files exist.
438
+ * @param {Object} ctx
439
+ * @returns {E2ECheck[]}
440
+ */
441
+ function checkNewAgentFiles(ctx) {
442
+ const checks = [];
443
+ for (const agentFile of (ctx.new_agent_files || [])) {
444
+ const exists = fs.existsSync(agentFile);
445
+ checks.push({
446
+ name: 'AGENT-FILE-EXISTS',
447
+ stepName: 'extension',
448
+ passed: exists,
449
+ expected: 'file exists',
450
+ actual: exists ? 'file exists' : 'file not found',
451
+ detail: agentFile,
452
+ });
453
+ }
454
+ return checks;
455
+ }
456
+
457
+ /**
458
+ * Check new workflow directories exist.
459
+ * @param {Object} ctx
460
+ * @returns {E2ECheck[]}
461
+ */
462
+ function checkNewWorkflowDirs(ctx) {
463
+ const checks = [];
464
+ for (const wfDir of (ctx.new_workflow_dirs || [])) {
465
+ const exists = fs.existsSync(wfDir);
466
+ checks.push({
467
+ name: 'WORKFLOW-DIR-EXISTS',
468
+ stepName: 'extension',
469
+ passed: exists,
470
+ expected: 'directory exists',
471
+ actual: exists ? 'directory exists' : 'directory not found',
472
+ detail: wfDir,
473
+ });
474
+ }
475
+ return checks;
476
+ }
477
+
478
+ /**
479
+ * Check new contract files exist.
480
+ * @param {Object} ctx
481
+ * @returns {E2ECheck[]}
482
+ */
483
+ function checkNewContractFiles(ctx) {
484
+ const checks = [];
485
+ for (const contractFile of (ctx.new_contract_files || [])) {
486
+ const exists = fs.existsSync(contractFile);
487
+ checks.push({
488
+ name: 'CONTRACT-FILE-EXISTS',
489
+ stepName: 'extension',
490
+ passed: exists,
491
+ expected: 'file exists',
492
+ actual: exists ? 'file exists' : 'file not found',
493
+ detail: contractFile,
494
+ });
495
+ }
496
+ return checks;
497
+ }
498
+
499
+ /**
500
+ * Verify existing agents still present in agent-registry.js.
501
+ * @param {Object} ctx
502
+ * @param {string} projectRoot
503
+ * @returns {E2ECheck}
504
+ */
505
+ function checkExistingAgentsRegistry(ctx, projectRoot) {
506
+ const registryPath = path.join(projectRoot, 'scripts/update/lib/agent-registry.js');
507
+ if (!fs.existsSync(registryPath)) {
508
+ return {
509
+ name: 'EXISTING-AGENTS-REGISTRY',
510
+ stepName: 'extension-regression',
511
+ passed: false,
512
+ expected: 'existing agents preserved in registry',
513
+ actual: 'agent-registry.js not found',
514
+ };
515
+ }
516
+
517
+ const content = fs.readFileSync(registryPath, 'utf8');
518
+ const existingIds = (ctx.existing_agent_ids || []);
519
+
520
+ // If none of the existing agents appear in the registry, they are team-local only — skip gracefully
521
+ if (existingIds.length > 0 && existingIds.every(id => !content.includes(`id: '${id}'`))) {
522
+ return {
523
+ name: 'EXISTING-AGENTS-REGISTRY',
524
+ stepName: 'extension-regression',
525
+ passed: true,
526
+ expected: 'existing agents preserved in registry',
527
+ actual: 'existing agents not in registry scope (team-local only)',
528
+ };
529
+ }
530
+
531
+ // Check each existing agent that IS in the registry — it must still be there
532
+ const missing = existingIds.filter(id => !content.includes(`id: '${id}'`));
533
+ return {
534
+ name: 'EXISTING-AGENTS-REGISTRY',
535
+ stepName: 'extension-regression',
536
+ passed: missing.length === 0,
537
+ expected: 'existing agents preserved in registry',
538
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
539
+ };
540
+ }
541
+
542
+ /**
543
+ * Verify existing agents still present in config.yaml.
544
+ * @param {Object} ctx
545
+ * @returns {E2ECheck}
546
+ */
547
+ function checkExistingAgentsConfig(ctx) {
548
+ const configPath = ctx.config_yaml_path;
549
+ if (!configPath || !fs.existsSync(configPath)) {
550
+ return {
551
+ name: 'EXISTING-AGENTS-CONFIG',
552
+ stepName: 'extension-regression',
553
+ passed: false,
554
+ expected: 'existing agents preserved',
555
+ actual: 'config.yaml not found',
556
+ };
557
+ }
558
+
559
+ let config;
560
+ try {
561
+ config = yaml.load(fs.readFileSync(configPath, 'utf8'));
562
+ } catch {
563
+ return {
564
+ name: 'EXISTING-AGENTS-CONFIG',
565
+ stepName: 'extension-regression',
566
+ passed: false,
567
+ expected: 'existing agents preserved',
568
+ actual: 'cannot parse config.yaml',
569
+ };
570
+ }
571
+
572
+ const agents = config.agents || [];
573
+ const missing = (ctx.existing_agent_ids || []).filter(id => !agents.includes(id));
574
+ return {
575
+ name: 'EXISTING-AGENTS-CONFIG',
576
+ stepName: 'extension-regression',
577
+ passed: missing.length === 0,
578
+ expected: 'existing agents preserved',
579
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
580
+ };
581
+ }
582
+
583
+ /**
584
+ * Verify existing agents still present in module-help.csv.
585
+ * @param {Object} ctx
586
+ * @returns {E2ECheck}
587
+ */
588
+ function checkExistingAgentsCsv(ctx) {
589
+ const csvPath = ctx.module_help_csv_path;
590
+ if (!csvPath || !fs.existsSync(csvPath)) {
591
+ return {
592
+ name: 'EXISTING-AGENTS-CSV',
593
+ stepName: 'extension-regression',
594
+ passed: false,
595
+ expected: 'existing agent rows preserved',
596
+ actual: 'module-help.csv not found',
597
+ };
598
+ }
599
+
600
+ const content = fs.readFileSync(csvPath, 'utf8');
601
+ const lines = content.trim().split('\n').slice(1); // skip header
602
+ // Extract agent column (index 8) from each row using RFC 4180-aware parser.
603
+ const agentIds = lines.map(line => {
604
+ const cols = parseCsvRow(line);
605
+ return cols[8] || '';
606
+ });
607
+ const missing = (ctx.existing_agent_ids || []).filter(id => !agentIds.includes(id));
608
+ return {
609
+ name: 'EXISTING-AGENTS-CSV',
610
+ stepName: 'extension-regression',
611
+ passed: missing.length === 0,
612
+ expected: 'existing agent rows preserved',
613
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
614
+ };
615
+ }
616
+
617
+ // ── Skill extension validation ────────────────────────────────────────
618
+
619
+ /**
620
+ * Run end-to-end validation on a skill/workflow extension (add-skill-to-existing-agent).
621
+ *
622
+ * Checks: append results (registry workflow, config workflow, CSV), structural (workflow files),
623
+ * regression (existing workflows unchanged, registry require).
624
+ *
625
+ * @param {Object} skillContext - Context from the add-skill workflow
626
+ * @param {string} skillContext.new_workflow_name - New workflow name
627
+ * @param {string} skillContext.agent_id - Target agent ID
628
+ * @param {string[]} skillContext.existing_workflow_names - Existing workflow names to verify unchanged
629
+ * @param {string[]} skillContext.new_workflow_files - New workflow file paths (workflow.md, template)
630
+ * @param {string} skillContext.config_yaml_path - Path to config.yaml
631
+ * @param {string} skillContext.module_help_csv_path - Path to module-help.csv
632
+ * @param {Object} skillContext.registry_append_result - Result from appendWorkflowToBlock
633
+ * @param {Object} skillContext.config_append_result - Result from appendConfigWorkflow
634
+ * @param {Object} skillContext.csv_append_result - Result from appendCsvRow
635
+ * @param {string} projectRoot - Absolute path to project root
636
+ * @returns {Promise<E2EValidationResult>}
637
+ */
638
+ async function validateSkillExtension(skillContext, projectRoot) {
639
+ const checks = [];
640
+ const errors = [];
641
+
642
+ // --- Append result checks ---
643
+ checks.push(checkWorkflowRegistryAppend(skillContext));
644
+ checks.push(checkConfigWorkflowAppend(skillContext));
645
+ checks.push(checkCsvWorkflowAppend(skillContext));
646
+
647
+ // --- Structural checks for new files ---
648
+ checks.push(...checkNewWorkflowFiles(skillContext));
649
+
650
+ // --- Activation menu check ---
651
+ checks.push(checkActivationMenuUpdated(skillContext));
652
+
653
+ // --- Extension regression: existing workflows unchanged ---
654
+ checks.push(checkExistingWorkflowsRegistry(skillContext, projectRoot));
655
+ checks.push(checkExistingWorkflowsConfig(skillContext));
656
+ checks.push(checkExistingWorkflowsCsv(skillContext));
657
+
658
+ // --- Standard regression ---
659
+ checks.push(checkRegistryRegression(projectRoot));
660
+ checks.push(await checkVortexRegression(projectRoot));
661
+
662
+ const valid = checks.every(c => c.passed);
663
+ if (!valid) {
664
+ for (const c of checks) {
665
+ if (!c.passed) {
666
+ errors.push(`${c.name}: expected ${c.expected || '(pass)'}, got ${c.actual || '(fail)'}`);
667
+ }
668
+ }
669
+ }
670
+
671
+ return { valid, checks, errors };
672
+ }
673
+
674
+ /**
675
+ * Check registry workflow append result.
676
+ * @param {Object} ctx
677
+ * @returns {E2ECheck}
678
+ */
679
+ function checkWorkflowRegistryAppend(ctx) {
680
+ const result = ctx.registry_append_result || {};
681
+ return {
682
+ name: 'WORKFLOW-REGISTRY-APPEND',
683
+ stepName: 'skill-extension',
684
+ passed: result.success === true && Array.isArray(result.written) && result.written.includes(ctx.new_workflow_name),
685
+ expected: `success with ${ctx.new_workflow_name} written`,
686
+ actual: result.success ? `written: ${(result.written || []).join(', ')}` : `failed: ${(result.errors || []).join(', ')}`,
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Check config workflow append result.
692
+ * @param {Object} ctx
693
+ * @returns {E2ECheck}
694
+ */
695
+ function checkConfigWorkflowAppend(ctx) {
696
+ const result = ctx.config_append_result || {};
697
+ return {
698
+ name: 'CONFIG-WORKFLOW-APPEND',
699
+ stepName: 'skill-extension',
700
+ passed: result.success === true,
701
+ expected: 'success',
702
+ actual: result.success ? 'success' : `failed: ${(result.errors || []).join(', ')}`,
703
+ };
704
+ }
705
+
706
+ /**
707
+ * Check CSV workflow append result.
708
+ * @param {Object} ctx
709
+ * @returns {E2ECheck}
710
+ */
711
+ function checkCsvWorkflowAppend(ctx) {
712
+ const result = ctx.csv_append_result || {};
713
+ return {
714
+ name: 'CSV-WORKFLOW-APPEND',
715
+ stepName: 'skill-extension',
716
+ passed: result.success === true,
717
+ expected: 'success',
718
+ actual: result.success ? `success (${result.rowCount} rows)` : `failed: ${(result.errors || []).join(', ')}`,
719
+ };
720
+ }
721
+
722
+ /**
723
+ * Check new workflow files exist on disk.
724
+ * @param {Object} ctx
725
+ * @returns {E2ECheck[]}
726
+ */
727
+ function checkNewWorkflowFiles(ctx) {
728
+ const checks = [];
729
+ for (const wfFile of (ctx.new_workflow_files || [])) {
730
+ const exists = fs.existsSync(wfFile);
731
+ checks.push({
732
+ name: 'WORKFLOW-FILE-EXISTS',
733
+ stepName: 'skill-extension',
734
+ passed: exists,
735
+ expected: 'file exists',
736
+ actual: exists ? 'file exists' : 'file not found',
737
+ detail: wfFile,
738
+ });
739
+ }
740
+ return checks;
741
+ }
742
+
743
+ /**
744
+ * Check that the agent's activation menu contains a new <item> for the workflow.
745
+ * @param {Object} ctx
746
+ * @returns {E2ECheck}
747
+ */
748
+ function checkActivationMenuUpdated(ctx) {
749
+ const agentPath = ctx.agent_file_path;
750
+ if (!agentPath || !fs.existsSync(agentPath)) {
751
+ return {
752
+ name: 'ACTIVATION-MENU-UPDATED',
753
+ stepName: 'skill-extension',
754
+ passed: false,
755
+ expected: 'agent .md contains new <item> in <menu>',
756
+ actual: 'agent file not found',
757
+ };
758
+ }
759
+
760
+ const content = fs.readFileSync(agentPath, 'utf8');
761
+ const workflowName = ctx.new_workflow_name || '';
762
+ const hasItem = content.includes(`workflows/${workflowName}/`) && content.includes('<item');
763
+ return {
764
+ name: 'ACTIVATION-MENU-UPDATED',
765
+ stepName: 'skill-extension',
766
+ passed: hasItem,
767
+ expected: `<item> with workflows/${workflowName}/ in agent menu`,
768
+ actual: hasItem ? 'found' : 'new workflow item not found in activation menu',
769
+ };
770
+ }
771
+
772
+ /**
773
+ * Verify existing workflows still present in agent-registry.js.
774
+ * @param {Object} ctx
775
+ * @param {string} projectRoot
776
+ * @returns {E2ECheck}
777
+ */
778
+ function checkExistingWorkflowsRegistry(ctx, projectRoot) {
779
+ const registryPath = path.join(projectRoot, 'scripts/update/lib/agent-registry.js');
780
+ if (!fs.existsSync(registryPath)) {
781
+ return {
782
+ name: 'EXISTING-WORKFLOWS-REGISTRY',
783
+ stepName: 'skill-extension-regression',
784
+ passed: false,
785
+ expected: 'existing workflows preserved in registry',
786
+ actual: 'agent-registry.js not found',
787
+ };
788
+ }
789
+
790
+ const content = fs.readFileSync(registryPath, 'utf8');
791
+ const existingNames = (ctx.existing_workflow_names || []);
792
+
793
+ // If none of the existing workflows appear in the registry, they are team-local only — skip gracefully
794
+ if (existingNames.length > 0 && existingNames.every(n => !content.includes(`name: '${n}'`))) {
795
+ return {
796
+ name: 'EXISTING-WORKFLOWS-REGISTRY',
797
+ stepName: 'skill-extension-regression',
798
+ passed: true,
799
+ expected: 'existing workflows preserved in registry',
800
+ actual: 'existing workflows not in registry scope (team-local only)',
801
+ };
802
+ }
803
+
804
+ // Check each existing workflow that IS in the registry — it must still be there
805
+ const missing = existingNames.filter(n => !content.includes(`name: '${n}'`));
806
+ return {
807
+ name: 'EXISTING-WORKFLOWS-REGISTRY',
808
+ stepName: 'skill-extension-regression',
809
+ passed: missing.length === 0,
810
+ expected: 'existing workflows preserved in registry',
811
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
812
+ };
813
+ }
814
+
815
+ /**
816
+ * Verify existing workflows still present in config.yaml.
817
+ * @param {Object} ctx
818
+ * @returns {E2ECheck}
819
+ */
820
+ function checkExistingWorkflowsConfig(ctx) {
821
+ const configPath = ctx.config_yaml_path;
822
+ if (!configPath || !fs.existsSync(configPath)) {
823
+ return {
824
+ name: 'EXISTING-WORKFLOWS-CONFIG',
825
+ stepName: 'skill-extension-regression',
826
+ passed: false,
827
+ expected: 'existing workflows preserved',
828
+ actual: 'config.yaml not found',
829
+ };
830
+ }
831
+
832
+ let config;
833
+ try {
834
+ config = yaml.load(fs.readFileSync(configPath, 'utf8'));
835
+ } catch {
836
+ return {
837
+ name: 'EXISTING-WORKFLOWS-CONFIG',
838
+ stepName: 'skill-extension-regression',
839
+ passed: false,
840
+ expected: 'existing workflows preserved',
841
+ actual: 'cannot parse config.yaml',
842
+ };
843
+ }
844
+
845
+ const workflows = config.workflows || [];
846
+ const missing = (ctx.existing_workflow_names || []).filter(n => !workflows.includes(n));
847
+ return {
848
+ name: 'EXISTING-WORKFLOWS-CONFIG',
849
+ stepName: 'skill-extension-regression',
850
+ passed: missing.length === 0,
851
+ expected: 'existing workflows preserved',
852
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
853
+ };
854
+ }
855
+
856
+ /**
857
+ * Verify existing workflow rows still present in module-help.csv.
858
+ * @param {Object} ctx
859
+ * @returns {E2ECheck}
860
+ */
861
+ function checkExistingWorkflowsCsv(ctx) {
862
+ const csvPath = ctx.module_help_csv_path;
863
+ if (!csvPath || !fs.existsSync(csvPath)) {
864
+ return {
865
+ name: 'EXISTING-WORKFLOWS-CSV',
866
+ stepName: 'skill-extension-regression',
867
+ passed: false,
868
+ expected: 'existing workflow rows preserved',
869
+ actual: 'module-help.csv not found',
870
+ };
871
+ }
872
+
873
+ const content = fs.readFileSync(csvPath, 'utf8');
874
+ const lines = content.trim().split('\n').slice(1); // skip header
875
+ // Extract workflow name column (index 2) from each row using RFC 4180-aware parser.
876
+ const workflowNames = lines.map(line => {
877
+ const cols = parseCsvRow(line);
878
+ return (cols[2] || '').trim();
879
+ });
880
+ const missing = (ctx.existing_workflow_names || []).filter(n => {
881
+ // Check by title case version (CSV stores title case)
882
+ const titleCase = n.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
883
+ return !workflowNames.includes(titleCase) && !workflowNames.includes(n);
884
+ });
885
+ return {
886
+ name: 'EXISTING-WORKFLOWS-CSV',
887
+ stepName: 'skill-extension-regression',
888
+ passed: missing.length === 0,
889
+ expected: 'existing workflow rows preserved',
890
+ actual: missing.length === 0 ? 'all preserved' : `missing: ${missing.join(', ')}`,
891
+ };
892
+ }
893
+
894
+ module.exports = {
895
+ validateTeam,
896
+ validateExtension,
897
+ validateSkillExtension,
898
+ };