aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
387 lines (334 loc) • 11.3 kB
JavaScript
/**
* Manifest Generator
*
* Generates CSV manifest files for agents, workers, and tasks.
* Scans the .aios-core directory structure to build comprehensive manifests.
*
* @module manifest-generator
* @version 1.0.0
* @story 2.13 - Manifest System
*/
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
/**
* CSV escape helper - escapes special characters in CSV values
* @param {string} value - Value to escape
* @returns {string} Escaped value
*/
function escapeCSV(value) {
if (value === null || value === undefined) return '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}
/**
* Parse YAML front matter from markdown files
* Handles various YAML formats and parsing errors gracefully
* @param {string} content - File content
* @returns {object|null} Parsed YAML or null
*/
function parseYAMLFromMarkdown(content) {
// Look for YAML in code blocks (```yaml ... ```)
const yamlBlockMatch = content.match(/```yaml\n([\s\S]*?)```/);
if (yamlBlockMatch) {
try {
return yaml.load(yamlBlockMatch[1]);
} catch (_e) {
// Try to extract just the agent section if full parse fails
return extractAgentSection(yamlBlockMatch[1]);
}
}
// Look for YAML front matter (--- ... ---)
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontMatterMatch) {
try {
return yaml.load(frontMatterMatch[1]);
} catch (_e) {
return extractAgentSection(frontMatterMatch[1]);
}
}
return null;
}
/**
* Extract agent section from YAML when full parse fails
* @param {string} yamlContent - YAML content string
* @returns {object|null} Agent data or null
*/
function extractAgentSection(yamlContent) {
try {
// Find agent: block and extract key fields using regex
const agentMatch = yamlContent.match(/^agent:\s*\n((?: {2}.+\n)*)/m);
if (!agentMatch) return null;
const agentLines = agentMatch[1];
// Extract individual fields
const nameMatch = agentLines.match(/name:\s*(.+)/);
const idMatch = agentLines.match(/id:\s*(.+)/);
const titleMatch = agentLines.match(/title:\s*(.+)/);
const iconMatch = agentLines.match(/icon:\s*(.+)/);
const whenToUseMatch = agentLines.match(/whenToUse:\s*(.+)/);
// Find persona_profile section
const personaMatch = yamlContent.match(/persona_profile:\s*\n(?: {2}.+\n)*/m);
let archetype = null;
if (personaMatch) {
const archetypeMatch = personaMatch[0].match(/archetype:\s*(.+)/);
if (archetypeMatch) archetype = archetypeMatch[1].trim();
}
if (idMatch || nameMatch) {
return {
agent: {
name: nameMatch ? nameMatch[1].trim() : null,
id: idMatch ? idMatch[1].trim() : null,
title: titleMatch ? titleMatch[1].trim() : null,
icon: iconMatch ? iconMatch[1].trim() : null,
whenToUse: whenToUseMatch ? whenToUseMatch[1].trim() : null,
},
persona_profile: archetype ? { archetype } : null,
};
}
} catch (_e) {
// Fallback failed
}
return null;
}
/**
* Manifest Generator class
*/
class ManifestGenerator {
constructor(options = {}) {
this.basePath = options.basePath || process.cwd();
this.aiosCoreDir = path.join(this.basePath, '.aios-core');
this.manifestDir = path.join(this.aiosCoreDir, 'manifests');
this.version = '2.1.0';
}
/**
* Generate all manifests
* @returns {Promise<object>} Generation results
*/
async generateAll() {
const startTime = Date.now();
const results = {
agents: null,
workers: null,
tasks: null,
errors: [],
duration: 0,
};
try {
// Ensure manifest directory exists
await fs.mkdir(this.manifestDir, { recursive: true });
// Generate all manifests in parallel
const [agents, workers, tasks] = await Promise.all([
this.generateAgentsManifest(),
this.generateWorkersManifest(),
this.generateTasksManifest(),
]);
results.agents = agents;
results.workers = workers;
results.tasks = tasks;
} catch (error) {
results.errors.push(error.message);
}
results.duration = Date.now() - startTime;
return results;
}
/**
* Generate agents.csv manifest
* @returns {Promise<object>} Generation result
*/
async generateAgentsManifest() {
const agentsDir = path.join(this.aiosCoreDir, 'development', 'agents');
const outputPath = path.join(this.manifestDir, 'agents.csv');
const agents = [];
const errors = [];
try {
const files = await fs.readdir(agentsDir);
const mdFiles = files.filter(f => f.endsWith('.md'));
for (const file of mdFiles) {
try {
const filePath = path.join(agentsDir, file);
const content = await fs.readFile(filePath, 'utf8');
const parsed = parseYAMLFromMarkdown(content);
if (parsed && parsed.agent) {
const agent = parsed.agent;
const persona = parsed.persona_profile || parsed.persona || {};
agents.push({
id: agent.id || file.replace('.md', ''),
name: agent.name || 'Unknown',
archetype: persona.archetype || agent.title || 'Agent',
icon: agent.icon || '🤖',
version: this.version,
status: 'active',
file_path: `.aios-core/development/agents/${file}`,
when_to_use: agent.whenToUse || '',
});
}
} catch (e) {
errors.push(`Error parsing ${file}: ${e.message}`);
}
}
// Generate CSV content
const header = 'id,name,archetype,icon,version,status,file_path,when_to_use';
const rows = agents.map(a =>
[a.id, a.name, a.archetype, a.icon, a.version, a.status, a.file_path, a.when_to_use]
.map(escapeCSV)
.join(','),
);
const csvContent = [header, ...rows].join('\n');
await fs.writeFile(outputPath, csvContent, 'utf8');
return {
success: true,
count: agents.length,
path: outputPath,
errors,
};
} catch (error) {
return {
success: false,
count: 0,
path: outputPath,
errors: [error.message],
};
}
}
/**
* Generate workers.csv manifest from service registry
* @returns {Promise<object>} Generation result
*/
async generateWorkersManifest() {
const registryPath = path.join(this.aiosCoreDir, 'core', 'registry', 'service-registry.json');
const outputPath = path.join(this.manifestDir, 'workers.csv');
try {
const registryContent = await fs.readFile(registryPath, 'utf8');
const registry = JSON.parse(registryContent);
const workers = registry.workers.map(w => ({
id: w.id,
name: w.name,
category: w.category,
subcategory: w.subcategory || '',
executor_types: (w.executorTypes || []).join(';'),
tags: (w.tags || []).join(';'),
file_path: w.path,
status: 'active',
}));
// Generate CSV content
const header = 'id,name,category,subcategory,executor_types,tags,file_path,status';
const rows = workers.map(w =>
[w.id, w.name, w.category, w.subcategory, w.executor_types, w.tags, w.file_path, w.status]
.map(escapeCSV)
.join(','),
);
const csvContent = [header, ...rows].join('\n');
await fs.writeFile(outputPath, csvContent, 'utf8');
return {
success: true,
count: workers.length,
path: outputPath,
errors: [],
};
} catch (error) {
return {
success: false,
count: 0,
path: outputPath,
errors: [error.message],
};
}
}
/**
* Generate tasks.csv manifest
* @returns {Promise<object>} Generation result
*/
async generateTasksManifest() {
const tasksDir = path.join(this.aiosCoreDir, 'development', 'tasks');
const outputPath = path.join(this.manifestDir, 'tasks.csv');
const tasks = [];
const errors = [];
try {
const files = await fs.readdir(tasksDir);
const mdFiles = files.filter(f => f.endsWith('.md'));
for (const file of mdFiles) {
try {
const filePath = path.join(tasksDir, file);
const content = await fs.readFile(filePath, 'utf8');
const parsed = parseYAMLFromMarkdown(content);
const taskId = file.replace('.md', '');
let taskName = taskId.split('-').map(w =>
w.charAt(0).toUpperCase() + w.slice(1),
).join(' ');
let category = 'general';
let format = 'TASK-FORMAT-V1';
let hasElicitation = false;
if (parsed) {
if (parsed.task) {
taskName = parsed.task.name || taskName;
category = parsed.task.category || category;
format = parsed.task.format || format;
hasElicitation = parsed.task.elicit === true || parsed.elicit === true;
}
if (parsed.name) taskName = parsed.name;
if (parsed.category) category = parsed.category;
if (parsed.format) format = parsed.format;
if (parsed.elicit !== undefined) hasElicitation = parsed.elicit;
}
// Detect category from filename prefix
if (taskId.startsWith('db-')) category = 'database';
else if (taskId.startsWith('qa-')) category = 'quality';
else if (taskId.startsWith('dev-')) category = 'development';
else if (taskId.startsWith('po-')) category = 'product';
else if (taskId.startsWith('github-')) category = 'devops';
// Detect elicitation from content
if (content.includes('elicit: true') || content.includes('elicit=true')) {
hasElicitation = true;
}
tasks.push({
id: taskId,
name: taskName,
category,
format,
has_elicitation: hasElicitation,
file_path: `.aios-core/development/tasks/${file}`,
status: 'active',
});
} catch (e) {
errors.push(`Error parsing ${file}: ${e.message}`);
}
}
// Generate CSV content
const header = 'id,name,category,format,has_elicitation,file_path,status';
const rows = tasks.map(t =>
[t.id, t.name, t.category, t.format, t.has_elicitation, t.file_path, t.status]
.map(escapeCSV)
.join(','),
);
const csvContent = [header, ...rows].join('\n');
await fs.writeFile(outputPath, csvContent, 'utf8');
return {
success: true,
count: tasks.length,
path: outputPath,
errors,
};
} catch (error) {
return {
success: false,
count: 0,
path: outputPath,
errors: [error.message],
};
}
}
}
// Factory function
function createManifestGenerator(options = {}) {
return new ManifestGenerator(options);
}
module.exports = {
ManifestGenerator,
createManifestGenerator,
escapeCSV,
parseYAMLFromMarkdown,
extractAgentSection,
};