zugzbot-sdd 1.5.29 → 1.5.31
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/.opencode/skills/sdd-secure-coding/SKILL.md +51 -0
- package/.opencode/tools/index.js +1 -0
- package/.opencode/tools/sdd_archive_and_commit.js +24 -2
- package/.opencode/tools/sdd_auto_healer.js +90 -0
- package/.opencode/tools/sdd_transition.js +64 -0
- package/AGENTS.md +1 -0
- package/package.json +1 -1
- package/skills/sdd-secure-coding/SKILL.md +51 -0
- package/tools/index.ts +2 -1
- package/tools/sdd_archive_and_commit.ts +24 -2
- package/tools/sdd_auto_healer.ts +102 -0
- package/tools/sdd_transition.ts +68 -0
|
@@ -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
|
+
'&': '&',
|
|
26
|
+
'<': '<',
|
|
27
|
+
'>': '>',
|
|
28
|
+
"'": ''',
|
|
29
|
+
'"': '"'
|
|
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/.opencode/tools/index.js
CHANGED
|
@@ -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';
|
|
@@ -186,14 +186,36 @@ export default tool({
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
// 6. Archivar la carpeta físicamente (ANTES del commit para que quede registrado de forma atómica)
|
|
189
|
-
|
|
189
|
+
// Calcular el siguiente prefijo de secuencia cronológica (ej: 0001_2026-05-30_143015)
|
|
190
|
+
let seqPrefix = "0001";
|
|
191
|
+
const archiveRoot = path.join(projectRoot, ".openspec/changes/archive");
|
|
192
|
+
if (fs.existsSync(archiveRoot)) {
|
|
193
|
+
try {
|
|
194
|
+
const dirs = fs.readdirSync(archiveRoot).filter(f => fs.statSync(path.join(archiveRoot, f)).isDirectory());
|
|
195
|
+
let maxSeq = 0;
|
|
196
|
+
dirs.forEach(d => {
|
|
197
|
+
const match = d.match(/^(\d{4})_/);
|
|
198
|
+
if (match) {
|
|
199
|
+
const seq = parseInt(match[1], 10);
|
|
200
|
+
if (seq > maxSeq)
|
|
201
|
+
maxSeq = seq;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
seqPrefix = String(maxSeq + 1).padStart(4, "0");
|
|
205
|
+
}
|
|
206
|
+
catch (e) { }
|
|
207
|
+
}
|
|
208
|
+
const now = new Date();
|
|
209
|
+
const timeStr = now.toTimeString().split(" ")[0].replace(/:/g, ""); // e.g. "143015"
|
|
210
|
+
const archiveFolderName = `${seqPrefix}_${dateStr}_${timeStr}-${args.changeName}`;
|
|
211
|
+
const archiveDir = path.join(archiveRoot, archiveFolderName);
|
|
190
212
|
try {
|
|
191
213
|
if (fs.existsSync(archiveDir)) {
|
|
192
214
|
fs.rmSync(archiveDir, { recursive: true, force: true });
|
|
193
215
|
}
|
|
194
216
|
fs.mkdirSync(path.dirname(archiveDir), { recursive: true });
|
|
195
217
|
moveRecursive(changeDir, archiveDir);
|
|
196
|
-
report.push(`✓ Carpeta archivada en: .openspec/changes/archive/${
|
|
218
|
+
report.push(`✓ Carpeta archivada en: .openspec/changes/archive/${archiveFolderName}/`);
|
|
197
219
|
}
|
|
198
220
|
catch (e) {
|
|
199
221
|
return `[SDD Archive Error] Error crítico archivando carpetas: ${e.message}`;
|
|
@@ -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/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ Queda terminantemente prohibido para cualquier agente del swarm (incluyendo al O
|
|
|
15
15
|
- **No Trabajo en Caliente**: Está prohibido proponer código fuente, diseños HTML/CSS o parches técnicos directamente al usuario en el chat principal sin antes haber completado la **Fase 1 (Planificación e Interrogación)** y obtenido su visto bueno explícito.
|
|
16
16
|
- **Rol del Orquestador**: `@zugzbot` debe educar siempre al usuario sobre el flujo de SDD cuando se solicite una nueva característica o cambio. Debe generar un **Roadmap de las 6 Fases de SDD de una línea por fase** y delegar la Fase 1 de inmediato.
|
|
17
17
|
- **Flujo de Trabajo Estricto**: Todo cambio lógico debe iniciarse a través de la delegación estructurada hacia `@sdd-planner`.
|
|
18
|
+
- **Slug Semántico del Cambio (kebab-case) [CRÍTICO]**: Al iniciar un ciclo (transición a Fase 1), el orquestador `@zugzbot` o `@sdd-planner` debe generar obligatoriamente un nombre de cambio (`changeName` o `change_name`) en kebab-case que describa de manera precisa el requerimiento del usuario (ej: `crear-modulo-auth` o `setup-clasp-mocker`). Queda terminantemente prohibido utilizar el valor genérico 'nuevo-cambio' o nombres sin relación semántica.
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
package/package.json
CHANGED
|
@@ -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
|
+
'&': '&',
|
|
26
|
+
'<': '<',
|
|
27
|
+
'>': '>',
|
|
28
|
+
"'": ''',
|
|
29
|
+
'"': '"'
|
|
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';
|
|
@@ -185,14 +185,36 @@ export default tool({
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// 6. Archivar la carpeta físicamente (ANTES del commit para que quede registrado de forma atómica)
|
|
188
|
-
|
|
188
|
+
// Calcular el siguiente prefijo de secuencia cronológica (ej: 0001_2026-05-30_143015)
|
|
189
|
+
let seqPrefix = "0001";
|
|
190
|
+
const archiveRoot = path.join(projectRoot, ".openspec/changes/archive");
|
|
191
|
+
if (fs.existsSync(archiveRoot)) {
|
|
192
|
+
try {
|
|
193
|
+
const dirs = fs.readdirSync(archiveRoot).filter(f => fs.statSync(path.join(archiveRoot, f)).isDirectory());
|
|
194
|
+
let maxSeq = 0;
|
|
195
|
+
dirs.forEach(d => {
|
|
196
|
+
const match = d.match(/^(\d{4})_/);
|
|
197
|
+
if (match) {
|
|
198
|
+
const seq = parseInt(match[1], 10);
|
|
199
|
+
if (seq > maxSeq) maxSeq = seq;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
seqPrefix = String(maxSeq + 1).padStart(4, "0");
|
|
203
|
+
} catch (e) {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const now = new Date();
|
|
207
|
+
const timeStr = now.toTimeString().split(" ")[0].replace(/:/g, ""); // e.g. "143015"
|
|
208
|
+
const archiveFolderName = `${seqPrefix}_${dateStr}_${timeStr}-${args.changeName}`;
|
|
209
|
+
const archiveDir = path.join(archiveRoot, archiveFolderName);
|
|
210
|
+
|
|
189
211
|
try {
|
|
190
212
|
if (fs.existsSync(archiveDir)) {
|
|
191
213
|
fs.rmSync(archiveDir, { recursive: true, force: true })
|
|
192
214
|
}
|
|
193
215
|
fs.mkdirSync(path.dirname(archiveDir), { recursive: true })
|
|
194
216
|
moveRecursive(changeDir, archiveDir)
|
|
195
|
-
report.push(`✓ Carpeta archivada en: .openspec/changes/archive/${
|
|
217
|
+
report.push(`✓ Carpeta archivada en: .openspec/changes/archive/${archiveFolderName}/`)
|
|
196
218
|
} catch (e: any) {
|
|
197
219
|
return `[SDD Archive Error] Error crítico archivando carpetas: ${e.message}`
|
|
198
220
|
}
|
|
@@ -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
|
+
})
|
package/tools/sdd_transition.ts
CHANGED
|
@@ -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
|
|