@saulwade/swl-ses 2.4.3 → 2.5.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.
Files changed (197) hide show
  1. package/CLAUDE.md +194 -241
  2. package/README.md +600 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/abogado-diablo-swl.md +145 -0
  6. package/agentes/accesibilidad-wcag-swl.md +690 -690
  7. package/agentes/arquitecto-swl.md +267 -267
  8. package/agentes/auto-evolucion-swl.md +908 -908
  9. package/agentes/backend-api-swl.md +1 -1
  10. package/agentes/backend-csharp-swl.md +420 -420
  11. package/agentes/backend-go-swl.md +390 -390
  12. package/agentes/backend-java-swl.md +281 -281
  13. package/agentes/backend-node-swl.md +1 -1
  14. package/agentes/backend-python-swl.md +1 -1
  15. package/agentes/backend-rust-swl.md +364 -364
  16. package/agentes/backend-workers-swl.md +482 -482
  17. package/agentes/cloud-infra-swl.md +509 -509
  18. package/agentes/consolidador-swl.md +541 -541
  19. package/agentes/datos-swl.md +1 -1
  20. package/agentes/depurador-swl.md +352 -352
  21. package/agentes/devops-ci-swl.md +400 -400
  22. package/agentes/disenador-ui-swl.md +569 -569
  23. package/agentes/documentador-swl.md +345 -345
  24. package/agentes/frontend-angular-swl.md +621 -621
  25. package/agentes/frontend-css-swl.md +716 -716
  26. package/agentes/frontend-react-swl.md +692 -692
  27. package/agentes/frontend-swl.md +496 -496
  28. package/agentes/frontend-tailwind-swl.md +826 -826
  29. package/agentes/gh-fix-ci-swl.md +6 -1
  30. package/agentes/implementador-swl.md +1 -1
  31. package/agentes/investigador-swl.md +432 -432
  32. package/agentes/investigador-ux-swl.md +505 -505
  33. package/agentes/llm-apps-swl.md +1 -1
  34. package/agentes/migrador-swl.md +442 -442
  35. package/agentes/mobile-android-swl.md +511 -511
  36. package/agentes/mobile-cross-swl.md +541 -541
  37. package/agentes/mobile-ios-swl.md +502 -502
  38. package/agentes/mobile-testing-swl.md +302 -302
  39. package/agentes/nemesis-auditor-swl.md +285 -285
  40. package/agentes/notificador-swl.md +1 -1
  41. package/agentes/observabilidad-swl.md +438 -438
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +10 -1
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +1 -1
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +1 -1
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/bin/lib/bot-comandos.js +1 -1
  66. package/bin/swl-ses.js +6 -0
  67. package/comandos/swl/adoptar-proyecto.md +14 -2
  68. package/comandos/swl/configurar-ci.md +8 -1
  69. package/comandos/swl/deuda-codigo.md +97 -97
  70. package/comandos/swl/discutir-fase.md +22 -118
  71. package/comandos/swl/fix.md +118 -0
  72. package/comandos/swl/nuevo-proyecto.md +54 -3
  73. package/comandos/swl/predecir.md +32 -2
  74. package/comandos/swl/seguridad.md +189 -0
  75. package/comandos/swl/status.md +5 -3
  76. package/habilidades/aprendizaje-continuo/SKILL.md +3 -1
  77. package/habilidades/discutir-fase/SKILL.md +84 -81
  78. package/habilidades/discutir-fase/recursos/plantilla-contexto.md +136 -0
  79. package/habilidades/doc-sync/SKILL.md +3 -1
  80. package/habilidades/doubt-driven-review/SKILL.md +15 -1
  81. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  82. package/habilidades/estructura-proyecto-claude/SKILL.md +11 -2
  83. package/habilidades/harness-claude-code/SKILL.md +3 -1
  84. package/habilidades/instalar-sistema/SKILL.md +3 -1
  85. package/habilidades/meta-reglas-extendido/SKILL.md +92 -0
  86. package/habilidades/meta-reglas-extendido/recursos/analisis-previo-tareas-grandes.md +186 -0
  87. package/habilidades/meta-reglas-extendido/recursos/analizar-directorios-antes-de-escribir.md +235 -0
  88. package/habilidades/meta-reglas-extendido/recursos/api-diseno.md +413 -0
  89. package/habilidades/meta-reglas-extendido/recursos/arquitectura.md +491 -0
  90. package/habilidades/meta-reglas-extendido/recursos/arreglar-al-detectar.md +264 -0
  91. package/habilidades/meta-reglas-extendido/recursos/debatir-antes-de-aceptar.md +152 -0
  92. package/habilidades/meta-reglas-extendido/recursos/git-workflow.md +259 -0
  93. package/habilidades/meta-reglas-extendido/recursos/gobernanza.md +291 -0
  94. package/habilidades/meta-reglas-extendido/recursos/memoria-consolidada.md +263 -0
  95. package/habilidades/meta-reglas-extendido/recursos/seguridad-agentes.md +443 -0
  96. package/habilidades/meta-reglas-extendido/recursos/sesiones-paralelas.md +190 -0
  97. package/habilidades/meta-reglas-extendido/recursos/sin-duplicacion-reglas-globales.md +179 -0
  98. package/habilidades/meta-reglas-extendido/recursos/skills-estandar.md +394 -0
  99. package/habilidades/meta-reglas-extendido/recursos/usar-code-review-graph.md +156 -0
  100. package/habilidades/meta-reglas-extendido/recursos/usar-context7.md +236 -0
  101. package/habilidades/meta-reglas-extendido/recursos/usar-sistema-swl.md +253 -0
  102. package/habilidades/meta-reglas-extendido/recursos/verificar-citas-normativas.md +527 -0
  103. package/habilidades/meta-skills-estandar/SKILL.md +3 -1
  104. package/habilidades/nuevo-proyecto/SKILL.md +20 -3
  105. package/habilidades/php-experto/SKILL.md +10 -3
  106. package/habilidades/{filament-admin/SKILL.md → php-experto/recursos/filament-admin.md} +23 -39
  107. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  108. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  109. package/habilidades/proceso-debate-adversarial/recursos/personas.md +5 -4
  110. package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -0
  111. package/hooks/check-update.js +19 -10
  112. package/hooks/contexto-subagente.js +68 -68
  113. package/hooks/degradacion-instintos.js +1 -1
  114. package/hooks/extraccion-aprendizajes.js +2 -2
  115. package/hooks/lib/briefing.js +3 -3
  116. package/hooks/lib/nudge-tracker.js +1 -1
  117. package/hooks/lib/otlp-exporter.js +1 -1
  118. package/hooks/lib/webhook-dedup.js +1 -1
  119. package/hooks/session-briefing.js +1 -1
  120. package/llms.txt +6 -6
  121. package/manifiestos/canonical-hashes.json +713 -52
  122. package/manifiestos/hooks-config.json +469 -469
  123. package/manifiestos/invariantes-criticos.json +30 -30
  124. package/manifiestos/modulos.json +168 -135
  125. package/manifiestos/perfiles.json +0 -2
  126. package/manifiestos/skills-lock.json +49 -56
  127. package/package.json +7 -5
  128. package/plantillas/github-workflows/README.md +15 -1
  129. package/plantillas/github-workflows/swl-devsecops.yml +70 -0
  130. package/plugin.json +5 -5
  131. package/reglas/analisis-previo-tareas-grandes.md +30 -156
  132. package/reglas/analizar-directorios-antes-de-escribir.md +30 -211
  133. package/reglas/api-diseno.md +28 -398
  134. package/reglas/arquitectura.md +35 -456
  135. package/reglas/arreglar-al-detectar.md +30 -230
  136. package/reglas/debatir-antes-de-aceptar.md +30 -143
  137. package/reglas/docs.md +7 -0
  138. package/reglas/estilo-codigo.md +9 -0
  139. package/reglas/fragmentos-compartidos.md +6 -0
  140. package/reglas/git-workflow.md +44 -240
  141. package/reglas/gobernanza.md +23 -262
  142. package/reglas/memoria-consolidada.md +34 -228
  143. package/reglas/performance.md +8 -0
  144. package/reglas/pruebas.md +12 -0
  145. package/reglas/seguridad-agentes.md +37 -418
  146. package/reglas/seguridad.md +12 -0
  147. package/reglas/sesiones-paralelas.md +29 -162
  148. package/reglas/sin-duplicacion-reglas-globales.md +25 -166
  149. package/reglas/skills-estandar.md +23 -373
  150. package/reglas/usar-code-review-graph.md +31 -140
  151. package/reglas/usar-context7.md +30 -208
  152. package/reglas/usar-sistema-swl.md +47 -242
  153. package/reglas/verificar-citas-normativas.md +47 -537
  154. package/scripts/actualizar.js +253 -253
  155. package/scripts/audit-tools/auditar-relleno-inventario.js +145 -0
  156. package/scripts/auditar-clases-conocidas.js +106 -0
  157. package/scripts/bootstrap-instintos.js +2 -2
  158. package/scripts/canario-hooks.js +166 -0
  159. package/scripts/cli/configurar-ci.js +2 -1
  160. package/scripts/evidencia-valor.js +93 -0
  161. package/scripts/field-report.js +1 -1
  162. package/scripts/generar-comandos.js +143 -0
  163. package/scripts/generar-inventario.js +236 -23
  164. package/scripts/generar-matriz-lenguajes.js +1 -1
  165. package/scripts/instalador.js +15 -1
  166. package/scripts/lib/configurar-ci.js +10 -3
  167. package/scripts/lib/diary-entry.js +3 -1
  168. package/scripts/lib/drift-detector.js +1 -1
  169. package/scripts/lib/evidencia-valor.js +189 -0
  170. package/scripts/lib/expandir-targets.js +71 -71
  171. package/scripts/lib/frontmatter-md.js +63 -0
  172. package/scripts/lib/parsear-opciones.js +2 -0
  173. package/scripts/lib/prune-componentes.js +180 -0
  174. package/scripts/lib/reglas-globales-conocidas.json +16 -2
  175. package/scripts/lib/scoring-instintos.js +2 -2
  176. package/scripts/lib/toml-merge.js +204 -204
  177. package/scripts/lib/transformadores/claude.js +1 -1
  178. package/scripts/lib/transformadores/codex.js +1 -1
  179. package/scripts/lib/transformadores/copilot.js +1 -1
  180. package/scripts/lib/transformadores/cursor.js +1 -1
  181. package/scripts/lib/transformadores/gemini.js +22 -2
  182. package/scripts/lib/transformadores/opencode.js +1 -1
  183. package/scripts/mcp-server/auth.js +105 -105
  184. package/scripts/mcp-server/cache.js +106 -106
  185. package/scripts/prune.js +102 -0
  186. package/scripts/publicar.js +18 -2
  187. package/scripts/tui/pantallas/inspect.js +175 -175
  188. package/scripts/tui/pantallas/uninstall-wizard.js +210 -210
  189. package/scripts/tui/pantallas/update-wizard.js +234 -234
  190. package/scripts/tui/pantallas/welcome.js +189 -189
  191. package/habilidades/paid-media-tracking/SKILL.md +0 -269
  192. package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +0 -220
  193. package/habilidades/paid-media-tracking/recursos/google-ads-api.md +0 -215
  194. package/habilidades/tracking-measurement/SKILL.md +0 -239
  195. package/habilidades/tracking-measurement/recursos/consent-mode.md +0 -231
  196. package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +0 -216
  197. package/habilidades/tracking-measurement/recursos/meta-capi.md +0 -262
@@ -1,189 +1,189 @@
1
- 'use strict';
2
-
3
- /**
4
- * Pantalla Welcome de la TUI de swl-ses.
5
- *
6
- * Diseño:
7
- * - Logo ASCII centrado en la parte superior
8
- * - Tabla de runtimes detectados (nombre, scope, versión instalada, estado)
9
- * - Pie con atajos: Enter continuar / Esc salir
10
- *
11
- * No-TTY: imprime versión texto plano y retorna { continuar: true } inmediatamente.
12
- */
13
-
14
- const path = require('path');
15
-
16
- const render = require('../lib/render');
17
- const teclas = require('../lib/teclas');
18
- const { colores, semantico, iconos } = require('../lib/colores');
19
-
20
- // Logo en caracteres ASCII art compactos (5 líneas)
21
- const LOGO = [
22
- ' ███████ ██ ██ ██ ███████ ███████ ███████ ',
23
- ' ██ ██ ██ ██ ██ ██ ██ ',
24
- ' ███████ ██ █ ██ ██ ███████ █████ ███████ ',
25
- ' ██ ██ ███ ██ ██ ██ ██ ██ ',
26
- ' ███████ ███ ███ ███████ ███████ ███████ ███████ ',
27
- ];
28
-
29
- const SUBTITULO = 'Sistema de ingeniería de software auto-evolutivo multi-runtime';
30
-
31
- /**
32
- * Detecta runtimes instalados leyendo desde scripts/lib/detectar-runtime.
33
- * No falla si la lib no está disponible — devuelve array vacío.
34
- */
35
- function detectarInstalaciones() {
36
- let detectarRuntimes;
37
- let cargarEstado;
38
- try {
39
- ({ detectarRuntimes, scopesReales } = require('../../lib/detectar-runtime'));
40
- ({ cargarEstado } = require('../../lib/estado'));
41
- } catch (_) {
42
- return [];
43
- }
44
-
45
- const runtimes = detectarRuntimes();
46
- const filas = [];
47
- for (const runtime of runtimes) {
48
- // Dedup + guard anti-fantasma: desde HOME, el local relativo resuelve al
49
- // global propio (duplicado) o al de OTRO runtime (p.ej. .claude de
50
- // OpenClaude = global de Claude Code) — ver scopesReales().
51
- const dirs = scopesReales(runtime).map(({ dir, esGlobal }) => (
52
- { ruta: dir, scope: esGlobal ? 'global' : 'proyecto' }
53
- ));
54
- for (const { ruta, scope } of dirs) {
55
- try {
56
- const estado = cargarEstado(ruta);
57
- if (!estado) continue;
58
- filas.push({
59
- runtime: runtime.nombre,
60
- scope,
61
- version: estado.versionSistema || 'desconocida',
62
- perfil: estado.perfil || '-',
63
- componentes: estado.componentesInstalados?.length || 0,
64
- });
65
- } catch (_) {
66
- // ignorar errores de lectura
67
- }
68
- }
69
- }
70
- return filas;
71
- }
72
-
73
- /**
74
- * Pinta el contenido completo de la pantalla. Idempotente — se puede invocar
75
- * múltiples veces (resize, re-render).
76
- */
77
- function _renderizar(filas, version) {
78
- render.limpiarPantalla();
79
- const { cols, rows } = render.obtenerDimensiones();
80
-
81
- // Logo centrado
82
- const filaLogo = 2;
83
- for (let i = 0; i < LOGO.length; i++) {
84
- const linea = LOGO[i];
85
- const colLogo = Math.max(1, Math.floor((cols - linea.length) / 2));
86
- render.escribirEn(filaLogo + i, colLogo, semantico.titulo(linea));
87
- }
88
-
89
- // Subtítulo + versión
90
- const filaSub = filaLogo + LOGO.length + 1;
91
- const subFinal = `${SUBTITULO} ${colores.dim('v' + version)}`;
92
- const colSub = Math.max(1, Math.floor((cols - render.anchoVisual(subFinal)) / 2));
93
- render.escribirEn(filaSub, colSub, subFinal);
94
-
95
- // Tabla de runtimes detectados
96
- const filaTabla = filaSub + 3;
97
- if (filas.length === 0) {
98
- const mensaje = colores.dim('No se detectaron instalaciones SWL en este equipo.');
99
- const colMsg = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
100
- render.escribirEn(filaTabla, colMsg, mensaje);
101
- } else {
102
- render.escribirEn(filaTabla - 1, 4, semantico.enfasis('Instalaciones detectadas:'));
103
- const cabecera =
104
- render.rellenarDer(colores.dim('Runtime'), 14) +
105
- render.rellenarDer(colores.dim('Scope'), 10) +
106
- render.rellenarDer(colores.dim('Versión'), 12) +
107
- render.rellenarDer(colores.dim('Perfil'), 18) +
108
- colores.dim('Componentes');
109
- render.escribirEn(filaTabla, 4, cabecera);
110
-
111
- filas.slice(0, rows - filaTabla - 4).forEach((f, i) => {
112
- const estado = f.version === version
113
- ? semantico.exito(iconos.check)
114
- : semantico.warn(iconos.warn);
115
- const linea =
116
- render.rellenarDer(`${estado} ${f.runtime}`, 14) +
117
- render.rellenarDer(f.scope, 10) +
118
- render.rellenarDer('v' + f.version, 12) +
119
- render.rellenarDer(f.perfil, 18) +
120
- colores.cyan(String(f.componentes));
121
- render.escribirEn(filaTabla + 1 + i, 4, linea);
122
- });
123
- }
124
-
125
- // Mensaje central de continuación
126
- const filaCont = rows - 4;
127
- const mensaje = `Presiona ${semantico.cursor('Enter')} para continuar al menú principal o ${semantico.cursor('Esc')} para salir.`;
128
- const colCont = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
129
- render.escribirEn(filaCont, colCont, mensaje);
130
-
131
- // Pie con atajos
132
- render.dibujarPiePagina([
133
- ['Enter', 'continuar'],
134
- ['Esc', 'salir'],
135
- ]);
136
- }
137
-
138
- /**
139
- * Muestra la pantalla Welcome y espera input del usuario.
140
- *
141
- * @returns {Promise<{ continuar: boolean, instalaciones: Array }>}
142
- */
143
- function mostrarWelcome() {
144
- const version = require('../../../package.json').version;
145
- const filas = detectarInstalaciones();
146
-
147
- return new Promise((resolve) => {
148
- // Modo no-TTY: salida texto plano sin esperar input
149
- if (!render.ES_TTY || !process.stdin.isTTY) {
150
- console.log('');
151
- console.log(` swl-ses v${version}`);
152
- console.log(` ${SUBTITULO}`);
153
- console.log('');
154
- if (filas.length > 0) {
155
- console.log(' Instalaciones detectadas:');
156
- for (const f of filas) {
157
- console.log(` ${f.runtime} (${f.scope}): v${f.version}, perfil ${f.perfil}`);
158
- }
159
- } else {
160
- console.log(' No se detectaron instalaciones SWL.');
161
- }
162
- console.log('');
163
- resolve({ continuar: true, instalaciones: filas });
164
- return;
165
- }
166
-
167
- render.iniciarModoTui();
168
- _renderizar(filas, version);
169
-
170
- // Re-renderizar en resize
171
- const onResize = () => _renderizar(filas, version);
172
- process.stdout.on('resize', onResize);
173
-
174
- const teclado = teclas.crearTeclado();
175
-
176
- function finalizar(continuar) {
177
- teclado.desactivar();
178
- process.stdout.removeListener('resize', onResize);
179
- render.salirModoTui();
180
- resolve({ continuar, instalaciones: filas });
181
- }
182
-
183
- teclado.on('return', () => finalizar(true));
184
- teclado.on('escape', () => finalizar(false));
185
- teclado.activar();
186
- });
187
- }
188
-
189
- module.exports = { mostrarWelcome, detectarInstalaciones, LOGO, SUBTITULO };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pantalla Welcome de la TUI de swl-ses.
5
+ *
6
+ * Diseño:
7
+ * - Logo ASCII centrado en la parte superior
8
+ * - Tabla de runtimes detectados (nombre, scope, versión instalada, estado)
9
+ * - Pie con atajos: Enter continuar / Esc salir
10
+ *
11
+ * No-TTY: imprime versión texto plano y retorna { continuar: true } inmediatamente.
12
+ */
13
+
14
+ const path = require('path');
15
+
16
+ const render = require('../lib/render');
17
+ const teclas = require('../lib/teclas');
18
+ const { colores, semantico, iconos } = require('../lib/colores');
19
+
20
+ // Logo en caracteres ASCII art compactos (5 líneas)
21
+ const LOGO = [
22
+ ' ███████ ██ ██ ██ ███████ ███████ ███████ ',
23
+ ' ██ ██ ██ ██ ██ ██ ██ ',
24
+ ' ███████ ██ █ ██ ██ ███████ █████ ███████ ',
25
+ ' ██ ██ ███ ██ ██ ██ ██ ██ ',
26
+ ' ███████ ███ ███ ███████ ███████ ███████ ███████ ',
27
+ ];
28
+
29
+ const SUBTITULO = 'Sistema de ingeniería de software auto-evolutivo multi-runtime';
30
+
31
+ /**
32
+ * Detecta runtimes instalados leyendo desde scripts/lib/detectar-runtime.
33
+ * No falla si la lib no está disponible — devuelve array vacío.
34
+ */
35
+ function detectarInstalaciones() {
36
+ let detectarRuntimes;
37
+ let cargarEstado;
38
+ try {
39
+ ({ detectarRuntimes, scopesReales } = require('../../lib/detectar-runtime'));
40
+ ({ cargarEstado } = require('../../lib/estado'));
41
+ } catch (_) {
42
+ return [];
43
+ }
44
+
45
+ const runtimes = detectarRuntimes();
46
+ const filas = [];
47
+ for (const runtime of runtimes) {
48
+ // Dedup + guard anti-fantasma: desde HOME, el local relativo resuelve al
49
+ // global propio (duplicado) o al de OTRO runtime (p.ej. .claude de
50
+ // OpenClaude = global de Claude Code) — ver scopesReales().
51
+ const dirs = scopesReales(runtime).map(({ dir, esGlobal }) => (
52
+ { ruta: dir, scope: esGlobal ? 'global' : 'proyecto' }
53
+ ));
54
+ for (const { ruta, scope } of dirs) {
55
+ try {
56
+ const estado = cargarEstado(ruta);
57
+ if (!estado) continue;
58
+ filas.push({
59
+ runtime: runtime.nombre,
60
+ scope,
61
+ version: estado.versionSistema || 'desconocida',
62
+ perfil: estado.perfil || '-',
63
+ componentes: estado.componentesInstalados?.length || 0,
64
+ });
65
+ } catch (_) {
66
+ // ignorar errores de lectura
67
+ }
68
+ }
69
+ }
70
+ return filas;
71
+ }
72
+
73
+ /**
74
+ * Pinta el contenido completo de la pantalla. Idempotente — se puede invocar
75
+ * múltiples veces (resize, re-render).
76
+ */
77
+ function _renderizar(filas, version) {
78
+ render.limpiarPantalla();
79
+ const { cols, rows } = render.obtenerDimensiones();
80
+
81
+ // Logo centrado
82
+ const filaLogo = 2;
83
+ for (let i = 0; i < LOGO.length; i++) {
84
+ const linea = LOGO[i];
85
+ const colLogo = Math.max(1, Math.floor((cols - linea.length) / 2));
86
+ render.escribirEn(filaLogo + i, colLogo, semantico.titulo(linea));
87
+ }
88
+
89
+ // Subtítulo + versión
90
+ const filaSub = filaLogo + LOGO.length + 1;
91
+ const subFinal = `${SUBTITULO} ${colores.dim('v' + version)}`;
92
+ const colSub = Math.max(1, Math.floor((cols - render.anchoVisual(subFinal)) / 2));
93
+ render.escribirEn(filaSub, colSub, subFinal);
94
+
95
+ // Tabla de runtimes detectados
96
+ const filaTabla = filaSub + 3;
97
+ if (filas.length === 0) {
98
+ const mensaje = colores.dim('No se detectaron instalaciones SWL en este equipo.');
99
+ const colMsg = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
100
+ render.escribirEn(filaTabla, colMsg, mensaje);
101
+ } else {
102
+ render.escribirEn(filaTabla - 1, 4, semantico.enfasis('Instalaciones detectadas:'));
103
+ const cabecera =
104
+ render.rellenarDer(colores.dim('Runtime'), 14) +
105
+ render.rellenarDer(colores.dim('Scope'), 10) +
106
+ render.rellenarDer(colores.dim('Versión'), 12) +
107
+ render.rellenarDer(colores.dim('Perfil'), 18) +
108
+ colores.dim('Componentes');
109
+ render.escribirEn(filaTabla, 4, cabecera);
110
+
111
+ filas.slice(0, rows - filaTabla - 4).forEach((f, i) => {
112
+ const estado = f.version === version
113
+ ? semantico.exito(iconos.check)
114
+ : semantico.warn(iconos.warn);
115
+ const linea =
116
+ render.rellenarDer(`${estado} ${f.runtime}`, 14) +
117
+ render.rellenarDer(f.scope, 10) +
118
+ render.rellenarDer('v' + f.version, 12) +
119
+ render.rellenarDer(f.perfil, 18) +
120
+ colores.cyan(String(f.componentes));
121
+ render.escribirEn(filaTabla + 1 + i, 4, linea);
122
+ });
123
+ }
124
+
125
+ // Mensaje central de continuación
126
+ const filaCont = rows - 4;
127
+ const mensaje = `Presiona ${semantico.cursor('Enter')} para continuar al menú principal o ${semantico.cursor('Esc')} para salir.`;
128
+ const colCont = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
129
+ render.escribirEn(filaCont, colCont, mensaje);
130
+
131
+ // Pie con atajos
132
+ render.dibujarPiePagina([
133
+ ['Enter', 'continuar'],
134
+ ['Esc', 'salir'],
135
+ ]);
136
+ }
137
+
138
+ /**
139
+ * Muestra la pantalla Welcome y espera input del usuario.
140
+ *
141
+ * @returns {Promise<{ continuar: boolean, instalaciones: Array }>}
142
+ */
143
+ function mostrarWelcome() {
144
+ const version = require('../../../package.json').version;
145
+ const filas = detectarInstalaciones();
146
+
147
+ return new Promise((resolve) => {
148
+ // Modo no-TTY: salida texto plano sin esperar input
149
+ if (!render.ES_TTY || !process.stdin.isTTY) {
150
+ console.log('');
151
+ console.log(` swl-ses v${version}`);
152
+ console.log(` ${SUBTITULO}`);
153
+ console.log('');
154
+ if (filas.length > 0) {
155
+ console.log(' Instalaciones detectadas:');
156
+ for (const f of filas) {
157
+ console.log(` ${f.runtime} (${f.scope}): v${f.version}, perfil ${f.perfil}`);
158
+ }
159
+ } else {
160
+ console.log(' No se detectaron instalaciones SWL.');
161
+ }
162
+ console.log('');
163
+ resolve({ continuar: true, instalaciones: filas });
164
+ return;
165
+ }
166
+
167
+ render.iniciarModoTui();
168
+ _renderizar(filas, version);
169
+
170
+ // Re-renderizar en resize
171
+ const onResize = () => _renderizar(filas, version);
172
+ process.stdout.on('resize', onResize);
173
+
174
+ const teclado = teclas.crearTeclado();
175
+
176
+ function finalizar(continuar) {
177
+ teclado.desactivar();
178
+ process.stdout.removeListener('resize', onResize);
179
+ render.salirModoTui();
180
+ resolve({ continuar, instalaciones: filas });
181
+ }
182
+
183
+ teclado.on('return', () => finalizar(true));
184
+ teclado.on('escape', () => finalizar(false));
185
+ teclado.activar();
186
+ });
187
+ }
188
+
189
+ module.exports = { mostrarWelcome, detectarInstalaciones, LOGO, SUBTITULO };
@@ -1,269 +0,0 @@
1
- ---
2
- name: paid-media-tracking
3
- description: >
4
- Implementación técnica de herramientas para campañas de publicidad pagada:
5
- Google Ads API, análisis de search terms, conversión tracking server-side,
6
- auditoría de cuentas programática y dashboards de rendimiento. Cargar cuando
7
- se construya una integración con Google Ads API, se implemente análisis
8
- automático de search terms, se diseñe un sistema de medición de ROAS,
9
- o se audite la implementación técnica de conversión tracking.
10
- version: "1.0.0"
11
- herramientasPermitidas: [Read]
12
- evolvable: true # default para skill estandar
13
- exclusiones:
14
- - "No cargar para tracking de conversiones en el browser o server-side con GTM, GA4, Meta CAPI — para implementación de tracking web cargar `tracking-measurement`."
15
- - "No cargar para estrategia de pujas, definición de presupuesto o copy de anuncios — esas son decisiones de negocio y marketing, no técnicas."
16
- - "No cargar para integraciones con plataformas distintas a Google Ads (Meta Ads, TikTok Ads, LinkedIn Ads) sin relación con la API de Google — para otras plataformas usar el SDK correspondiente."
17
- - "No cargar para dashboards de métricas generales de negocio sin datos de Google Ads — para dashboards de observabilidad de sistemas cargar `monitoring-alertas`."
18
- ---
19
- # paid-media-tracking — Herramientas técnicas para publicidad pagada
20
-
21
- ## Cuándo NO cargar
22
-
23
- - La tarea es tracking de conversiones en el browser o server-side (GTM, GA4, Meta CAPI): cargar `tracking-measurement`.
24
- - La tarea es estrategia de pujas, presupuesto o copy de anuncios: son decisiones de negocio, no de ingeniería.
25
- - La integración es con Meta Ads, TikTok Ads o LinkedIn Ads sin relación con Google Ads API: usar el SDK del proveedor.
26
- - La tarea es un dashboard de métricas generales de negocio sin datos específicos de Google Ads: cargar `monitoring-alertas`.
27
-
28
- ## Cuándo cargar este skill
29
-
30
- ```
31
- ✅ Construir reportes automáticos de Google Ads (Python, Node)
32
- ✅ Implementar un sistema de alertas sobre rendimiento de campañas
33
- ✅ Integrar Google Ads API con un CRM o backend
34
- ✅ Diseñar el schema de dataLayer para campañas de e-commerce
35
- ✅ Construir un dashboard de ROAS con datos de la API
36
- ✅ Automatizar la gestión de negative keywords programáticamente
37
- ✅ Implementar auditoría técnica de cuentas Google Ads
38
- ❌ Definir estrategia de pujas (es trabajo del PPC strategist, no del ingeniero)
39
- ❌ Crear copys de anuncios (creativo, no técnico)
40
- ❌ Tomar decisiones de presupuesto (decisión de negocio)
41
- ```
42
-
43
- > Para tracking de conversiones browser/server-side (GTM, GA4, Meta CAPI),
44
- > cargar `Skill("tracking-measurement")` en su lugar o en paralelo.
45
-
46
- ## Conceptos clave
47
-
48
- - **ROAS** (Return on Ad Spend): `valor_conversiones / costo`. Métrica primaria de rentabilidad de campañas.
49
- - **Micros**: Google Ads almacena costos en micros (millonésimas). `cost_micros / 1_000_000` = valor real.
50
- - **GAQL**: Google Ads Query Language. SQL-like para consultar la API. Recursos, segmentos y métricas tienen nombres específicos.
51
- - **MCC** (Manager Customer Center): cuenta paraguas que agrupa sub-cuentas. El `login_customer_id` debe apuntar al MCC.
52
- - **Search Term View**: recurso de la API que expone los términos exactos que activaron los anuncios — diferente a las keywords configuradas.
53
-
54
- ## Reglas obligatorias
55
-
56
- ### Regla 1 — Siempre dividir `cost_micros` entre 1,000,000
57
-
58
- Google Ads devuelve todos los costos en micros. Operar con micros produce
59
- cifras de ROAS, CPA y presupuesto completamente erróneas.
60
-
61
- ```python
62
- # MAL — opera con micros directamente
63
- roas = conversiones_valor / row.metrics.cost_micros
64
-
65
- # BIEN — convierte primero
66
- costo = row.metrics.cost_micros / 1_000_000
67
- roas = conversiones_valor / costo if costo > 0 else 0
68
- ```
69
-
70
- **Verificación**: El costo de una campaña típica en MXN debe estar en
71
- el rango de decenas a miles, nunca en millones.
72
-
73
- ### Regla 2 — Manejar `GoogleAdsException` siempre
74
-
75
- La API devuelve errores granulares por campo y operación. Un `except Exception`
76
- silencia información crítica (quota excedida, permiso denegado, ID inválido).
77
-
78
- ```python
79
- from google.ads.googleads.errors import GoogleAdsException
80
-
81
- try:
82
- response = ga_service.search(customer_id=customer_id, query=query)
83
- except GoogleAdsException as ex:
84
- for error in ex.failure.errors:
85
- logger.error(
86
- "Error Google Ads [%s]: %s",
87
- error.error_code,
88
- error.message,
89
- )
90
- raise
91
- ```
92
-
93
- **Verificación**: Los logs deben mostrar `error_code` y `message` específicos,
94
- no solo el texto genérico de la excepción.
95
-
96
- ### Regla 3 — `customer_id` sin guiones
97
-
98
- La Google Ads API rechaza el formato `123-456-7890`. El formato correcto es
99
- la cadena de dígitos sin guiones: `"1234567890"`.
100
-
101
- ```python
102
- # MAL
103
- customer_id = "123-456-7890"
104
-
105
- # BIEN
106
- customer_id = "1234567890"
107
- # O limpiar al recibir del usuario:
108
- customer_id = customer_id.replace("-", "")
109
- ```
110
-
111
- **Verificación**: Un `INVALID_CUSTOMER_ID` en los logs indica este problema.
112
-
113
- ### Regla 4 — Nunca hardcodear credenciales de la API
114
-
115
- `developer_token`, `refresh_token` y `client_secret` son credenciales de
116
- producción. Van en variables de entorno o en un gestor de secretos.
117
-
118
- ```python
119
- # MAL
120
- client = GoogleAdsClient.load_from_dict({
121
- "developer_token": "ABcDeFgHiJkLmNoPq",
122
- "refresh_token": "1//0gXyz...",
123
- })
124
-
125
- # BIEN
126
- client = GoogleAdsClient.load_from_dict({
127
- "developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
128
- "client_id": settings.GOOGLE_ADS_CLIENT_ID,
129
- "client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
130
- "refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
131
- "login_customer_id": settings.GOOGLE_ADS_MCC_ID,
132
- })
133
- ```
134
-
135
- ### Regla 5 — Cachear respuestas de la API al menos 1 hora
136
-
137
- La API tiene quota diaria limitada (1,000 operaciones/día en nivel básico).
138
- Llamar la API en cada request HTTP agota la quota en minutos con tráfico real.
139
-
140
- ```python
141
- from functools import lru_cache
142
- import time
143
-
144
- _cache: dict = {}
145
- _TTL = 3600 # 1 hora
146
-
147
- def obtener_con_cache(clave: str, fn_obtener):
148
- """Caché simple con TTL para respuestas de Google Ads API."""
149
- ahora = time.time()
150
- if clave in _cache and (ahora - _cache[clave]["ts"]) < _TTL:
151
- return _cache[clave]["data"]
152
- datos = fn_obtener()
153
- _cache[clave] = {"data": datos, "ts": ahora}
154
- return datos
155
- ```
156
-
157
- ## Google Ads API — cliente Python
158
-
159
- ```python
160
- from google.ads.googleads.client import GoogleAdsClient
161
- from google.ads.googleads.errors import GoogleAdsException
162
-
163
- client = GoogleAdsClient.load_from_dict({
164
- "developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
165
- "client_id": settings.GOOGLE_ADS_CLIENT_ID,
166
- "client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
167
- "refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
168
- "login_customer_id": settings.GOOGLE_ADS_MCC_ID,
169
- })
170
-
171
- def obtener_rendimiento_campanas(customer_id: str, dias: int = 30) -> list[dict]:
172
- """Obtiene métricas de rendimiento de campañas de los últimos N días."""
173
- ga_service = client.get_service("GoogleAdsService")
174
-
175
- query = f"""
176
- SELECT
177
- campaign.id,
178
- campaign.name,
179
- campaign.status,
180
- metrics.impressions,
181
- metrics.clicks,
182
- metrics.conversions,
183
- metrics.cost_micros,
184
- metrics.conversions_value
185
- FROM campaign
186
- WHERE segments.date DURING LAST_{dias}_DAYS
187
- AND campaign.status = 'ENABLED'
188
- ORDER BY metrics.cost_micros DESC
189
- """
190
-
191
- try:
192
- response = ga_service.search(customer_id=customer_id, query=query)
193
- resultados = []
194
- for row in response:
195
- costo = row.metrics.cost_micros / 1_000_000
196
- roas = (row.metrics.conversions_value / costo) if costo > 0 else 0
197
-
198
- resultados.append({
199
- "id": str(row.campaign.id),
200
- "nombre": row.campaign.name,
201
- "impresiones": row.metrics.impressions,
202
- "clicks": row.metrics.clicks,
203
- "conversiones": row.metrics.conversions,
204
- "costo": round(costo, 2),
205
- "roas": round(roas, 2),
206
- })
207
- return resultados
208
-
209
- except GoogleAdsException as ex:
210
- for error in ex.failure.errors:
211
- raise ValueError(f"Error Google Ads: {error.message}") from ex
212
- ```
213
-
214
- ## Análisis de search terms para detección de desperdicio
215
-
216
- Consultar la implementación completa de `analizar_search_terms` en
217
- [recursos/auditoria-tracking.md](recursos/auditoria-tracking.md).
218
-
219
- Patrón de detección de desperdicio: si `costo > 50` y `conversiones == 0`,
220
- el término es candidato a negative keyword. Analizar bigramas y trigramas
221
- (n=2 y n=3) sobre el vocabulario de search terms para encontrar patrones
222
- de términos irrelevantes frecuentes.
223
-
224
- ## Detección de discrepancias entre plataformas
225
-
226
- Consultar la función `ejecutar_auditoria_completa` en
227
- [recursos/auditoria-tracking.md](recursos/auditoria-tracking.md).
228
-
229
- Umbral aceptable: discrepancia < 15% entre Google Ads y GA4.
230
- Causas frecuentes: ventanas de conversión distintas, modelos de atribución
231
- diferentes (last-click vs data-driven), o conversiones offline no importadas.
232
-
233
- ## Anti-patrones críticos
234
-
235
- | Anti-patrón | Problema | Solución |
236
- |-------------|----------|----------|
237
- | Operar con `cost_micros` sin convertir | ROAS y CPA erróneos por factor 1,000,000 | Dividir siempre entre `1_000_000` |
238
- | `customer_id` con guiones `123-456-7890` | `INVALID_CUSTOMER_ID` de la API | `customer_id.replace("-", "")` |
239
- | Llamar la API en cada request HTTP | Quota agotada en minutos | Caché con TTL de 1 hora mínimo |
240
- | Credenciales en código fuente | Exposición en repositorio | Variables de entorno obligatorias |
241
- | Filtrar sin considerar `campaign.status` | `PAUSED`/`REMOVED` contaminan reportes activos | Filtro explícito por status en GAQL |
242
- | Usar `login_customer_id` de sub-cuenta en MCC | Accede a cuenta incorrecta o da error de permisos | Siempre el ID del MCC para cuentas administradas |
243
-
244
- ## Checklist de verificación
245
-
246
- - [ ] `cost_micros` se divide entre `1_000_000` antes de cualquier operación aritmética
247
- - [ ] `GoogleAdsException` capturada con iteración sobre `ex.failure.errors`
248
- - [ ] `customer_id` sin guiones antes de llamar a la API
249
- - [ ] Credenciales en variables de entorno (`settings.*`), no hardcodeadas
250
- - [ ] Respuestas de la API con caché de al menos 1 hora
251
- - [ ] Filtro de `campaign.status` en todas las queries de campañas activas
252
- - [ ] `search_stream` usado para queries que puedan devolver >10,000 filas
253
-
254
- ## Referencias a recursos
255
-
256
- | Tema | Archivo |
257
- |------|---------|
258
- | Google Ads API: autenticación, quota, GAQL avanzado, batch operations | [recursos/google-ads-api.md](recursos/google-ads-api.md) |
259
- | Auditoría técnica: checklist de cuenta, métricas de calidad, reporte Python | [recursos/auditoria-tracking.md](recursos/auditoria-tracking.md) |
260
-
261
- ## Gotchas / Errores comunes no obvios
262
-
263
- **La GAQL query con `WHERE segments.date DURING LAST_30_DAYS` incluye el día actual con datos parciales, inflando el conteo de conversiones del período**: si se corre el reporte a las 10 AM, el día de hoy tiene solo 10 horas de datos pero se incluye en el agregado de 30 días como si fuera un día completo. Para comparativas de períodos (mes actual vs mes anterior), esto produce distorsión. Causa: `LAST_N_DAYS` incluye el día actual con datos hasta el momento de la query. Fix: usar `BETWEEN YYYY-MM-DD AND YYYY-MM-DD` con fechas explícitas excluyendo el día actual, calculadas en Python: `fecha_fin = datetime.today() - timedelta(days=1)`.
264
-
265
- **El `login_customer_id` correcto para MCC es el ID de la cuenta administradora, pero las queries retornan datos de TODAS las sub-cuentas mezclados si no se especifica `customer_id` por sub-cuenta**: si el MCC administra 50 cuentas y se hace una query de campañas sin filtrar por `customer_id`, la API devuelve campañas de todas las sub-cuentas con los mismos nombres, haciendo imposible distinguir a qué cuenta pertenece cada fila. Causa: el `login_customer_id` habilita el acceso al MCC pero `customer_id` en `search()` determina el scope. Fix: iterar sobre los sub-`customer_id` obtenidos de `CustomerService` y hacer una query por cuenta, o usar el campo `customer.id` en el `SELECT` de GAQL para incluir el ID de cuenta en cada fila del resultado.
266
-
267
- **`search_stream` no puede usarse con queries que incluyen `ORDER BY` porque la API de streaming no garantiza orden y lanza `STREAMING_NOT_SUPPORTED_FOR_SORTED_QUERIES`**: al migrar de `search()` a `search_stream()` para manejar resultados grandes, las queries con `ORDER BY metrics.cost_micros DESC` fallan con un error poco descriptivo. Causa: el streaming de Google Ads API envía filas en el orden de procesamiento interno del servidor, no en el orden especificado. Fix: usar `search()` con paginación para queries ordenadas, y `search_stream()` solo para queries sin `ORDER BY`. Si se necesita orden + volumen grande, hacer el sort en Python después de recibir todos los resultados.
268
-
269
- **Las conversiones de Google Ads tienen una ventana de atribución de 30-90 días, por lo que los datos de ROAS de los últimos 7 días siguen cambiando durante semanas después**: un reporte ejecutado el lunes puede mostrar ROAS de 2.1 para la semana anterior, pero el mismo período consultado 3 semanas después puede mostrar ROAS de 2.8 porque se atribuyeron conversiones tardías. Causa: la API devuelve los datos con las conversiones atribuidas hasta el momento de la query, no las del período. Fix: para reportes comparativos históricos, documentar la fecha de extracción y no comparar el período "de esta semana" contra períodos anteriores sin usar datos ya consolidados (más de 90 días atrás). Para dashboards en tiempo real, aceptar que los últimos 90 días tienen datos en evolución y mostrar la fecha de actualización.