@saulwade/swl-ses 1.0.1 → 1.1.2

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.
Files changed (113) hide show
  1. package/CLAUDE.md +8 -5
  2. package/README.md +3 -3
  3. package/agentes/accesibilidad-wcag-swl.md +5 -7
  4. package/agentes/arquitecto-swl.md +5 -3
  5. package/agentes/auto-evolucion-swl.md +42 -12
  6. package/agentes/backend-api-swl.md +5 -3
  7. package/agentes/backend-csharp-swl.md +5 -3
  8. package/agentes/backend-go-swl.md +5 -3
  9. package/agentes/backend-java-swl.md +5 -3
  10. package/agentes/backend-node-swl.md +5 -3
  11. package/agentes/backend-python-swl.md +5 -3
  12. package/agentes/backend-rust-swl.md +5 -3
  13. package/agentes/backend-workers-swl.md +5 -3
  14. package/agentes/cloud-infra-swl.md +5 -6
  15. package/agentes/consolidador-swl.md +5 -3
  16. package/agentes/datos-swl.md +5 -7
  17. package/agentes/depurador-swl.md +6 -3
  18. package/agentes/devops-ci-swl.md +5 -3
  19. package/agentes/disenador-ui-swl.md +5 -7
  20. package/agentes/documentador-swl.md +5 -3
  21. package/agentes/frontend-angular-swl.md +5 -11
  22. package/agentes/frontend-css-swl.md +5 -9
  23. package/agentes/frontend-react-swl.md +5 -9
  24. package/agentes/frontend-swl.md +5 -9
  25. package/agentes/frontend-tailwind-swl.md +5 -9
  26. package/agentes/implementador-swl.md +6 -3
  27. package/agentes/investigador-swl.md +5 -3
  28. package/agentes/investigador-ux-swl.md +5 -9
  29. package/agentes/llm-apps-swl.md +5 -3
  30. package/agentes/migrador-swl.md +6 -3
  31. package/agentes/mobile-android-swl.md +5 -3
  32. package/agentes/mobile-cross-swl.md +5 -3
  33. package/agentes/mobile-ios-swl.md +5 -3
  34. package/agentes/mobile-testing-swl.md +5 -3
  35. package/agentes/notificador-swl.md +5 -3
  36. package/agentes/observabilidad-swl.md +5 -3
  37. package/agentes/orquestador-swl.md +29 -8
  38. package/agentes/pagos-swl.md +5 -3
  39. package/agentes/perfilador-usuario-swl.md +4 -2
  40. package/agentes/planificador-swl.md +5 -3
  41. package/agentes/producto-prd-swl.md +5 -3
  42. package/agentes/red-team-swl.md +4 -2
  43. package/agentes/release-manager-swl.md +6 -8
  44. package/agentes/rendimiento-swl.md +5 -6
  45. package/agentes/resolutor-build-swl.md +5 -3
  46. package/agentes/revisor-angular-swl.md +5 -3
  47. package/agentes/revisor-codigo-swl.md +90 -4
  48. package/agentes/revisor-csharp-swl.md +5 -3
  49. package/agentes/revisor-go-swl.md +5 -3
  50. package/agentes/revisor-java-swl.md +5 -3
  51. package/agentes/revisor-kotlin-swl.md +5 -3
  52. package/agentes/revisor-nextjs-swl.md +5 -3
  53. package/agentes/revisor-php-swl.md +5 -3
  54. package/agentes/revisor-react-swl.md +5 -3
  55. package/agentes/revisor-rust-swl.md +5 -3
  56. package/agentes/revisor-seguridad-swl.md +5 -3
  57. package/agentes/revisor-swift-swl.md +5 -3
  58. package/agentes/revisor-typescript-swl.md +5 -3
  59. package/agentes/sre-swl.md +5 -3
  60. package/agentes/tdd-qa-swl.md +5 -3
  61. package/agentes/ux-disenador-swl.md +5 -9
  62. package/comandos/swl/evaluar-skill.md +18 -0
  63. package/comandos/swl/evolucion-estado.md +49 -0
  64. package/comandos/swl/release.md +77 -1
  65. package/comandos/swl/salud.md +23 -0
  66. package/habilidades/checklist-seguridad/SKILL.md +57 -1
  67. package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
  68. package/habilidades/fastapi-experto/SKILL.md +10 -1
  69. package/habilidades/manejo-errores/.evolved.json +8 -8
  70. package/habilidades/manejo-errores/SKILL.md +63 -4
  71. package/habilidades/patrones-python/SKILL.md +5 -4
  72. package/habilidades/release-semver/.evolved.json +8 -8
  73. package/habilidades/release-semver/SKILL.md +85 -1
  74. package/hooks/auto-evolucion.js +35 -1
  75. package/hooks/clasificador-mensajes.js +50 -3
  76. package/hooks/lib/agent-routing.js +107 -0
  77. package/hooks/lib/delegation-tracker.js +162 -44
  78. package/hooks/lib/evolution-tracker.js +12 -3
  79. package/hooks/lib/memory-search.js +59 -1
  80. package/hooks/lib/nudge-tracker.js +10 -1
  81. package/hooks/lib/provenance-tracker.js +11 -3
  82. package/hooks/lib/text-similarity.js +241 -0
  83. package/hooks/metricas-evolucion.js +168 -1
  84. package/hooks/monitor-contexto.js +54 -6
  85. package/hooks/preservar-estado-pre-compact.js +11 -1
  86. package/hooks/risk-scoring.js +10 -1
  87. package/hooks/tracking-costos.js +10 -1
  88. package/hooks/validar-formato-post-subagente.js +140 -0
  89. package/hooks/validar-memoria-hook.js +218 -0
  90. package/manifiestos/agent-output-schemas.json +57 -0
  91. package/manifiestos/hooks-config.json +18 -0
  92. package/manifiestos/modulos.json +3 -0
  93. package/manifiestos/skills-lock.json +1065 -0
  94. package/package.json +1 -1
  95. package/plugin.json +1 -1
  96. package/reglas/arquitectura.md +20 -0
  97. package/reglas/fragmentos-compartidos.md +152 -0
  98. package/reglas/gobernanza.md +10 -1
  99. package/reglas/seguridad-agentes.md +12 -0
  100. package/reglas/skills-estandar.md +19 -0
  101. package/schemas/agent-frontmatter.schema.json +18 -0
  102. package/scripts/auditar-agentes-gaps.js +9 -1
  103. package/scripts/auditar-cobertura-frameworks.js +9 -1
  104. package/scripts/auditar-skills-gaps.js +9 -1
  105. package/scripts/bootstrap-instintos.js +11 -1
  106. package/scripts/generar-inventario.js +112 -9
  107. package/scripts/generar-matriz-lenguajes.js +271 -0
  108. package/scripts/generar-skills-lock.js +190 -0
  109. package/scripts/lib/estado.js +12 -2
  110. package/scripts/lib/gitignore-manifest.js +32 -2
  111. package/scripts/migrar-csv-a-array.js +168 -0
  112. package/scripts/migrar-fase-dominio.js +201 -0
  113. package/scripts/publicar.js +88 -18
@@ -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 };
@@ -62,6 +62,26 @@ function leerPkg() {
62
62
  return JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
63
63
  }
64
64
 
65
+ /**
66
+ * Clasifica el error de `npm whoami` capturado en stderr para distinguir
67
+ * causas raíz comunes y permitir mensajes accionables al usuario.
68
+ *
69
+ * Tipos retornados:
70
+ * - 'no-token' : no hay _authToken configurado para ese registry.
71
+ * - 'token-401' : token presente pero rechazado (expirado/revocado).
72
+ * - 'token-403' : token válido pero sin permiso (cuenta sin acceso al scope).
73
+ * - 'registry-404' : el registry no responde el endpoint whoami (URL incorrecta).
74
+ * - 'desconocido' : error de red, npm no en PATH, timeout, etc.
75
+ */
76
+ function clasificarErrorAuth(stderr, mensaje) {
77
+ const blob = (stderr || '') + '\n' + (mensaje || '');
78
+ if (/\b401\b/.test(blob)) return 'token-401';
79
+ if (/\b403\b/.test(blob)) return 'token-403';
80
+ if (/\b404\b/.test(blob)) return 'registry-404';
81
+ if (/ENEEDAUTH|need to authorize/i.test(blob)) return 'no-token';
82
+ return 'desconocido';
83
+ }
84
+
65
85
  function verificarLogin(registry) {
66
86
  try {
67
87
  const result = npmExec(['whoami', `--registry=${registry}`], {
@@ -69,16 +89,66 @@ function verificarLogin(registry) {
69
89
  encoding: 'utf-8',
70
90
  timeout: 15_000,
71
91
  });
72
- return String(result).trim();
92
+ return { ok: true, usuario: String(result).trim() };
73
93
  } catch (err) {
74
- // Distinguir "no autenticado" (lo más común) de "error de red" o "npm
75
- // no en PATH" requiere inspección del mensaje. Emitir el motivo real
76
- // a stderr para que el usuario lo vea pero retornar null al caller
77
- // que ya tiene un mensaje específico para "No autenticado".
78
- const motivo = String(err.message || err).split('\n')[0].slice(0, 120);
79
- process.stderr.write(`[verificarLogin ${registry}] ${motivo}\n`);
80
- return null;
94
+ // Capturar stderr del proceso para clasificar la causa raíz.
95
+ // execFileSync expone stderr en err.stderr cuando stdio fue 'pipe'.
96
+ const stderrBuf = err.stderr ? String(err.stderr) : '';
97
+ const motivo = (stderrBuf || String(err.message || err)).split('\n')[0].slice(0, 200);
98
+ const tipo = clasificarErrorAuth(stderrBuf, err.message);
99
+ process.stderr.write(`[verificarLogin ${registry}] ${tipo}: ${motivo}\n`);
100
+ return { ok: false, tipo, motivo };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Imprime guía accionable según el tipo de fallo de auth y el registry.
106
+ * Se llama desde publicarNpmjs/publicarGitHub cuando verificarLogin falla.
107
+ */
108
+ function imprimirGuiaAuth(registry, pkgName, tipo) {
109
+ const esGithub = /npm\.pkg\.github\.com/.test(registry);
110
+ console.error('');
111
+ switch (tipo) {
112
+ case 'token-401':
113
+ console.error('CAUSA: token de autenticación rechazado (HTTP 401 — expirado o revocado).');
114
+ if (esGithub) {
115
+ console.error('FIX: GitHub Packages NO acepta `npm login`. Genera un nuevo PAT en');
116
+ console.error(' Settings → Developer settings → Personal access tokens (classic)');
117
+ console.error(' con scopes `read:packages` y `write:packages`, y reemplaza la línea');
118
+ console.error(' en ~/.npmrc:');
119
+ console.error(' //npm.pkg.github.com/:_authToken=<NUEVO_TOKEN>');
120
+ } else {
121
+ console.error(`FIX: npm login --registry=${registry}`);
122
+ console.error(' (abrirá el navegador para autenticar y reescribirá ~/.npmrc).');
123
+ }
124
+ break;
125
+ case 'token-403':
126
+ console.error('CAUSA: token válido pero la cuenta no tiene permiso al scope del paquete.');
127
+ console.error(`FIX: Verifica el dueño del scope con:`);
128
+ console.error(` npm owner ls ${pkgName} --registry=${registry}`);
129
+ console.error(' Si la cuenta autenticada no aparece como owner, autenticate con la');
130
+ console.error(' cuenta dueña del scope o pide ser agregado como maintainer.');
131
+ break;
132
+ case 'registry-404':
133
+ console.error('CAUSA: el registry no responde el endpoint whoami (URL probablemente incorrecta).');
134
+ console.error(`FIX: Verifica que la URL sea exactamente '${registry}' (incluyendo https:// y trailing slash si aplica).`);
135
+ break;
136
+ case 'no-token':
137
+ console.error('CAUSA: no hay token configurado para este registry en ~/.npmrc.');
138
+ if (esGithub) {
139
+ console.error('FIX: GitHub Packages requiere PAT manual. Agrega a ~/.npmrc:');
140
+ console.error(' //npm.pkg.github.com/:_authToken=<TU_PAT>');
141
+ console.error(' (NO uses `npm login` con GitHub Packages — devuelve 404/403.)');
142
+ } else {
143
+ console.error(`FIX: npm login --registry=${registry}`);
144
+ }
145
+ break;
146
+ default:
147
+ console.error('CAUSA: error desconocido al consultar whoami.');
148
+ console.error(' Verifica conectividad de red y que `npm` esté en PATH.');
149
+ console.error(` Para diagnóstico manual: npm whoami --registry=${registry}`);
81
150
  }
151
+ console.error('');
82
152
  }
83
153
 
84
154
  function copiarDir(src, dest) {
@@ -165,13 +235,13 @@ function publicarNpmjs(pkg, dryRun) {
165
235
  console.log(`Paquete: ${pkg.name}@${pkg.version}`);
166
236
  console.log(`Registry: ${NPMJS_REGISTRY}`);
167
237
 
168
- const usuario = verificarLogin(NPMJS_REGISTRY);
169
- if (!usuario) {
170
- console.error('ERROR: No autenticado en npmjs.');
171
- console.error(`Ejecuta: npm login --registry=${NPMJS_REGISTRY}`);
238
+ const auth = verificarLogin(NPMJS_REGISTRY);
239
+ if (!auth.ok) {
240
+ console.error(`ERROR: No autenticado en npmjs (${auth.tipo}).`);
241
+ imprimirGuiaAuth(NPMJS_REGISTRY, pkg.name, auth.tipo);
172
242
  return false;
173
243
  }
174
- console.log(`Autenticado como: ${usuario}`);
244
+ console.log(`Autenticado como: ${auth.usuario}`);
175
245
 
176
246
  const args = ['publish', `--registry=${NPMJS_REGISTRY}`, '--access', 'public'];
177
247
  if (dryRun) args.push('--dry-run');
@@ -190,13 +260,13 @@ function publicarGitHub(pkg, dryRun) {
190
260
  console.log(`Paquete: ${GITHUB_NAME}@${pkg.version}`);
191
261
  console.log(`Registry: ${GITHUB_REGISTRY}`);
192
262
 
193
- const usuario = verificarLogin(GITHUB_REGISTRY);
194
- if (!usuario) {
195
- console.error('ERROR: No autenticado en GitHub Packages.');
196
- console.error(`Ejecuta: npm login --registry=${GITHUB_REGISTRY}`);
263
+ const auth = verificarLogin(GITHUB_REGISTRY);
264
+ if (!auth.ok) {
265
+ console.error(`ERROR: No autenticado en GitHub Packages (${auth.tipo}).`);
266
+ imprimirGuiaAuth(GITHUB_REGISTRY, GITHUB_NAME, auth.tipo);
197
267
  return false;
198
268
  }
199
- console.log(`Autenticado como: ${usuario}`);
269
+ console.log(`Autenticado como: ${auth.usuario}`);
200
270
 
201
271
  const tmpDir = prepararDirectorioTemporal(pkg, GITHUB_NAME, GITHUB_REGISTRY);
202
272