zugzbot-sdd 1.5.28 → 1.5.30

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.
@@ -0,0 +1,51 @@
1
+ # Skill: SDD Secure Coding (Threat Modeling & Sanitization)
2
+
3
+ Esta habilidad impone directivas de codificación segura obligatorias para el enjambre de agentes en todas las fases de **Construcción (F2)** y **Calidad (F3)**, garantizando que el software desarrollado sea inherentemente inmune a vulnerabilidades de seguridad clásicas.
4
+
5
+ ## Trigger
6
+
7
+ Se activa obligatoriamente al modificar código relacionado con:
8
+ 1. Entrada de texto o manipulación del DOM por parte del usuario (Forms, inputs, query params).
9
+ 2. Interacciones con bases de datos, APIs de persistencia o lectura/escritura de ficheros locales.
10
+ 3. Actualización de paquetes o importación de nuevas librerías.
11
+
12
+ ## Directivas de Seguridad del Swarm
13
+
14
+ Todos los subagentes deben aplicar estrictamente las siguientes reglas operativas:
15
+
16
+ ### 1. Inmunización contra XSS (Cross-Site Scripting)
17
+ Queda estrictamente prohibido inyectar HTML de forma directa en el DOM sin previa sanitización.
18
+ * **Acción:** Utiliza siempre métodos nativos seguros de asignación como `textContent` o `innerText` en lugar de `innerHTML` o `dangerouslySetInnerHTML`.
19
+ * Si es mandatorio renderizar HTML dinámico, pasa el contenido por un sanitizador de confianza (como `DOMPurify` o una función de escape robusta local):
20
+
21
+ ```javascript
22
+ function escapeHTML(str) {
23
+ return str.replace(/[&<>'"]/g,
24
+ tag => ({
25
+ '&': '&amp;',
26
+ '<': '&lt;',
27
+ '>': '&gt;',
28
+ "'": '&#39;',
29
+ '"': '&quot;'
30
+ }[tag] || tag)
31
+ );
32
+ }
33
+ ```
34
+
35
+ ### 2. Parametrización Absoluta (SQL & Command Injections)
36
+ Cualquier consulta a base de datos, llamada a bash o comandos de ejecución del sistema no debe concatenar strings de variables de usuario.
37
+ * **Acción:** Utiliza queries preparadas (parameterized queries) o valida rigurosamente los parámetros antes de pasarlos a ejecutores dinámicos.
38
+
39
+ ### 3. Escaneo Pre-Commit de Secretos
40
+ Ningún secreto, clave de API, tokens de autenticación o credenciales privadas de Git debe subirse a producción.
41
+ * **Acción:** El `@sdd-tester` y `@sdd-archiver` deben auditar activamente las diferencias en Git (`git diff`) mediante patrones regex de entropía para detectar strings sospechosos.
42
+
43
+ ## Criterios de Aceptación (QA)
44
+
45
+ - `[ ]` **Seguridad DOM:** Cero uso de asignaciones directas de HTML inseguras (`innerHTML`).
46
+ - `[ ]` **Persistencia Limpia:** Todas las consultas externas utilizan interfaces tipadas o parametrizadas.
47
+ - `[ ]` **Cero Secretos:** No se filtran API keys en el área de preparación de Git.
48
+
49
+ ## Tags
50
+
51
+ #sdd #security #xss #sanitization #secrets #sql-injection #secure-coder
@@ -22,3 +22,4 @@ export { default as sdd_spec_compliance_linter } from './sdd_spec_compliance_lin
22
22
  export { default as sdd_sandbox_patcher } from './sdd_sandbox_patcher.js';
23
23
  export { default as sdd_context_pruner } from './sdd_context_pruner.js';
24
24
  export { default as gas_clasp_tools } from './gas_clasp_tools.js';
25
+ export { default as sdd_auto_healer } from './sdd_auto_healer.js';
@@ -185,18 +185,7 @@ export default tool({
185
185
  report.push(`⚠️ No se pudo restablecer el lockfile: ${e.message}`);
186
186
  }
187
187
  }
188
- // 6. Confirmación Git Atómica (usa temp commit msg, antes de archivar)
189
- if (fs.existsSync(path.join(projectRoot, ".git"))) {
190
- try {
191
- execSync("git add .", { cwd: projectRoot, stdio: "ignore" });
192
- execSync(`git commit -F "${tempCommitMsgPath}"`, { cwd: projectRoot, stdio: "ignore" });
193
- report.push(`✓ Commit de Git ejecutado usando el mensaje semántico`);
194
- }
195
- catch (e) {
196
- report.push(`⚠️ Git Commit falló o no había cambios pendientes de código: ${e.message}`);
197
- }
198
- }
199
- // 8. Archivar la carpeta físicamente (DESPUÉS del commit)
188
+ // 6. Archivar la carpeta físicamente (ANTES del commit para que quede registrado de forma atómica)
200
189
  const archiveDir = path.join(projectRoot, ".openspec/changes/archive", `${dateStr}-${args.changeName}`);
201
190
  try {
202
191
  if (fs.existsSync(archiveDir)) {
@@ -209,7 +198,18 @@ export default tool({
209
198
  catch (e) {
210
199
  return `[SDD Archive Error] Error crítico archivando carpetas: ${e.message}`;
211
200
  }
212
- // 9. Limpiar archivo temporal de commit message
201
+ // 7. Confirmación Git Atómica (incluye la carpeta archivada y la eliminación de la activa)
202
+ if (fs.existsSync(path.join(projectRoot, ".git"))) {
203
+ try {
204
+ execSync("git add .", { cwd: projectRoot, stdio: "ignore" });
205
+ execSync(`git commit -F "${tempCommitMsgPath}"`, { cwd: projectRoot, stdio: "ignore" });
206
+ report.push(`✓ Commit de Git ejecutado usando el mensaje semántico (incluye archivos archivados)`);
207
+ }
208
+ catch (e) {
209
+ report.push(`⚠️ Git Commit falló o no había cambios pendientes de código: ${e.message}`);
210
+ }
211
+ }
212
+ // 8. Limpiar archivo temporal de commit message
213
213
  try {
214
214
  if (fs.existsSync(tempCommitMsgPath)) {
215
215
  fs.unlinkSync(tempCommitMsgPath);
@@ -0,0 +1,90 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ export default tool({
5
+ description: "Intenta corregir de forma autónoma errores sintácticos y de compilación simples (comillas rotas, llaves huérfanas, imports duplicados) en base a los logs de error del linter o compilador.",
6
+ args: {
7
+ errorLogs: tool.schema.string().describe("El log de error bruto del compilador o linter."),
8
+ targetFile: tool.schema.string().optional().describe("Archivo sugerido que contiene el error. Si no se provee, se detectará del log.")
9
+ },
10
+ async execute(args, context) {
11
+ const projectRoot = context.worktree || context.directory;
12
+ const report = ["━━━ sdd_auto_healer ━━━"];
13
+ let detectedFile = args.targetFile;
14
+ // 1. Detectar archivo del log si no se provee
15
+ if (!detectedFile) {
16
+ const fileMatch = args.errorLogs.match(/([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)/i);
17
+ if (fileMatch) {
18
+ detectedFile = fileMatch[1];
19
+ }
20
+ }
21
+ if (!detectedFile) {
22
+ return `[SDD AutoHealer] No se pudo identificar ningún archivo objetivo del log de error. Omitiendo.`;
23
+ }
24
+ const fullPath = path.join(projectRoot, detectedFile);
25
+ if (!fs.existsSync(fullPath)) {
26
+ return `[SDD AutoHealer] El archivo detectado no existe en el disco: ${detectedFile}`;
27
+ }
28
+ try {
29
+ let content = fs.readFileSync(fullPath, "utf-8");
30
+ let modified = false;
31
+ // ── MÉTODOS DE AUTOCURACIÓN ESTÁNDAR ──
32
+ // 1. Corregir llaves/paréntesis sin cerrar simples al final de archivos JS/TS
33
+ if (args.errorLogs.includes("Unexpected end of input") || args.errorLogs.includes("Unclosed")) {
34
+ const openBraces = (content.match(/\{/g) || []).length;
35
+ const closeBraces = (content.match(/\}/g) || []).length;
36
+ if (openBraces > closeBraces) {
37
+ content += "\n" + "}".repeat(openBraces - closeBraces);
38
+ report.push(`✓ Autocurado: Se añadieron ${openBraces - closeBraces} llaves '}' faltantes al final del archivo.`);
39
+ modified = true;
40
+ }
41
+ const openParens = (content.match(/\(/g) || []).length;
42
+ const closeParens = (content.match(/\)/g) || []).length;
43
+ if (openParens > closeParens) {
44
+ content += "\n" + ")".repeat(openParens - closeParens);
45
+ report.push(`✓ Autocurado: Se añadieron ${openParens - closeParens} paréntesis ')' faltantes al final del archivo.`);
46
+ modified = true;
47
+ }
48
+ }
49
+ // 2. Resolver imports duplicados simples
50
+ if (args.errorLogs.includes("has already been declared") || args.errorLogs.includes("Duplicate identifier")) {
51
+ const lines = content.split("\n");
52
+ const seenImports = new Set();
53
+ const cleanedLines = lines.filter(line => {
54
+ if (line.trim().startsWith("import ")) {
55
+ if (seenImports.has(line.trim())) {
56
+ report.push(`✓ Autocurado: Import duplicado eliminado de raíz: '${line.trim()}'`);
57
+ modified = true;
58
+ return false;
59
+ }
60
+ seenImports.add(line.trim());
61
+ }
62
+ return true;
63
+ });
64
+ if (modified) {
65
+ content = cleanedLines.join("\n");
66
+ }
67
+ }
68
+ // 3. Corregir punto y coma faltante o errores de formato de JSON simples
69
+ if (detectedFile.endsWith(".json") && (args.errorLogs.includes("JSON") || args.errorLogs.includes("Unexpected token"))) {
70
+ // Eliminar comas colgantes simples al final de objetos JSON (trailing commas)
71
+ const repaired = content.replace(/,(\s*[\]}])/g, "$1");
72
+ if (repaired !== content) {
73
+ content = repaired;
74
+ report.push(`✓ Autocurado: Se removió una coma colgante en el archivo JSON.`);
75
+ modified = true;
76
+ }
77
+ }
78
+ // 4. Guardar archivo si fue modificado
79
+ if (modified) {
80
+ fs.writeFileSync(fullPath, content, "utf-8");
81
+ report.push(`✓ Archivo '${detectedFile}' guardado y reparado de manera autónoma.`);
82
+ return report.join("\n");
83
+ }
84
+ return `[SDD AutoHealer] No se pudo encontrar un patrón de error auto-curable conocido para '${detectedFile}'.`;
85
+ }
86
+ catch (e) {
87
+ return `[SDD AutoHealer] Error intentando auto-curar archivo: ${e.message || e}`;
88
+ }
89
+ }
90
+ });
@@ -344,6 +344,70 @@ export default tool({
344
344
  }
345
345
  };
346
346
  fs.appendFileSync(historyPath, JSON.stringify(logEntry) + "\n", "utf-8");
347
+ // GENERACIÓN AUTOMÁTICA DEL DASHBOARD DE ANALÍTICAS
348
+ try {
349
+ const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
350
+ const historyEntries = lines.map(l => JSON.parse(l));
351
+ let totalCost = 0;
352
+ let totalInputTokens = 0;
353
+ let totalOutputTokens = 0;
354
+ const allModels = new Set();
355
+ const phaseCounts = {};
356
+ historyEntries.forEach(entry => {
357
+ if (entry.analytics) {
358
+ totalCost = Math.max(totalCost, entry.analytics.cumulative_cost_usd || 0);
359
+ totalInputTokens = Math.max(totalInputTokens, entry.analytics.cumulative_tokens_input || 0);
360
+ totalOutputTokens = Math.max(totalOutputTokens, entry.analytics.cumulative_tokens_output || 0);
361
+ if (Array.isArray(entry.analytics.models_used)) {
362
+ entry.analytics.models_used.forEach((m) => allModels.add(m));
363
+ }
364
+ }
365
+ phaseCounts[entry.phase] = (phaseCounts[entry.phase] || 0) + 1;
366
+ });
367
+ // Crear barras visuales estéticas en consola Markdown
368
+ const budgetLimit = 2.00; // Presupuesto sugerido de $2.00 USD
369
+ const costPercentage = Math.min(100, Math.round((totalCost / budgetLimit) * 100));
370
+ const barLength = 20;
371
+ const filledLength = Math.round((costPercentage / 100) * barLength);
372
+ const emptyLength = barLength - filledLength;
373
+ const progressBar = "█".repeat(filledLength) + "░".repeat(emptyLength);
374
+ const analyticsMarkdown = `# 📊 Tablero de Analíticas del Swarm: ${lockfile.change_name}
375
+
376
+ Este reporte es generado de forma autónoma por la herramienta **sdd_transition** al cierre de cada fase para auditar la economía de tokens, presupuestos y eficiencia del enjambre multi-agente.
377
+
378
+ > [!NOTE]
379
+ > **Resumen Financiero de la Sesión:**
380
+ > - **Presupuesto Consumido:** $${totalCost.toFixed(4)} USD
381
+ > - **Límite de Presupuesto:** $${budgetLimit.toFixed(2)} USD
382
+ > - **Eficiencia de Tokens:** Entrada: ${totalInputTokens.toLocaleString()} | Salida: ${totalOutputTokens.toLocaleString()}
383
+ > - **Modelos Utilizados:** ${Array.from(allModels).join(", ") || "Ninguno registrado"}
384
+
385
+ ### 💳 Control de Presupuesto Consumido ($${totalCost.toFixed(4)} / $${budgetLimit.toFixed(2)})
386
+ \`\`\`text
387
+ [${progressBar}] ${costPercentage}% de límite
388
+ \`\`\`
389
+
390
+ ## 🔄 Historial Completo del Swarm (Fases e Iteraciones)
391
+
392
+ | Marca de Tiempo | Fase | Subagente | Estado | Iteración | Motivo |
393
+ | :--- | :--- | :--- | :--- | :--- | :--- |
394
+ ${historyEntries.map(e => `| ${e.timestamp.split("T")[1].substring(0, 8)} | F${e.phase} | \`@${e.subagent}\` | \`${e.status}\` | ${e.iteration} | ${e.reason} |`).join("\n")}
395
+
396
+ ---
397
+
398
+ ## 📈 Estadísticas de Frecuencia de Fases
399
+ - **Fase 0 (Explorer):** ${phaseCounts[0] || 0} visitas.
400
+ - **Fase 1 (Planner):** ${phaseCounts[1] || 0} visitas.
401
+ - **Fase 2 (Builder):** ${phaseCounts[2] || 0} visitas.
402
+ - **Fase 3 (Tester):** ${phaseCounts[3] || 0} visitas.
403
+ - **Fase 4 (Deployer):** ${phaseCounts[4] || 0} visitas.
404
+ - **Fase 5 (Archiver):** ${phaseCounts[5] || 0} visitas.
405
+
406
+ *Nota: Múltiples visitas a una misma fase indican reintentos o ciclos correctivos activos.*
407
+ `;
408
+ fs.writeFileSync(path.join(projectRoot, ".openspec/analytics.md"), analyticsMarkdown, "utf-8");
409
+ }
410
+ catch (err) { }
347
411
  }
348
412
  }
349
413
  // INTEGRACIÓN AUTOMÁTICA CON GIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot-sdd",
3
- "version": "1.5.28",
3
+ "version": "1.5.30",
4
4
  "description": "Zugzbot SDD Swarm - Spec-Driven Development Harness for OpenCode",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,51 @@
1
+ # Skill: SDD Secure Coding (Threat Modeling & Sanitization)
2
+
3
+ Esta habilidad impone directivas de codificación segura obligatorias para el enjambre de agentes en todas las fases de **Construcción (F2)** y **Calidad (F3)**, garantizando que el software desarrollado sea inherentemente inmune a vulnerabilidades de seguridad clásicas.
4
+
5
+ ## Trigger
6
+
7
+ Se activa obligatoriamente al modificar código relacionado con:
8
+ 1. Entrada de texto o manipulación del DOM por parte del usuario (Forms, inputs, query params).
9
+ 2. Interacciones con bases de datos, APIs de persistencia o lectura/escritura de ficheros locales.
10
+ 3. Actualización de paquetes o importación de nuevas librerías.
11
+
12
+ ## Directivas de Seguridad del Swarm
13
+
14
+ Todos los subagentes deben aplicar estrictamente las siguientes reglas operativas:
15
+
16
+ ### 1. Inmunización contra XSS (Cross-Site Scripting)
17
+ Queda estrictamente prohibido inyectar HTML de forma directa en el DOM sin previa sanitización.
18
+ * **Acción:** Utiliza siempre métodos nativos seguros de asignación como `textContent` o `innerText` en lugar de `innerHTML` o `dangerouslySetInnerHTML`.
19
+ * Si es mandatorio renderizar HTML dinámico, pasa el contenido por un sanitizador de confianza (como `DOMPurify` o una función de escape robusta local):
20
+
21
+ ```javascript
22
+ function escapeHTML(str) {
23
+ return str.replace(/[&<>'"]/g,
24
+ tag => ({
25
+ '&': '&amp;',
26
+ '<': '&lt;',
27
+ '>': '&gt;',
28
+ "'": '&#39;',
29
+ '"': '&quot;'
30
+ }[tag] || tag)
31
+ );
32
+ }
33
+ ```
34
+
35
+ ### 2. Parametrización Absoluta (SQL & Command Injections)
36
+ Cualquier consulta a base de datos, llamada a bash o comandos de ejecución del sistema no debe concatenar strings de variables de usuario.
37
+ * **Acción:** Utiliza queries preparadas (parameterized queries) o valida rigurosamente los parámetros antes de pasarlos a ejecutores dinámicos.
38
+
39
+ ### 3. Escaneo Pre-Commit de Secretos
40
+ Ningún secreto, clave de API, tokens de autenticación o credenciales privadas de Git debe subirse a producción.
41
+ * **Acción:** El `@sdd-tester` y `@sdd-archiver` deben auditar activamente las diferencias en Git (`git diff`) mediante patrones regex de entropía para detectar strings sospechosos.
42
+
43
+ ## Criterios de Aceptación (QA)
44
+
45
+ - `[ ]` **Seguridad DOM:** Cero uso de asignaciones directas de HTML inseguras (`innerHTML`).
46
+ - `[ ]` **Persistencia Limpia:** Todas las consultas externas utilizan interfaces tipadas o parametrizadas.
47
+ - `[ ]` **Cero Secretos:** No se filtran API keys en el área de preparación de Git.
48
+
49
+ ## Tags
50
+
51
+ #sdd #security #xss #sanitization #secrets #sql-injection #secure-coder
package/tools/index.ts CHANGED
@@ -21,4 +21,5 @@ export { default as sdd_test_scaffold_generator } from './sdd_test_scaffold_gene
21
21
  export { default as sdd_spec_compliance_linter } from './sdd_spec_compliance_linter.js';
22
22
  export { default as sdd_sandbox_patcher } from './sdd_sandbox_patcher.js';
23
23
  export { default as sdd_context_pruner } from './sdd_context_pruner.js';
24
- export { default as gas_clasp_tools } from './gas_clasp_tools.js';
24
+ export { default as gas_clasp_tools } from './gas_clasp_tools.js';
25
+ export { default as sdd_auto_healer } from './sdd_auto_healer.js';
@@ -184,18 +184,7 @@ export default tool({
184
184
  }
185
185
  }
186
186
 
187
- // 6. Confirmación Git Atómica (usa temp commit msg, antes de archivar)
188
- if (fs.existsSync(path.join(projectRoot, ".git"))) {
189
- try {
190
- execSync("git add .", { cwd: projectRoot, stdio: "ignore" })
191
- execSync(`git commit -F "${tempCommitMsgPath}"`, { cwd: projectRoot, stdio: "ignore" })
192
- report.push(`✓ Commit de Git ejecutado usando el mensaje semántico`)
193
- } catch (e: any) {
194
- report.push(`⚠️ Git Commit falló o no había cambios pendientes de código: ${e.message}`)
195
- }
196
- }
197
-
198
- // 8. Archivar la carpeta físicamente (DESPUÉS del commit)
187
+ // 6. Archivar la carpeta físicamente (ANTES del commit para que quede registrado de forma atómica)
199
188
  const archiveDir = path.join(projectRoot, ".openspec/changes/archive", `${dateStr}-${args.changeName}`)
200
189
  try {
201
190
  if (fs.existsSync(archiveDir)) {
@@ -208,7 +197,18 @@ export default tool({
208
197
  return `[SDD Archive Error] Error crítico archivando carpetas: ${e.message}`
209
198
  }
210
199
 
211
- // 9. Limpiar archivo temporal de commit message
200
+ // 7. Confirmación Git Atómica (incluye la carpeta archivada y la eliminación de la activa)
201
+ if (fs.existsSync(path.join(projectRoot, ".git"))) {
202
+ try {
203
+ execSync("git add .", { cwd: projectRoot, stdio: "ignore" })
204
+ execSync(`git commit -F "${tempCommitMsgPath}"`, { cwd: projectRoot, stdio: "ignore" })
205
+ report.push(`✓ Commit de Git ejecutado usando el mensaje semántico (incluye archivos archivados)`)
206
+ } catch (e: any) {
207
+ report.push(`⚠️ Git Commit falló o no había cambios pendientes de código: ${e.message}`)
208
+ }
209
+ }
210
+
211
+ // 8. Limpiar archivo temporal de commit message
212
212
  try {
213
213
  if (fs.existsSync(tempCommitMsgPath)) {
214
214
  fs.unlinkSync(tempCommitMsgPath)
@@ -0,0 +1,102 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import fs from "fs"
3
+ import path from "path"
4
+
5
+ export default tool({
6
+ description: "Intenta corregir de forma autónoma errores sintácticos y de compilación simples (comillas rotas, llaves huérfanas, imports duplicados) en base a los logs de error del linter o compilador.",
7
+ args: {
8
+ errorLogs: tool.schema.string().describe("El log de error bruto del compilador o linter."),
9
+ targetFile: tool.schema.string().optional().describe("Archivo sugerido que contiene el error. Si no se provee, se detectará del log.")
10
+ },
11
+ async execute(args, context) {
12
+ const projectRoot = context.worktree || context.directory;
13
+ const report: string[] = ["━━━ sdd_auto_healer ━━━"];
14
+
15
+ let detectedFile = args.targetFile;
16
+
17
+ // 1. Detectar archivo del log si no se provee
18
+ if (!detectedFile) {
19
+ const fileMatch = args.errorLogs.match(/([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)/i);
20
+ if (fileMatch) {
21
+ detectedFile = fileMatch[1];
22
+ }
23
+ }
24
+
25
+ if (!detectedFile) {
26
+ return `[SDD AutoHealer] No se pudo identificar ningún archivo objetivo del log de error. Omitiendo.`;
27
+ }
28
+
29
+ const fullPath = path.join(projectRoot, detectedFile);
30
+ if (!fs.existsSync(fullPath)) {
31
+ return `[SDD AutoHealer] El archivo detectado no existe en el disco: ${detectedFile}`;
32
+ }
33
+
34
+ try {
35
+ let content = fs.readFileSync(fullPath, "utf-8");
36
+ let modified = false;
37
+
38
+ // ── MÉTODOS DE AUTOCURACIÓN ESTÁNDAR ──
39
+
40
+ // 1. Corregir llaves/paréntesis sin cerrar simples al final de archivos JS/TS
41
+ if (args.errorLogs.includes("Unexpected end of input") || args.errorLogs.includes("Unclosed")) {
42
+ const openBraces = (content.match(/\{/g) || []).length;
43
+ const closeBraces = (content.match(/\}/g) || []).length;
44
+ if (openBraces > closeBraces) {
45
+ content += "\n" + "}".repeat(openBraces - closeBraces);
46
+ report.push(`✓ Autocurado: Se añadieron ${openBraces - closeBraces} llaves '}' faltantes al final del archivo.`);
47
+ modified = true;
48
+ }
49
+
50
+ const openParens = (content.match(/\(/g) || []).length;
51
+ const closeParens = (content.match(/\)/g) || []).length;
52
+ if (openParens > closeParens) {
53
+ content += "\n" + ")".repeat(openParens - closeParens);
54
+ report.push(`✓ Autocurado: Se añadieron ${openParens - closeParens} paréntesis ')' faltantes al final del archivo.`);
55
+ modified = true;
56
+ }
57
+ }
58
+
59
+ // 2. Resolver imports duplicados simples
60
+ if (args.errorLogs.includes("has already been declared") || args.errorLogs.includes("Duplicate identifier")) {
61
+ const lines = content.split("\n");
62
+ const seenImports = new Set<string>();
63
+ const cleanedLines = lines.filter(line => {
64
+ if (line.trim().startsWith("import ")) {
65
+ if (seenImports.has(line.trim())) {
66
+ report.push(`✓ Autocurado: Import duplicado eliminado de raíz: '${line.trim()}'`);
67
+ modified = true;
68
+ return false;
69
+ }
70
+ seenImports.add(line.trim());
71
+ }
72
+ return true;
73
+ });
74
+ if (modified) {
75
+ content = cleanedLines.join("\n");
76
+ }
77
+ }
78
+
79
+ // 3. Corregir punto y coma faltante o errores de formato de JSON simples
80
+ if (detectedFile.endsWith(".json") && (args.errorLogs.includes("JSON") || args.errorLogs.includes("Unexpected token"))) {
81
+ // Eliminar comas colgantes simples al final de objetos JSON (trailing commas)
82
+ const repaired = content.replace(/,(\s*[\]}])/g, "$1");
83
+ if (repaired !== content) {
84
+ content = repaired;
85
+ report.push(`✓ Autocurado: Se removió una coma colgante en el archivo JSON.`);
86
+ modified = true;
87
+ }
88
+ }
89
+
90
+ // 4. Guardar archivo si fue modificado
91
+ if (modified) {
92
+ fs.writeFileSync(fullPath, content, "utf-8");
93
+ report.push(`✓ Archivo '${detectedFile}' guardado y reparado de manera autónoma.`);
94
+ return report.join("\n");
95
+ }
96
+
97
+ return `[SDD AutoHealer] No se pudo encontrar un patrón de error auto-curable conocido para '${detectedFile}'.`;
98
+ } catch (e: any) {
99
+ return `[SDD AutoHealer] Error intentando auto-curar archivo: ${e.message || e}`;
100
+ }
101
+ }
102
+ })
@@ -358,6 +358,74 @@ export default tool({
358
358
  }
359
359
  };
360
360
  fs.appendFileSync(historyPath, JSON.stringify(logEntry) + "\n", "utf-8");
361
+
362
+ // GENERACIÓN AUTOMÁTICA DEL DASHBOARD DE ANALÍTICAS
363
+ try {
364
+ const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
365
+ const historyEntries = lines.map(l => JSON.parse(l));
366
+
367
+ let totalCost = 0;
368
+ let totalInputTokens = 0;
369
+ let totalOutputTokens = 0;
370
+ const allModels = new Set<string>();
371
+ const phaseCounts: { [key: number]: number } = {};
372
+
373
+ historyEntries.forEach(entry => {
374
+ if (entry.analytics) {
375
+ totalCost = Math.max(totalCost, entry.analytics.cumulative_cost_usd || 0);
376
+ totalInputTokens = Math.max(totalInputTokens, entry.analytics.cumulative_tokens_input || 0);
377
+ totalOutputTokens = Math.max(totalOutputTokens, entry.analytics.cumulative_tokens_output || 0);
378
+ if (Array.isArray(entry.analytics.models_used)) {
379
+ entry.analytics.models_used.forEach((m: string) => allModels.add(m));
380
+ }
381
+ }
382
+ phaseCounts[entry.phase] = (phaseCounts[entry.phase] || 0) + 1;
383
+ });
384
+
385
+ // Crear barras visuales estéticas en consola Markdown
386
+ const budgetLimit = 2.00; // Presupuesto sugerido de $2.00 USD
387
+ const costPercentage = Math.min(100, Math.round((totalCost / budgetLimit) * 100));
388
+ const barLength = 20;
389
+ const filledLength = Math.round((costPercentage / 100) * barLength);
390
+ const emptyLength = barLength - filledLength;
391
+ const progressBar = "█".repeat(filledLength) + "░".repeat(emptyLength);
392
+
393
+ const analyticsMarkdown = `# 📊 Tablero de Analíticas del Swarm: ${lockfile.change_name}
394
+
395
+ Este reporte es generado de forma autónoma por la herramienta **sdd_transition** al cierre de cada fase para auditar la economía de tokens, presupuestos y eficiencia del enjambre multi-agente.
396
+
397
+ > [!NOTE]
398
+ > **Resumen Financiero de la Sesión:**
399
+ > - **Presupuesto Consumido:** $${totalCost.toFixed(4)} USD
400
+ > - **Límite de Presupuesto:** $${budgetLimit.toFixed(2)} USD
401
+ > - **Eficiencia de Tokens:** Entrada: ${totalInputTokens.toLocaleString()} | Salida: ${totalOutputTokens.toLocaleString()}
402
+ > - **Modelos Utilizados:** ${Array.from(allModels).join(", ") || "Ninguno registrado"}
403
+
404
+ ### 💳 Control de Presupuesto Consumido ($${totalCost.toFixed(4)} / $${budgetLimit.toFixed(2)})
405
+ \`\`\`text
406
+ [${progressBar}] ${costPercentage}% de límite
407
+ \`\`\`
408
+
409
+ ## 🔄 Historial Completo del Swarm (Fases e Iteraciones)
410
+
411
+ | Marca de Tiempo | Fase | Subagente | Estado | Iteración | Motivo |
412
+ | :--- | :--- | :--- | :--- | :--- | :--- |
413
+ ${historyEntries.map(e => `| ${e.timestamp.split("T")[1].substring(0, 8)} | F${e.phase} | \`@${e.subagent}\` | \`${e.status}\` | ${e.iteration} | ${e.reason} |`).join("\n")}
414
+
415
+ ---
416
+
417
+ ## 📈 Estadísticas de Frecuencia de Fases
418
+ - **Fase 0 (Explorer):** ${phaseCounts[0] || 0} visitas.
419
+ - **Fase 1 (Planner):** ${phaseCounts[1] || 0} visitas.
420
+ - **Fase 2 (Builder):** ${phaseCounts[2] || 0} visitas.
421
+ - **Fase 3 (Tester):** ${phaseCounts[3] || 0} visitas.
422
+ - **Fase 4 (Deployer):** ${phaseCounts[4] || 0} visitas.
423
+ - **Fase 5 (Archiver):** ${phaseCounts[5] || 0} visitas.
424
+
425
+ *Nota: Múltiples visitas a una misma fase indican reintentos o ciclos correctivos activos.*
426
+ `;
427
+ fs.writeFileSync(path.join(projectRoot, ".openspec/analytics.md"), analyticsMarkdown, "utf-8");
428
+ } catch (err) {}
361
429
  }
362
430
  }
363
431