@saulwade/swl-ses 1.3.1 → 1.3.3

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 (34) hide show
  1. package/CLAUDE.md +199 -199
  2. package/README.md +1 -1
  3. package/bin/swl-ses.js +77 -5
  4. package/comandos/swl/aprender.md +1 -1
  5. package/comandos/swl/claudemd.md +141 -136
  6. package/comandos/swl/configurar-ci.md +227 -227
  7. package/comandos/swl/evolucion-estado.md +1 -1
  8. package/comandos/swl/evolucionar.md +3 -3
  9. package/comandos/swl/inbox.md +1 -1
  10. package/comandos/swl/reflect-skills.md +1 -1
  11. package/comandos/swl/salud.md +7 -7
  12. package/comandos/swl/skill-search.md +4 -4
  13. package/manifiestos/perfiles.json +2 -1
  14. package/manifiestos/skills-lock.json +1093 -1093
  15. package/package.json +87 -87
  16. package/plugin.json +343 -343
  17. package/scripts/auditar-claudemd.js +297 -297
  18. package/scripts/bootstrap-instintos.js +1 -1
  19. package/scripts/cli/audit-agents-gaps.js +36 -0
  20. package/scripts/cli/audit-claudemd.js +43 -0
  21. package/scripts/cli/audit-coverage-frameworks.js +39 -0
  22. package/scripts/cli/bootstrap-instincts.js +38 -0
  23. package/scripts/cli/configure-branch-protection.js +42 -0
  24. package/scripts/cli/generate-skills-lock.js +31 -0
  25. package/scripts/cli/inbox-tmux-inject.js +49 -0
  26. package/scripts/cli/reflect-skills.js +40 -0
  27. package/scripts/cli/run-skill-evals.js +47 -0
  28. package/scripts/cli/skill-discovery.js +38 -0
  29. package/scripts/cli/verify-evolution.js +36 -0
  30. package/scripts/generar-skills-lock.js +190 -190
  31. package/scripts/inbox-tmux-inject.js +6 -0
  32. package/scripts/lib/autostart-windows.js +51 -28
  33. package/scripts/lib/skill-discovery.js +11 -3
  34. package/scripts/verificar-evolucion.js +1 -1
@@ -1,190 +1,190 @@
1
- #!/usr/bin/env node
2
- /**
3
- * generar-skills-lock.js
4
- *
5
- * Genera `manifiestos/skills-lock.json` con SHA256 de cada `habilidades/*\/SKILL.md`.
6
- * Permite detectar drift silencioso: alguien edita un skill localmente sin
7
- * commit y nadie lo nota hasta que falla. El lock se regenera en cada
8
- * `/swl:release` y se valida en `/swl:salud`.
9
- *
10
- * Uso:
11
- * node scripts/generar-skills-lock.js # genera y escribe
12
- * node scripts/generar-skills-lock.js --check # compara, falla si hay drift
13
- * node scripts/generar-skills-lock.js --json # imprime resumen JSON
14
- *
15
- * Origen: análisis de temp/design.md-main/skills-lock.json (mayo 2026).
16
- *
17
- * Zero-dependencies. Compatible Windows (CRLF-safe), Node 18+.
18
- */
19
-
20
- 'use strict';
21
-
22
- const fs = require('fs');
23
- const path = require('path');
24
- const crypto = require('crypto');
25
-
26
- const ROOT = path.resolve(__dirname, '..');
27
-
28
- // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
29
- let atomicWriteSync;
30
- try {
31
- ({ atomicWriteSync } = require(path.join(ROOT, 'hooks', 'lib', 'atomic-write.js')));
32
- } catch {
33
- atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
34
- }
35
- const HABILIDADES_DIR = path.join(ROOT, 'habilidades');
36
- const LOCK_FILE = path.join(ROOT, 'manifiestos', 'skills-lock.json');
37
- const LOCK_VERSION = 1;
38
-
39
- const CHECK = process.argv.includes('--check');
40
- const JSON_OUT = process.argv.includes('--json');
41
-
42
- function sha256(buffer) {
43
- return crypto.createHash('sha256').update(buffer).digest('hex');
44
- }
45
-
46
- function leerFrontmatter(contenido) {
47
- const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
48
- if (!m) return {};
49
- const fm = {};
50
- for (const linea of m[1].split(/\r?\n/)) {
51
- const kv = linea.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
52
- if (kv) fm[kv[1]] = kv[2].trim();
53
- }
54
- return fm;
55
- }
56
-
57
- function generarLock() {
58
- if (!fs.existsSync(HABILIDADES_DIR)) {
59
- console.error(`ERROR: directorio no encontrado: ${HABILIDADES_DIR}`);
60
- process.exit(2);
61
- }
62
-
63
- const skills = [];
64
- const dirs = fs
65
- .readdirSync(HABILIDADES_DIR, { withFileTypes: true })
66
- .filter((d) => d.isDirectory())
67
- .map((d) => d.name)
68
- .sort();
69
-
70
- for (const dir of dirs) {
71
- const skillFile = path.join(HABILIDADES_DIR, dir, 'SKILL.md');
72
- if (!fs.existsSync(skillFile)) {
73
- // Skill sin SKILL.md — anomalía pero no fatal
74
- skills.push({
75
- nombre: dir,
76
- path: `habilidades/${dir}/SKILL.md`,
77
- hash: null,
78
- bytes: 0,
79
- warning: 'skill-md-faltante',
80
- });
81
- continue;
82
- }
83
-
84
- const buffer = fs.readFileSync(skillFile);
85
- const contenido = buffer.toString('utf-8');
86
- const fm = leerFrontmatter(contenido);
87
-
88
- skills.push({
89
- nombre: fm.name || dir,
90
- path: `habilidades/${dir}/SKILL.md`,
91
- hash: `sha256:${sha256(buffer)}`,
92
- bytes: buffer.length,
93
- version: fm.version || null,
94
- });
95
- }
96
-
97
- // Hash agregado del lock completo (excluyendo timestamps)
98
- const datosCanonicos = JSON.stringify(
99
- skills.map((s) => ({ nombre: s.nombre, hash: s.hash })),
100
- null,
101
- 0
102
- );
103
- const lockHash = `sha256:${sha256(Buffer.from(datosCanonicos, 'utf-8'))}`;
104
-
105
- return {
106
- lockfileVersion: LOCK_VERSION,
107
- generatedAt: new Date().toISOString(),
108
- skillsCount: skills.length,
109
- lockHash,
110
- skills,
111
- };
112
- }
113
-
114
- function comparar(actual, anterior) {
115
- if (!anterior) return { drifted: [], faltantes: actual.skills, nuevos: [] };
116
-
117
- const anteriorMap = new Map(anterior.skills.map((s) => [s.nombre, s]));
118
- const actualMap = new Map(actual.skills.map((s) => [s.nombre, s]));
119
-
120
- const drifted = [];
121
- const nuevos = [];
122
- const faltantes = [];
123
-
124
- for (const s of actual.skills) {
125
- const prev = anteriorMap.get(s.nombre);
126
- if (!prev) {
127
- nuevos.push(s.nombre);
128
- } else if (prev.hash !== s.hash) {
129
- drifted.push({ nombre: s.nombre, antes: prev.hash, ahora: s.hash });
130
- }
131
- }
132
- for (const s of anterior.skills) {
133
- if (!actualMap.has(s.nombre)) {
134
- faltantes.push(s.nombre);
135
- }
136
- }
137
-
138
- return { drifted, faltantes, nuevos };
139
- }
140
-
141
- function main() {
142
- const actual = generarLock();
143
-
144
- if (CHECK) {
145
- if (!fs.existsSync(LOCK_FILE)) {
146
- console.error(`ERROR: ${LOCK_FILE} no existe. Ejecutar sin --check para generarlo.`);
147
- process.exit(1);
148
- }
149
- const anterior = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
150
- const diff = comparar(actual, anterior);
151
- const total = diff.drifted.length + diff.faltantes.length + diff.nuevos.length;
152
-
153
- if (JSON_OUT) {
154
- console.log(JSON.stringify({ ok: total === 0, ...diff }, null, 2));
155
- } else if (total === 0) {
156
- console.log(`✓ skills-lock OK: ${actual.skillsCount} skills sin drift`);
157
- } else {
158
- console.log(`✗ skills-lock DRIFT detectado:`);
159
- if (diff.drifted.length) {
160
- console.log(` Modificados (${diff.drifted.length}):`);
161
- diff.drifted.forEach((d) => console.log(` - ${d.nombre}`));
162
- }
163
- if (diff.nuevos.length) {
164
- console.log(` Nuevos (${diff.nuevos.length}):`);
165
- diff.nuevos.forEach((n) => console.log(` - ${n}`));
166
- }
167
- if (diff.faltantes.length) {
168
- console.log(` Faltantes (${diff.faltantes.length}):`);
169
- diff.faltantes.forEach((n) => console.log(` - ${n}`));
170
- }
171
- console.log(`\n Para regenerar: node scripts/generar-skills-lock.js`);
172
- }
173
- process.exit(total === 0 ? 0 : 1);
174
- }
175
-
176
- // Generar
177
- atomicWriteSync(LOCK_FILE, JSON.stringify(actual, null, 2) + '\n', 'utf-8');
178
- if (JSON_OUT) {
179
- console.log(JSON.stringify({ ok: true, file: LOCK_FILE, count: actual.skillsCount }));
180
- } else {
181
- console.log(`✓ Generado ${LOCK_FILE}`);
182
- console.log(` ${actual.skillsCount} skills, lockHash=${actual.lockHash.slice(0, 23)}...`);
183
- }
184
- }
185
-
186
- if (require.main === module) {
187
- main();
188
- }
189
-
190
- module.exports = { generarLock, comparar };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generar-skills-lock.js
4
+ *
5
+ * Genera `manifiestos/skills-lock.json` con SHA256 de cada `habilidades/*\/SKILL.md`.
6
+ * Permite detectar drift silencioso: alguien edita un skill localmente sin
7
+ * commit y nadie lo nota hasta que falla. El lock se regenera en cada
8
+ * `/swl:release` y se valida en `/swl:salud`.
9
+ *
10
+ * Uso:
11
+ * node scripts/generar-skills-lock.js # genera y escribe
12
+ * node scripts/generar-skills-lock.js --check # compara, falla si hay drift
13
+ * node scripts/generar-skills-lock.js --json # imprime resumen JSON
14
+ *
15
+ * Origen: análisis de temp/design.md-main/skills-lock.json (mayo 2026).
16
+ *
17
+ * Zero-dependencies. Compatible Windows (CRLF-safe), Node 18+.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const crypto = require('crypto');
25
+
26
+ const ROOT = path.resolve(__dirname, '..');
27
+
28
+ // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
29
+ let atomicWriteSync;
30
+ try {
31
+ ({ atomicWriteSync } = require(path.join(ROOT, 'hooks', 'lib', 'atomic-write.js')));
32
+ } catch {
33
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
34
+ }
35
+ const HABILIDADES_DIR = path.join(ROOT, 'habilidades');
36
+ const LOCK_FILE = path.join(ROOT, 'manifiestos', 'skills-lock.json');
37
+ const LOCK_VERSION = 1;
38
+
39
+ const CHECK = process.argv.includes('--check');
40
+ const JSON_OUT = process.argv.includes('--json');
41
+
42
+ function sha256(buffer) {
43
+ return crypto.createHash('sha256').update(buffer).digest('hex');
44
+ }
45
+
46
+ function leerFrontmatter(contenido) {
47
+ const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
48
+ if (!m) return {};
49
+ const fm = {};
50
+ for (const linea of m[1].split(/\r?\n/)) {
51
+ const kv = linea.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
52
+ if (kv) fm[kv[1]] = kv[2].trim();
53
+ }
54
+ return fm;
55
+ }
56
+
57
+ function generarLock() {
58
+ if (!fs.existsSync(HABILIDADES_DIR)) {
59
+ console.error(`ERROR: directorio no encontrado: ${HABILIDADES_DIR}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const skills = [];
64
+ const dirs = fs
65
+ .readdirSync(HABILIDADES_DIR, { withFileTypes: true })
66
+ .filter((d) => d.isDirectory())
67
+ .map((d) => d.name)
68
+ .sort();
69
+
70
+ for (const dir of dirs) {
71
+ const skillFile = path.join(HABILIDADES_DIR, dir, 'SKILL.md');
72
+ if (!fs.existsSync(skillFile)) {
73
+ // Skill sin SKILL.md — anomalía pero no fatal
74
+ skills.push({
75
+ nombre: dir,
76
+ path: `habilidades/${dir}/SKILL.md`,
77
+ hash: null,
78
+ bytes: 0,
79
+ warning: 'skill-md-faltante',
80
+ });
81
+ continue;
82
+ }
83
+
84
+ const buffer = fs.readFileSync(skillFile);
85
+ const contenido = buffer.toString('utf-8');
86
+ const fm = leerFrontmatter(contenido);
87
+
88
+ skills.push({
89
+ nombre: fm.name || dir,
90
+ path: `habilidades/${dir}/SKILL.md`,
91
+ hash: `sha256:${sha256(buffer)}`,
92
+ bytes: buffer.length,
93
+ version: fm.version || null,
94
+ });
95
+ }
96
+
97
+ // Hash agregado del lock completo (excluyendo timestamps)
98
+ const datosCanonicos = JSON.stringify(
99
+ skills.map((s) => ({ nombre: s.nombre, hash: s.hash })),
100
+ null,
101
+ 0
102
+ );
103
+ const lockHash = `sha256:${sha256(Buffer.from(datosCanonicos, 'utf-8'))}`;
104
+
105
+ return {
106
+ lockfileVersion: LOCK_VERSION,
107
+ generatedAt: new Date().toISOString(),
108
+ skillsCount: skills.length,
109
+ lockHash,
110
+ skills,
111
+ };
112
+ }
113
+
114
+ function comparar(actual, anterior) {
115
+ if (!anterior) return { drifted: [], faltantes: actual.skills, nuevos: [] };
116
+
117
+ const anteriorMap = new Map(anterior.skills.map((s) => [s.nombre, s]));
118
+ const actualMap = new Map(actual.skills.map((s) => [s.nombre, s]));
119
+
120
+ const drifted = [];
121
+ const nuevos = [];
122
+ const faltantes = [];
123
+
124
+ for (const s of actual.skills) {
125
+ const prev = anteriorMap.get(s.nombre);
126
+ if (!prev) {
127
+ nuevos.push(s.nombre);
128
+ } else if (prev.hash !== s.hash) {
129
+ drifted.push({ nombre: s.nombre, antes: prev.hash, ahora: s.hash });
130
+ }
131
+ }
132
+ for (const s of anterior.skills) {
133
+ if (!actualMap.has(s.nombre)) {
134
+ faltantes.push(s.nombre);
135
+ }
136
+ }
137
+
138
+ return { drifted, faltantes, nuevos };
139
+ }
140
+
141
+ function main() {
142
+ const actual = generarLock();
143
+
144
+ if (CHECK) {
145
+ if (!fs.existsSync(LOCK_FILE)) {
146
+ console.error(`ERROR: ${LOCK_FILE} no existe. Ejecutar sin --check para generarlo.`);
147
+ process.exit(1);
148
+ }
149
+ const anterior = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
150
+ const diff = comparar(actual, anterior);
151
+ const total = diff.drifted.length + diff.faltantes.length + diff.nuevos.length;
152
+
153
+ if (JSON_OUT) {
154
+ console.log(JSON.stringify({ ok: total === 0, ...diff }, null, 2));
155
+ } else if (total === 0) {
156
+ console.log(`✓ skills-lock OK: ${actual.skillsCount} skills sin drift`);
157
+ } else {
158
+ console.log(`✗ skills-lock DRIFT detectado:`);
159
+ if (diff.drifted.length) {
160
+ console.log(` Modificados (${diff.drifted.length}):`);
161
+ diff.drifted.forEach((d) => console.log(` - ${d.nombre}`));
162
+ }
163
+ if (diff.nuevos.length) {
164
+ console.log(` Nuevos (${diff.nuevos.length}):`);
165
+ diff.nuevos.forEach((n) => console.log(` - ${n}`));
166
+ }
167
+ if (diff.faltantes.length) {
168
+ console.log(` Faltantes (${diff.faltantes.length}):`);
169
+ diff.faltantes.forEach((n) => console.log(` - ${n}`));
170
+ }
171
+ console.log(`\n Para regenerar: node scripts/generar-skills-lock.js`);
172
+ }
173
+ process.exit(total === 0 ? 0 : 1);
174
+ }
175
+
176
+ // Generar
177
+ atomicWriteSync(LOCK_FILE, JSON.stringify(actual, null, 2) + '\n', 'utf-8');
178
+ if (JSON_OUT) {
179
+ console.log(JSON.stringify({ ok: true, file: LOCK_FILE, count: actual.skillsCount }));
180
+ } else {
181
+ console.log(`✓ Generado ${LOCK_FILE}`);
182
+ console.log(` ${actual.skillsCount} skills, lockHash=${actual.lockHash.slice(0, 23)}...`);
183
+ }
184
+ }
185
+
186
+ if (require.main === module) {
187
+ main();
188
+ }
189
+
190
+ module.exports = { generarLock, comparar, main };
@@ -159,3 +159,9 @@ function main() {
159
159
  }
160
160
 
161
161
  if (require.main === module) main();
162
+
163
+ // Exporta main para que el wrapper del bin CLI
164
+ // (`scripts/cli/inbox-tmux-inject.js`) pueda invocarla sin depender del
165
+ // guard `require.main === module`, que solo es true cuando el script se
166
+ // ejecuta como punto de entrada directo (no cuando es require()d).
167
+ module.exports = { main };
@@ -72,6 +72,55 @@ function _resolverUsuario(username, user) {
72
72
  return (raw && _RX_USUARIO_SEGURO.test(raw)) ? raw : '$env:USERNAME';
73
73
  }
74
74
 
75
+ /**
76
+ * Construye el script de PowerShell que registra el Scheduled Task.
77
+ *
78
+ * Pura — no ejecuta nada, solo retorna el string del script. Esto permite
79
+ * testear el contenido sin invocar PowerShell real, y aísla las invariantes
80
+ * críticas del script (presencia de `-UserId` en `New-ScheduledTaskPrincipal`,
81
+ * orden de parámetros, valores escapados).
82
+ *
83
+ * Invariantes obligatorias:
84
+ * - `New-ScheduledTaskPrincipal` DEBE incluir `-UserId` cuando se usa
85
+ * `-LogonType Interactive`. PowerShell rechaza la llamada con
86
+ * ParameterBindingException si falta. Bug histórico 2026-05-11.
87
+ * - `nodePath` y `daemonPath` se escapan duplicando comillas simples
88
+ * (convención PowerShell para strings literales).
89
+ * - El nombre de usuario se resuelve y valida con `_resolverUsuario()`,
90
+ * que cae a `$env:USERNAME` si el valor tiene caracteres peligrosos.
91
+ *
92
+ * @param {string} nodeExe — path absoluto a node.exe
93
+ * @param {string} rutaDaemon — path absoluto al daemon JS
94
+ * @returns {string} script de PowerShell listo para `_ejecutarPowerShell`
95
+ */
96
+ function _construirScriptPS(nodeExe, rutaDaemon) {
97
+ const nodePath = String(nodeExe).replace(/'/g, "''");
98
+ const daemonPath = String(rutaDaemon).replace(/'/g, "''");
99
+ // REM-3: validar USERNAME antes de interpolarlo. _resolverUsuario rechaza
100
+ // caracteres peligrosos y cae a '$env:USERNAME' si el valor es sospechoso.
101
+ const usuario = _resolverUsuario();
102
+
103
+ return `
104
+ $action = New-ScheduledTaskAction -Execute '${nodePath}' -Argument '"${daemonPath}"'
105
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User '${usuario}'
106
+ $settings = New-ScheduledTaskSettingsSet \`
107
+ -StartWhenAvailable \`
108
+ -RestartCount 3 \`
109
+ -RestartInterval (New-TimeSpan -Minutes 2) \`
110
+ -AllowStartIfOnBatteries \`
111
+ -DontStopIfGoingOnBatteries \`
112
+ -ExecutionTimeLimit ([TimeSpan]::Zero)
113
+ $principal = New-ScheduledTaskPrincipal -UserId '${usuario}' -LogonType Interactive -RunLevel Limited
114
+ Register-ScheduledTask \`
115
+ -TaskName '${TASK_NAME}' \`
116
+ -Action $action \`
117
+ -Trigger $trigger \`
118
+ -Settings $settings \`
119
+ -Principal $principal \`
120
+ -Force
121
+ `.trim();
122
+ }
123
+
75
124
  /**
76
125
  * Construye la ruta absoluta al daemon bin/swl-telegram-bot.js
77
126
  * a partir de __dirname (scripts/lib/), NO hardcodeada.
@@ -178,33 +227,7 @@ function install() {
178
227
 
179
228
  // Construir el script de PowerShell con parámetros en variables separadas
180
229
  // Nunca concatenar dentro del script — los valores ya están en variables escapadas
181
- const nodePath = nodeExe.replace(/'/g, "''"); // Escape de comillas simples en PS
182
- const daemonPath = rutaDaemon.replace(/'/g, "''");
183
-
184
- // REM-3: validar USERNAME antes de interpolarlo en el script de PowerShell.
185
- // _resolverUsuario usa un regex para rechazar caracteres peligrosos y cae a
186
- // '$env:USERNAME' si el valor es sospechoso.
187
- const usuario = _resolverUsuario();
188
-
189
- const psScript = `
190
- $action = New-ScheduledTaskAction -Execute '${nodePath}' -Argument '"${daemonPath}"'
191
- $trigger = New-ScheduledTaskTrigger -AtLogOn -User '${usuario}'
192
- $settings = New-ScheduledTaskSettingsSet \`
193
- -StartWhenAvailable \`
194
- -RestartCount 3 \`
195
- -RestartInterval (New-TimeSpan -Minutes 2) \`
196
- -AllowStartIfOnBatteries \`
197
- -DontStopIfGoingOnBatteries \`
198
- -ExecutionTimeLimit ([TimeSpan]::Zero)
199
- $principal = New-ScheduledTaskPrincipal -LogonType Interactive -RunLevel Limited
200
- Register-ScheduledTask \`
201
- -TaskName '${TASK_NAME}' \`
202
- -Action $action \`
203
- -Trigger $trigger \`
204
- -Settings $settings \`
205
- -Principal $principal \`
206
- -Force
207
- `.trim();
230
+ const psScript = _construirScriptPS(nodeExe, rutaDaemon);
208
231
 
209
232
  _log('INFO', 'Registrando Scheduled Task vía PowerShell (RunLevel: Limited, sin elevación)...');
210
233
  const resultado = _ejecutarPowerShell(psScript);
@@ -304,4 +327,4 @@ if ($task) { $task.State } else { 'NOT_FOUND' }
304
327
  return { instalado: true, estado: estadoNorm };
305
328
  }
306
329
 
307
- module.exports = { install, uninstall, status, _resolverUsuario };
330
+ module.exports = { install, uninstall, status, _resolverUsuario, _construirScriptPS };
@@ -187,13 +187,15 @@ function buscar(cwd, query) {
187
187
  // Exports
188
188
  // ---------------------------------------------------------------------------
189
189
 
190
- module.exports = { descubrir, buscar, cargarIndice, escanear };
191
-
192
190
  // ---------------------------------------------------------------------------
193
191
  // CLI directo
194
192
  // ---------------------------------------------------------------------------
195
193
 
196
- if (require.main === module) {
194
+ // Extraído a función nombrada para que el wrapper del bin CLI
195
+ // (`scripts/cli/skill-discovery.js`) pueda invocarla sin depender del guard
196
+ // `require.main === module`, que solo se cumple cuando el script se ejecuta
197
+ // como punto de entrada directo (no cuando es require()d desde otro módulo).
198
+ function main() {
197
199
  const cwd = process.cwd();
198
200
  const args = process.argv.slice(2);
199
201
 
@@ -232,3 +234,9 @@ if (require.main === module) {
232
234
  process.stdout.write(`Uso: Skill("nombre-de-habilidad")\n\n`);
233
235
  }
234
236
  }
237
+
238
+ if (require.main === module) {
239
+ main();
240
+ }
241
+
242
+ module.exports = { descubrir, buscar, cargarIndice, escanear, main };
@@ -286,4 +286,4 @@ function main() {
286
286
 
287
287
  if (require.main === module) main();
288
288
 
289
- module.exports = { verificarArchivo, extraerFrontmatter, leerCampo, obtenerVersionEnHEAD };
289
+ module.exports = { verificarArchivo, extraerFrontmatter, leerCampo, obtenerVersionEnHEAD, main };