@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
@@ -10,13 +10,32 @@
10
10
  * - Blocked toolsets (sin delegación recursiva, sin memoria compartida)
11
11
  * - Reporte estructurado con duración y métricas
12
12
  *
13
- * Estado: en memoria por sesión (se resetea al reiniciar Claude Code).
13
+ * Persistencia: estado serializado a `${TMPDIR}/swl-delegation-${sessionId}.json`.
14
+ * Como cada hook se invoca en proceso Node fresco, el estado en variables de
15
+ * módulo no sobrevive entre invocaciones — el archivo es la fuente de verdad.
16
+ *
17
+ * Reconciliación tras /compact, /clear o reinicio:
18
+ * - El cliente Claude Code emite un nuevo session_id en esos eventos.
19
+ * - Al pasar un sessionId nuevo a las funciones API, se lee/escribe un
20
+ * archivo distinto, descartando el estado anterior automáticamente.
21
+ * - `cleanupStaleSessions()` purga archivos cuya última escritura supera el
22
+ * TTL (default 1h) — protege contra leaks por sesiones que terminan sin
23
+ * señalizar fin.
24
+ *
25
+ * Compatibilidad: si no se pasa sessionId (modo legacy), se usa estado
26
+ * en memoria del proceso. Funciona para tests unitarios y para invocaciones
27
+ * dentro de un mismo proceso, NO para coordinación entre hooks distintos.
28
+ *
14
29
  * Integración: risk-scoring.js consulta profundidad, tracking-costos.js
15
30
  * captura métricas por subagente.
16
31
  *
17
32
  * @module hooks/lib/delegation-tracker
18
33
  */
19
34
 
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+ const os = require('os');
38
+
20
39
  // ---------------------------------------------------------------------------
21
40
  // Constantes
22
41
  // ---------------------------------------------------------------------------
@@ -27,18 +46,96 @@ const MAX_DELEGATION_DEPTH = 2;
27
46
  /** Máximo de subagentes ejecutándose en paralelo. */
28
47
  const MAX_CONCURRENT_CHILDREN = 3;
29
48
 
49
+ /** TTL de archivos de estado huérfanos (ms). Default 1h. */
50
+ const STALE_TTL_MS = 60 * 60 * 1000;
51
+
30
52
  // ---------------------------------------------------------------------------
31
- // Estado en memoria (por sesión)
53
+ // Persistencia file-based (por sessionId) y memoria (legacy / fallback)
32
54
  // ---------------------------------------------------------------------------
33
55
 
34
- /** Stack de delegaciones activas. */
35
- const _delegationStack = [];
56
+ /** Estado en memoria del proceso (modo legacy, sin sessionId). */
57
+ const _memState = {
58
+ delegationStack: [],
59
+ activeChildren: 0,
60
+ completedDelegations: [],
61
+ };
62
+
63
+ /** Path del archivo de estado para una sesión. */
64
+ function _statePath(sessionId) {
65
+ return path.join(os.tmpdir(), `swl-delegation-${sessionId}.json`);
66
+ }
36
67
 
37
- /** Contador de hijos activos. */
38
- let _activeChildren = 0;
68
+ /** Lee el estado de una sesión desde archivo, o devuelve estado vacío. */
69
+ function _readState(sessionId) {
70
+ if (!sessionId) return _memState;
71
+ try {
72
+ const raw = fs.readFileSync(_statePath(sessionId), 'utf8');
73
+ const parsed = JSON.parse(raw);
74
+ return {
75
+ delegationStack: Array.isArray(parsed.delegationStack) ? parsed.delegationStack : [],
76
+ activeChildren: Number.isInteger(parsed.activeChildren) ? parsed.activeChildren : 0,
77
+ completedDelegations: Array.isArray(parsed.completedDelegations) ? parsed.completedDelegations : [],
78
+ };
79
+ } catch (_) {
80
+ return { delegationStack: [], activeChildren: 0, completedDelegations: [] };
81
+ }
82
+ }
83
+
84
+ /** Escribe el estado de una sesión a archivo (atómico-ish: write + rename). */
85
+ function _writeState(sessionId, state) {
86
+ if (!sessionId) {
87
+ _memState.delegationStack = state.delegationStack;
88
+ _memState.activeChildren = state.activeChildren;
89
+ _memState.completedDelegations = state.completedDelegations;
90
+ return;
91
+ }
92
+ try {
93
+ const target = _statePath(sessionId);
94
+ const tmp = target + '.tmp';
95
+ fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
96
+ fs.renameSync(tmp, target);
97
+ } catch (_) {
98
+ // No bloquear el flujo si falla escritura — el módulo es advisory.
99
+ }
100
+ }
39
101
 
40
- /** Historial de delegaciones completadas (para métricas). */
41
- const _completedDelegations = [];
102
+ /**
103
+ * Purga archivos de estado de sesiones huérfanas.
104
+ * Una sesión "muerta" es la que no recibió update en STALE_TTL_MS.
105
+ *
106
+ * Llamar al iniciar trabajo nuevo o desde `/swl:salud` para evitar leaks.
107
+ *
108
+ * @param {number} [ttlMs=STALE_TTL_MS] - TTL en ms.
109
+ * @returns {{ removed: string[], kept: string[] }}
110
+ */
111
+ function cleanupStaleSessions(ttlMs = STALE_TTL_MS) {
112
+ const tmpDir = os.tmpdir();
113
+ const removed = [];
114
+ const kept = [];
115
+ const now = Date.now();
116
+ let entries;
117
+ try {
118
+ entries = fs.readdirSync(tmpDir);
119
+ } catch (_) {
120
+ return { removed, kept };
121
+ }
122
+ for (const entry of entries) {
123
+ if (!/^swl-delegation-.+\.json$/.test(entry)) continue;
124
+ const fullPath = path.join(tmpDir, entry);
125
+ try {
126
+ const stat = fs.statSync(fullPath);
127
+ if (now - stat.mtimeMs > ttlMs) {
128
+ fs.unlinkSync(fullPath);
129
+ removed.push(entry);
130
+ } else {
131
+ kept.push(entry);
132
+ }
133
+ } catch (_) {
134
+ // Archivo desapareció en condición de carrera — ignorar.
135
+ }
136
+ }
137
+ return { removed, kept };
138
+ }
42
139
 
43
140
  // ---------------------------------------------------------------------------
44
141
  // API pública
@@ -47,26 +144,29 @@ const _completedDelegations = [];
47
144
  /**
48
145
  * Verifica si una delegación es permitida.
49
146
  *
50
- * @param {string} agentName - Nombre del agente que solicita delegar.
147
+ * @param {string} agentName - Nombre del agente que solicita delegar.
148
+ * @param {string} [sessionId] - ID de sesión Claude Code. Si se omite, usa
149
+ * estado en memoria (modo legacy).
51
150
  * @returns {{ allowed: boolean, reason?: string, depth?: number }}
52
151
  */
53
- function canDelegate(agentName) {
54
- const currentDepth = _delegationStack.length;
152
+ function canDelegate(agentName, sessionId) {
153
+ const state = _readState(sessionId);
154
+ const currentDepth = state.delegationStack.length;
55
155
 
56
156
  if (currentDepth >= MAX_DELEGATION_DEPTH) {
57
157
  return {
58
158
  allowed: false,
59
159
  reason: `Profundidad máxima de delegación alcanzada (${MAX_DELEGATION_DEPTH}). ` +
60
- `Stack actual: ${_delegationStack.map(d => d.agentName).join(' → ')}. ` +
160
+ `Stack actual: ${state.delegationStack.map(d => d.agentName).join(' → ')}. ` +
61
161
  `No se permite delegación adicional para prevenir recursión.`,
62
162
  };
63
163
  }
64
164
 
65
- if (_activeChildren >= MAX_CONCURRENT_CHILDREN) {
165
+ if (state.activeChildren >= MAX_CONCURRENT_CHILDREN) {
66
166
  return {
67
167
  allowed: false,
68
168
  reason: `Máximo de subagentes concurrentes alcanzado (${MAX_CONCURRENT_CHILDREN}). ` +
69
- `Activos: ${_delegationStack.filter(d => d.active).map(d => d.agentName).join(', ')}. ` +
169
+ `Activos: ${state.delegationStack.filter(d => d.active).map(d => d.agentName).join(', ')}. ` +
70
170
  `Esperar a que terminen antes de delegar.`,
71
171
  };
72
172
  }
@@ -77,23 +177,26 @@ function canDelegate(agentName) {
77
177
  /**
78
178
  * Registra el inicio de una delegación.
79
179
  *
80
- * @param {string} agentName - Nombre del agente delegado.
81
- * @param {string} [taskId] - ID de la tarea (para correlación).
180
+ * @param {string} agentName - Nombre del agente delegado.
181
+ * @param {string} [taskId] - ID de la tarea (para correlación).
182
+ * @param {string} [sessionId] - ID de sesión Claude Code.
82
183
  * @returns {string} ID único de la delegación (para trackDelegationComplete).
83
184
  */
84
- function trackDelegationStart(agentName, taskId) {
185
+ function trackDelegationStart(agentName, taskId, sessionId) {
186
+ const state = _readState(sessionId);
85
187
  const delegationId = `del-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
86
188
 
87
- _delegationStack.push({
189
+ state.delegationStack.push({
88
190
  delegationId,
89
191
  agentName,
90
192
  taskId: taskId || delegationId,
91
- depth: _delegationStack.length,
193
+ depth: state.delegationStack.length,
92
194
  startTime: Date.now(),
93
195
  active: true,
94
196
  });
95
197
 
96
- _activeChildren++;
198
+ state.activeChildren += 1;
199
+ _writeState(sessionId, state);
97
200
  return delegationId;
98
201
  }
99
202
 
@@ -105,18 +208,19 @@ function trackDelegationStart(agentName, taskId) {
105
208
  * @param {string} [result.status='completed'] - Estado final.
106
209
  * @param {number} [result.tokensUsed] - Tokens consumidos.
107
210
  * @param {number} [result.toolCalls] - Herramientas invocadas.
211
+ * @param {string} [sessionId] - ID de sesión Claude Code.
108
212
  * @returns {object|null} Métricas de la delegación, o null si no se encontró.
109
213
  */
110
- function trackDelegationComplete(delegationId, result = {}) {
111
- const idx = _delegationStack.findIndex(d => d.delegationId === delegationId);
214
+ function trackDelegationComplete(delegationId, result = {}, sessionId) {
215
+ const state = _readState(sessionId);
216
+ const idx = state.delegationStack.findIndex(d => d.delegationId === delegationId);
112
217
  if (idx < 0) return null;
113
218
 
114
- const entry = _delegationStack[idx];
219
+ const entry = state.delegationStack[idx];
115
220
  entry.active = false;
116
- _activeChildren = Math.max(0, _activeChildren - 1);
221
+ state.activeChildren = Math.max(0, state.activeChildren - 1);
117
222
 
118
- // Remover del stack
119
- _delegationStack.splice(idx, 1);
223
+ state.delegationStack.splice(idx, 1);
120
224
 
121
225
  const completed = {
122
226
  delegationId,
@@ -129,19 +233,17 @@ function trackDelegationComplete(delegationId, result = {}) {
129
233
  toolCalls: result.toolCalls || 0,
130
234
  };
131
235
 
132
- _completedDelegations.push(completed);
133
-
134
- // Mantener solo las últimas 50 delegaciones completadas
135
- if (_completedDelegations.length > 50) {
136
- _completedDelegations.shift();
137
- }
236
+ state.completedDelegations.push(completed);
237
+ if (state.completedDelegations.length > 50) state.completedDelegations.shift();
138
238
 
239
+ _writeState(sessionId, state);
139
240
  return completed;
140
241
  }
141
242
 
142
243
  /**
143
244
  * Obtiene el estado actual de delegación.
144
245
  *
246
+ * @param {string} [sessionId] - ID de sesión Claude Code.
145
247
  * @returns {{
146
248
  * currentDepth: number,
147
249
  * activeChildren: number,
@@ -151,19 +253,20 @@ function trackDelegationComplete(delegationId, result = {}) {
151
253
  * recentCompleted: object[]
152
254
  * }}
153
255
  */
154
- function getDelegationState() {
256
+ function getDelegationState(sessionId) {
257
+ const state = _readState(sessionId);
155
258
  return {
156
- currentDepth: _delegationStack.length,
157
- activeChildren: _activeChildren,
259
+ currentDepth: state.delegationStack.length,
260
+ activeChildren: state.activeChildren,
158
261
  maxDepth: MAX_DELEGATION_DEPTH,
159
262
  maxConcurrent: MAX_CONCURRENT_CHILDREN,
160
- stack: _delegationStack.map(d => ({
263
+ stack: state.delegationStack.map(d => ({
161
264
  agent: d.agentName,
162
265
  depth: d.depth,
163
266
  elapsedMs: Date.now() - d.startTime,
164
267
  active: d.active,
165
268
  })),
166
- recentCompleted: _completedDelegations.slice(-10),
269
+ recentCompleted: state.completedDelegations.slice(-10),
167
270
  };
168
271
  }
169
272
 
@@ -171,19 +274,32 @@ function getDelegationState() {
171
274
  * Calcula el factor de riesgo adicional por profundidad de delegación.
172
275
  * Diseñado para integrarse con risk-scoring.js.
173
276
  *
277
+ * @param {string} [sessionId] - ID de sesión Claude Code.
174
278
  * @returns {number} Factor entre 0.0 y 0.30 (0.15 por nivel de profundidad).
175
279
  */
176
- function getDelegationRiskFactor() {
177
- return Math.min(_delegationStack.length * 0.15, 0.30);
280
+ function getDelegationRiskFactor(sessionId) {
281
+ const state = _readState(sessionId);
282
+ return Math.min(state.delegationStack.length * 0.15, 0.30);
178
283
  }
179
284
 
180
285
  /**
181
- * Resetea el estado de delegación (para tests o nueva sesión).
286
+ * Resetea el estado de delegación (para tests o reconciliación explícita).
287
+ *
288
+ * @param {string} [sessionId] - ID de sesión a resetear. Si se omite, resetea
289
+ * el estado en memoria (modo legacy).
182
290
  */
183
- function reset() {
184
- _delegationStack.length = 0;
185
- _activeChildren = 0;
186
- _completedDelegations.length = 0;
291
+ function reset(sessionId) {
292
+ if (!sessionId) {
293
+ _memState.delegationStack.length = 0;
294
+ _memState.activeChildren = 0;
295
+ _memState.completedDelegations.length = 0;
296
+ return;
297
+ }
298
+ try {
299
+ fs.unlinkSync(_statePath(sessionId));
300
+ } catch (_) {
301
+ // No existe → ya está reseteado.
302
+ }
187
303
  }
188
304
 
189
305
  module.exports = {
@@ -193,6 +309,8 @@ module.exports = {
193
309
  getDelegationState,
194
310
  getDelegationRiskFactor,
195
311
  reset,
312
+ cleanupStaleSessions,
196
313
  MAX_DELEGATION_DEPTH,
197
314
  MAX_CONCURRENT_CHILDREN,
315
+ STALE_TTL_MS,
198
316
  };
@@ -19,6 +19,15 @@
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
 
22
+ // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
23
+ let atomicWriteSync, atomicWriteJSON;
24
+ try {
25
+ ({ atomicWriteSync, atomicWriteJSON } = require('./atomic-write'));
26
+ } catch {
27
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
28
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
29
+ }
30
+
22
31
  // ---------------------------------------------------------------------------
23
32
  // Constantes
24
33
  // ---------------------------------------------------------------------------
@@ -167,7 +176,7 @@ function markAsEvolved(filePath, meta) {
167
176
  frontmatter = frontmatter.trimEnd() + '\n' + newFields.join('\n');
168
177
 
169
178
  const newContent = prefix + frontmatter + suffix + rest;
170
- fs.writeFileSync(filePath, newContent, 'utf8');
179
+ atomicWriteSync(filePath, newContent, 'utf8');
171
180
 
172
181
  return { marked: true };
173
182
  } catch (err) {
@@ -200,7 +209,7 @@ function _writeSidecar(filePath, meta) {
200
209
  ...(meta.note && { evolvedNote: meta.note }),
201
210
  };
202
211
 
203
- fs.writeFileSync(sidecarPath, JSON.stringify(data, null, 2), 'utf8');
212
+ atomicWriteJSON(sidecarPath, data);
204
213
  return { marked: true };
205
214
  }
206
215
 
@@ -368,7 +377,7 @@ function mergeEvolved(destino, origen, versionNueva) {
368
377
  ]).flat(),
369
378
  ].join('\n');
370
379
 
371
- fs.writeFileSync(diffPath, diffContent, 'utf8');
380
+ atomicWriteSync(diffPath, diffContent, 'utf8');
372
381
 
373
382
  return { merged: true, diffPath, diffsCount: diffs.length };
374
383
  } catch (err) {
@@ -499,8 +499,66 @@ function fetch(baseDir, id) {
499
499
  return null;
500
500
  }
501
501
 
502
+ // ---------------------------------------------------------------------------
503
+ // Instrumentación de utilidad — registra cada búsqueda a JSONL para que
504
+ // metricas-evolucion.js pueda calcular utilidad como proxy.
505
+ //
506
+ // Formato del registro:
507
+ // { ts, sessionId, op: 'search'|'timeline'|'fetch', query, resultsCount,
508
+ // baseDir }
509
+ //
510
+ // Opt-out: SWL_MEMORY_TELEMETRY=0 desactiva el logging.
511
+ // ---------------------------------------------------------------------------
512
+
513
+ function _logUsage(op, baseDir, query, resultsCount) {
514
+ if (process.env.SWL_MEMORY_TELEMETRY === '0') return;
515
+ try {
516
+ const dir = path.join(baseDir, '.planning', 'evolucion');
517
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
518
+ const entry = {
519
+ ts: new Date().toISOString(),
520
+ sessionId: String(process.env.SWL_SESSION_ID || process.env.CLAUDE_SESSION_ID || 'default'),
521
+ op,
522
+ query: typeof query === 'string' ? query.slice(0, 200) : String(query || '').slice(0, 200),
523
+ resultsCount: Number.isInteger(resultsCount) ? resultsCount : 0,
524
+ };
525
+ fs.appendFileSync(
526
+ path.join(dir, 'memory-usage.jsonl'),
527
+ JSON.stringify(entry) + '\n',
528
+ 'utf8',
529
+ );
530
+ } catch { /* nunca bloquear por fallo de telemetría */ }
531
+ }
532
+
533
+ const _searchInstrumented = (baseDir, query, filtros = {}) => {
534
+ const results = search(baseDir, query, filtros);
535
+ _logUsage('search', baseDir, query, Array.isArray(results) ? results.length : 0);
536
+ return results;
537
+ };
538
+
539
+ const _timelineInstrumented = (baseDir, ids) => {
540
+ const results = timeline(baseDir, ids);
541
+ const idsCount = Array.isArray(ids) ? ids.length : 0;
542
+ _logUsage('timeline', baseDir, `[${idsCount} ids]`, Array.isArray(results) ? results.length : 0);
543
+ return results;
544
+ };
545
+
546
+ const _fetchInstrumented = (baseDir, id) => {
547
+ const result = fetch(baseDir, id);
548
+ _logUsage('fetch', baseDir, String(id), result ? 1 : 0);
549
+ return result;
550
+ };
551
+
502
552
  // ---------------------------------------------------------------------------
503
553
  // Exports
504
554
  // ---------------------------------------------------------------------------
505
555
 
506
- module.exports = { search, timeline, fetch };
556
+ module.exports = {
557
+ search: _searchInstrumented,
558
+ timeline: _timelineInstrumented,
559
+ fetch: _fetchInstrumented,
560
+ // Versiones sin instrumentación, para tests internos.
561
+ _searchRaw: search,
562
+ _timelineRaw: timeline,
563
+ _fetchRaw: fetch,
564
+ };
@@ -23,6 +23,15 @@
23
23
 
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
+
27
+ // Escritura atómica para reescritura completa del JSONL (marcar accionado).
28
+ // El append diario sigue usando appendFileSync (ver más abajo).
29
+ let atomicWriteSync;
30
+ try {
31
+ ({ atomicWriteSync } = require('./atomic-write'));
32
+ } catch {
33
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
34
+ }
26
35
  const crypto = require('crypto');
27
36
 
28
37
  let atomicWriteJSON;
@@ -161,7 +170,7 @@ function markAccionado(id, by = 'desconocido') {
161
170
  ensureDir();
162
171
  const lines = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
163
172
  try {
164
- fs.writeFileSync(LOG_PATH, lines, 'utf8');
173
+ atomicWriteSync(LOG_PATH, lines, 'utf8');
165
174
  return true;
166
175
  } catch {
167
176
  return false;
@@ -29,6 +29,14 @@
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
31
 
32
+ // Escritura atómica obligatoria para meta.json (regla CLAUDE.md).
33
+ let atomicWriteJSON;
34
+ try {
35
+ ({ atomicWriteJSON } = require('./atomic-write'));
36
+ } catch {
37
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
38
+ }
39
+
32
40
  // ---------------------------------------------------------------------------
33
41
  // Constantes
34
42
  // ---------------------------------------------------------------------------
@@ -93,13 +101,13 @@ function registrarProveniencia(componentDir, datos) {
93
101
  Object.assign(existing, meta);
94
102
  existing.history = existing.history.slice(-5); // Máximo 5 entradas de historial
95
103
 
96
- fs.writeFileSync(metaPath, JSON.stringify(existing, null, 2), 'utf8');
104
+ atomicWriteJSON(metaPath, existing);
97
105
  } catch {
98
106
  // Error leyendo existente — sobrescribir
99
- fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
107
+ atomicWriteJSON(metaPath, meta);
100
108
  }
101
109
  } else {
102
- fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
110
+ atomicWriteJSON(metaPath, meta);
103
111
  }
104
112
  }
105
113