@saulwade/swl-ses 1.0.0 → 1.1.1
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/CLAUDE.md +8 -5
- package/README.md +2 -2
- package/agentes/accesibilidad-wcag-swl.md +5 -7
- package/agentes/arquitecto-swl.md +5 -3
- package/agentes/auto-evolucion-swl.md +42 -12
- package/agentes/backend-api-swl.md +5 -3
- package/agentes/backend-csharp-swl.md +5 -3
- package/agentes/backend-go-swl.md +5 -3
- package/agentes/backend-java-swl.md +5 -3
- package/agentes/backend-node-swl.md +5 -3
- package/agentes/backend-python-swl.md +5 -3
- package/agentes/backend-rust-swl.md +5 -3
- package/agentes/backend-workers-swl.md +5 -3
- package/agentes/cloud-infra-swl.md +5 -6
- package/agentes/consolidador-swl.md +5 -3
- package/agentes/datos-swl.md +5 -7
- package/agentes/depurador-swl.md +6 -3
- package/agentes/devops-ci-swl.md +5 -3
- package/agentes/disenador-ui-swl.md +5 -7
- package/agentes/documentador-swl.md +5 -3
- package/agentes/frontend-angular-swl.md +5 -11
- package/agentes/frontend-css-swl.md +5 -9
- package/agentes/frontend-react-swl.md +5 -9
- package/agentes/frontend-swl.md +5 -9
- package/agentes/frontend-tailwind-swl.md +5 -9
- package/agentes/implementador-swl.md +6 -3
- package/agentes/investigador-swl.md +5 -3
- package/agentes/investigador-ux-swl.md +5 -9
- package/agentes/llm-apps-swl.md +5 -3
- package/agentes/migrador-swl.md +6 -3
- package/agentes/mobile-android-swl.md +5 -3
- package/agentes/mobile-cross-swl.md +5 -3
- package/agentes/mobile-ios-swl.md +5 -3
- package/agentes/mobile-testing-swl.md +5 -3
- package/agentes/notificador-swl.md +5 -3
- package/agentes/observabilidad-swl.md +5 -3
- package/agentes/orquestador-swl.md +29 -8
- package/agentes/pagos-swl.md +5 -3
- package/agentes/perfilador-usuario-swl.md +4 -2
- package/agentes/planificador-swl.md +5 -3
- package/agentes/producto-prd-swl.md +5 -3
- package/agentes/red-team-swl.md +4 -2
- package/agentes/release-manager-swl.md +6 -8
- package/agentes/rendimiento-swl.md +5 -6
- package/agentes/resolutor-build-swl.md +5 -3
- package/agentes/revisor-angular-swl.md +5 -3
- package/agentes/revisor-codigo-swl.md +5 -3
- package/agentes/revisor-csharp-swl.md +5 -3
- package/agentes/revisor-go-swl.md +5 -3
- package/agentes/revisor-java-swl.md +5 -3
- package/agentes/revisor-kotlin-swl.md +5 -3
- package/agentes/revisor-nextjs-swl.md +5 -3
- package/agentes/revisor-php-swl.md +5 -3
- package/agentes/revisor-react-swl.md +5 -3
- package/agentes/revisor-rust-swl.md +5 -3
- package/agentes/revisor-seguridad-swl.md +5 -3
- package/agentes/revisor-swift-swl.md +5 -3
- package/agentes/revisor-typescript-swl.md +5 -3
- package/agentes/sre-swl.md +5 -3
- package/agentes/tdd-qa-swl.md +5 -3
- package/agentes/ux-disenador-swl.md +5 -9
- package/bin/swl-ses.js +27 -6
- package/comandos/swl/evaluar-skill.md +18 -0
- package/comandos/swl/evolucion-estado.md +49 -0
- package/comandos/swl/release.md +41 -0
- package/comandos/swl/salud.md +23 -0
- package/hooks/auto-evolucion.js +35 -1
- package/hooks/clasificador-mensajes.js +50 -3
- package/hooks/lib/agent-routing.js +107 -0
- package/hooks/lib/delegation-tracker.js +162 -44
- package/hooks/lib/evolution-tracker.js +12 -3
- package/hooks/lib/memory-search.js +59 -1
- package/hooks/lib/nudge-tracker.js +10 -1
- package/hooks/lib/provenance-tracker.js +11 -3
- package/hooks/lib/text-similarity.js +241 -0
- package/hooks/metricas-evolucion.js +168 -1
- package/hooks/monitor-contexto.js +54 -6
- package/hooks/preservar-estado-pre-compact.js +11 -1
- package/hooks/risk-scoring.js +10 -1
- package/hooks/tracking-costos.js +10 -1
- package/hooks/validar-formato-post-subagente.js +140 -0
- package/hooks/validar-memoria-hook.js +218 -0
- package/manifiestos/agent-output-schemas.json +57 -0
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +5 -1
- package/manifiestos/skills-lock.json +1065 -0
- package/package.json +4 -6
- package/plugin.json +1 -1
- package/reglas/arquitectura.md +20 -0
- package/reglas/fragmentos-compartidos.md +152 -0
- package/reglas/gobernanza.md +10 -1
- package/reglas/seguridad-agentes.md +12 -0
- package/reglas/skills-estandar.md +19 -0
- package/reglas/usar-context7.md +226 -0
- package/schemas/agent-frontmatter.schema.json +18 -0
- package/scripts/auditar-agentes-gaps.js +9 -1
- package/scripts/auditar-cobertura-frameworks.js +9 -1
- package/scripts/auditar-skills-gaps.js +9 -1
- package/scripts/bootstrap-instintos.js +11 -1
- package/scripts/generar-inventario.js +112 -9
- package/scripts/generar-matriz-lenguajes.js +271 -0
- package/scripts/generar-skills-lock.js +190 -0
- package/scripts/lib/estado.js +12 -2
- package/scripts/lib/gitignore-manifest.js +32 -2
- package/scripts/lib/semantic-search.js +10 -0
- package/scripts/migrar-csv-a-array.js +168 -0
- package/scripts/migrar-fase-dominio.js +201 -0
- package/scripts/validar-userland-vacio.js +110 -0
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
const fs = require("fs");
|
|
19
19
|
const path = require("path");
|
|
20
20
|
|
|
21
|
+
// Atomic write para .gitignore y manifest. Estos archivos viven en el
|
|
22
|
+
// repo del usuario; corromperlos durante install puede romper su workflow.
|
|
23
|
+
let atomicWriteSync;
|
|
24
|
+
try {
|
|
25
|
+
({ atomicWriteSync } = require(path.join(__dirname, "..", "..", "hooks", "lib", "atomic-write.js")));
|
|
26
|
+
} catch {
|
|
27
|
+
atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const MARKER_INICIO = "# [swl-ses:inicio]";
|
|
22
31
|
const MARKER_FIN = "# [swl-ses:fin]";
|
|
23
32
|
const MANIFEST_FILE = ".swl-ses";
|
|
@@ -126,6 +135,27 @@ const ENTRADAS_POR_RUNTIME = {
|
|
|
126
135
|
* @returns {string} Ruta al .gitignore
|
|
127
136
|
*/
|
|
128
137
|
function actualizarGitignore(cwd, extras = []) {
|
|
138
|
+
// Guarda defensiva: el bloque inyectado dice "swl-ses es tooling de
|
|
139
|
+
// desarrollo — no forma parte del proyecto". En el repo madre, swl-ses
|
|
140
|
+
// ES el proyecto, así que el bloque no aplica y rompería trackings
|
|
141
|
+
// legítimos (.planning/APRENDIZAJES.md, instintos/, hooks/). Si se
|
|
142
|
+
// detecta el package.json del madre, abortar sin tocar el .gitignore.
|
|
143
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
144
|
+
if (fs.existsSync(pkgPath)) {
|
|
145
|
+
try {
|
|
146
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
147
|
+
const esRepoMadre =
|
|
148
|
+
pkg.name === "@saulwade/swl-ses" ||
|
|
149
|
+
pkg.name === "@saul-wade/swl-ses" ||
|
|
150
|
+
pkg.name === "swl-ses";
|
|
151
|
+
if (esRepoMadre) {
|
|
152
|
+
return path.join(cwd, ".gitignore"); // no-op
|
|
153
|
+
}
|
|
154
|
+
} catch (_) {
|
|
155
|
+
// package.json corrupto — proceder con la lógica normal
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
129
159
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
130
160
|
let existente = fs.existsSync(gitignorePath)
|
|
131
161
|
? fs.readFileSync(gitignorePath, "utf-8")
|
|
@@ -168,7 +198,7 @@ function actualizarGitignore(cwd, extras = []) {
|
|
|
168
198
|
"",
|
|
169
199
|
].join("\n");
|
|
170
200
|
|
|
171
|
-
|
|
201
|
+
atomicWriteSync(gitignorePath, existente.trimEnd() + seccion, "utf-8");
|
|
172
202
|
return gitignorePath;
|
|
173
203
|
}
|
|
174
204
|
|
|
@@ -214,7 +244,7 @@ function escribirManifest(cwd, data) {
|
|
|
214
244
|
const manifestPath = path.join(cwd, MANIFEST_FILE);
|
|
215
245
|
const existente = leerManifest(cwd) || {};
|
|
216
246
|
const manifest = { ...existente, ...data };
|
|
217
|
-
|
|
247
|
+
atomicWriteSync(
|
|
218
248
|
manifestPath,
|
|
219
249
|
JSON.stringify(manifest, null, 2) + "\n",
|
|
220
250
|
"utf-8",
|
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
* Backend: el módulo NO requiere Xenova. Si SWL_SEMANTIC_BACKEND=xenova y
|
|
27
27
|
* @xenova/transformers está instalado, se usa para embeddings. Si no, Jaccard.
|
|
28
28
|
*
|
|
29
|
+
* Instalación opt-in del backend de embeddings:
|
|
30
|
+
*
|
|
31
|
+
* npm install -g @xenova/transformers
|
|
32
|
+
* export SWL_SEMANTIC_BACKEND=xenova
|
|
33
|
+
*
|
|
34
|
+
* @xenova/transformers ya NO es dependencia del paquete swl-ses (v1.0.1+)
|
|
35
|
+
* porque arrastra `prebuild-install@7.1.3` deprecated y sumaba ~50 MB al
|
|
36
|
+
* install para una feature opt-in raramente usada. El usuario que quiera
|
|
37
|
+
* el backend semántico lo instala manualmente.
|
|
38
|
+
*
|
|
29
39
|
* @module scripts/lib/semantic-search
|
|
30
40
|
*/
|
|
31
41
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migración 3A — convierte campos CSV a array YAML inline en frontmatter de
|
|
4
|
+
* agentes. Afecta `tools`, `skillsInvocables`, `skillsRestringidos`.
|
|
5
|
+
*
|
|
6
|
+
* Antes: tools: Read, Write, Edit, Bash
|
|
7
|
+
* Después: tools: [Read, Write, Edit, Bash]
|
|
8
|
+
*
|
|
9
|
+
* Razón: el schema declara `type: array` pero el uso real es CSV string. YAML
|
|
10
|
+
* estándar NO convierte CSV a array — el parser produce una sola string. Esto
|
|
11
|
+
* causó bugs históricos documentados en CLAUDE.md ("regla obligatoria CSV
|
|
12
|
+
* estricto"). Migrar a array inline elimina la ambigüedad.
|
|
13
|
+
*
|
|
14
|
+
* Idempotente: skip si ya es array.
|
|
15
|
+
*
|
|
16
|
+
* Casos especiales detectados en agentes:
|
|
17
|
+
* - skillsInvocables: <ninguno> → mantener literal
|
|
18
|
+
* - skillsRestringidos: ninguno → convertir a [] (array vacío)
|
|
19
|
+
* - skillsRestringidos: \n - x\n → ya es array YAML multilinea, mantener
|
|
20
|
+
*
|
|
21
|
+
* Uso:
|
|
22
|
+
* node scripts/migrar-csv-a-array.js [--dry-run]
|
|
23
|
+
*
|
|
24
|
+
* Zero-dependencies. Compatible Windows (CRLF-safe), Node 18+.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const AGENTES_DIR = path.join(__dirname, '..', 'agentes');
|
|
33
|
+
const DRY_RUN = process.argv.includes('--dry-run');
|
|
34
|
+
|
|
35
|
+
// Campos que migran de CSV a array
|
|
36
|
+
const CAMPOS_CSV = ['tools', 'skillsInvocables', 'skillsRestringidos'];
|
|
37
|
+
|
|
38
|
+
// Valores especiales que NO son listas y deben preservarse
|
|
39
|
+
const VALORES_LITERAL = new Set(['ninguno', 'ninguna', '<ninguno>', '<ninguna>', 'none', 'null']);
|
|
40
|
+
|
|
41
|
+
function parsearValorCSV(valorStr) {
|
|
42
|
+
// Detectar si ya es array inline `[a, b, c]`
|
|
43
|
+
if (/^\s*\[/.test(valorStr)) return null;
|
|
44
|
+
|
|
45
|
+
// Limpiar
|
|
46
|
+
const valor = valorStr.trim();
|
|
47
|
+
if (!valor) return [];
|
|
48
|
+
|
|
49
|
+
// Valor especial literal
|
|
50
|
+
if (VALORES_LITERAL.has(valor.toLowerCase())) return [];
|
|
51
|
+
|
|
52
|
+
// CSV split
|
|
53
|
+
return valor
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function migrarFrontmatter(contenido) {
|
|
60
|
+
const match = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
61
|
+
if (!match) return { contenido, cambios: 0 };
|
|
62
|
+
|
|
63
|
+
const eol = match[0].includes('\r\n') ? '\r\n' : '\n';
|
|
64
|
+
const fmCompleto = match[0];
|
|
65
|
+
|
|
66
|
+
let nuevoFm = fmCompleto;
|
|
67
|
+
let cambios = 0;
|
|
68
|
+
|
|
69
|
+
for (const campo of CAMPOS_CSV) {
|
|
70
|
+
// Patrón 2 PRIMERO: array YAML multilínea
|
|
71
|
+
// skillsRestringidos:
|
|
72
|
+
// - angular-moderno
|
|
73
|
+
// - typescript-avanzado
|
|
74
|
+
// El campo está solo en su línea (sin valor) seguido de líneas con ` - X`
|
|
75
|
+
const re2 = new RegExp(`^(${campo}):[ \\t]*\\r?\\n((?:[ \\t]+-[ \\t]+[^\\r\\n]+\\r?\\n)+)`, 'm');
|
|
76
|
+
const m2 = nuevoFm.match(re2);
|
|
77
|
+
if (m2) {
|
|
78
|
+
const bloque = m2[2];
|
|
79
|
+
const items = bloque
|
|
80
|
+
.split(/\r?\n/)
|
|
81
|
+
.map((l) => l.trim())
|
|
82
|
+
.filter((l) => l.startsWith('- '))
|
|
83
|
+
.map((l) => l.slice(2).trim().replace(/^["']|["']$/g, ''));
|
|
84
|
+
if (items.length > 0) {
|
|
85
|
+
const arrayInline = `[${items.join(', ')}]`;
|
|
86
|
+
const reemplazo = `${campo}: ${arrayInline}${eol}`;
|
|
87
|
+
nuevoFm = nuevoFm.replace(re2, reemplazo);
|
|
88
|
+
cambios++;
|
|
89
|
+
}
|
|
90
|
+
continue; // procesado, no aplicar regex 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Patrón 1: campo en una sola línea con valor (CSV o array inline)
|
|
94
|
+
// tools: Read, Write
|
|
95
|
+
// skillsRestringidos: ninguno
|
|
96
|
+
// tools: [Read, Write] ← skip, ya es array
|
|
97
|
+
// CRÍTICO: usar [ \\t]* (NO \\s*) para no consumir newlines
|
|
98
|
+
const re1 = new RegExp(`^(${campo}):[ \\t]*([^\\r\\n]+)$`, 'm');
|
|
99
|
+
const m1 = nuevoFm.match(re1);
|
|
100
|
+
if (m1) {
|
|
101
|
+
const valorStr = m1[2];
|
|
102
|
+
const items = parsearValorCSV(valorStr);
|
|
103
|
+
if (items === null) continue; // ya es array, skip
|
|
104
|
+
const arrayInline = `[${items.join(', ')}]`;
|
|
105
|
+
const reemplazo = `${campo}: ${arrayInline}`;
|
|
106
|
+
if (m1[0] !== reemplazo) {
|
|
107
|
+
nuevoFm = nuevoFm.replace(re1, reemplazo);
|
|
108
|
+
cambios++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (cambios === 0) return { contenido, cambios: 0 };
|
|
114
|
+
|
|
115
|
+
return { contenido: contenido.replace(fmCompleto, nuevoFm), cambios };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function migrarAgente(archivo) {
|
|
119
|
+
const contenido = fs.readFileSync(archivo, 'utf-8');
|
|
120
|
+
const { contenido: nuevo, cambios } = migrarFrontmatter(contenido);
|
|
121
|
+
|
|
122
|
+
if (cambios === 0) {
|
|
123
|
+
return { archivo: path.basename(archivo), status: 'sin-cambios' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (DRY_RUN) {
|
|
127
|
+
return { archivo: path.basename(archivo), status: 'dry-run', cambios };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(archivo, nuevo, 'utf-8');
|
|
131
|
+
return { archivo: path.basename(archivo), status: 'migrado', cambios };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function main() {
|
|
135
|
+
console.log(`Migrando CSV → array YAML${DRY_RUN ? ' (dry-run)' : ''}...\n`);
|
|
136
|
+
|
|
137
|
+
const archivos = fs
|
|
138
|
+
.readdirSync(AGENTES_DIR)
|
|
139
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('_'))
|
|
140
|
+
.map((f) => path.join(AGENTES_DIR, f));
|
|
141
|
+
|
|
142
|
+
const resultados = archivos.map(migrarAgente);
|
|
143
|
+
|
|
144
|
+
const stats = {
|
|
145
|
+
'sin-cambios': 0,
|
|
146
|
+
migrado: 0,
|
|
147
|
+
'dry-run': 0,
|
|
148
|
+
totalCambios: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const r of resultados) {
|
|
152
|
+
stats[r.status] = (stats[r.status] || 0) + 1;
|
|
153
|
+
if (r.cambios) stats.totalCambios += r.cambios;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`Sin cambios: ${stats['sin-cambios']}`);
|
|
157
|
+
console.log(`Migrados: ${stats.migrado}`);
|
|
158
|
+
console.log(`Dry-run: ${stats['dry-run']}`);
|
|
159
|
+
console.log(`Total ediciones: ${stats.totalCambios}`);
|
|
160
|
+
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (require.main === module) {
|
|
165
|
+
main();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { parsearValorCSV, migrarFrontmatter };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migración Sprint 2B — añade campos `fase` y `dominio` al frontmatter de los
|
|
4
|
+
* 59 agentes de SWL. Skip si ya los tiene. Inserta antes de `exclusiones:` o
|
|
5
|
+
* al final del frontmatter.
|
|
6
|
+
*
|
|
7
|
+
* Idempotente: corre dos veces sin efectos.
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* node scripts/migrar-fase-dominio.js [--dry-run]
|
|
11
|
+
*
|
|
12
|
+
* Origen: análisis de temp/ + propuesta consolidada (mayo 2026).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const AGENTES_DIR = path.join(__dirname, '..', 'agentes');
|
|
21
|
+
const DRY_RUN = process.argv.includes('--dry-run');
|
|
22
|
+
|
|
23
|
+
// Mapeo agente → (fase, dominio). Vocabulario controlado del schema.
|
|
24
|
+
const MAPEO = {
|
|
25
|
+
'accesibilidad-wcag-swl': ['verify', 'ux'],
|
|
26
|
+
'arquitecto-swl': ['plan', 'general'],
|
|
27
|
+
'auto-evolucion-swl': ['learn', 'meta'],
|
|
28
|
+
'backend-api-swl': ['implement', 'backend'],
|
|
29
|
+
'backend-csharp-swl': ['implement', 'backend'],
|
|
30
|
+
'backend-go-swl': ['implement', 'backend'],
|
|
31
|
+
'backend-java-swl': ['implement', 'backend'],
|
|
32
|
+
'backend-node-swl': ['implement', 'backend'],
|
|
33
|
+
'backend-python-swl': ['implement', 'backend'],
|
|
34
|
+
'backend-rust-swl': ['implement', 'backend'],
|
|
35
|
+
'backend-workers-swl': ['implement', 'backend'],
|
|
36
|
+
'cloud-infra-swl': ['release', 'infra'],
|
|
37
|
+
'consolidador-swl': ['learn', 'meta'],
|
|
38
|
+
'datos-swl': ['implement', 'data'],
|
|
39
|
+
'depurador-swl': ['implement', 'general'],
|
|
40
|
+
'devops-ci-swl': ['release', 'infra'],
|
|
41
|
+
'disenador-ui-swl': ['plan', 'ux'],
|
|
42
|
+
'documentador-swl': ['meta', 'docs'],
|
|
43
|
+
'frontend-angular-swl': ['implement', 'frontend'],
|
|
44
|
+
'frontend-css-swl': ['implement', 'frontend'],
|
|
45
|
+
'frontend-react-swl': ['implement', 'frontend'],
|
|
46
|
+
'frontend-swl': ['implement', 'frontend'],
|
|
47
|
+
'frontend-tailwind-swl': ['implement', 'frontend'],
|
|
48
|
+
'implementador-swl': ['implement', 'general'],
|
|
49
|
+
'investigador-swl': ['discover', 'general'],
|
|
50
|
+
'investigador-ux-swl': ['discover', 'ux'],
|
|
51
|
+
'llm-apps-swl': ['implement', 'backend'],
|
|
52
|
+
'migrador-swl': ['implement', 'data'],
|
|
53
|
+
'mobile-android-swl': ['implement', 'mobile'],
|
|
54
|
+
'mobile-cross-swl': ['implement', 'mobile'],
|
|
55
|
+
'mobile-ios-swl': ['implement', 'mobile'],
|
|
56
|
+
'mobile-testing-swl': ['verify', 'quality'],
|
|
57
|
+
'notificador-swl': ['meta', 'meta'],
|
|
58
|
+
'observabilidad-swl': ['release', 'infra'],
|
|
59
|
+
'orquestador-swl': ['meta', 'process'],
|
|
60
|
+
'pagos-swl': ['implement', 'backend'],
|
|
61
|
+
'perfilador-usuario-swl': ['meta', 'meta'],
|
|
62
|
+
'planificador-swl': ['plan', 'process'],
|
|
63
|
+
'producto-prd-swl': ['discover', 'process'],
|
|
64
|
+
'red-team-swl': ['verify', 'security'],
|
|
65
|
+
'release-manager-swl': ['release', 'process'],
|
|
66
|
+
'rendimiento-swl': ['verify', 'quality'],
|
|
67
|
+
'resolutor-build-swl': ['implement', 'general'],
|
|
68
|
+
'revisor-angular-swl': ['verify', 'quality'],
|
|
69
|
+
'revisor-codigo-swl': ['verify', 'quality'],
|
|
70
|
+
'revisor-csharp-swl': ['verify', 'quality'],
|
|
71
|
+
'revisor-go-swl': ['verify', 'quality'],
|
|
72
|
+
'revisor-java-swl': ['verify', 'quality'],
|
|
73
|
+
'revisor-kotlin-swl': ['verify', 'quality'],
|
|
74
|
+
'revisor-nextjs-swl': ['verify', 'quality'],
|
|
75
|
+
'revisor-php-swl': ['verify', 'quality'],
|
|
76
|
+
'revisor-react-swl': ['verify', 'quality'],
|
|
77
|
+
'revisor-rust-swl': ['verify', 'quality'],
|
|
78
|
+
'revisor-seguridad-swl': ['verify', 'security'],
|
|
79
|
+
'revisor-swift-swl': ['verify', 'quality'],
|
|
80
|
+
'revisor-typescript-swl': ['verify', 'quality'],
|
|
81
|
+
'sre-swl': ['release', 'infra'],
|
|
82
|
+
'tdd-qa-swl': ['verify', 'quality'],
|
|
83
|
+
'ux-disenador-swl': ['plan', 'ux'],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function migrarAgente(nombre) {
|
|
87
|
+
const archivo = path.join(AGENTES_DIR, `${nombre}.md`);
|
|
88
|
+
if (!fs.existsSync(archivo)) {
|
|
89
|
+
return { nombre, status: 'no-existe' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const par = MAPEO[nombre];
|
|
93
|
+
if (!par) {
|
|
94
|
+
return { nombre, status: 'sin-mapeo' };
|
|
95
|
+
}
|
|
96
|
+
const [fase, dominio] = par;
|
|
97
|
+
|
|
98
|
+
const contenido = fs.readFileSync(archivo, 'utf-8');
|
|
99
|
+
|
|
100
|
+
// Detectar el frontmatter delimitado por `---` (acepta CRLF y LF)
|
|
101
|
+
const match = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return { nombre, status: 'sin-frontmatter' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const frontmatter = match[1];
|
|
107
|
+
|
|
108
|
+
// Idempotencia: skip si ya tiene fase y dominio
|
|
109
|
+
const tieneFase = /^fase:\s*\S+/m.test(frontmatter);
|
|
110
|
+
const tieneDominio = /^dominio:\s*\S+/m.test(frontmatter);
|
|
111
|
+
if (tieneFase && tieneDominio) {
|
|
112
|
+
return { nombre, status: 'ya-migrado', fase, dominio };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Insertar antes de `exclusiones:` o al final del frontmatter
|
|
116
|
+
let nuevoFrontmatter;
|
|
117
|
+
if (/^exclusiones:/m.test(frontmatter)) {
|
|
118
|
+
nuevoFrontmatter = frontmatter.replace(
|
|
119
|
+
/^(exclusiones:)/m,
|
|
120
|
+
`fase: ${fase}\ndominio: ${dominio}\n$1`
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
// Sin exclusiones: agregar al final del frontmatter
|
|
124
|
+
nuevoFrontmatter = frontmatter.trimEnd() + `\nfase: ${fase}\ndominio: ${dominio}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Preservar el line ending detectado
|
|
128
|
+
const eol = match[0].includes('\r\n') ? '\r\n' : '\n';
|
|
129
|
+
const fm = nuevoFrontmatter.replace(/\r\n/g, '\n').replace(/\n/g, eol);
|
|
130
|
+
const nuevoContenido = contenido.replace(match[0], `---${eol}${fm}${eol}---${eol}`);
|
|
131
|
+
|
|
132
|
+
if (DRY_RUN) {
|
|
133
|
+
return { nombre, status: 'dry-run-ok', fase, dominio };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fs.writeFileSync(archivo, nuevoContenido, 'utf-8');
|
|
137
|
+
return { nombre, status: 'migrado', fase, dominio };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main() {
|
|
141
|
+
console.log(`Migrando fase/dominio en agentes${DRY_RUN ? ' (dry-run)' : ''}...\n`);
|
|
142
|
+
|
|
143
|
+
const archivosEnDisco = fs
|
|
144
|
+
.readdirSync(AGENTES_DIR)
|
|
145
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('_'))
|
|
146
|
+
.map((f) => f.replace(/\.md$/, ''));
|
|
147
|
+
|
|
148
|
+
const resultados = archivosEnDisco.map(migrarAgente);
|
|
149
|
+
|
|
150
|
+
const stats = {
|
|
151
|
+
migrado: 0,
|
|
152
|
+
'ya-migrado': 0,
|
|
153
|
+
'dry-run-ok': 0,
|
|
154
|
+
'sin-mapeo': [],
|
|
155
|
+
'sin-frontmatter': [],
|
|
156
|
+
'no-existe': [],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (const r of resultados) {
|
|
160
|
+
if (typeof stats[r.status] === 'number') {
|
|
161
|
+
stats[r.status]++;
|
|
162
|
+
} else if (Array.isArray(stats[r.status])) {
|
|
163
|
+
stats[r.status].push(r.nombre);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Migrados: ${stats.migrado}`);
|
|
168
|
+
console.log(`Ya migrados (skip): ${stats['ya-migrado']}`);
|
|
169
|
+
console.log(`Dry-run OK: ${stats['dry-run-ok']}`);
|
|
170
|
+
if (stats['sin-mapeo'].length) {
|
|
171
|
+
console.log(`\n⚠️ Sin mapeo en MAPEO (${stats['sin-mapeo'].length}):`);
|
|
172
|
+
stats['sin-mapeo'].forEach((n) => console.log(` - ${n}`));
|
|
173
|
+
}
|
|
174
|
+
if (stats['sin-frontmatter'].length) {
|
|
175
|
+
console.log(`\n❌ Sin frontmatter detectable:`);
|
|
176
|
+
stats['sin-frontmatter'].forEach((n) => console.log(` - ${n}`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Validación: el mapeo debe cubrir todos los agentes en disco
|
|
180
|
+
const enMapeo = new Set(Object.keys(MAPEO));
|
|
181
|
+
const enDisco = new Set(archivosEnDisco);
|
|
182
|
+
const faltanEnMapeo = [...enDisco].filter((n) => !enMapeo.has(n));
|
|
183
|
+
const sobranEnMapeo = [...enMapeo].filter((n) => !enDisco.has(n));
|
|
184
|
+
|
|
185
|
+
if (faltanEnMapeo.length) {
|
|
186
|
+
console.log(`\n⚠️ Agentes en disco sin entrada en MAPEO:`);
|
|
187
|
+
faltanEnMapeo.forEach((n) => console.log(` - ${n}`));
|
|
188
|
+
}
|
|
189
|
+
if (sobranEnMapeo.length) {
|
|
190
|
+
console.log(`\nℹ️ Entradas en MAPEO sin archivo en disco (probablemente renombrados):`);
|
|
191
|
+
sobranEnMapeo.forEach((n) => console.log(` - ${n}`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
process.exit(stats['sin-frontmatter'].length || faltanEnMapeo.length ? 1 : 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (require.main === module) {
|
|
198
|
+
main();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { MAPEO, migrarAgente };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* validar-userland-vacio.js — guard pre-publish.
|
|
6
|
+
*
|
|
7
|
+
* Verifica que `_userland/` solo contenga archivos `.gitkeep` antes de
|
|
8
|
+
* empaquetar el tarball. Si algún contributor accidentalmente commiteó
|
|
9
|
+
* agentes, skills o credenciales experimentales en `_userland/`, este
|
|
10
|
+
* gate aborta el publish con exit 1 y lista los archivos extraños.
|
|
11
|
+
*
|
|
12
|
+
* Por qué existe (defensa en profundidad):
|
|
13
|
+
* `_userland/` es el directorio donde el USUARIO instala su contenido
|
|
14
|
+
* custom. En el repo del sistema debe permanecer vacío (solo
|
|
15
|
+
* `.gitkeep` para preservar la estructura). El campo `files` de
|
|
16
|
+
* package.json incluye `_userland/`, así que cualquier archivo real
|
|
17
|
+
* que se cuele entrará al tarball publicado a npm — exposición
|
|
18
|
+
* potencial de IP o secretos. Aunque `.gitignore` y `.npmignore` ya
|
|
19
|
+
* cubren los vectores típicos (.env, credenciales), este guard
|
|
20
|
+
* garantiza que la regla "_userland/ vacío en distribución" se cumpla
|
|
21
|
+
* determinísticamente.
|
|
22
|
+
*
|
|
23
|
+
* Variables de entorno:
|
|
24
|
+
* SWL_USERLAND_GUARD_ALLOW=archivo1,archivo2 — permite archivos extra
|
|
25
|
+
* (uso excepcional documentado en commit que active la variable).
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 — OK, _userland/ solo contiene .gitkeep
|
|
29
|
+
* 1 — hay archivos extraños; lista impresa a stderr
|
|
30
|
+
* 2 — error de invariante (cwd incorrecto, _userland/ no existe)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
|
|
36
|
+
const USERLAND_DIR = '_userland';
|
|
37
|
+
const PERMITIDOS_BASE = new Set(['.gitkeep', '.gitignore']);
|
|
38
|
+
|
|
39
|
+
function log(msg) { process.stdout.write(`[userland-guard] ${msg}\n`); }
|
|
40
|
+
function err(msg) { process.stderr.write(`[userland-guard] ERROR: ${msg}\n`); }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normaliza una ruta relativa al separador POSIX (`/`) para que la
|
|
44
|
+
* comparación contra la allowlist funcione idéntica en Windows y POSIX.
|
|
45
|
+
*/
|
|
46
|
+
function normalizar(rel) {
|
|
47
|
+
return rel.replace(/\\/g, '/');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function obtenerExtras() {
|
|
51
|
+
const extras = process.env.SWL_USERLAND_GUARD_ALLOW;
|
|
52
|
+
if (!extras) return new Set();
|
|
53
|
+
return new Set(extras.split(',').map(s => normalizar(s.trim())).filter(Boolean));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listarRecursivo(dir, baseDir = dir, acumulador = []) {
|
|
57
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const full = path.join(dir, entry.name);
|
|
60
|
+
const rel = normalizar(path.relative(baseDir, full));
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
listarRecursivo(full, baseDir, acumulador);
|
|
63
|
+
} else if (entry.isFile()) {
|
|
64
|
+
acumulador.push(rel);
|
|
65
|
+
} else if (entry.isSymbolicLink()) {
|
|
66
|
+
// Symlinks dentro del paquete distribuido son anti-patrón.
|
|
67
|
+
acumulador.push(`${rel} (symlink)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return acumulador;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main() {
|
|
74
|
+
const cwd = process.cwd();
|
|
75
|
+
const userland = path.join(cwd, USERLAND_DIR);
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(userland)) {
|
|
78
|
+
err(`${USERLAND_DIR}/ no existe en ${cwd}. ¿cwd incorrecto?`);
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const extrasPermitidos = obtenerExtras();
|
|
83
|
+
const archivos = listarRecursivo(userland);
|
|
84
|
+
|
|
85
|
+
const noPermitidos = archivos.filter(rel => {
|
|
86
|
+
const nombre = path.basename(rel);
|
|
87
|
+
if (PERMITIDOS_BASE.has(nombre)) return false;
|
|
88
|
+
if (extrasPermitidos.has(rel)) return false;
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (noPermitidos.length > 0) {
|
|
93
|
+
err(`${USERLAND_DIR}/ contiene ${noPermitidos.length} archivo(s) que no deberían distribuirse en el tarball npm:`);
|
|
94
|
+
for (const rel of noPermitidos) {
|
|
95
|
+
err(` - ${USERLAND_DIR}/${rel}`);
|
|
96
|
+
}
|
|
97
|
+
err('');
|
|
98
|
+
err('Acciones posibles:');
|
|
99
|
+
err(` 1. Mover el contenido fuera de ${USERLAND_DIR}/ si es del sistema base`);
|
|
100
|
+
err(` 2. Borrarlo si era código experimental local`);
|
|
101
|
+
err(` 3. Permitir explícitamente con SWL_USERLAND_GUARD_ALLOW="ruta1,ruta2"`);
|
|
102
|
+
err(' (documentar la excepción en el mensaje de commit)');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log(`OK: ${USERLAND_DIR}/ solo contiene ${archivos.length} archivo(s) permitido(s).`);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|