aios-core 2.3.1 → 3.1.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.
- package/.aios-core/core-config.yaml +54 -0
- package/.aios-core/development/agents/squad-creator.md +261 -0
- package/.aios-core/development/scripts/squad/index.js +36 -2
- package/.aios-core/development/scripts/squad/squad-designer.js +1010 -0
- package/.aios-core/development/scripts/squad/squad-generator.js +1317 -0
- package/.aios-core/development/tasks/github-devops-github-pr-automation.md +240 -3
- package/.aios-core/development/tasks/squad-creator-create.md +289 -0
- package/.aios-core/development/tasks/squad-creator-design.md +334 -0
- package/.aios-core/development/tasks/squad-creator-download.md +65 -0
- package/.aios-core/development/tasks/squad-creator-list.md +225 -0
- package/.aios-core/development/tasks/squad-creator-publish.md +86 -0
- package/.aios-core/development/tasks/squad-creator-sync-synkra.md +83 -0
- package/.aios-core/infrastructure/templates/core-config/core-config-greenfield.tmpl.yaml +41 -0
- package/.aios-core/schemas/squad-design-schema.json +299 -0
- package/package.json +1 -1
- package/squads/.designs/duplicate-test-design.yaml +23 -0
- package/squads/.designs/force-test-design.yaml +23 -0
- package/squads/.designs/nested-test-design.yaml +23 -0
- package/squads/.designs/test-squad-design.yaml +23 -0
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squad Designer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes documentation and generates squad blueprints
|
|
5
|
+
* with intelligent agent and task recommendations.
|
|
6
|
+
*
|
|
7
|
+
* @module squad-designer
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @see Story SQS-9: Squad Designer
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default output path for blueprints
|
|
18
|
+
* @constant {string}
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_DESIGNS_PATH = './squads/.designs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimum confidence threshold for recommendations
|
|
24
|
+
* @constant {number}
|
|
25
|
+
*/
|
|
26
|
+
const MIN_CONFIDENCE_THRESHOLD = 0.5;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keywords that indicate workflow actions
|
|
30
|
+
* @constant {string[]}
|
|
31
|
+
*/
|
|
32
|
+
const ACTION_KEYWORDS = [
|
|
33
|
+
'create', 'add', 'new', 'generate', 'build',
|
|
34
|
+
'update', 'edit', 'modify', 'change', 'patch',
|
|
35
|
+
'delete', 'remove', 'cancel', 'archive',
|
|
36
|
+
'get', 'fetch', 'retrieve', 'list', 'search', 'find', 'query',
|
|
37
|
+
'process', 'handle', 'manage', 'execute', 'run',
|
|
38
|
+
'validate', 'verify', 'check', 'approve', 'reject',
|
|
39
|
+
'send', 'notify', 'alert', 'email', 'publish',
|
|
40
|
+
'import', 'export', 'sync', 'migrate', 'transform',
|
|
41
|
+
'login', 'logout', 'authenticate', 'authorize',
|
|
42
|
+
'upload', 'download', 'save', 'load',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Keywords that indicate integrations
|
|
47
|
+
* @constant {string[]}
|
|
48
|
+
*/
|
|
49
|
+
const INTEGRATION_KEYWORDS = [
|
|
50
|
+
'api', 'rest', 'graphql', 'webhook', 'endpoint',
|
|
51
|
+
'database', 'db', 'sql', 'nosql', 'redis', 'postgres', 'mysql', 'mongodb',
|
|
52
|
+
'aws', 'azure', 'gcp', 'cloud', 's3', 'lambda',
|
|
53
|
+
'stripe', 'paypal', 'payment', 'gateway',
|
|
54
|
+
'slack', 'discord', 'email', 'sms', 'twilio',
|
|
55
|
+
'oauth', 'jwt', 'auth0', 'firebase',
|
|
56
|
+
'github', 'gitlab', 'bitbucket',
|
|
57
|
+
'docker', 'kubernetes', 'k8s',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Keywords that indicate stakeholder roles
|
|
62
|
+
* @constant {string[]}
|
|
63
|
+
*/
|
|
64
|
+
const ROLE_KEYWORDS = [
|
|
65
|
+
'user', 'admin', 'administrator', 'manager', 'owner',
|
|
66
|
+
'customer', 'client', 'buyer', 'seller', 'vendor',
|
|
67
|
+
'developer', 'engineer', 'devops', 'qa', 'tester',
|
|
68
|
+
'analyst', 'designer', 'architect',
|
|
69
|
+
'operator', 'support', 'agent', 'representative',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Error codes for SquadDesignerError
|
|
74
|
+
* @enum {string}
|
|
75
|
+
*/
|
|
76
|
+
const DesignerErrorCodes = {
|
|
77
|
+
NO_DOCUMENTATION: 'NO_DOCUMENTATION',
|
|
78
|
+
PARSE_ERROR: 'PARSE_ERROR',
|
|
79
|
+
EMPTY_ANALYSIS: 'EMPTY_ANALYSIS',
|
|
80
|
+
BLUEPRINT_EXISTS: 'BLUEPRINT_EXISTS',
|
|
81
|
+
INVALID_BLUEPRINT: 'INVALID_BLUEPRINT',
|
|
82
|
+
SAVE_ERROR: 'SAVE_ERROR',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom error class for Squad Designer operations
|
|
87
|
+
* @extends Error
|
|
88
|
+
*/
|
|
89
|
+
class SquadDesignerError extends Error {
|
|
90
|
+
/**
|
|
91
|
+
* Create a SquadDesignerError
|
|
92
|
+
* @param {string} code - Error code from DesignerErrorCodes enum
|
|
93
|
+
* @param {string} message - Human-readable error message
|
|
94
|
+
* @param {string} [suggestion] - Suggested fix for the error
|
|
95
|
+
*/
|
|
96
|
+
constructor(code, message, suggestion) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = 'SquadDesignerError';
|
|
99
|
+
this.code = code;
|
|
100
|
+
this.suggestion = suggestion || '';
|
|
101
|
+
|
|
102
|
+
if (Error.captureStackTrace) {
|
|
103
|
+
Error.captureStackTrace(this, SquadDesignerError);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create error for no documentation provided
|
|
109
|
+
* @returns {SquadDesignerError}
|
|
110
|
+
*/
|
|
111
|
+
static noDocumentation() {
|
|
112
|
+
return new SquadDesignerError(
|
|
113
|
+
DesignerErrorCodes.NO_DOCUMENTATION,
|
|
114
|
+
'No documentation provided for analysis',
|
|
115
|
+
'Provide documentation via --docs flag or paste text interactively',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create error for parse failure
|
|
121
|
+
* @param {string} filePath - Path that failed to parse
|
|
122
|
+
* @param {string} reason - Parse failure reason
|
|
123
|
+
* @returns {SquadDesignerError}
|
|
124
|
+
*/
|
|
125
|
+
static parseError(filePath, reason) {
|
|
126
|
+
return new SquadDesignerError(
|
|
127
|
+
DesignerErrorCodes.PARSE_ERROR,
|
|
128
|
+
`Failed to parse documentation: ${filePath} - ${reason}`,
|
|
129
|
+
'Check file format (supported: .md, .yaml, .yml, .json, .txt)',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create error for empty analysis result
|
|
135
|
+
* @returns {SquadDesignerError}
|
|
136
|
+
*/
|
|
137
|
+
static emptyAnalysis() {
|
|
138
|
+
return new SquadDesignerError(
|
|
139
|
+
DesignerErrorCodes.EMPTY_ANALYSIS,
|
|
140
|
+
'No domain concepts could be extracted from documentation',
|
|
141
|
+
'Provide more detailed documentation with clear entities and workflows',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create error for existing blueprint
|
|
147
|
+
* @param {string} blueprintPath - Path where blueprint exists
|
|
148
|
+
* @returns {SquadDesignerError}
|
|
149
|
+
*/
|
|
150
|
+
static blueprintExists(blueprintPath) {
|
|
151
|
+
return new SquadDesignerError(
|
|
152
|
+
DesignerErrorCodes.BLUEPRINT_EXISTS,
|
|
153
|
+
`Blueprint already exists at ${blueprintPath}`,
|
|
154
|
+
'Use --force to overwrite or choose a different output path',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Squad Designer class
|
|
161
|
+
* Analyzes documentation and generates squad blueprints
|
|
162
|
+
*/
|
|
163
|
+
class SquadDesigner {
|
|
164
|
+
/**
|
|
165
|
+
* Create a SquadDesigner
|
|
166
|
+
* @param {Object} options - Designer options
|
|
167
|
+
* @param {string} [options.designsPath] - Path to designs directory
|
|
168
|
+
*/
|
|
169
|
+
constructor(options = {}) {
|
|
170
|
+
this.designsPath = options.designsPath || DEFAULT_DESIGNS_PATH;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ===========================================================================
|
|
174
|
+
// DOCUMENTATION COLLECTION
|
|
175
|
+
// ===========================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Collect and normalize documentation from various sources
|
|
179
|
+
* @param {Object} options - Collection options
|
|
180
|
+
* @param {string|string[]} [options.docs] - File paths or text content
|
|
181
|
+
* @param {string} [options.text] - Direct text input
|
|
182
|
+
* @param {string} [options.domain] - Domain hint
|
|
183
|
+
* @returns {Promise<Object>} Normalized documentation
|
|
184
|
+
*/
|
|
185
|
+
async collectDocumentation(options) {
|
|
186
|
+
const sources = [];
|
|
187
|
+
|
|
188
|
+
// Handle file paths
|
|
189
|
+
if (options.docs) {
|
|
190
|
+
const paths = Array.isArray(options.docs)
|
|
191
|
+
? options.docs
|
|
192
|
+
: options.docs.split(',').map(p => p.trim());
|
|
193
|
+
|
|
194
|
+
for (const filePath of paths) {
|
|
195
|
+
try {
|
|
196
|
+
const content = await this.readDocumentationFile(filePath);
|
|
197
|
+
sources.push({
|
|
198
|
+
type: 'file',
|
|
199
|
+
path: filePath,
|
|
200
|
+
content,
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw SquadDesignerError.parseError(filePath, error.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Handle direct text input
|
|
209
|
+
if (options.text) {
|
|
210
|
+
sources.push({
|
|
211
|
+
type: 'text',
|
|
212
|
+
path: null,
|
|
213
|
+
content: options.text,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (sources.length === 0) {
|
|
218
|
+
throw SquadDesignerError.noDocumentation();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
sources,
|
|
223
|
+
domainHint: options.domain || null,
|
|
224
|
+
mergedContent: sources.map(s => s.content).join('\n\n---\n\n'),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Read and parse a documentation file
|
|
230
|
+
* @param {string} filePath - Path to file
|
|
231
|
+
* @returns {Promise<string>} File content as text
|
|
232
|
+
*/
|
|
233
|
+
async readDocumentationFile(filePath) {
|
|
234
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
235
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
236
|
+
|
|
237
|
+
switch (ext) {
|
|
238
|
+
case '.yaml':
|
|
239
|
+
case '.yml':
|
|
240
|
+
// Convert YAML to readable text
|
|
241
|
+
try {
|
|
242
|
+
const parsed = yaml.load(content);
|
|
243
|
+
return this.yamlToText(parsed);
|
|
244
|
+
} catch {
|
|
245
|
+
return content; // Return raw if parse fails
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case '.json':
|
|
249
|
+
// Convert JSON to readable text
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(content);
|
|
252
|
+
return this.jsonToText(parsed);
|
|
253
|
+
} catch {
|
|
254
|
+
return content;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case '.md':
|
|
258
|
+
case '.txt':
|
|
259
|
+
default:
|
|
260
|
+
return content;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Convert YAML object to readable text
|
|
266
|
+
* @param {Object} obj - Parsed YAML object
|
|
267
|
+
* @param {number} [depth=0] - Current depth for indentation
|
|
268
|
+
* @returns {string} Text representation
|
|
269
|
+
*/
|
|
270
|
+
yamlToText(obj, depth = 0) {
|
|
271
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
272
|
+
return String(obj);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const indent = ' '.repeat(depth);
|
|
276
|
+
const lines = [];
|
|
277
|
+
|
|
278
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
lines.push(`${indent}${key}:`);
|
|
281
|
+
for (const item of value) {
|
|
282
|
+
if (typeof item === 'object') {
|
|
283
|
+
lines.push(`${indent} - ${this.yamlToText(item, depth + 2)}`);
|
|
284
|
+
} else {
|
|
285
|
+
lines.push(`${indent} - ${item}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
289
|
+
lines.push(`${indent}${key}:`);
|
|
290
|
+
lines.push(this.yamlToText(value, depth + 1));
|
|
291
|
+
} else {
|
|
292
|
+
lines.push(`${indent}${key}: ${value}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert JSON object to readable text
|
|
301
|
+
* @param {Object} obj - Parsed JSON object
|
|
302
|
+
* @returns {string} Text representation
|
|
303
|
+
*/
|
|
304
|
+
jsonToText(obj) {
|
|
305
|
+
return this.yamlToText(obj); // Reuse YAML converter
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ===========================================================================
|
|
309
|
+
// DOMAIN ANALYSIS
|
|
310
|
+
// ===========================================================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Analyze documentation and extract domain concepts
|
|
314
|
+
* @param {Object} documentation - Normalized documentation from collectDocumentation
|
|
315
|
+
* @returns {Object} Analysis result with entities, workflows, integrations, stakeholders
|
|
316
|
+
*/
|
|
317
|
+
analyzeDomain(documentation) {
|
|
318
|
+
const content = documentation.mergedContent.toLowerCase();
|
|
319
|
+
const originalContent = documentation.mergedContent;
|
|
320
|
+
|
|
321
|
+
const analysis = {
|
|
322
|
+
domain: this.extractDomain(originalContent, documentation.domainHint),
|
|
323
|
+
entities: this.extractEntities(originalContent),
|
|
324
|
+
workflows: this.extractWorkflows(content, originalContent),
|
|
325
|
+
integrations: this.extractIntegrations(content),
|
|
326
|
+
stakeholders: this.extractStakeholders(content),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Validate we extracted something useful
|
|
330
|
+
if (
|
|
331
|
+
analysis.entities.length === 0 &&
|
|
332
|
+
analysis.workflows.length === 0
|
|
333
|
+
) {
|
|
334
|
+
throw SquadDesignerError.emptyAnalysis();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return analysis;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Extract domain name from content
|
|
342
|
+
* @param {string} content - Original content
|
|
343
|
+
* @param {string|null} hint - Domain hint if provided
|
|
344
|
+
* @returns {string} Domain name
|
|
345
|
+
*/
|
|
346
|
+
extractDomain(content, hint) {
|
|
347
|
+
if (hint) {
|
|
348
|
+
return this.toDomainName(hint);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Try to extract from title/heading
|
|
352
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
353
|
+
if (titleMatch) {
|
|
354
|
+
return this.toDomainName(titleMatch[1]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Try to extract from "name:" in YAML
|
|
358
|
+
const nameMatch = content.match(/name:\s*(.+)/i);
|
|
359
|
+
if (nameMatch) {
|
|
360
|
+
return this.toDomainName(nameMatch[1]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return 'custom-domain';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Convert text to domain name format
|
|
368
|
+
* @param {string} text - Input text
|
|
369
|
+
* @returns {string} Kebab-case domain name
|
|
370
|
+
*/
|
|
371
|
+
toDomainName(text) {
|
|
372
|
+
return text
|
|
373
|
+
.toLowerCase()
|
|
374
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
375
|
+
.replace(/\s+/g, '-')
|
|
376
|
+
.replace(/-+/g, '-')
|
|
377
|
+
.replace(/^-|-$/g, '')
|
|
378
|
+
.substring(0, 50);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Extract entities (nouns, concepts) from content
|
|
383
|
+
* @param {string} content - Original content (preserves case)
|
|
384
|
+
* @returns {string[]} List of entities
|
|
385
|
+
*/
|
|
386
|
+
extractEntities(content) {
|
|
387
|
+
const entities = new Set();
|
|
388
|
+
|
|
389
|
+
// Find capitalized words (likely entities)
|
|
390
|
+
const capitalizedPattern = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)*)\b/g;
|
|
391
|
+
let match;
|
|
392
|
+
while ((match = capitalizedPattern.exec(content)) !== null) {
|
|
393
|
+
const word = match[1];
|
|
394
|
+
// Filter out common non-entity words
|
|
395
|
+
if (!this.isCommonWord(word) && word.length > 2) {
|
|
396
|
+
entities.add(word);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Find words in backticks or quotes (often entities in docs)
|
|
401
|
+
const quotedPattern = /[`"']([A-Za-z][A-Za-z0-9_]+)[`"']/g;
|
|
402
|
+
while ((match = quotedPattern.exec(content)) !== null) {
|
|
403
|
+
const word = match[1];
|
|
404
|
+
if (!this.isCommonWord(word) && word.length > 2) {
|
|
405
|
+
entities.add(this.toTitleCase(word));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Array.from(entities).slice(0, 20); // Limit to top 20
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check if word is a common non-entity word
|
|
414
|
+
* @param {string} word - Word to check
|
|
415
|
+
* @returns {boolean} True if common word
|
|
416
|
+
*/
|
|
417
|
+
isCommonWord(word) {
|
|
418
|
+
const commonWords = new Set([
|
|
419
|
+
'The', 'This', 'That', 'These', 'Those', 'What', 'When', 'Where', 'Which',
|
|
420
|
+
'How', 'Why', 'Who', 'All', 'Any', 'Some', 'Each', 'Every', 'Both',
|
|
421
|
+
'Few', 'More', 'Most', 'Other', 'Such', 'No', 'Not', 'Only', 'Same',
|
|
422
|
+
'Than', 'Too', 'Very', 'Just', 'But', 'And', 'For', 'With', 'From',
|
|
423
|
+
'About', 'Into', 'Through', 'During', 'Before', 'After', 'Above', 'Below',
|
|
424
|
+
'Between', 'Under', 'Again', 'Further', 'Then', 'Once', 'Here', 'There',
|
|
425
|
+
'True', 'False', 'Null', 'None', 'Yes', 'No', 'Example', 'Note', 'Warning',
|
|
426
|
+
'Error', 'Success', 'Failure', 'Status', 'Type', 'Name', 'Value', 'Data',
|
|
427
|
+
'File', 'Path', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Function',
|
|
428
|
+
'Class', 'Method', 'Property', 'Parameter', 'Return', 'Input', 'Output',
|
|
429
|
+
'Request', 'Response', 'Result', 'Config', 'Options', 'Settings',
|
|
430
|
+
]);
|
|
431
|
+
return commonWords.has(word);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Convert string to title case
|
|
436
|
+
* @param {string} str - Input string
|
|
437
|
+
* @returns {string} Title case string
|
|
438
|
+
*/
|
|
439
|
+
toTitleCase(str) {
|
|
440
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Extract workflows from content
|
|
445
|
+
* @param {string} lowerContent - Lowercase content
|
|
446
|
+
* @param {string} originalContent - Original content
|
|
447
|
+
* @returns {string[]} List of workflow names
|
|
448
|
+
*/
|
|
449
|
+
extractWorkflows(lowerContent, originalContent) {
|
|
450
|
+
const workflows = new Set();
|
|
451
|
+
|
|
452
|
+
// Find action + noun patterns
|
|
453
|
+
for (const action of ACTION_KEYWORDS) {
|
|
454
|
+
const pattern = new RegExp(`\\b${action}[\\s-]+([a-z]+)`, 'gi');
|
|
455
|
+
let match;
|
|
456
|
+
while ((match = pattern.exec(lowerContent)) !== null) {
|
|
457
|
+
const noun = match[1];
|
|
458
|
+
if (noun.length > 2 && !this.isStopWord(noun)) {
|
|
459
|
+
workflows.add(`${action}-${noun}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Find numbered steps or bullet points with actions
|
|
465
|
+
const stepPattern = /(?:^|\n)\s*(?:\d+\.|[-*])\s*([A-Za-z]+)\s+(?:the\s+)?([a-z]+)/gi;
|
|
466
|
+
let match;
|
|
467
|
+
while ((match = stepPattern.exec(originalContent)) !== null) {
|
|
468
|
+
const verb = match[1].toLowerCase();
|
|
469
|
+
const noun = match[2].toLowerCase();
|
|
470
|
+
if (ACTION_KEYWORDS.includes(verb) && !this.isStopWord(noun)) {
|
|
471
|
+
workflows.add(`${verb}-${noun}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return Array.from(workflows).slice(0, 15); // Limit to top 15
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Check if word is a stop word
|
|
480
|
+
* @param {string} word - Word to check
|
|
481
|
+
* @returns {boolean} True if stop word
|
|
482
|
+
*/
|
|
483
|
+
isStopWord(word) {
|
|
484
|
+
const stopWords = new Set([
|
|
485
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
486
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
487
|
+
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
|
|
488
|
+
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
|
|
489
|
+
'it', 'its', 'this', 'that', 'these', 'those', 'all', 'each', 'every',
|
|
490
|
+
]);
|
|
491
|
+
return stopWords.has(word.toLowerCase());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Extract integrations from content
|
|
496
|
+
* @param {string} content - Lowercase content
|
|
497
|
+
* @returns {string[]} List of integrations
|
|
498
|
+
*/
|
|
499
|
+
extractIntegrations(content) {
|
|
500
|
+
const integrations = new Set();
|
|
501
|
+
|
|
502
|
+
for (const keyword of INTEGRATION_KEYWORDS) {
|
|
503
|
+
if (content.includes(keyword)) {
|
|
504
|
+
integrations.add(this.toTitleCase(keyword));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Find API endpoints mentioned
|
|
509
|
+
const apiPattern = /(?:api|endpoint)[:\s]+([a-z/_-]+)/gi;
|
|
510
|
+
let match;
|
|
511
|
+
while ((match = apiPattern.exec(content)) !== null) {
|
|
512
|
+
integrations.add(`API: ${match[1]}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return Array.from(integrations).slice(0, 10);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Extract stakeholders from content
|
|
520
|
+
* @param {string} content - Lowercase content
|
|
521
|
+
* @returns {string[]} List of stakeholders
|
|
522
|
+
*/
|
|
523
|
+
extractStakeholders(content) {
|
|
524
|
+
const stakeholders = new Set();
|
|
525
|
+
|
|
526
|
+
for (const role of ROLE_KEYWORDS) {
|
|
527
|
+
const pattern = new RegExp(`\\b${role}s?\\b`, 'gi');
|
|
528
|
+
if (pattern.test(content)) {
|
|
529
|
+
stakeholders.add(this.toTitleCase(role));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return Array.from(stakeholders).slice(0, 10);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ===========================================================================
|
|
537
|
+
// RECOMMENDATION GENERATION
|
|
538
|
+
// ===========================================================================
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Generate agent recommendations based on analysis
|
|
542
|
+
* @param {Object} analysis - Domain analysis result
|
|
543
|
+
* @returns {Array} Recommended agents with confidence scores
|
|
544
|
+
*/
|
|
545
|
+
generateAgentRecommendations(analysis) {
|
|
546
|
+
const agents = [];
|
|
547
|
+
const usedWorkflows = new Set();
|
|
548
|
+
|
|
549
|
+
// Group workflows by main action category
|
|
550
|
+
const workflowGroups = this.groupWorkflowsByCategory(analysis.workflows);
|
|
551
|
+
|
|
552
|
+
for (const [category, workflows] of Object.entries(workflowGroups)) {
|
|
553
|
+
if (workflows.length === 0) continue;
|
|
554
|
+
|
|
555
|
+
const agentId = `${analysis.domain}-${category}`;
|
|
556
|
+
const commands = workflows.map(w => w.replace(/-/g, '-'));
|
|
557
|
+
|
|
558
|
+
// Calculate confidence based on workflow clarity
|
|
559
|
+
const confidence = Math.min(
|
|
560
|
+
0.95,
|
|
561
|
+
0.6 + (workflows.length * 0.05) + (commands.length > 3 ? 0.1 : 0),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
agents.push({
|
|
565
|
+
id: this.toKebabCase(agentId),
|
|
566
|
+
role: this.generateAgentRole(category, workflows, analysis.domain),
|
|
567
|
+
commands: commands.slice(0, 6), // Max 6 commands per agent
|
|
568
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
569
|
+
user_added: false,
|
|
570
|
+
user_modified: false,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
workflows.forEach(w => usedWorkflows.add(w));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// If we have entities but no clear workflows, create a generic manager
|
|
577
|
+
if (agents.length === 0 && analysis.entities.length > 0) {
|
|
578
|
+
const mainEntity = analysis.entities[0].toLowerCase();
|
|
579
|
+
agents.push({
|
|
580
|
+
id: `${mainEntity}-manager`,
|
|
581
|
+
role: `Manages ${mainEntity} lifecycle and operations`,
|
|
582
|
+
commands: [`create-${mainEntity}`, `update-${mainEntity}`, `delete-${mainEntity}`, `list-${mainEntity}s`],
|
|
583
|
+
confidence: 0.65,
|
|
584
|
+
user_added: false,
|
|
585
|
+
user_modified: false,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return agents;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Group workflows by category
|
|
594
|
+
* @param {string[]} workflows - List of workflows
|
|
595
|
+
* @returns {Object} Grouped workflows
|
|
596
|
+
*/
|
|
597
|
+
groupWorkflowsByCategory(workflows) {
|
|
598
|
+
const groups = {
|
|
599
|
+
manager: [],
|
|
600
|
+
processor: [],
|
|
601
|
+
handler: [],
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
for (const workflow of workflows) {
|
|
605
|
+
const [action] = workflow.split('-');
|
|
606
|
+
|
|
607
|
+
if (['create', 'update', 'delete', 'add', 'remove', 'edit'].includes(action)) {
|
|
608
|
+
groups.manager.push(workflow);
|
|
609
|
+
} else if (['process', 'transform', 'migrate', 'sync', 'import', 'export'].includes(action)) {
|
|
610
|
+
groups.processor.push(workflow);
|
|
611
|
+
} else {
|
|
612
|
+
groups.handler.push(workflow);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Remove empty groups
|
|
617
|
+
return Object.fromEntries(
|
|
618
|
+
Object.entries(groups).filter(([, v]) => v.length > 0),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Generate agent role description
|
|
624
|
+
* @param {string} category - Agent category
|
|
625
|
+
* @param {string[]} workflows - Agent workflows
|
|
626
|
+
* @param {string} domain - Domain name
|
|
627
|
+
* @returns {string} Role description
|
|
628
|
+
*/
|
|
629
|
+
generateAgentRole(category, workflows, domain) {
|
|
630
|
+
const domainTitle = domain.split('-').map(w => this.toTitleCase(w)).join(' ');
|
|
631
|
+
|
|
632
|
+
switch (category) {
|
|
633
|
+
case 'manager':
|
|
634
|
+
return `Manages ${domainTitle} resources and lifecycle operations`;
|
|
635
|
+
case 'processor':
|
|
636
|
+
return `Processes and transforms ${domainTitle} data`;
|
|
637
|
+
case 'handler':
|
|
638
|
+
return `Handles ${domainTitle} events and operations`;
|
|
639
|
+
default:
|
|
640
|
+
return `Manages ${domainTitle} ${category} operations`;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Convert string to kebab-case
|
|
646
|
+
* @param {string} str - Input string
|
|
647
|
+
* @returns {string} Kebab-case string
|
|
648
|
+
*/
|
|
649
|
+
toKebabCase(str) {
|
|
650
|
+
return str
|
|
651
|
+
.toLowerCase()
|
|
652
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
653
|
+
.replace(/^-|-$/g, '');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Generate task recommendations based on analysis and agents
|
|
658
|
+
* @param {Object} analysis - Domain analysis result
|
|
659
|
+
* @param {Array} agents - Recommended agents
|
|
660
|
+
* @returns {Array} Recommended tasks with confidence scores
|
|
661
|
+
*/
|
|
662
|
+
generateTaskRecommendations(analysis, agents) {
|
|
663
|
+
const tasks = [];
|
|
664
|
+
|
|
665
|
+
for (const agent of agents) {
|
|
666
|
+
for (const command of agent.commands) {
|
|
667
|
+
const taskName = `${command}.md`;
|
|
668
|
+
const entrada = this.generateTaskEntrada(command, analysis);
|
|
669
|
+
const saida = this.generateTaskSaida(command, analysis);
|
|
670
|
+
|
|
671
|
+
// Calculate confidence based on entrada/saida clarity
|
|
672
|
+
const confidence = Math.min(
|
|
673
|
+
0.95,
|
|
674
|
+
0.5 + (entrada.length * 0.1) + (saida.length * 0.1),
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
tasks.push({
|
|
678
|
+
name: taskName.replace('.md', ''),
|
|
679
|
+
agent: agent.id,
|
|
680
|
+
entrada,
|
|
681
|
+
saida,
|
|
682
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return tasks;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Generate task entrada (inputs)
|
|
692
|
+
* @param {string} command - Command name
|
|
693
|
+
* @param {Object} analysis - Domain analysis
|
|
694
|
+
* @returns {string[]} Input parameters
|
|
695
|
+
*/
|
|
696
|
+
generateTaskEntrada(command, analysis) {
|
|
697
|
+
const inputs = [];
|
|
698
|
+
const [action, ...rest] = command.split('-');
|
|
699
|
+
const subject = rest.join('_');
|
|
700
|
+
|
|
701
|
+
switch (action) {
|
|
702
|
+
case 'create':
|
|
703
|
+
case 'add':
|
|
704
|
+
inputs.push(`${subject}_data`);
|
|
705
|
+
if (analysis.stakeholders.length > 0) {
|
|
706
|
+
inputs.push('created_by');
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
|
|
710
|
+
case 'update':
|
|
711
|
+
case 'edit':
|
|
712
|
+
case 'modify':
|
|
713
|
+
inputs.push(`${subject}_id`);
|
|
714
|
+
inputs.push('updates');
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
case 'delete':
|
|
718
|
+
case 'remove':
|
|
719
|
+
inputs.push(`${subject}_id`);
|
|
720
|
+
break;
|
|
721
|
+
|
|
722
|
+
case 'get':
|
|
723
|
+
case 'fetch':
|
|
724
|
+
case 'retrieve':
|
|
725
|
+
inputs.push(`${subject}_id`);
|
|
726
|
+
break;
|
|
727
|
+
|
|
728
|
+
case 'list':
|
|
729
|
+
case 'search':
|
|
730
|
+
case 'find':
|
|
731
|
+
inputs.push('filters');
|
|
732
|
+
inputs.push('pagination');
|
|
733
|
+
break;
|
|
734
|
+
|
|
735
|
+
case 'process':
|
|
736
|
+
case 'transform':
|
|
737
|
+
inputs.push('source_data');
|
|
738
|
+
inputs.push('options');
|
|
739
|
+
break;
|
|
740
|
+
|
|
741
|
+
default:
|
|
742
|
+
inputs.push(`${subject}_id`);
|
|
743
|
+
inputs.push('options');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return inputs;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Generate task saida (outputs)
|
|
751
|
+
* @param {string} command - Command name
|
|
752
|
+
* @param {Object} _analysis - Domain analysis (reserved for future use)
|
|
753
|
+
* @returns {string[]} Output parameters
|
|
754
|
+
*/
|
|
755
|
+
generateTaskSaida(command, _analysis) {
|
|
756
|
+
const outputs = [];
|
|
757
|
+
const [action, ...rest] = command.split('-');
|
|
758
|
+
const subject = rest.join('_');
|
|
759
|
+
|
|
760
|
+
switch (action) {
|
|
761
|
+
case 'create':
|
|
762
|
+
case 'add':
|
|
763
|
+
outputs.push(`${subject}_id`);
|
|
764
|
+
outputs.push('status');
|
|
765
|
+
break;
|
|
766
|
+
|
|
767
|
+
case 'update':
|
|
768
|
+
case 'edit':
|
|
769
|
+
case 'modify':
|
|
770
|
+
outputs.push(`updated_${subject}`);
|
|
771
|
+
outputs.push('changelog');
|
|
772
|
+
break;
|
|
773
|
+
|
|
774
|
+
case 'delete':
|
|
775
|
+
case 'remove':
|
|
776
|
+
outputs.push('success');
|
|
777
|
+
outputs.push('deleted_at');
|
|
778
|
+
break;
|
|
779
|
+
|
|
780
|
+
case 'get':
|
|
781
|
+
case 'fetch':
|
|
782
|
+
case 'retrieve':
|
|
783
|
+
outputs.push(subject);
|
|
784
|
+
break;
|
|
785
|
+
|
|
786
|
+
case 'list':
|
|
787
|
+
case 'search':
|
|
788
|
+
case 'find':
|
|
789
|
+
outputs.push(`${subject}_list`);
|
|
790
|
+
outputs.push('total_count');
|
|
791
|
+
break;
|
|
792
|
+
|
|
793
|
+
case 'process':
|
|
794
|
+
case 'transform':
|
|
795
|
+
outputs.push('result_data');
|
|
796
|
+
outputs.push('metrics');
|
|
797
|
+
break;
|
|
798
|
+
|
|
799
|
+
default:
|
|
800
|
+
outputs.push('result');
|
|
801
|
+
outputs.push('status');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return outputs;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ===========================================================================
|
|
808
|
+
// BLUEPRINT GENERATION
|
|
809
|
+
// ===========================================================================
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Generate complete blueprint
|
|
813
|
+
* @param {Object} options - Blueprint options
|
|
814
|
+
* @param {Object} options.analysis - Domain analysis
|
|
815
|
+
* @param {Object} options.recommendations - Agent and task recommendations
|
|
816
|
+
* @param {Object} options.metadata - Blueprint metadata
|
|
817
|
+
* @param {Object} [options.userAdjustments] - User modifications
|
|
818
|
+
* @returns {Object} Complete squad blueprint
|
|
819
|
+
*/
|
|
820
|
+
generateBlueprint(options) {
|
|
821
|
+
const { analysis, recommendations, metadata, userAdjustments } = options;
|
|
822
|
+
|
|
823
|
+
// Calculate overall confidence
|
|
824
|
+
const agentConfidences = recommendations.agents.map(a => a.confidence);
|
|
825
|
+
const taskConfidences = recommendations.tasks.map(t => t.confidence);
|
|
826
|
+
const allConfidences = [...agentConfidences, ...taskConfidences];
|
|
827
|
+
const overallConfidence = allConfidences.length > 0
|
|
828
|
+
? allConfidences.reduce((a, b) => a + b, 0) / allConfidences.length
|
|
829
|
+
: 0.5;
|
|
830
|
+
|
|
831
|
+
// Determine template based on recommendations
|
|
832
|
+
const template = this.determineTemplate(recommendations);
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
squad: {
|
|
836
|
+
name: `${analysis.domain}-squad`,
|
|
837
|
+
description: `Squad for ${analysis.domain.replace(/-/g, ' ')} management`,
|
|
838
|
+
domain: analysis.domain,
|
|
839
|
+
},
|
|
840
|
+
analysis: {
|
|
841
|
+
entities: analysis.entities,
|
|
842
|
+
workflows: analysis.workflows,
|
|
843
|
+
integrations: analysis.integrations,
|
|
844
|
+
stakeholders: analysis.stakeholders,
|
|
845
|
+
},
|
|
846
|
+
recommendations: {
|
|
847
|
+
agents: recommendations.agents,
|
|
848
|
+
tasks: recommendations.tasks,
|
|
849
|
+
template,
|
|
850
|
+
config_mode: 'extend',
|
|
851
|
+
},
|
|
852
|
+
metadata: {
|
|
853
|
+
created_at: metadata.created_at || new Date().toISOString(),
|
|
854
|
+
source_docs: metadata.source_docs || [],
|
|
855
|
+
user_adjustments: userAdjustments?.count || 0,
|
|
856
|
+
overall_confidence: Math.round(overallConfidence * 100) / 100,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Determine best template based on recommendations
|
|
863
|
+
* @param {Object} recommendations - Recommendations
|
|
864
|
+
* @returns {string} Template name
|
|
865
|
+
*/
|
|
866
|
+
determineTemplate(recommendations) {
|
|
867
|
+
const hasDataProcessing = recommendations.tasks.some(t =>
|
|
868
|
+
t.name.includes('process') || t.name.includes('transform') ||
|
|
869
|
+
t.name.includes('import') || t.name.includes('export'),
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
if (hasDataProcessing) {
|
|
873
|
+
return 'etl';
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (recommendations.tasks.length === 0) {
|
|
877
|
+
return 'agent-only';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return 'basic';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ===========================================================================
|
|
884
|
+
// BLUEPRINT PERSISTENCE
|
|
885
|
+
// ===========================================================================
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Save blueprint to file
|
|
889
|
+
* @param {Object} blueprint - Squad blueprint
|
|
890
|
+
* @param {string} [outputPath] - Output path (optional)
|
|
891
|
+
* @param {Object} [options] - Save options
|
|
892
|
+
* @param {boolean} [options.force] - Overwrite existing
|
|
893
|
+
* @returns {Promise<string>} Path to saved file
|
|
894
|
+
*/
|
|
895
|
+
async saveBlueprint(blueprint, outputPath, options = {}) {
|
|
896
|
+
const designsDir = outputPath || this.designsPath;
|
|
897
|
+
|
|
898
|
+
// Ensure designs directory exists
|
|
899
|
+
await fs.mkdir(designsDir, { recursive: true });
|
|
900
|
+
|
|
901
|
+
const filename = `${blueprint.squad.name}-design.yaml`;
|
|
902
|
+
const filePath = path.join(designsDir, filename);
|
|
903
|
+
|
|
904
|
+
// Check if file exists
|
|
905
|
+
if (!options.force) {
|
|
906
|
+
try {
|
|
907
|
+
await fs.access(filePath);
|
|
908
|
+
throw SquadDesignerError.blueprintExists(filePath);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (error.code !== 'ENOENT') {
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Generate YAML content
|
|
917
|
+
const yamlContent = this.blueprintToYaml(blueprint);
|
|
918
|
+
|
|
919
|
+
// Write file
|
|
920
|
+
await fs.writeFile(filePath, yamlContent, 'utf-8');
|
|
921
|
+
|
|
922
|
+
return filePath;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Convert blueprint to YAML string
|
|
927
|
+
* @param {Object} blueprint - Blueprint object
|
|
928
|
+
* @returns {string} YAML content
|
|
929
|
+
*/
|
|
930
|
+
blueprintToYaml(blueprint) {
|
|
931
|
+
const header = `# Squad Design Blueprint
|
|
932
|
+
# Generated by *design-squad
|
|
933
|
+
# Source: ${blueprint.metadata.source_docs.join(', ') || 'Interactive input'}
|
|
934
|
+
# Created: ${blueprint.metadata.created_at}
|
|
935
|
+
|
|
936
|
+
`;
|
|
937
|
+
return header + yaml.dump(blueprint, {
|
|
938
|
+
indent: 2,
|
|
939
|
+
lineWidth: 100,
|
|
940
|
+
noRefs: true,
|
|
941
|
+
sortKeys: false,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Load blueprint from file
|
|
947
|
+
* @param {string} blueprintPath - Path to blueprint file
|
|
948
|
+
* @returns {Promise<Object>} Loaded blueprint
|
|
949
|
+
*/
|
|
950
|
+
async loadBlueprint(blueprintPath) {
|
|
951
|
+
try {
|
|
952
|
+
const content = await fs.readFile(blueprintPath, 'utf-8');
|
|
953
|
+
return yaml.load(content);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
throw new SquadDesignerError(
|
|
956
|
+
DesignerErrorCodes.INVALID_BLUEPRINT,
|
|
957
|
+
`Failed to load blueprint: ${error.message}`,
|
|
958
|
+
'Check that the blueprint file exists and is valid YAML',
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Validate blueprint structure
|
|
965
|
+
* @param {Object} blueprint - Blueprint to validate
|
|
966
|
+
* @returns {Object} Validation result { valid, errors }
|
|
967
|
+
*/
|
|
968
|
+
validateBlueprint(blueprint) {
|
|
969
|
+
const errors = [];
|
|
970
|
+
|
|
971
|
+
// Check required top-level keys
|
|
972
|
+
if (!blueprint.squad) {
|
|
973
|
+
errors.push('Missing required key: squad');
|
|
974
|
+
} else {
|
|
975
|
+
if (!blueprint.squad.name) errors.push('Missing squad.name');
|
|
976
|
+
if (!blueprint.squad.domain) errors.push('Missing squad.domain');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (!blueprint.recommendations) {
|
|
980
|
+
errors.push('Missing required key: recommendations');
|
|
981
|
+
} else {
|
|
982
|
+
if (!Array.isArray(blueprint.recommendations.agents)) {
|
|
983
|
+
errors.push('recommendations.agents must be an array');
|
|
984
|
+
}
|
|
985
|
+
if (!Array.isArray(blueprint.recommendations.tasks)) {
|
|
986
|
+
errors.push('recommendations.tasks must be an array');
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (!blueprint.metadata) {
|
|
991
|
+
errors.push('Missing required key: metadata');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
valid: errors.length === 0,
|
|
996
|
+
errors,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
module.exports = {
|
|
1002
|
+
SquadDesigner,
|
|
1003
|
+
SquadDesignerError,
|
|
1004
|
+
DesignerErrorCodes,
|
|
1005
|
+
DEFAULT_DESIGNS_PATH,
|
|
1006
|
+
MIN_CONFIDENCE_THRESHOLD,
|
|
1007
|
+
ACTION_KEYWORDS,
|
|
1008
|
+
INTEGRATION_KEYWORDS,
|
|
1009
|
+
ROLE_KEYWORDS,
|
|
1010
|
+
};
|