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