@saulwade/swl-ses 1.3.8 → 1.4.0

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 (128) hide show
  1. package/CLAUDE.md +12 -4
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-webhook-server.js +198 -0
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +21 -1
  7. package/comandos/swl/claudemd.md +14 -1
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/exportar-vault.md +108 -0
  10. package/comandos/swl/nuevo-proyecto.md +24 -2
  11. package/gateway/adapters/base.js +109 -0
  12. package/gateway/adapters/discord.js +167 -0
  13. package/gateway/adapters/email.js +221 -0
  14. package/gateway/adapters/slack.js +192 -0
  15. package/gateway/adapters/telegram.js +183 -0
  16. package/gateway/adapters/webhook.js +113 -0
  17. package/gateway/adapters/whatsapp.js +214 -0
  18. package/gateway/agent-executor.js +322 -0
  19. package/gateway/command-relay.js +271 -0
  20. package/gateway/cron/jobs.js +263 -0
  21. package/gateway/cron/scheduler.js +322 -0
  22. package/gateway/cron/store.js +335 -0
  23. package/gateway/index.js +320 -0
  24. package/gateway/lib/event-channel.js +191 -0
  25. package/gateway/session.js +131 -0
  26. package/gateway/webhook-server.js +324 -0
  27. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  28. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  29. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  30. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  31. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  32. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  33. package/habilidades/eval-framework/SKILL.md +212 -212
  34. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  35. package/habilidades/harness-claude-code/SKILL.md +299 -299
  36. package/habilidades/infra-github-actions/SKILL.md +166 -166
  37. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  38. package/habilidades/manejo-errores/.evolved.json +8 -8
  39. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  40. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  41. package/habilidades/nextjs-testing/SKILL.md +89 -5
  42. package/habilidades/node-experto/SKILL.md +37 -1
  43. package/habilidades/patrones-python/SKILL.md +229 -229
  44. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  45. package/habilidades/planear-fase/SKILL.md +319 -319
  46. package/habilidades/react-experto/SKILL.md +45 -4
  47. package/habilidades/release-semver/.evolved.json +8 -8
  48. package/habilidades/tdd-workflow/SKILL.md +36 -4
  49. package/habilidades/testing-python/SKILL.md +340 -340
  50. package/hooks/claudemd-bloat-detector.js +161 -161
  51. package/hooks/inyeccion-contexto.js +8 -3
  52. package/hooks/lib/agent-routing.js +107 -107
  53. package/hooks/lib/auto-consolidator.js +335 -335
  54. package/hooks/lib/error-classifier.js +308 -308
  55. package/hooks/lib/merkle-audit.js +96 -96
  56. package/hooks/lib/provenance-tracker.js +191 -191
  57. package/hooks/lib/rate-limit-ip.js +177 -0
  58. package/hooks/lib/rate-limit-tracker.js +253 -253
  59. package/hooks/lib/resource-quota.js +122 -122
  60. package/hooks/lib/retry-jitter.js +165 -165
  61. package/hooks/lib/skill-auditor.js +588 -588
  62. package/hooks/lib/sync-status.js +228 -228
  63. package/hooks/lib/taint-tracker.js +107 -107
  64. package/hooks/lib/text-similarity.js +241 -241
  65. package/hooks/lib/toon-compressor.js +245 -245
  66. package/hooks/lib/webhook-dedup.js +184 -0
  67. package/hooks/lib/webhook-verify.js +123 -0
  68. package/hooks/proteccion-rutas.js +120 -15
  69. package/hooks/registro-turnos.js +209 -209
  70. package/hooks/sugerir-regenerar-inventario.js +170 -170
  71. package/hooks/validar-formato-post-subagente.js +140 -140
  72. package/hooks/validar-memoria-hook.js +218 -218
  73. package/instintos/prompt-appendices.yaml +57 -57
  74. package/manifiestos/agent-output-schemas.json +57 -57
  75. package/manifiestos/modulos.json +1 -0
  76. package/manifiestos/skills-lock.json +34 -34
  77. package/package.json +5 -3
  78. package/plantillas/auditor-veto-template.md +105 -105
  79. package/plantillas/github-workflows/README.md +47 -47
  80. package/plantillas/github-workflows/release-please.yml +44 -44
  81. package/plantillas/github-workflows/swl-ci.yml +107 -107
  82. package/plantillas/github-workflows/swl-security.yml +51 -51
  83. package/plugin.json +1 -1
  84. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  85. package/reglas/arreglar-al-detectar.md +147 -147
  86. package/reglas/fragmentos-compartidos.md +152 -152
  87. package/reglas/harness-claude-code.md +213 -213
  88. package/reglas/usar-context7.md +226 -226
  89. package/reglas/usar-sistema-swl.md +251 -0
  90. package/schemas/diary-entry.schema.json +80 -80
  91. package/scripts/benchmark-memoria.js +167 -167
  92. package/scripts/comandos/skills.js +251 -2
  93. package/scripts/configurar-branch-protection.js +418 -418
  94. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  95. package/scripts/field-report.js +199 -199
  96. package/scripts/generar-checklists-consolidados.js +273 -273
  97. package/scripts/generar-inventario.js +420 -420
  98. package/scripts/generar-matriz-lenguajes.js +271 -271
  99. package/scripts/lib/artefactos-python.js +43 -43
  100. package/scripts/lib/benchmark-metrics.js +160 -160
  101. package/scripts/lib/budget-enforcer.js +252 -252
  102. package/scripts/lib/configurar-ci.js +380 -380
  103. package/scripts/lib/contadores-inventario.js +217 -217
  104. package/scripts/lib/detectar-stack-detallado.js +307 -307
  105. package/scripts/lib/diary-entry.js +234 -234
  106. package/scripts/lib/eval-metrics-store.js +218 -218
  107. package/scripts/lib/eval-quality.js +171 -171
  108. package/scripts/lib/eval-schemas.js +144 -144
  109. package/scripts/lib/eval-self-correct.js +106 -106
  110. package/scripts/lib/eval-validator.js +185 -185
  111. package/scripts/lib/jaccard-similarity.js +98 -98
  112. package/scripts/lib/longmemeval-runner.js +125 -125
  113. package/scripts/lib/npm-version.js +261 -261
  114. package/scripts/lib/paquetes-conocidos.js +50 -50
  115. package/scripts/lib/prompt-builder.js +264 -264
  116. package/scripts/lib/rrf-fusion.js +175 -175
  117. package/scripts/lib/scoring-instintos.js +277 -277
  118. package/scripts/lib/semantic-search.js +252 -252
  119. package/scripts/limpiar-artefactos-python.js +131 -131
  120. package/scripts/mcp-server/README.md +128 -128
  121. package/scripts/mcp-server/handlers.js +206 -206
  122. package/scripts/migrar-csv-a-array.js +168 -168
  123. package/scripts/migrar-fase-dominio.js +201 -201
  124. package/scripts/publicar.js +511 -511
  125. package/scripts/run-eval.js +141 -141
  126. package/scripts/validar-manifest.js +195 -195
  127. package/scripts/validar-userland-vacio.js +110 -110
  128. package/scripts/verificar-release.js +110 -0
@@ -0,0 +1,335 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cron Store — Persistencia de jobs CRON en JSON.
5
+ *
6
+ * Almacena jobs en .planning/cron/jobs.json con escritura atómica.
7
+ * Cada job tiene: id, name, schedule, command, deliver, status, nextRun, lastRunAt.
8
+ *
9
+ * @module gateway/cron/store
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const { atomicWriteJSON } = require('../../hooks/lib/atomic-write');
16
+ const { parseSchedule, computeNextRun } = require('./jobs');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Constantes
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const CRON_DIR = '.planning/cron';
23
+ const JOBS_FILE = 'jobs.json';
24
+ const LOG_FILE = 'log.json';
25
+ const AGENT_STATE_FILE = 'agent-state.json';
26
+ const MAX_LOG_ENTRIES = 500;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ function ensureDir(baseDir) {
33
+ const dir = path.join(baseDir, CRON_DIR);
34
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
35
+ return dir;
36
+ }
37
+
38
+ function jobsPath(baseDir) {
39
+ return path.join(ensureDir(baseDir), JOBS_FILE);
40
+ }
41
+
42
+ function logPath(baseDir) {
43
+ return path.join(ensureDir(baseDir), LOG_FILE);
44
+ }
45
+
46
+ function agentStatePath(baseDir) {
47
+ return path.join(ensureDir(baseDir), AGENT_STATE_FILE);
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // CRUD de jobs
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Carga todos los jobs.
56
+ * @param {string} baseDir
57
+ * @returns {object[]}
58
+ */
59
+ function loadJobs(baseDir) {
60
+ const p = jobsPath(baseDir);
61
+ try {
62
+ if (!fs.existsSync(p)) return [];
63
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
64
+ } catch (_) {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Persiste todos los jobs atómicamente.
71
+ * @param {string} baseDir
72
+ * @param {object[]} jobs
73
+ */
74
+ function saveJobs(baseDir, jobs) {
75
+ atomicWriteJSON(jobsPath(baseDir), jobs);
76
+ }
77
+
78
+ /**
79
+ * Agrega un job nuevo.
80
+ *
81
+ * @param {string} baseDir
82
+ * @param {object} opts
83
+ * @param {string} opts.name - Nombre descriptivo.
84
+ * @param {string} opts.schedule - Schedule string (ej: "every 2h", "0 9 * * 1-5").
85
+ * @param {string} opts.command - Comando a ejecutar.
86
+ * @param {string} [opts.deliver='local'] - Destino: "local", "telegram", "discord".
87
+ * @param {boolean} [opts.enabled=true]
88
+ * @returns {object} Job creado.
89
+ */
90
+ function addJob(baseDir, opts) {
91
+ const jobs = loadJobs(baseDir);
92
+
93
+ const parsed = parseSchedule(opts.schedule);
94
+ const id = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
95
+
96
+ const job = {
97
+ id,
98
+ name: opts.name,
99
+ schedule: opts.schedule,
100
+ parsedSchedule: parsed,
101
+ command: opts.command,
102
+ deliver: opts.deliver || 'local',
103
+ status: opts.enabled !== false ? 'scheduled' : 'paused',
104
+ nextRun: null,
105
+ lastRunAt: null,
106
+ createdAt: new Date().toISOString(),
107
+ repeat: parsed.kind === 'once' ? { times: 1, completed: 0 } : null,
108
+ };
109
+
110
+ // Calcular próxima ejecución
111
+ job.nextRun = computeNextRun(job);
112
+
113
+ jobs.push(job);
114
+ saveJobs(baseDir, jobs);
115
+ return job;
116
+ }
117
+
118
+ /**
119
+ * Elimina un job por ID.
120
+ * @param {string} baseDir
121
+ * @param {string} jobId
122
+ * @returns {boolean}
123
+ */
124
+ function removeJob(baseDir, jobId) {
125
+ const jobs = loadJobs(baseDir);
126
+ const idx = jobs.findIndex(j => j.id === jobId);
127
+ if (idx < 0) return false;
128
+ jobs.splice(idx, 1);
129
+ saveJobs(baseDir, jobs);
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Pausa un job.
135
+ * @param {string} baseDir
136
+ * @param {string} jobId
137
+ * @returns {boolean}
138
+ */
139
+ function pauseJob(baseDir, jobId) {
140
+ const jobs = loadJobs(baseDir);
141
+ const job = jobs.find(j => j.id === jobId);
142
+ if (!job) return false;
143
+ job.status = 'paused';
144
+ saveJobs(baseDir, jobs);
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Reanuda un job pausado.
150
+ * @param {string} baseDir
151
+ * @param {string} jobId
152
+ * @returns {boolean}
153
+ */
154
+ function resumeJob(baseDir, jobId) {
155
+ const jobs = loadJobs(baseDir);
156
+ const job = jobs.find(j => j.id === jobId);
157
+ if (!job) return false;
158
+ job.status = 'scheduled';
159
+ if (!job.nextRun) job.nextRun = computeNextRun(job);
160
+ saveJobs(baseDir, jobs);
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Marca un job como ejecutado y calcula su próxima ejecución.
166
+ * @param {string} baseDir
167
+ * @param {string} jobId
168
+ * @param {object} [result={}] - Resultado de la ejecución.
169
+ */
170
+ function markExecuted(baseDir, jobId, result = {}) {
171
+ const jobs = loadJobs(baseDir);
172
+ const job = jobs.find(j => j.id === jobId);
173
+ if (!job) return;
174
+
175
+ job.lastRunAt = new Date().toISOString();
176
+
177
+ // Actualizar repeticiones
178
+ if (job.repeat) {
179
+ job.repeat.completed = (job.repeat.completed || 0) + 1;
180
+ if (job.repeat.times !== null && job.repeat.completed >= job.repeat.times) {
181
+ job.status = 'completed';
182
+ job.nextRun = null;
183
+ }
184
+ }
185
+
186
+ // Calcular próxima ejecución si aún está activo
187
+ if (job.status === 'scheduled') {
188
+ job.nextRun = computeNextRun(job);
189
+ }
190
+
191
+ saveJobs(baseDir, jobs);
192
+
193
+ // Registrar en log
194
+ _appendLog(baseDir, {
195
+ jobId,
196
+ jobName: job.name,
197
+ executedAt: job.lastRunAt,
198
+ status: result.status || 'completed',
199
+ output: (result.output || '').substring(0, 1000),
200
+ });
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Log de ejecuciones
205
+ // ---------------------------------------------------------------------------
206
+
207
+ function _appendLog(baseDir, entry) {
208
+ const p = logPath(baseDir);
209
+ let log = [];
210
+ try {
211
+ if (fs.existsSync(p)) {
212
+ log = JSON.parse(fs.readFileSync(p, 'utf8'));
213
+ }
214
+ } catch (e) { /* log malformado — resetear */ }
215
+
216
+ log.push(entry);
217
+
218
+ // Mantener solo las últimas MAX_LOG_ENTRIES entradas
219
+ if (log.length > MAX_LOG_ENTRIES) {
220
+ log = log.slice(-MAX_LOG_ENTRIES);
221
+ }
222
+
223
+ atomicWriteJSON(p, log);
224
+ }
225
+
226
+ /**
227
+ * Lee el log de ejecuciones.
228
+ * @param {string} baseDir
229
+ * @param {number} [limit=20]
230
+ * @returns {object[]}
231
+ */
232
+ function readLog(baseDir, limit = 20) {
233
+ const p = logPath(baseDir);
234
+ try {
235
+ if (!fs.existsSync(p)) return [];
236
+ const log = JSON.parse(fs.readFileSync(p, 'utf8'));
237
+ return log.slice(-limit);
238
+ } catch (_) {
239
+ return [];
240
+ }
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // State per agent (portado de rowboat agent-schedule/state-repo.ts)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * Carga el estado histórico de un agente cron.
249
+ * Rastrea: lastRunAt, consecutiveErrors, runCount, lastError.
250
+ *
251
+ * @param {string} baseDir
252
+ * @param {string} jobId
253
+ * @returns {{ lastRunAt: string|null, consecutiveErrors: number, runCount: number, lastError: string|null, lastStatus: string|null }}
254
+ */
255
+ function getAgentState(baseDir, jobId) {
256
+ const p = agentStatePath(baseDir);
257
+ const DEFAULT = { lastRunAt: null, consecutiveErrors: 0, runCount: 0, lastError: null, lastStatus: null };
258
+ try {
259
+ if (!fs.existsSync(p)) return { ...DEFAULT };
260
+ const all = JSON.parse(fs.readFileSync(p, 'utf8'));
261
+ return all[jobId] ? { ...DEFAULT, ...all[jobId] } : { ...DEFAULT };
262
+ } catch (_) {
263
+ return { ...DEFAULT };
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Actualiza el estado de un agente cron tras una ejecución.
269
+ *
270
+ * @param {string} baseDir
271
+ * @param {string} jobId
272
+ * @param {{ status: 'completed'|'failed'|'skipped', error?: string }} result
273
+ */
274
+ function updateAgentState(baseDir, jobId, result) {
275
+ const p = agentStatePath(baseDir);
276
+ let all = {};
277
+ try {
278
+ if (fs.existsSync(p)) all = JSON.parse(fs.readFileSync(p, 'utf8'));
279
+ } catch (e) { /* agent-state malformado — resetear */ }
280
+
281
+ const prev = all[jobId] || { lastRunAt: null, consecutiveErrors: 0, runCount: 0, lastError: null, lastStatus: null };
282
+
283
+ all[jobId] = {
284
+ lastRunAt: new Date().toISOString(),
285
+ runCount: (prev.runCount || 0) + 1,
286
+ lastStatus: result.status,
287
+ lastError: result.status === 'failed' ? (result.error || 'unknown error') : null,
288
+ consecutiveErrors: result.status === 'failed'
289
+ ? (prev.consecutiveErrors || 0) + 1
290
+ : 0, // reset en éxito
291
+ };
292
+
293
+ atomicWriteJSON(p, all);
294
+ }
295
+
296
+ /**
297
+ * Lee el historial resumido de todos los agentes cron.
298
+ * Útil para diagnóstico: ver qué jobs tienen errores consecutivos.
299
+ *
300
+ * @param {string} baseDir
301
+ * @returns {Array<{ jobId: string, jobName: string, state: object }>}
302
+ */
303
+ function getAllAgentStates(baseDir) {
304
+ const p = agentStatePath(baseDir);
305
+ try {
306
+ if (!fs.existsSync(p)) return [];
307
+ const all = JSON.parse(fs.readFileSync(p, 'utf8'));
308
+ const jobs = loadJobs(baseDir);
309
+ return Object.entries(all).map(([jobId, state]) => {
310
+ const job = jobs.find(j => j.id === jobId);
311
+ return { jobId, jobName: job?.name || jobId, state };
312
+ });
313
+ } catch (_) {
314
+ return [];
315
+ }
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Exports
320
+ // ---------------------------------------------------------------------------
321
+
322
+ module.exports = {
323
+ loadJobs,
324
+ saveJobs,
325
+ addJob,
326
+ removeJob,
327
+ pauseJob,
328
+ resumeJob,
329
+ markExecuted,
330
+ readLog,
331
+ getAgentState,
332
+ updateAgentState,
333
+ getAllAgentStates,
334
+ CRON_DIR,
335
+ };
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Gateway Runner — Orquestador multi-plataforma para SWL-SES.
6
+ *
7
+ * Conecta el sistema SWL con plataformas de mensajería externas
8
+ * (Telegram, Discord, Webhook) para notificaciones bidireccionales.
9
+ *
10
+ * Arquitectura:
11
+ * Claude Code (SWL) ←→ .planning/comms/ ←→ Gateway ←→ Plataformas
12
+ *
13
+ * Flujos:
14
+ * SWL → Plataforma: Polling de .planning/comms/ cada N segundos
15
+ * Plataforma → SWL: Escribir comandos en .planning/comms/
16
+ *
17
+ * Inspirado en Hermes Agent (gateway/run.py).
18
+ *
19
+ * Uso:
20
+ * node gateway/index.js [baseDir]
21
+ * node gateway/index.js --config manifiestos/gateway-config.json
22
+ *
23
+ * @module gateway/index
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Constantes
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const DEFAULT_POLL_INTERVAL = 2000; // 2 segundos
34
+ const COMMS_DIR = '.planning/comms';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Carga de configuración
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Carga la configuración del gateway desde archivo o defaults.
42
+ * @param {string} baseDir
43
+ * @returns {object}
44
+ */
45
+ function loadConfig(baseDir) {
46
+ const configPath = path.join(baseDir, 'manifiestos', 'gateway-config.json');
47
+ const defaults = {
48
+ enabled: false,
49
+ pollIntervalMs: DEFAULT_POLL_INTERVAL,
50
+ adapters: {
51
+ telegram: { enabled: false },
52
+ discord: { enabled: false },
53
+ webhook: { enabled: false },
54
+ },
55
+ notifications: {
56
+ onSessionComplete: true,
57
+ onCheckpoint: true,
58
+ onError: true,
59
+ onRelease: true,
60
+ onBuildFail: true,
61
+ },
62
+ };
63
+
64
+ try {
65
+ if (fs.existsSync(configPath)) {
66
+ const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
67
+ return { ...defaults, ...loaded, adapters: { ...defaults.adapters, ...loaded.adapters } };
68
+ }
69
+ } catch (_) {}
70
+
71
+ return defaults;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Gateway Runner
76
+ // ---------------------------------------------------------------------------
77
+
78
+ class GatewayRunner {
79
+ constructor(baseDir) {
80
+ this.baseDir = baseDir;
81
+ this.config = loadConfig(baseDir);
82
+ this.adapters = [];
83
+ this._pollInterval = null;
84
+ this._running = false;
85
+
86
+ // CommandRelay opt-in — activo si config.relay.enabled === true.
87
+ // Permite recibir comandos bidireccionales desde canales externos
88
+ // (ej. Telegram) hacia Claude Code con validaciones de seguridad.
89
+ try {
90
+ const CommandRelay = require('./command-relay');
91
+ this.relay = new CommandRelay(baseDir, this.config);
92
+ } catch (_) {
93
+ this.relay = null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Inicializa y arranca todos los adaptadores habilitados.
99
+ */
100
+ async start() {
101
+ if (!this.config.enabled) {
102
+ console.log('[gateway] Gateway deshabilitado en configuración. Activar con enabled: true.');
103
+ return;
104
+ }
105
+
106
+ console.log('[gateway] Iniciando gateway...');
107
+
108
+ // Cargar adaptadores habilitados
109
+ const adapterConfigs = this.config.adapters;
110
+
111
+ if (adapterConfigs.telegram?.enabled) {
112
+ const TelegramAdapter = require('./adapters/telegram');
113
+ const adapter = new TelegramAdapter(adapterConfigs.telegram);
114
+ adapter.onMessage((msg) => this._handleIncoming(msg));
115
+ await adapter.start();
116
+ if (adapter.running) this.adapters.push(adapter);
117
+ }
118
+
119
+ if (adapterConfigs.discord?.enabled) {
120
+ const DiscordAdapter = require('./adapters/discord');
121
+ const adapter = new DiscordAdapter(adapterConfigs.discord);
122
+ adapter.onMessage((msg) => this._handleIncoming(msg));
123
+ await adapter.start();
124
+ if (adapter.running) this.adapters.push(adapter);
125
+ }
126
+
127
+ if (adapterConfigs.webhook?.enabled) {
128
+ const WebhookAdapter = require('./adapters/webhook');
129
+ const adapter = new WebhookAdapter(adapterConfigs.webhook);
130
+ await adapter.start();
131
+ if (adapter.running) this.adapters.push(adapter);
132
+ }
133
+
134
+ if (adapterConfigs.whatsapp?.enabled) {
135
+ const WhatsAppAdapter = require('./adapters/whatsapp');
136
+ const adapter = new WhatsAppAdapter(adapterConfigs.whatsapp);
137
+ adapter.onMessage((msg) => this._handleIncoming(msg));
138
+ await adapter.start();
139
+ if (adapter.running) this.adapters.push(adapter);
140
+ }
141
+
142
+ if (adapterConfigs.slack?.enabled) {
143
+ const SlackAdapter = require('./adapters/slack');
144
+ const adapter = new SlackAdapter(adapterConfigs.slack);
145
+ adapter.onMessage((msg) => this._handleIncoming(msg));
146
+ await adapter.start();
147
+ if (adapter.running) this.adapters.push(adapter);
148
+ }
149
+
150
+ if (adapterConfigs.email?.enabled) {
151
+ const EmailAdapter = require('./adapters/email');
152
+ const adapter = new EmailAdapter(adapterConfigs.email);
153
+ await adapter.start();
154
+ if (adapter.running) this.adapters.push(adapter);
155
+ }
156
+
157
+ if (this.adapters.length === 0) {
158
+ console.log('[gateway] Ningún adaptador habilitado o disponible.');
159
+ return;
160
+ }
161
+
162
+ console.log(`[gateway] ${this.adapters.length} adaptador(es) activo(s): ${this.adapters.map(a => a.name).join(', ')}`);
163
+
164
+ // Iniciar polling de agent-comms
165
+ this._running = true;
166
+ this._pollInterval = setInterval(
167
+ () => this._pollComms(),
168
+ this.config.pollIntervalMs || DEFAULT_POLL_INTERVAL,
169
+ );
170
+
171
+ console.log(`[gateway] Polling activo (cada ${this.config.pollIntervalMs || DEFAULT_POLL_INTERVAL}ms).`);
172
+ }
173
+
174
+ /**
175
+ * Detiene todos los adaptadores y el polling.
176
+ */
177
+ async stop() {
178
+ this._running = false;
179
+ if (this._pollInterval) {
180
+ clearInterval(this._pollInterval);
181
+ this._pollInterval = null;
182
+ }
183
+
184
+ for (const adapter of this.adapters) {
185
+ await adapter.stop();
186
+ }
187
+
188
+ this.adapters = [];
189
+ console.log('[gateway] Detenido.');
190
+ }
191
+
192
+ /**
193
+ * Poll de .planning/comms/ buscando mensajes tipo gateway_notification.
194
+ * @private
195
+ */
196
+ _pollComms() {
197
+ const commsDir = path.join(this.baseDir, COMMS_DIR);
198
+ if (!fs.existsSync(commsDir)) return;
199
+
200
+ let files;
201
+ try {
202
+ files = fs.readdirSync(commsDir)
203
+ .filter(f => f.startsWith('msg-') && f.endsWith('.json'));
204
+ } catch (_) {
205
+ return;
206
+ }
207
+
208
+ for (const file of files) {
209
+ const filePath = path.join(commsDir, file);
210
+ let msg;
211
+ try {
212
+ msg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
213
+ } catch (_) {
214
+ continue;
215
+ }
216
+
217
+ // Solo procesar mensajes de gateway pendientes
218
+ if (msg.status !== 'pending') continue;
219
+ if (msg.type !== 'gateway_notification' && msg.type !== 'gateway_command_response') continue;
220
+
221
+ // Despachar a adaptador(es) correspondientes
222
+ const target = msg.to || 'all';
223
+ this._dispatch(target, msg);
224
+
225
+ // Marcar como procesado
226
+ try {
227
+ msg.status = 'processed';
228
+ msg.processedAt = new Date().toISOString();
229
+ fs.writeFileSync(filePath, JSON.stringify(msg, null, 2), 'utf8');
230
+ } catch (_) {}
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Despacha un mensaje a el/los adaptador(es) objetivo.
236
+ * @private
237
+ */
238
+ _dispatch(target, message) {
239
+ for (const adapter of this.adapters) {
240
+ if (target === 'all' || target === adapter.name || target === 'broadcast') {
241
+ adapter.send(message).catch(err => {
242
+ console.error(`[gateway] Error despachando a ${adapter.name}: ${err.message}`);
243
+ });
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Maneja un mensaje entrante de una plataforma.
250
+ * Escribe en .planning/comms/ para que Claude Code lo procese.
251
+ * Adicionalmente, si el CommandRelay está habilitado para la plataforma
252
+ * y el usuario, encola en .planning/inbox/ para consumo vía /swl:inbox.
253
+ * @private
254
+ */
255
+ _handleIncoming(message) {
256
+ const commsDir = path.join(this.baseDir, COMMS_DIR);
257
+ if (!fs.existsSync(commsDir)) {
258
+ fs.mkdirSync(commsDir, { recursive: true });
259
+ }
260
+
261
+ const id = `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
262
+ const msg = {
263
+ id,
264
+ type: 'gateway_command',
265
+ from: `${message.platform}:${message.userId || 'unknown'}`,
266
+ to: 'swl-system',
267
+ payload: {
268
+ platform: message.platform,
269
+ chatId: message.chatId,
270
+ userId: message.userId,
271
+ userName: message.userName,
272
+ text: message.text,
273
+ command: message.command,
274
+ args: message.args,
275
+ },
276
+ timestamp: new Date().toISOString(),
277
+ status: 'pending',
278
+ };
279
+
280
+ const filePath = path.join(commsDir, `${id}.json`);
281
+ try {
282
+ fs.writeFileSync(filePath, JSON.stringify(msg, null, 2), 'utf8');
283
+ } catch (_) {}
284
+
285
+ // Relay bidireccional: encolar también en .planning/inbox/ si aplica.
286
+ if (this.relay && this.relay.habilitado()) {
287
+ try {
288
+ this.relay.recibirComando(message);
289
+ } catch (_) { /* silencioso */ }
290
+ }
291
+ }
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Entrypoint CLI
296
+ // ---------------------------------------------------------------------------
297
+
298
+ if (require.main === module) {
299
+ const baseDir = process.argv[2] || process.cwd();
300
+
301
+ const gateway = new GatewayRunner(baseDir);
302
+
303
+ const cleanup = async () => {
304
+ await gateway.stop();
305
+ process.exit(0);
306
+ };
307
+ process.on('SIGTERM', cleanup);
308
+ process.on('SIGINT', cleanup);
309
+
310
+ gateway.start().catch(err => {
311
+ console.error(`[gateway] Error fatal: ${err.message}`);
312
+ process.exit(1);
313
+ });
314
+ }
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // Exports
318
+ // ---------------------------------------------------------------------------
319
+
320
+ module.exports = { GatewayRunner, loadConfig };