acs-framework-cli
Version:
🚀 CLI inteligente para configurar automáticamente el Augmented Context Standard (ACS) Framework. Context-as-Code que convierte tu conocimiento en un activo versionado.
451 lines (361 loc) • 16 kB
JavaScript
/**
* run-agent-safe.js - Ejecutor seguro del agente de documentación
*
* Ejecuta el agente de documentación en un entorno controlado con validaciones
* automáticas para evitar "delirios" y cambios no justificados.
*/
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const { execSync } = require('child_process');
const ProjectFactsExtractor = require('./extract-facts');
class SafeAgentRunner {
constructor() {
this.targetDir = process.cwd();
this.contextDir = path.join(this.targetDir, '.context');
this.backupDir = path.join(this.targetDir, '.context-backup');
this.factsFile = path.join('/tmp', `acs-facts-${Date.now()}.json`);
}
async run() {
try {
console.log(chalk.blue.bold('\n🤖 ACS Framework - Safe Agent Runner'));
console.log(chalk.gray('Ejecutando agente de documentación con validaciones de seguridad...\n'));
// Paso 1: Verificar prerrequisitos
await this.checkPrerequisites();
// Paso 2: Crear backup de documentación actual
await this.createBackup();
// Paso 3: Extraer facts del proyecto
console.log(chalk.yellow('📊 Extrayendo facts del proyecto...'));
await this.extractProjectFacts();
// Paso 4: Crear prompt de agente con facts
const agentPrompt = await this.generateAgentPrompt();
console.log(chalk.green('✅ Prompt generado con facts del proyecto'));
// Paso 5: Mostrar instrucciones para el usuario
await this.showInstructions(agentPrompt);
// Paso 6: Esperar cambios del agente (manual por ahora)
await this.waitForAgentChanges();
// Paso 6.5: Aplicar correcciones automáticas
await this.autoFixCommonIssues();
// Paso 7: Validar cambios
const validationResult = await this.validateChanges();
// Paso 8: Decidir si aceptar, rechazar o pedir revisión
await this.handleValidationResult(validationResult);
} catch (error) {
console.error(chalk.red('❌ Error durante la ejecución:'), error.message);
await this.restoreBackup();
process.exit(1);
}
}
async checkPrerequisites() {
// Verificar que existe .context
if (!await fs.pathExists(this.contextDir)) {
throw new Error('No se encontró carpeta .context/. Ejecuta `acs-init` primero.');
}
// Verificar que es un proyecto git
try {
execSync('git status', { cwd: this.targetDir, stdio: 'ignore' });
} catch (error) {
console.log(chalk.yellow('⚠️ No es un repositorio git. Se recomienda usar git para trackear cambios.'));
}
}
async createBackup() {
console.log(chalk.yellow('💾 Creando backup de documentación actual...'));
if (await fs.pathExists(this.backupDir)) {
await fs.remove(this.backupDir);
}
await fs.copy(this.contextDir, this.backupDir);
console.log(chalk.green('✅ Backup creado en .context-backup/'));
}
async extractProjectFacts() {
const extractor = new ProjectFactsExtractor(this.targetDir);
const facts = await extractor.extract();
await fs.writeJson(this.factsFile, facts, { spaces: 2 });
console.log(chalk.green(`✅ Facts extraídos: ${this.factsFile}`));
return facts;
}
async generateAgentPrompt() {
const facts = await fs.readJson(this.factsFile);
const promptTemplate = `
🤖 AGENTE DOCUMENTADOR ACS - PROMPT CON FACTS VERIFICADOS
Tu trabajo es actualizar SOLO lo necesario en la documentación ACS Framework usando ÚNICAMENTE los facts verificados del proyecto.
📊 FACTS VERIFICADOS DEL PROYECTO:
\`\`\`json
${JSON.stringify(facts, null, 2)}
\`\`\`
🚫 REGLAS CRÍTICAS (NUNCA VIOLAR):
1. SOLO usar tecnologías que aparecen en dependencies/devDependencies de los facts
2. SOLO usar comandos que aparecen en scripts de package.json de los facts
3. NUNCA inventar componentes que no aparecen en la estructura de directorios
4. NUNCA cambiar información de contacto existente
5. NUNCA agregar versiones que no están en los facts
6. Si facts.git.origin existe, usar esa URL real; si no, REMOVER placeholder [URL_DEL_REPO]
✅ PERMITIDO ACTUALIZAR:
- Placeholders con información de facts verificados
- Stack tecnológico usando facts.tech.dependencies exactas
- Comandos usando facts.tech.scripts exactos
- Estructura usando facts.structure.directories reales
- Versiones usando facts.tech.exactVersions cuando disponible
❌ PROHIBIDO:
- Agregar tecnologías no presentes en facts
- Inventar comandos no presentes en scripts
- Cambiar responsables/contactos
- Agregar componentes no presentes en estructura real
- Usar placeholders prohibidos: [URL_DEL_REPO], [Definir...], [Pendiente...]
FORMATO DE RESPUESTA REQUERIDO:
Para cada archivo que cambies, proporciona:
1. Lista de cambios específicos realizados
2. Facts usados como evidencia
3. Justificación del cambio
ARCHIVOS A REVISAR (solo cambiar si hay placeholders o info incorrecta):
- README.md
- ARCHITECTURE.md
- RULES.md
- SYSTEM_PROMPT.md
- FEATURES.md
- CHANGELOG.md
NO hagas cambios cosméticos o de reescritura sin evidencia clara de error.
`;
return promptTemplate;
}
async showInstructions(agentPrompt) {
console.log(chalk.cyan('\n📋 INSTRUCCIONES:'));
console.log('1. Copia el siguiente prompt simple y efectivo');
console.log('2. Pégalo en tu IA preferida (ChatGPT, Claude, etc.)');
console.log('3. Adjunta el contenido de la carpeta .context/ Y el archivo AGENT_PROMPT.md');
console.log('4. Aplica los cambios sugeridos por la IA');
console.log('5. Presiona ENTER cuando hayas terminado\n');
// Nuevo prompt optimizado y simple
const optimizedPrompt = `Actualiza la documentación de .context siguiendo las instrucciones del AGENT_PROMPT.md
🔍 FACTS VERIFICADOS DEL PROYECTO:
${agentPrompt.split('📊 FACTS VERIFICADOS DEL PROYECTO:')[1]?.split('🚫 REGLAS CRÍTICAS')[0] || 'Ver archivos adjuntos'}
💡 Usa el proceso de 3 pasos del AGENT_PROMPT.md y estos facts como evidencia verificada.`;
console.log(chalk.blue('📄 PROMPT OPTIMIZADO PARA IA:'));
console.log(chalk.gray('================================================================================'));
console.log(optimizedPrompt);
console.log(chalk.gray('================================================================================\n'));
// Guardar prompt en archivo temporal para fácil copia
const promptFile = path.join('/tmp', `acs-agent-prompt-${Date.now()}.txt`);
await fs.writeFile(promptFile, optimizedPrompt);
console.log(chalk.yellow(`💾 Prompt guardado en: ${promptFile}`));
console.log(chalk.yellow(` Puedes hacer: cat "${promptFile}" | pbcopy (macOS)\n`));
console.log(chalk.green('💡 TIP: Este prompt es mucho más efectivo porque:'));
console.log(chalk.green(' • Usa el AGENT_PROMPT.md (optimizado con ingeniería de prompts)'));
console.log(chalk.green(' • Mantiene los facts como evidencia'));
console.log(chalk.green(' • Da estructura mental clara a la IA'));
}
async waitForAgentChanges() {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
readline.question(chalk.cyan('⏳ Presiona ENTER cuando hayas aplicado los cambios del agente... '), () => {
readline.close();
resolve();
});
});
}
async autoFixCommonIssues() {
console.log(chalk.cyan('🔧 Aplicando correcciones automáticas...'));
const facts = await fs.readJson(this.factsFile);
const gitOrigin = facts.git?.origin;
if (gitOrigin) {
// Auto-reemplazar [URL_DEL_REPO] con la URL real del repositorio
const filesToFix = ['README.md', 'ARCHITECTURE.md', 'RULES.md', 'FEATURES.md'];
for (const file of filesToFix) {
const filePath = path.join(this.contextDir, file);
if (await fs.pathExists(filePath)) {
let content = await fs.readFile(filePath, 'utf8');
const originalContent = content;
content = content.replace(/\[URL_DEL_REPO\]/gi, gitOrigin);
if (content !== originalContent) {
await fs.writeFile(filePath, content);
console.log(chalk.green(` ✅ ${file}: Reemplazado [URL_DEL_REPO] con ${gitOrigin}`));
}
}
}
}
}
async validateChanges() {
console.log(chalk.yellow('\n🔍 Validando cambios realizados por el agente...'));
// Ejecutar validador desde el directorio del ACS CLI
const acsCliDir = __dirname; // Directorio donde está el ACS CLI
try {
const validatorOutput = execSync('node validate.js', {
cwd: acsCliDir, // Cambiar a directorio del CLI, no del proyecto usuario
env: { ...process.env, TARGET_DIR: this.targetDir }, // Pasar directorio target como env var
encoding: 'utf8',
stdio: 'pipe'
});
console.log(validatorOutput);
} catch (error) {
console.log(chalk.red('Error ejecutando validador:', error.message));
}
// Detectar cambios específicos
const changes = await this.detectChanges();
// Analizar si los cambios están justificados
const analysis = await this.analyzeChanges(changes);
return {
validatorResults: {}, // Simplified for now
changes,
analysis
};
}
async detectChanges() {
const changes = [];
const files = ['README.md', 'ARCHITECTURE.md', 'RULES.md', 'SYSTEM_PROMPT.md', 'FEATURES.md', 'CHANGELOG.md'];
for (const file of files) {
const currentPath = path.join(this.contextDir, file);
const backupPath = path.join(this.backupDir, file);
if (await fs.pathExists(currentPath) && await fs.pathExists(backupPath)) {
const current = await fs.readFile(currentPath, 'utf8');
const backup = await fs.readFile(backupPath, 'utf8');
if (current !== backup) {
changes.push({
file,
hasChanges: true,
sizeDiff: current.length - backup.length,
linesAdded: current.split('\n').length - backup.split('\n').length
});
}
}
}
return changes;
}
async analyzeChanges(changes) {
const facts = await fs.readJson(this.factsFile);
const analysis = {
safe: true,
warnings: [],
critical: [],
summary: ''
};
for (const change of changes) {
if (!change.hasChanges) continue;
const currentContent = await fs.readFile(path.join(this.contextDir, change.file), 'utf8');
// Verificar que no se introdujeron tecnologías inventadas como stack principal
const knownDeps = Object.keys({
...facts.tech.dependencies || {},
...facts.tech.devDependencies || {}
});
const commonTechs = ['react', 'vue', 'angular', 'laravel', 'django', 'express'];
commonTechs.forEach(tech => {
// Solo flagear si se menciona como stack principal, no en ejemplos
const stackMentionPatterns = [
new RegExp(`\\*\\*Stack:\\*\\*.*${tech}`, 'i'),
new RegExp(`Stack tecnológico.*${tech}`, 'i'),
new RegExp(`Tecnologías principales.*${tech}`, 'i'),
new RegExp(`"name": ".*${tech}`, 'i')
];
const hasStackMention = stackMentionPatterns.some(pattern => pattern.test(currentContent));
const hasInDependencies = knownDeps.some(dep => dep.toLowerCase().includes(tech));
if (hasStackMention && !hasInDependencies) {
analysis.critical.push(`${change.file}: Menciona '${tech}' como stack principal pero no está en dependencias`);
analysis.safe = false;
}
});
// Verificar placeholders prohibidos (solo los que la IA debería completar)
const criticalPatterns = [
/git clone \[URL_DEL_REPO\]/i, // Solo crítico si aparece en comando git clone
/\[comando_[^\]]*\]/i,
/\[Stack por definir\]/i,
/\[definir\]/i,
/\[Tecnologías por definir\]/i,
/\[descripción\]/i,
/\[responsable\]/i,
/\[usuario_prueba\]/i,
/\[password_prueba\]/i
];
const warningPatterns = [
// Placeholders que pueden ser válidos pero se monitorean
/\[Definir pipeline[^\]]*\]/i,
/\[Definir proceso[^\]]*\]/i,
/\[Pendiente definir\].*(?:CI\/CD|deployment|infraestructura)/i
];
criticalPatterns.forEach(pattern => {
if (pattern.test(currentContent)) {
analysis.critical.push(`${change.file}: Contiene placeholder que la IA debería completar: ${pattern}`);
analysis.safe = false;
}
});
warningPatterns.forEach(pattern => {
if (pattern.test(currentContent)) {
analysis.warnings.push(`${change.file}: Contiene placeholder de decisión humana: ${pattern}`);
}
});
// Cambios muy grandes son sospechosos
if (Math.abs(change.sizeDiff) > 2000) {
analysis.warnings.push(`${change.file}: Cambio muy grande (${change.sizeDiff} caracteres)`);
}
}
analysis.summary = analysis.safe ?
'Cambios parecen seguros y basados en facts' :
'Cambios contienen problemas críticos que deben corregirse';
return analysis;
}
async handleValidationResult(result) {
const { analysis, changes } = result;
console.log(chalk.cyan('\n📊 RESULTADO DE VALIDACIÓN:'));
if (changes.length === 0) {
console.log(chalk.yellow('⚠️ No se detectaron cambios. El agente no modificó ningún archivo.'));
console.log(chalk.gray(' Esto puede indicar que la documentación ya estaba correcta.'));
return;
}
// Mostrar cambios detectados
console.log(chalk.blue(`📝 Archivos modificados: ${changes.length}`));
changes.forEach(change => {
if (change.hasChanges) {
const sizeIndicator = change.sizeDiff > 0 ? `+${change.sizeDiff}` : change.sizeDiff;
console.log(` - ${change.file}: ${sizeIndicator} caracteres`);
}
});
// Mostrar análisis de seguridad
if (analysis.critical.length > 0) {
console.log(chalk.red('\n🚫 PROBLEMAS CRÍTICOS DETECTADOS:'));
analysis.critical.forEach(issue => {
console.log(chalk.red(` - ${issue}`));
});
console.log(chalk.red('\n❌ CAMBIOS RECHAZADOS automáticamente'));
console.log(chalk.yellow('💡 Corrige estos problemas y vuelve a ejecutar el agente'));
await this.restoreBackup();
return;
}
if (analysis.warnings.length > 0) {
console.log(chalk.yellow('\n⚠️ ADVERTENCIAS:'));
analysis.warnings.forEach(warning => {
console.log(chalk.yellow(` - ${warning}`));
});
}
if (analysis.safe) {
console.log(chalk.green('\n✅ CAMBIOS APROBADOS'));
console.log(chalk.green(' Los cambios parecen seguros y basados en facts del proyecto'));
// Limpiar backup
await fs.remove(this.backupDir);
await fs.remove(this.factsFile);
console.log(chalk.cyan('\n🎉 Documentación actualizada exitosamente'));
} else {
console.log(chalk.red('\n❌ CAMBIOS RECHAZADOS'));
await this.restoreBackup();
}
}
async restoreBackup() {
console.log(chalk.yellow('\n🔄 Restaurando backup...'));
if (await fs.pathExists(this.backupDir)) {
await fs.remove(this.contextDir);
await fs.move(this.backupDir, this.contextDir);
console.log(chalk.green('✅ Documentación restaurada al estado anterior'));
}
}
}
// Hacer ACSValidator disponible para importar
module.exports = SafeAgentRunner;
// CLI execution
if (require.main === module) {
const runner = new SafeAgentRunner();
runner.run().catch(error => {
console.error('Error:', error.message);
process.exit(1);
});
}
module.exports = SafeAgentRunner;