aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
1,406 lines (1,213 loc) • 44 kB
JavaScript
/**
* Squad Generator
*
* Generates squad structure following task-first architecture.
* Used by the *create-squad task of the squad-creator agent.
*
* @module squad-generator
* @version 1.0.0
* @see Story SQS-4: Squad Creator Agent + Tasks
*/
const fs = require('fs').promises;
const path = require('path');
const { execSync } = require('child_process');
const yaml = require('js-yaml');
/**
* Default path for squads directory
* @constant {string}
*/
const DEFAULT_SQUADS_PATH = './squads';
/**
* Default path for squad designs directory
* @constant {string}
*/
const DEFAULT_DESIGNS_PATH = './squads/.designs';
/**
* Path to squad design schema
* @constant {string}
*/
const SQUAD_DESIGN_SCHEMA_PATH = path.join(__dirname, '../../schemas/squad-design-schema.json');
/**
* Default AIOS minimum version
* @constant {string}
*/
const DEFAULT_AIOS_MIN_VERSION = '2.1.0';
/**
* Available templates
* @constant {string[]}
*/
const AVAILABLE_TEMPLATES = ['basic', 'etl', 'agent-only'];
/**
* Available config modes
* @constant {string[]}
*/
const CONFIG_MODES = ['extend', 'override', 'none'];
/**
* Available licenses
* @constant {string[]}
*/
const AVAILABLE_LICENSES = ['MIT', 'Apache-2.0', 'ISC', 'UNLICENSED'];
/**
* Directories to create in squad structure
* @constant {string[]}
*/
const SQUAD_DIRECTORIES = [
'',
'config',
'agents',
'tasks',
'workflows',
'checklists',
'templates',
'tools',
'scripts',
'data',
];
/**
* Error codes for SquadGeneratorError
* @enum {string}
*/
const GeneratorErrorCodes = {
INVALID_NAME: 'INVALID_NAME',
SQUAD_EXISTS: 'SQUAD_EXISTS',
PERMISSION_DENIED: 'PERMISSION_DENIED',
TEMPLATE_NOT_FOUND: 'TEMPLATE_NOT_FOUND',
INVALID_CONFIG_MODE: 'INVALID_CONFIG_MODE',
BLUEPRINT_NOT_FOUND: 'BLUEPRINT_NOT_FOUND',
BLUEPRINT_INVALID: 'BLUEPRINT_INVALID',
BLUEPRINT_PARSE_ERROR: 'BLUEPRINT_PARSE_ERROR',
SCHEMA_NOT_FOUND: 'SCHEMA_NOT_FOUND',
};
/**
* Custom error class for Squad Generator operations
* @extends Error
*/
class SquadGeneratorError extends Error {
/**
* Create a SquadGeneratorError
* @param {string} code - Error code from GeneratorErrorCodes enum
* @param {string} message - Human-readable error message
* @param {string} [suggestion] - Suggested fix for the error
*/
constructor(code, message, suggestion) {
super(message);
this.name = 'SquadGeneratorError';
this.code = code;
this.suggestion = suggestion || '';
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SquadGeneratorError);
}
}
/**
* Create error for invalid squad name
* @param {string} name - Invalid name provided
* @returns {SquadGeneratorError}
*/
static invalidName(name) {
return new SquadGeneratorError(
GeneratorErrorCodes.INVALID_NAME,
`Invalid squad name "${name}": must be kebab-case (lowercase letters, numbers, hyphens)`,
'Use format: my-squad-name (lowercase, hyphens only)',
);
}
/**
* Create error for existing squad
* @param {string} name - Squad name that exists
* @param {string} squadPath - Path where squad exists
* @returns {SquadGeneratorError}
*/
static squadExists(name, squadPath) {
return new SquadGeneratorError(
GeneratorErrorCodes.SQUAD_EXISTS,
`Squad "${name}" already exists at ${squadPath}`,
`Choose a different name or delete existing squad: rm -rf ${squadPath}`,
);
}
/**
* Create error for invalid template
* @param {string} template - Invalid template name
* @returns {SquadGeneratorError}
*/
static templateNotFound(template) {
return new SquadGeneratorError(
GeneratorErrorCodes.TEMPLATE_NOT_FOUND,
`Template "${template}" not found`,
`Available templates: ${AVAILABLE_TEMPLATES.join(', ')}`,
);
}
/**
* Create error for invalid config mode
* @param {string} mode - Invalid config mode
* @returns {SquadGeneratorError}
*/
static invalidConfigMode(mode) {
return new SquadGeneratorError(
GeneratorErrorCodes.INVALID_CONFIG_MODE,
`Invalid config mode "${mode}"`,
`Available modes: ${CONFIG_MODES.join(', ')}`,
);
}
/**
* Create error for blueprint not found
* @param {string} blueprintPath - Path that doesn't exist
* @returns {SquadGeneratorError}
*/
static blueprintNotFound(blueprintPath) {
return new SquadGeneratorError(
GeneratorErrorCodes.BLUEPRINT_NOT_FOUND,
`Blueprint not found at "${blueprintPath}"`,
'Generate a blueprint first: *design-squad --docs ./your-docs.md',
);
}
/**
* Create error for blueprint parse error
* @param {string} blueprintPath - Path to blueprint
* @param {string} parseError - Parse error message
* @returns {SquadGeneratorError}
*/
static blueprintParseError(blueprintPath, parseError) {
return new SquadGeneratorError(
GeneratorErrorCodes.BLUEPRINT_PARSE_ERROR,
`Failed to parse blueprint at "${blueprintPath}": ${parseError}`,
'Ensure blueprint is valid YAML format',
);
}
/**
* Create error for invalid blueprint
* @param {string[]} validationErrors - List of validation errors
* @returns {SquadGeneratorError}
*/
static blueprintInvalid(validationErrors) {
return new SquadGeneratorError(
GeneratorErrorCodes.BLUEPRINT_INVALID,
`Blueprint validation failed:\n - ${validationErrors.join('\n - ')}`,
'Fix the validation errors and try again',
);
}
/**
* Create error for schema not found
* @param {string} schemaPath - Path to schema
* @returns {SquadGeneratorError}
*/
static schemaNotFound(schemaPath) {
return new SquadGeneratorError(
GeneratorErrorCodes.SCHEMA_NOT_FOUND,
`Schema not found at "${schemaPath}"`,
'Ensure AIOS is properly installed',
);
}
}
/**
* Get git user name
* @returns {string} Git user name or 'Unknown'
*/
function getGitUserName() {
try {
const name = execSync('git config user.name', { encoding: 'utf-8' }).trim();
return name || 'Unknown';
} catch {
return 'Unknown';
}
}
/**
* Validate squad name (kebab-case)
* @param {string} name - Name to validate
* @returns {boolean} True if valid
*/
function isValidSquadName(name) {
// Must be kebab-case: lowercase letters, numbers, and hyphens
// Must start with letter, end with letter or number
// Minimum 2 characters
return /^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) && name.length >= 2;
}
/**
* Extract slash prefix from squad name
* @param {string} name - Squad name
* @returns {string} Slash prefix
*/
function extractSlashPrefix(name) {
// Remove -squad suffix if present
return name.replace(/-squad$/, '');
}
/**
* Safely quote YAML values that may contain special characters
* @param {string} val - Value to quote
* @returns {string} Safely quoted value
*/
function safeYamlValue(val) {
if (!val) return '""';
// Quote if contains special YAML characters or leading/trailing spaces
if (/[:\n\r"']/.test(val) || val.startsWith(' ') || val.endsWith(' ')) {
return `"${val.replace(/"/g, '\\"')}"`;
}
return val;
}
// =============================================================================
// TEMPLATES
// =============================================================================
/**
* Generate squad.yaml content
* @param {Object} config - Squad configuration
* @returns {string} YAML content
*/
function generateSquadYaml(config) {
const components = {
tasks: config.includeTask ? ['example-agent-task.md'] : [],
agents: config.includeAgent ? ['example-agent.md'] : [],
workflows: [],
checklists: [],
templates: [],
tools: [],
scripts: [],
};
// For etl template, add more components
if (config.template === 'etl') {
components.agents = ['data-extractor.md', 'data-transformer.md'];
components.tasks = [
'extract-data.md',
'transform-data.md',
'load-data.md',
];
components.scripts = ['utils.js'];
}
// For agent-only template
if (config.template === 'agent-only') {
components.agents = ['primary-agent.md', 'helper-agent.md'];
components.tasks = [];
}
// SQS-10: Use project configs if available, otherwise use local paths
let configSection;
if (config.configMode === 'none') {
configSection = {};
} else if (config._useProjectConfigs && config._projectConfigs) {
// Reference project-level config files
configSection = {
extends: config.configMode,
'coding-standards': config._projectConfigs['coding-standards'] || 'config/coding-standards.md',
'tech-stack': config._projectConfigs['tech-stack'] || 'config/tech-stack.md',
'source-tree': config._projectConfigs['source-tree'] || 'config/source-tree.md',
};
} else {
// Fallback to local config files
configSection = {
extends: config.configMode,
'coding-standards': 'config/coding-standards.md',
'tech-stack': 'config/tech-stack.md',
'source-tree': 'config/source-tree.md',
};
}
const yaml = `name: ${config.name}
version: 1.0.0
description: ${safeYamlValue(config.description || 'Custom squad')}
author: ${safeYamlValue(config.author || 'Unknown')}
license: ${config.license || 'MIT'}
slashPrefix: ${extractSlashPrefix(config.name)}
aios:
minVersion: "${config.aiosMinVersion || DEFAULT_AIOS_MIN_VERSION}"
type: squad
components:
tasks:${components.tasks.length ? '\n - ' + components.tasks.join('\n - ') : ' []'}
agents:${components.agents.length ? '\n - ' + components.agents.join('\n - ') : ' []'}
workflows: []
checklists: []
templates: []
tools: []
scripts:${components.scripts.length ? '\n - ' + components.scripts.join('\n - ') : ' []'}
config:${
config.configMode === 'none'
? ' {}'
: `
extends: ${configSection.extends}
coding-standards: ${configSection['coding-standards']}
tech-stack: ${configSection['tech-stack']}
source-tree: ${configSection['source-tree']}`
}
dependencies:
node: []
python: []
squads: []
tags:
- custom
`;
return yaml;
}
/**
* Generate README.md content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateReadme(config) {
return `# ${config.name}
${config.description || 'Custom AIOS squad.'}
## Installation
This squad is installed locally in your project:
\`\`\`
./squads/${config.name}/
\`\`\`
## Usage
Activate agents from this squad and use their commands.
### Available Agents
${config.includeAgent || config.template !== 'basic' ? '- **example-agent** - Example agent (customize or remove)' : '_No agents defined yet_'}
### Available Tasks
${config.includeTask || config.template === 'etl' ? '- **example-agent-task** - Example task (customize or remove)' : '_No tasks defined yet_'}
## Configuration
This squad ${config.configMode === 'extend' ? 'extends' : config.configMode === 'override' ? 'overrides' : 'does not inherit'} the core AIOS configuration.
## Development
1. Add agents in \`agents/\` directory
2. Add tasks in \`tasks/\` directory (task-first architecture!)
3. Update \`squad.yaml\` components section
4. Validate: \`@squad-creator *validate-squad ${config.name}\`
## License
${config.license || 'MIT'}
`;
}
/**
* Generate coding-standards.md content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateCodingStandards(config) {
return `# Coding Standards - ${config.name}
> This file ${config.configMode === 'extend' ? 'extends' : config.configMode === 'override' ? 'overrides' : 'is independent of'} the core AIOS coding standards.
## Code Style
- Follow consistent naming conventions
- Use meaningful variable and function names
- Keep functions small and focused
- Document complex logic with comments
## File Organization
- Place agents in \`agents/\` directory
- Place tasks in \`tasks/\` directory
- Place utilities in \`scripts/\` directory
## Testing
- Write tests for all new functionality
- Maintain test coverage above 80%
- Use descriptive test names
## Documentation
- Document all public APIs
- Include examples in documentation
- Keep README.md up to date
`;
}
/**
* Generate tech-stack.md content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateTechStack(config) {
return `# Tech Stack - ${config.name}
## Runtime
- Node.js >= 18.x
- AIOS >= ${config.aiosMinVersion || DEFAULT_AIOS_MIN_VERSION}
## Dependencies
_Add your squad's dependencies here_
## Development Tools
- ESLint for code quality
- Jest for testing
- Prettier for formatting
`;
}
/**
* Generate source-tree.md content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateSourceTree(config) {
return `# Source Tree - ${config.name}
\`\`\`
${config.name}/
├── squad.yaml # Squad manifest
├── README.md # Documentation
├── config/ # Configuration files
│ ├── coding-standards.md
│ ├── tech-stack.md
│ └── source-tree.md
├── agents/ # Agent definitions
├── tasks/ # Task definitions
├── workflows/ # Multi-step workflows
├── checklists/ # Validation checklists
├── templates/ # Document templates
├── tools/ # Custom tools
├── scripts/ # Utility scripts
└── data/ # Static data
\`\`\`
## Directory Purpose
| Directory | Purpose |
|-----------|---------|
| \`agents/\` | Agent persona definitions (.md) |
| \`tasks/\` | Executable task workflows (.md) |
| \`workflows/\` | Multi-step workflow definitions |
| \`checklists/\` | Validation and review checklists |
| \`templates/\` | Document and code templates |
| \`tools/\` | Custom tool definitions |
| \`scripts/\` | JavaScript/Python utilities |
| \`data/\` | Static data files |
`;
}
/**
* Generate example agent content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateExampleAgent(config) {
const agentName = config.template === 'etl' ? 'data-extractor' : 'example-agent';
const title = config.template === 'etl' ? 'Data Extractor' : 'Example Agent';
return `# ${agentName}
## Agent Definition
\`\`\`yaml
agent:
name: ${title.replace(/ /g, '')}
id: ${agentName}
title: ${title}
icon: "🤖"
whenToUse: "Use for ${config.template === 'etl' ? 'extracting data from sources' : 'example purposes - customize this'}"
persona:
role: ${config.template === 'etl' ? 'Data Extraction Specialist' : 'Example Specialist'}
style: Systematic, thorough
focus: ${config.template === 'etl' ? 'Extracting data efficiently' : 'Demonstrating squad structure'}
commands:
- name: help
description: "Show available commands"
- name: run
description: "${config.template === 'etl' ? 'Extract data from source' : 'Run example task'}"
task: ${config.template === 'etl' ? 'extract-data.md' : 'example-agent-task.md'}
\`\`\`
## Usage
\`\`\`
@${agentName}
*help
*run
\`\`\`
`;
}
/**
* Generate example task content
* @param {Object} config - Squad configuration
* @returns {string} Markdown content
*/
function generateExampleTask(config) {
const taskName = config.template === 'etl' ? 'extract-data' : 'example-agent-task';
const title = config.template === 'etl' ? 'Extract Data' : 'Example Task';
return `---
task: ${title}
responsavel: "@${config.template === 'etl' ? 'data-extractor' : 'example-agent'}"
responsavel_type: agent
atomic_layer: task
Entrada: |
- source: Data source path or URL
- format: Output format (json, csv, yaml)
Saida: |
- data: Extracted data
- status: Success or error message
Checklist:
- "[ ] Validate input parameters"
- "[ ] Connect to source"
- "[ ] Extract data"
- "[ ] Format output"
- "[ ] Return result"
---
# *${taskName.replace(/-/g, '-')}
${config.template === 'etl' ? 'Extracts data from the specified source.' : 'Example task demonstrating task-first architecture.'}
## Usage
\`\`\`
@${config.template === 'etl' ? 'data-extractor' : 'example-agent'}
*${taskName.replace('example-agent-', '')} --source ./data/input.json --format json
\`\`\`
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| \`--source\` | string | Yes | Data source path or URL |
| \`--format\` | string | No | Output format (default: json) |
## Example
\`\`\`javascript
// This is a placeholder - implement your logic here
async function execute(options) {
const { source, format } = options;
// TODO: Implement extraction logic
console.log(\`Extracting from \${source} as \${format}\`);
return { status: 'success', data: {} };
}
\`\`\`
`;
}
// =============================================================================
// SQUAD GENERATOR CLASS
// =============================================================================
/**
* Squad Generator class
* Generates complete squad structure with all necessary files
*/
class SquadGenerator {
/**
* Create a SquadGenerator
* @param {Object} options - Generator options
* @param {string} [options.squadsPath] - Path to squads directory
*/
constructor(options = {}) {
this.squadsPath = options.squadsPath || DEFAULT_SQUADS_PATH;
}
/**
* Check if a path exists
* @param {string} filePath - Path to check
* @returns {Promise<boolean>} True if exists
*/
async pathExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Detect project-level configuration files in docs/framework/
* Implements AC10.1: Project Config Detection
*
* @param {string} projectRoot - Project root directory
* @param {string} squadPath - Path to squad being created (for relative paths)
* @returns {Promise<Object|null>} Detected config paths (relative to squadPath) or null if not found
* @see Story SQS-10: Project Config Reference for Squads
*/
async detectProjectConfigs(projectRoot, squadPath) {
const frameworkDir = path.join(projectRoot, 'docs', 'framework');
// Check if docs/framework/ exists
if (!(await this.pathExists(frameworkDir))) {
return null;
}
// Config files to detect (case-insensitive variants)
const configFiles = {
'coding-standards': ['CODING-STANDARDS.md', 'coding-standards.md', 'Coding-Standards.md'],
'tech-stack': ['TECH-STACK.md', 'tech-stack.md', 'Tech-Stack.md'],
'source-tree': ['SOURCE-TREE.md', 'source-tree.md', 'Source-Tree.md'],
};
const detected = {};
for (const [key, variants] of Object.entries(configFiles)) {
for (const filename of variants) {
const fullPath = path.join(frameworkDir, filename);
if (await this.pathExists(fullPath)) {
// Calculate relative path from squad to project config
detected[key] = path.relative(squadPath, fullPath).replace(/\\/g, '/');
break;
}
}
}
// Only return if at least one config file was found
const foundCount = Object.keys(detected).length;
if (foundCount > 0) {
console.log(`[squad-generator] Detected ${foundCount} project config(s) in docs/framework/`);
return detected;
}
return null;
}
/**
* Validate generation configuration
* @param {Object} config - Configuration to validate
* @throws {SquadGeneratorError} If validation fails
*/
validateConfig(config) {
// Validate name
if (!config.name) {
throw new SquadGeneratorError(
GeneratorErrorCodes.INVALID_NAME,
'Squad name is required',
'Provide a name: *create-squad my-squad-name',
);
}
if (!isValidSquadName(config.name)) {
throw SquadGeneratorError.invalidName(config.name);
}
// Validate template
if (config.template && !AVAILABLE_TEMPLATES.includes(config.template)) {
throw SquadGeneratorError.templateNotFound(config.template);
}
// Validate config mode
if (config.configMode && !CONFIG_MODES.includes(config.configMode)) {
throw SquadGeneratorError.invalidConfigMode(config.configMode);
}
}
/**
* Generate a new squad
* @param {Object} config - Squad configuration
* @param {string} config.name - Squad name (kebab-case)
* @param {string} [config.description] - Squad description
* @param {string} [config.author] - Author name
* @param {string} [config.license] - License type
* @param {string} [config.template='basic'] - Template type
* @param {string} [config.configMode='extend'] - Config inheritance mode
* @param {boolean} [config.includeAgent=true] - Include example agent
* @param {boolean} [config.includeTask=true] - Include example task
* @param {string} [config.aiosMinVersion] - Minimum AIOS version
* @param {string} [config.projectRoot] - Project root directory (for detecting project configs)
* @returns {Promise<Object>} Generation result with path and files
* @throws {SquadGeneratorError} If generation fails
*/
async generate(config) {
// Set defaults
const fullConfig = {
name: config.name,
description: config.description || 'Custom squad',
author: config.author || getGitUserName(),
license: config.license || 'MIT',
template: config.template || 'basic',
configMode: config.configMode || 'extend',
includeAgent: config.includeAgent !== false,
includeTask: config.includeTask !== false,
aiosMinVersion: config.aiosMinVersion || DEFAULT_AIOS_MIN_VERSION,
projectRoot: config.projectRoot || process.cwd(),
};
// Validate configuration
this.validateConfig(fullConfig);
const squadPath = path.join(this.squadsPath, fullConfig.name);
// SQS-10: Detect project-level configs when configMode is 'extend'
let projectConfigs = null;
let useProjectConfigs = false;
if (fullConfig.configMode === 'extend') {
projectConfigs = await this.detectProjectConfigs(fullConfig.projectRoot, squadPath);
useProjectConfigs = projectConfigs !== null;
if (useProjectConfigs) {
console.log('[squad-generator] Using project-level configuration from docs/framework/');
}
}
// Store for use in squad.yaml generation
fullConfig._projectConfigs = projectConfigs;
fullConfig._useProjectConfigs = useProjectConfigs;
// Check if squad already exists
if (await this.pathExists(squadPath)) {
throw SquadGeneratorError.squadExists(fullConfig.name, squadPath);
}
// Create directories
for (const dir of SQUAD_DIRECTORIES) {
const dirPath = path.join(squadPath, dir);
await fs.mkdir(dirPath, { recursive: true });
}
// Track generated files
const files = [];
// Generate main files
const mainFiles = {
'squad.yaml': generateSquadYaml(fullConfig),
'README.md': generateReadme(fullConfig),
};
for (const [filename, content] of Object.entries(mainFiles)) {
const filePath = path.join(squadPath, filename);
await fs.writeFile(filePath, content, 'utf-8');
files.push(filePath);
}
// Generate config files (SQS-10: skip if using project configs)
if (useProjectConfigs) {
// Don't create local config files, just add .gitkeep to config directory
console.log('[squad-generator] Skipping local config file creation (using project-level configs)');
const gitkeepPath = path.join(squadPath, 'config', '.gitkeep');
await fs.writeFile(gitkeepPath, '# Config files are referenced from project docs/framework/\n', 'utf-8');
files.push(gitkeepPath);
} else {
// Fallback: Create local config files (AC10.3)
console.log('[squad-generator] Creating local configuration files');
const configFiles = {
'config/coding-standards.md': generateCodingStandards(fullConfig),
'config/tech-stack.md': generateTechStack(fullConfig),
'config/source-tree.md': generateSourceTree(fullConfig),
};
for (const [filename, content] of Object.entries(configFiles)) {
const filePath = path.join(squadPath, filename);
await fs.writeFile(filePath, content, 'utf-8');
files.push(filePath);
}
}
// Generate example agent if requested
if (fullConfig.includeAgent) {
const agentContent = generateExampleAgent(fullConfig);
const agentName =
fullConfig.template === 'etl' ? 'data-extractor.md' : 'example-agent.md';
const agentPath = path.join(squadPath, 'agents', agentName);
await fs.writeFile(agentPath, agentContent, 'utf-8');
files.push(agentPath);
// For ETL template, add second agent
if (fullConfig.template === 'etl') {
const transformerConfig = { ...fullConfig, template: 'basic' };
const transformerContent = generateExampleAgent(transformerConfig)
.replace(/data-extractor/g, 'data-transformer')
.replace(/Data Extractor/g, 'Data Transformer')
.replace(/extracting data/g, 'transforming data')
.replace(/extract-data/g, 'transform-data');
const transformerPath = path.join(squadPath, 'agents', 'data-transformer.md');
await fs.writeFile(transformerPath, transformerContent, 'utf-8');
files.push(transformerPath);
}
// For agent-only template, add agents
if (fullConfig.template === 'agent-only') {
const primaryContent = generateExampleAgent({ ...fullConfig, template: 'basic' })
.replace(/example-agent/g, 'primary-agent')
.replace(/Example Agent/g, 'Primary Agent');
const primaryPath = path.join(squadPath, 'agents', 'primary-agent.md');
await fs.writeFile(primaryPath, primaryContent, 'utf-8');
files.push(primaryPath);
const helperContent = generateExampleAgent({ ...fullConfig, template: 'basic' })
.replace(/example-agent/g, 'helper-agent')
.replace(/Example Agent/g, 'Helper Agent');
const helperPath = path.join(squadPath, 'agents', 'helper-agent.md');
await fs.writeFile(helperPath, helperContent, 'utf-8');
files.push(helperPath);
}
}
// Generate example task if requested
if (fullConfig.includeTask && fullConfig.template !== 'agent-only') {
const taskContent = generateExampleTask(fullConfig);
const taskName =
fullConfig.template === 'etl' ? 'extract-data.md' : 'example-agent-task.md';
const taskPath = path.join(squadPath, 'tasks', taskName);
await fs.writeFile(taskPath, taskContent, 'utf-8');
files.push(taskPath);
// For ETL template, add more tasks
if (fullConfig.template === 'etl') {
const transformTask = generateExampleTask(fullConfig)
.replace(/extract-data/g, 'transform-data')
.replace(/Extract Data/g, 'Transform Data')
.replace(/data-extractor/g, 'data-transformer')
.replace(/Extracts data/g, 'Transforms data');
const transformPath = path.join(squadPath, 'tasks', 'transform-data.md');
await fs.writeFile(transformPath, transformTask, 'utf-8');
files.push(transformPath);
const loadTask = generateExampleTask(fullConfig)
.replace(/extract-data/g, 'load-data')
.replace(/Extract Data/g, 'Load Data')
.replace(/data-extractor/g, 'data-loader')
.replace(/Extracts data/g, 'Loads data');
const loadPath = path.join(squadPath, 'tasks', 'load-data.md');
await fs.writeFile(loadPath, loadTask, 'utf-8');
files.push(loadPath);
}
}
// For ETL template, create utils.js script
if (fullConfig.template === 'etl') {
const utilsContent = `/**
* ETL Utilities
*
* Utility functions for ETL operations.
*/
/**
* Format data for output
* @param {Object} data - Data to format
* @param {string} format - Output format (json, csv, yaml)
* @returns {string} Formatted data
*/
function formatData(data, format = 'json') {
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'csv':
// Simple CSV conversion
if (Array.isArray(data) && data.length > 0) {
const headers = Object.keys(data[0]);
const rows = data.map(row => headers.map(h => row[h]).join(','));
return [headers.join(','), ...rows].join('\\n');
}
return '';
case 'yaml':
// Simple YAML conversion
return Object.entries(data)
.map(([k, v]) => \`\${k}: \${JSON.stringify(v)}\`)
.join('\\n');
default:
return JSON.stringify(data);
}
}
module.exports = { formatData };
`;
const utilsPath = path.join(squadPath, 'scripts', 'utils.js');
await fs.writeFile(utilsPath, utilsContent, 'utf-8');
files.push(utilsPath);
}
// Add .gitkeep to empty directories
const emptyDirs = ['workflows', 'checklists', 'templates', 'tools', 'data'];
if (!fullConfig.includeAgent) {
emptyDirs.push('agents');
}
if (!fullConfig.includeTask || fullConfig.template === 'agent-only') {
emptyDirs.push('tasks');
}
if (fullConfig.template !== 'etl') {
emptyDirs.push('scripts');
}
for (const dir of emptyDirs) {
const gitkeepPath = path.join(squadPath, dir, '.gitkeep');
// Only create .gitkeep if directory is empty
try {
const dirContents = await fs.readdir(path.join(squadPath, dir));
if (dirContents.length === 0) {
await fs.writeFile(gitkeepPath, '', 'utf-8');
files.push(gitkeepPath);
}
} catch {
// Directory might not exist, create .gitkeep anyway
await fs.writeFile(gitkeepPath, '', 'utf-8');
files.push(gitkeepPath);
}
}
return {
path: squadPath,
files,
config: fullConfig,
};
}
/**
* List local squads
* @returns {Promise<Array>} List of squad info objects
*/
async listLocal() {
const squads = [];
try {
const entries = await fs.readdir(this.squadsPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const squadPath = path.join(this.squadsPath, entry.name);
// Try to load manifest
try {
const manifestPath = path.join(squadPath, 'squad.yaml');
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
// Basic YAML parsing for key fields
const nameMatch = manifestContent.match(/^name:\s*(.+)$/m);
const versionMatch = manifestContent.match(/^version:\s*(.+)$/m);
const descriptionMatch = manifestContent.match(/^description:\s*(.+)$/m);
squads.push({
name: nameMatch ? nameMatch[1].trim() : entry.name,
version: versionMatch ? versionMatch[1].trim() : 'unknown',
description: descriptionMatch ? descriptionMatch[1].trim() : '',
path: squadPath,
});
} catch {
// Try config.yaml fallback
try {
const configPath = path.join(squadPath, 'config.yaml');
const configContent = await fs.readFile(configPath, 'utf-8');
const nameMatch = configContent.match(/^name:\s*(.+)$/m);
const versionMatch = configContent.match(/^version:\s*(.+)$/m);
const descriptionMatch = configContent.match(/^description:\s*(.+)$/m);
squads.push({
name: nameMatch ? nameMatch[1].trim() : entry.name,
version: versionMatch ? versionMatch[1].trim() : 'unknown',
description: descriptionMatch ? descriptionMatch[1].trim() : '',
path: squadPath,
deprecated: true, // Using config.yaml
});
} catch {
// No manifest found, still list but mark as invalid
squads.push({
name: entry.name,
version: 'unknown',
description: 'No manifest found',
path: squadPath,
invalid: true,
});
}
}
}
} catch (err) {
// Squads directory doesn't exist
if (err.code !== 'ENOENT') {
throw err;
}
}
return squads;
}
// ===========================================================================
// BLUEPRINT METHODS (--from-design support)
// ===========================================================================
/**
* Load a blueprint file from disk
* @param {string} blueprintPath - Path to blueprint YAML file
* @returns {Promise<Object>} Parsed blueprint object
* @throws {SquadGeneratorError} If file not found or parse error
*/
async loadBlueprint(blueprintPath) {
// Check if blueprint exists
if (!(await this.pathExists(blueprintPath))) {
throw SquadGeneratorError.blueprintNotFound(blueprintPath);
}
try {
const content = await fs.readFile(blueprintPath, 'utf-8');
const blueprint = yaml.load(content);
return blueprint;
} catch (err) {
if (err.name === 'YAMLException') {
throw SquadGeneratorError.blueprintParseError(blueprintPath, err.message);
}
throw err;
}
}
/**
* Validate a blueprint against the schema
* @param {Object} blueprint - Blueprint object to validate
* @returns {Object} Validation result with isValid and errors
*/
async validateBlueprint(blueprint) {
const errors = [];
// Required top-level fields
if (!blueprint.squad) {
errors.push('Missing required field: squad');
} else {
if (!blueprint.squad.name) {
errors.push('Missing required field: squad.name');
} else if (!isValidSquadName(blueprint.squad.name)) {
errors.push(`Invalid squad name "${blueprint.squad.name}": must be kebab-case`);
}
if (!blueprint.squad.domain) {
errors.push('Missing required field: squad.domain');
}
}
if (!blueprint.recommendations) {
errors.push('Missing required field: recommendations');
} else {
if (!Array.isArray(blueprint.recommendations.agents)) {
errors.push('recommendations.agents must be an array');
} else {
// Validate each agent
blueprint.recommendations.agents.forEach((agent, idx) => {
if (!agent.id) {
errors.push(`recommendations.agents[${idx}]: missing required field "id"`);
} else if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(agent.id)) {
errors.push(`recommendations.agents[${idx}]: id "${agent.id}" must be kebab-case`);
}
if (!agent.role) {
errors.push(`recommendations.agents[${idx}]: missing required field "role"`);
}
if (typeof agent.confidence !== 'number' || agent.confidence < 0 || agent.confidence > 1) {
errors.push(`recommendations.agents[${idx}]: confidence must be a number between 0 and 1`);
}
});
}
if (!Array.isArray(blueprint.recommendations.tasks)) {
errors.push('recommendations.tasks must be an array');
} else {
// Validate each task
blueprint.recommendations.tasks.forEach((task, idx) => {
if (!task.name) {
errors.push(`recommendations.tasks[${idx}]: missing required field "name"`);
} else if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(task.name)) {
errors.push(`recommendations.tasks[${idx}]: name "${task.name}" must be kebab-case`);
}
if (!task.agent) {
errors.push(`recommendations.tasks[${idx}]: missing required field "agent"`);
}
if (typeof task.confidence !== 'number' || task.confidence < 0 || task.confidence > 1) {
errors.push(`recommendations.tasks[${idx}]: confidence must be a number between 0 and 1`);
}
});
}
}
if (!blueprint.metadata) {
errors.push('Missing required field: metadata');
} else {
if (!blueprint.metadata.created_at) {
errors.push('Missing required field: metadata.created_at');
}
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Convert blueprint to SquadGenerator config format
* @param {Object} blueprint - Validated blueprint object
* @returns {Object} Config object for generate()
*/
blueprintToConfig(blueprint) {
const config = {
name: blueprint.squad.name,
description: blueprint.squad.description || `Squad for ${blueprint.squad.domain}`,
template: blueprint.recommendations.template || 'custom',
configMode: blueprint.recommendations.config_mode || 'extend',
includeAgent: false, // We'll add custom agents, not example ones
includeTask: false, // We'll add custom tasks, not example ones
// Store blueprint data for custom generation
_blueprint: blueprint,
};
return config;
}
/**
* Generate agent markdown content from blueprint recommendation
* @param {Object} agent - Agent recommendation from blueprint
* @param {string} squadName - Name of the squad
* @returns {string} Markdown content for agent file
*/
generateAgentFromBlueprint(agent, squadName) {
const commandsList = (agent.commands || [])
.map(cmd => ` - name: ${cmd}\n description: "${cmd.replace(/-/g, ' ')} operation"`)
.join('\n');
return `# ${agent.id}
## Agent Definition
\`\`\`yaml
agent:
name: ${agent.id.replace(/-/g, '')}
id: ${agent.id}
title: "${agent.role}"
icon: "🤖"
whenToUse: "${agent.role}"
persona:
role: ${agent.role}
style: Systematic, thorough
focus: Executing ${agent.id} responsibilities
commands:
- name: help
description: "Show available commands"
${commandsList}
\`\`\`
## Usage
\`\`\`
@${agent.id}
*help
\`\`\`
## Origin
Generated from squad design blueprint for ${squadName}.
Confidence: ${Math.round(agent.confidence * 100)}%
${agent.user_added ? 'Added by user during design refinement.' : ''}
${agent.user_modified ? 'Modified by user during design refinement.' : ''}
`;
}
/**
* Generate task markdown content from blueprint recommendation
* @param {Object} task - Task recommendation from blueprint
* @param {string} squadName - Name of the squad
* @returns {string} Markdown content for task file
*/
generateTaskFromBlueprint(task, squadName) {
const entradaList = (task.entrada || []).map(e => ` - ${e}`).join('\n');
const saidaList = (task.saida || []).map(s => ` - ${s}`).join('\n');
const checklistItems = (task.checklist || [
'[ ] Validate input parameters',
'[ ] Execute main logic',
'[ ] Format output',
'[ ] Return result',
]).map(item => ` - "${item.startsWith('[') ? item : '[ ] ' + item}"`).join('\n');
return `---
task: "${task.name.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}"
responsavel: "@${task.agent}"
responsavel_type: agent
atomic_layer: task
Entrada: |
${entradaList || ' - (no inputs defined)'}
Saida: |
${saidaList || ' - (no outputs defined)'}
Checklist:
${checklistItems}
---
# *${task.name}
Task generated from squad design blueprint for ${squadName}.
## Usage
\`\`\`
@${task.agent}
*${task.name}
\`\`\`
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
${(task.entrada || []).map(e => `| \`${e}\` | string | Yes | ${e.replace(/_/g, ' ')} |`).join('\n') || '| - | - | - | No parameters defined |'}
## Output
${(task.saida || []).map(s => `- **${s}**: ${s.replace(/_/g, ' ')}`).join('\n') || '- No outputs defined'}
## Origin
Confidence: ${Math.round(task.confidence * 100)}%
`;
}
/**
* Generate a squad from a blueprint file
* @param {string} blueprintPath - Path to blueprint YAML file
* @param {Object} [options] - Additional options
* @param {boolean} [options.force=false] - Force overwrite if squad exists
* @returns {Promise<Object>} Generation result with path, files, and blueprint info
* @throws {SquadGeneratorError} If blueprint is invalid or generation fails
*/
async generateFromBlueprint(blueprintPath, options = {}) {
// 1. Load blueprint
const blueprint = await this.loadBlueprint(blueprintPath);
// 2. Validate blueprint
const validation = await this.validateBlueprint(blueprint);
if (!validation.isValid) {
throw SquadGeneratorError.blueprintInvalid(validation.errors);
}
// 3. Convert to config
const config = this.blueprintToConfig(blueprint);
// Check for existing squad
const squadPath = path.join(this.squadsPath, config.name);
if (await this.pathExists(squadPath)) {
if (!options.force) {
throw SquadGeneratorError.squadExists(config.name, squadPath);
}
// If force, remove existing squad directory before regenerating
await fs.rm(squadPath, { recursive: true, force: true });
}
// 4. Generate base structure (without example agents/tasks)
const result = await this.generate(config);
// 5. Generate custom agents from blueprint
const agentFiles = [];
for (const agent of blueprint.recommendations.agents || []) {
const agentContent = this.generateAgentFromBlueprint(agent, config.name);
const agentPath = path.join(squadPath, 'agents', `${agent.id}.md`);
await fs.writeFile(agentPath, agentContent, 'utf-8');
agentFiles.push(agentPath);
}
// 6. Generate custom tasks from blueprint
const taskFiles = [];
for (const task of blueprint.recommendations.tasks || []) {
const taskContent = this.generateTaskFromBlueprint(task, config.name);
const taskPath = path.join(squadPath, 'tasks', `${task.name}.md`);
await fs.writeFile(taskPath, taskContent, 'utf-8');
taskFiles.push(taskPath);
}
// 7. Update squad.yaml with actual components
const squadYamlPath = path.join(squadPath, 'squad.yaml');
await this.updateSquadYamlComponents(squadYamlPath, blueprint);
// 8. Return result with blueprint info
return {
...result,
files: [...result.files, ...agentFiles, ...taskFiles],
blueprint: {
path: blueprintPath,
agents: blueprint.recommendations.agents?.length || 0,
tasks: blueprint.recommendations.tasks?.length || 0,
confidence: blueprint.metadata.overall_confidence || 0,
source_docs: blueprint.metadata.source_docs || [],
},
};
}
/**
* Update squad.yaml with actual components from blueprint
* @param {string} squadYamlPath - Path to squad.yaml
* @param {Object} blueprint - Blueprint object
*/
async updateSquadYamlComponents(squadYamlPath, blueprint) {
const content = await fs.readFile(squadYamlPath, 'utf-8');
const squadManifest = yaml.load(content);
// Update components
squadManifest.components = squadManifest.components || {};
squadManifest.components.agents = (blueprint.recommendations.agents || [])
.map(a => `${a.id}.md`);
squadManifest.components.tasks = (blueprint.recommendations.tasks || [])
.map(t => `${t.name}.md`);
// Add blueprint reference
squadManifest.blueprint = {
source: blueprint.metadata.source_docs || [],
created_at: blueprint.metadata.created_at,
confidence: blueprint.metadata.overall_confidence || 0,
};
// Write updated manifest
const updatedContent = yaml.dump(squadManifest, {
indent: 2,
lineWidth: 120,
quotingType: '"',
});
await fs.writeFile(squadYamlPath, updatedContent, 'utf-8');
}
}
module.exports = {
SquadGenerator,
SquadGeneratorError,
GeneratorErrorCodes,
AVAILABLE_TEMPLATES,
AVAILABLE_LICENSES,
CONFIG_MODES,
DEFAULT_SQUADS_PATH,
DEFAULT_DESIGNS_PATH,
DEFAULT_AIOS_MIN_VERSION,
SQUAD_DESIGN_SCHEMA_PATH,
isValidSquadName,
getGitUserName,
};