sdd-es 2.0.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 (101) hide show
  1. package/.claude/settings.json +51 -0
  2. package/.claude-plugin/marketplace.json +31 -0
  3. package/.claude-plugin/plugin.json +97 -0
  4. package/README.md +332 -0
  5. package/agents/arquitecto.md +148 -0
  6. package/agents/asesor-datos.md +163 -0
  7. package/agents/critico.md +142 -0
  8. package/agents/desarrollador-backend.md +242 -0
  9. package/agents/desarrollador-frontend.md +120 -0
  10. package/agents/disenador-api.md +108 -0
  11. package/agents/documentador.md +177 -0
  12. package/agents/investigador.md +174 -0
  13. package/agents/operaciones.md +105 -0
  14. package/agents/revisor.md +153 -0
  15. package/agents/seguridad.md +216 -0
  16. package/agents/tester.md +286 -0
  17. package/claude-hooks/post-write-conventions.js +412 -0
  18. package/claude-hooks/pre-tool-guard.js +159 -0
  19. package/cli/index.js +401 -0
  20. package/commands/sdd.aclarar.md +200 -0
  21. package/commands/sdd.analizar.md +241 -0
  22. package/commands/sdd.ayuda.md +227 -0
  23. package/commands/sdd.canary.md +60 -0
  24. package/commands/sdd.checklist.md +174 -0
  25. package/commands/sdd.comprimir.md +166 -0
  26. package/commands/sdd.configurar.md +195 -0
  27. package/commands/sdd.constitucion.md +343 -0
  28. package/commands/sdd.crear-app.md +168 -0
  29. package/commands/sdd.crear-mcp.md +174 -0
  30. package/commands/sdd.descubrir.md +269 -0
  31. package/commands/sdd.desplegar.md +155 -0
  32. package/commands/sdd.especificar.md +302 -0
  33. package/commands/sdd.estado.md +124 -0
  34. package/commands/sdd.glosario.md +108 -0
  35. package/commands/sdd.implementar.md +377 -0
  36. package/commands/sdd.importar.md +91 -0
  37. package/commands/sdd.mapear.md +120 -0
  38. package/commands/sdd.md +119 -0
  39. package/commands/sdd.planificar.md +372 -0
  40. package/commands/sdd.qa.md +108 -0
  41. package/commands/sdd.release.md +253 -0
  42. package/commands/sdd.retro.md +82 -0
  43. package/commands/sdd.snapshot.md +122 -0
  44. package/commands/sdd.tareas.md +300 -0
  45. package/commands/sdd.verificar.md +239 -0
  46. package/configuracion-ejemplo/hooks-ejemplo/antes_cada_tarea.sh +18 -0
  47. package/configuracion-ejemplo/hooks-ejemplo/antes_implementar.sh +45 -0
  48. package/configuracion-ejemplo/hooks-ejemplo/despues_especificar.sh +14 -0
  49. package/configuracion-ejemplo/hooks-ejemplo/despues_implementar.sh +36 -0
  50. package/configuracion-ejemplo/hooks-ejemplo/despues_planificar.sh +19 -0
  51. package/configuracion-ejemplo/hooks-ejemplo/guardia-seguridad.sh +367 -0
  52. package/configuracion-ejemplo/sdd.config.yaml +310 -0
  53. package/docs/AGENTES.md +74 -0
  54. package/docs/COMPRESION.md +155 -0
  55. package/docs/EJEMPLO-PRACTICA.md +383 -0
  56. package/docs/EJEMPLOS.md +212 -0
  57. package/docs/FABRICA.md +185 -0
  58. package/docs/FILOSOFIA.md +61 -0
  59. package/docs/FLUJO.md +149 -0
  60. package/docs/INICIO-RAPIDO.md +116 -0
  61. package/docs/MAPAS.md +113 -0
  62. package/docs/MODELOS.md +103 -0
  63. package/docs/PERSONALIZACION.md +152 -0
  64. package/instalar.ps1 +39 -0
  65. package/instalar.sh +22 -0
  66. package/mcp-figma/README.md +158 -0
  67. package/mcp-figma/package.json +7 -0
  68. package/mcp-figma/src/component-generator.js +162 -0
  69. package/mcp-figma/src/design-system-analyzer.js +247 -0
  70. package/mcp-figma/src/figma-client.js +75 -0
  71. package/mcp-figma/src/index.js +114 -0
  72. package/mcp-figma/src/mcp.js +97 -0
  73. package/mcp-figma/src/style-mapper.js +85 -0
  74. package/package.json +50 -0
  75. package/plantillas/analisis.md +57 -0
  76. package/plantillas/checklist-especificacion.md +66 -0
  77. package/plantillas/constitucion.md +104 -0
  78. package/plantillas/decision-arquitectura.md +39 -0
  79. package/plantillas/dependencias-mapa.md +89 -0
  80. package/plantillas/especificacion.md +108 -0
  81. package/plantillas/estructura-mapa.md +40 -0
  82. package/plantillas/glosario.md +22 -0
  83. package/plantillas/index-especificaciones.md +15 -0
  84. package/plantillas/mcp-server.md +147 -0
  85. package/plantillas/plan.md +152 -0
  86. package/plantillas/simbolos-mapa.md +57 -0
  87. package/plantillas/snapshot.md +54 -0
  88. package/plantillas/tareas.md +72 -0
  89. package/presets/enterprise.yaml +69 -0
  90. package/presets/lean.yaml +63 -0
  91. package/presets/startup.yaml +67 -0
  92. package/skills/compresion-tokens.md +264 -0
  93. package/skills/constitucion-constraint.md +78 -0
  94. package/skills/deteccion-stack.md +175 -0
  95. package/skills/enrutador-agentes.md +69 -0
  96. package/skills/gestion-estado.md +114 -0
  97. package/skills/indexador.md +199 -0
  98. package/skills/modo-guiado/SKILL.md +78 -0
  99. package/skills/orquestacion-ptc/SKILL.md +96 -0
  100. package/skills/validacion-spec.md +52 -0
  101. package/skills/verificador-implementacion.md +71 -0
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * post-write-conventions.js — Hook PostToolUse global de Claude Code
4
+ *
5
+ * Se activa después de Write, Edit, o MultiEdit sobre cualquier archivo.
6
+ * Detecta las convenciones del proyecto (dinámicamente + desde la constitución
7
+ * SDD-ES si existe) y valida el archivo recién modificado contra ellas.
8
+ *
9
+ * Resultado:
10
+ * - Violación bloqueante → exit 2 + mensaje claro
11
+ * - Sugerencia → exit 0 + mensaje a stderr (Claude lo ve y puede corregir)
12
+ * - OK → exit 0 silencioso
13
+ */
14
+
15
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
16
+ import { extname, basename, dirname, join, resolve } from "node:path";
17
+ import { createInterface } from "node:readline";
18
+
19
+ // ── Leer evento desde stdin ────────────────────────────────────────────────
20
+ const rl = createInterface({ input: process.stdin, terminal: false });
21
+ let raw = "";
22
+ rl.on("line", (l) => (raw += l + "\n"));
23
+ rl.on("close", () => main(raw.trim()));
24
+
25
+ // ── Helpers ────────────────────────────────────────────────────────────────
26
+
27
+ function tryRead(path) {
28
+ try { return readFileSync(path, "utf8"); } catch { return ""; }
29
+ }
30
+
31
+ function tryJSON(path) {
32
+ try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
33
+ }
34
+
35
+ function findUp(filename, from) {
36
+ let dir = resolve(from);
37
+ for (let i = 0; i < 8; i++) {
38
+ const candidate = join(dir, filename);
39
+ if (existsSync(candidate)) return candidate;
40
+ const parent = dirname(dir);
41
+ if (parent === dir) break;
42
+ dir = parent;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function findWorkspaceRoot(startFile) {
48
+ const dir = dirname(resolve(startFile));
49
+ const markers = ["package.json","pyproject.toml","Cargo.toml","go.mod","pom.xml",".git",".sdd"];
50
+ for (const m of markers) {
51
+ const p = findUp(m, dir);
52
+ if (p) return dirname(p);
53
+ }
54
+ return dir;
55
+ }
56
+
57
+ // ── Detección dinámica de convenciones ─────────────────────────────────────
58
+
59
+ function detectConventions(root, ext) {
60
+ const conv = {
61
+ // Naming
62
+ namingStyle: null, // "camelCase" | "snake_case" | "PascalCase" | "kebab-case"
63
+ // Indentación
64
+ indentChar: null, // "space" | "tab"
65
+ indentSize: null, // 2 | 4
66
+ // Comillas
67
+ quoteStyle: null, // "single" | "double"
68
+ // Punto y coma
69
+ semicolons: null, // true | false
70
+ // Longitud máxima de función (desde constitución)
71
+ maxFunctionLines: null,
72
+ // Cobertura mínima (desde constitución)
73
+ minCoverage: null,
74
+ // Prohibidos (desde constitución)
75
+ prohibitedPatterns: [],
76
+ // Fuente de detección
77
+ sources: [],
78
+ };
79
+
80
+ // ── 1. Desde constitución SDD-ES ────────────────────────────────────────
81
+ const constitucionPath = join(root, ".sdd", "memoria", "constitucion.md");
82
+ const constitucion = tryRead(constitucionPath);
83
+ if (constitucion) {
84
+ conv.sources.push("constitucion.md");
85
+
86
+ const maxFn = constitucion.match(/longitud[_\s-]*función[_\s-]*máxima[:\s]+(\d+)/i)
87
+ || constitucion.match(/max[_\s-]*function[_\s-]*lines?[:\s]+(\d+)/i)
88
+ || constitucion.match(/funciones?\s*[≤<=]\s*(\d+)\s*líneas/i);
89
+ if (maxFn) conv.maxFunctionLines = parseInt(maxFn[1]);
90
+
91
+ const cov = constitucion.match(/cobertura[_\s-]*(?:mínima|tests?)[:\s]+(\d+)/i);
92
+ if (cov) conv.minCoverage = parseInt(cov[1]);
93
+
94
+ // Patrones prohibidos declarados en constitución
95
+ const prohibited = [...constitucion.matchAll(/(?:NUNCA|PROHIBIDO|NO\s+USAR)[:\s]+`([^`]+)`/gi)];
96
+ conv.prohibitedPatterns = prohibited.map(m => m[1].trim());
97
+ }
98
+
99
+ // ── 2. Desde sdd.config.yaml ────────────────────────────────────────────
100
+ const configPath = join(root, ".sdd", "sdd.config.yaml");
101
+ const config = tryRead(configPath);
102
+ if (config) {
103
+ conv.sources.push("sdd.config.yaml");
104
+ const maxFn = config.match(/longitud_funcion_maxima:\s*(\d+)/);
105
+ if (maxFn && !conv.maxFunctionLines) conv.maxFunctionLines = parseInt(maxFn[1]);
106
+ const cov = config.match(/cobertura_tests_minima:\s*(\d+)/);
107
+ if (cov && !conv.minCoverage) conv.minCoverage = parseInt(cov[1]);
108
+ }
109
+
110
+ // ── 3. Desde ESLint ─────────────────────────────────────────────────────
111
+ const eslintFiles = [".eslintrc.json",".eslintrc.js",".eslintrc.yml",".eslintrc",
112
+ "eslint.config.js","eslint.config.mjs"];
113
+ for (const f of eslintFiles) {
114
+ const p = join(root, f);
115
+ if (!existsSync(p)) continue;
116
+ const content = tryRead(p);
117
+ conv.sources.push(f);
118
+ if (content.includes('"quotes"') || content.includes("'quotes'")) {
119
+ conv.quoteStyle = content.includes('"single"') || content.includes("'single'")
120
+ ? "single" : "double";
121
+ }
122
+ if (content.includes('"semi"') || content.includes("'semi'")) {
123
+ conv.semicolons = !content.includes('"never"') && !content.includes("'never'");
124
+ }
125
+ break;
126
+ }
127
+
128
+ // ── 4. Desde Prettier ───────────────────────────────────────────────────
129
+ const prettierFiles = [".prettierrc",".prettierrc.json",".prettierrc.js",
130
+ ".prettierrc.yml","prettier.config.js","prettier.config.mjs"];
131
+ for (const f of prettierFiles) {
132
+ const p = join(root, f);
133
+ if (!existsSync(p)) continue;
134
+ const content = tryRead(p);
135
+ conv.sources.push(f);
136
+ if (content.includes("singleQuote")) {
137
+ conv.quoteStyle = content.includes("true") ? "single" : "double";
138
+ }
139
+ if (content.includes("semi")) {
140
+ conv.semicolons = !content.includes('"semi": false') && !content.includes("semi: false");
141
+ }
142
+ if (content.includes("tabWidth")) {
143
+ const m = content.match(/tabWidth["\s:]+(\d)/);
144
+ if (m) conv.indentSize = parseInt(m[1]);
145
+ }
146
+ if (content.includes("useTabs")) {
147
+ conv.indentChar = content.includes("true") ? "tab" : "space";
148
+ }
149
+ break;
150
+ }
151
+
152
+ // ── 5. Desde .editorconfig ──────────────────────────────────────────────
153
+ const editorconfig = tryRead(join(root, ".editorconfig"));
154
+ if (editorconfig) {
155
+ conv.sources.push(".editorconfig");
156
+ if (!conv.indentChar) {
157
+ if (editorconfig.includes("indent_style = tab")) conv.indentChar = "tab";
158
+ if (editorconfig.includes("indent_style = space")) conv.indentChar = "space";
159
+ }
160
+ if (!conv.indentSize) {
161
+ const m = editorconfig.match(/indent_size\s*=\s*(\d)/);
162
+ if (m) conv.indentSize = parseInt(m[1]);
163
+ }
164
+ }
165
+
166
+ // ── 6. Desde tsconfig.json ──────────────────────────────────────────────
167
+ if ([".ts",".tsx"].includes(ext)) {
168
+ const tsconfig = tryJSON(join(root, "tsconfig.json"));
169
+ if (tsconfig) {
170
+ conv.sources.push("tsconfig.json");
171
+ // strict mode implica tipos estrictos obligatorios
172
+ if (tsconfig.compilerOptions?.strict) conv.strictTypes = true;
173
+ if (tsconfig.compilerOptions?.noImplicitAny) conv.noImplicitAny = true;
174
+ }
175
+ }
176
+
177
+ // ── 7. Desde ruff.toml (Python) ─────────────────────────────────────────
178
+ if (ext === ".py") {
179
+ const ruff = tryRead(join(root, "ruff.toml")) || tryRead(join(root, ".ruff.toml"));
180
+ if (ruff) {
181
+ conv.sources.push("ruff.toml");
182
+ const lineLen = ruff.match(/line-length\s*=\s*(\d+)/);
183
+ if (lineLen) conv.maxLineLength = parseInt(lineLen[1]);
184
+ }
185
+ }
186
+
187
+ // ── 8. Inferir desde código existente (si nada detectado aún) ───────────
188
+ if (!conv.namingStyle || !conv.indentChar) {
189
+ inferFromExistingCode(root, ext, conv);
190
+ }
191
+
192
+ return conv;
193
+ }
194
+
195
+ function inferFromExistingCode(root, ext, conv) {
196
+ // Busca hasta 3 archivos del mismo tipo para inferir patrones
197
+ const candidates = [];
198
+ try {
199
+ const dirs = ["src","lib","app","pkg","internal"];
200
+ for (const d of dirs) {
201
+ const dp = join(root, d);
202
+ if (!existsSync(dp)) continue;
203
+ const files = readdirSync(dp).filter(f => f.endsWith(ext)).slice(0, 2);
204
+ candidates.push(...files.map(f => join(dp, f)));
205
+ if (candidates.length >= 3) break;
206
+ }
207
+ } catch { /* sin acceso */ }
208
+
209
+ for (const f of candidates.slice(0, 3)) {
210
+ const content = tryRead(f);
211
+ if (!content) continue;
212
+
213
+ // Indentación: mirar primeras líneas indentadas
214
+ if (!conv.indentChar) {
215
+ const lines = content.split("\n").slice(0, 30);
216
+ const tabLine = lines.find(l => l.startsWith("\t"));
217
+ const spaceLine = lines.find(l => l.match(/^ {2,}/));
218
+ if (tabLine && !spaceLine) conv.indentChar = "tab";
219
+ else if (spaceLine && !tabLine) {
220
+ conv.indentChar = "space";
221
+ const m = spaceLine.match(/^( +)/);
222
+ if (m) conv.indentSize = m[1].length === 2 ? 2 : 4;
223
+ }
224
+ }
225
+
226
+ // Comillas JS/TS
227
+ if (!conv.quoteStyle && [".js",".ts",".jsx",".tsx"].includes(ext)) {
228
+ const singleCount = (content.match(/'/g) || []).length;
229
+ const doubleCount = (content.match(/"/g) || []).length;
230
+ if (singleCount > doubleCount * 1.5) conv.quoteStyle = "single";
231
+ else if (doubleCount > singleCount * 1.5) conv.quoteStyle = "double";
232
+ }
233
+
234
+ conv.sources.push(`inferido de ${basename(f)}`);
235
+ break;
236
+ }
237
+ }
238
+
239
+ // ── Validación del archivo ─────────────────────────────────────────────────
240
+
241
+ function validate(filePath, content, conv, ext) {
242
+ const errors = []; // bloqueantes
243
+ const warnings = []; // sugerencias
244
+
245
+ const lines = content.split("\n");
246
+ const name = basename(filePath);
247
+
248
+ // ── Indentación ─────────────────────────────────────────────────────────
249
+ if (conv.indentChar === "tab") {
250
+ const spacedLines = lines.filter((l, i) => i < 100 && /^ {2,}/.test(l) && !/^\s*\*/.test(l));
251
+ if (spacedLines.length > 3) {
252
+ errors.push(`indentación: el proyecto usa TABS pero se encontraron ${spacedLines.length} líneas con espacios`);
253
+ }
254
+ } else if (conv.indentChar === "space") {
255
+ const tabbedLines = lines.filter((l, i) => i < 100 && l.startsWith("\t"));
256
+ if (tabbedLines.length > 3) {
257
+ errors.push(`indentación: el proyecto usa ESPACIOS pero se encontraron ${tabbedLines.length} líneas con tabs`);
258
+ }
259
+ }
260
+
261
+ // ── Comillas JS/TS ──────────────────────────────────────────────────────
262
+ if (conv.quoteStyle && [".js",".ts",".jsx",".tsx",".mjs",".cjs"].includes(ext)) {
263
+ const wrong = conv.quoteStyle === "single"
264
+ ? lines.filter((l,i) => i < 200 && /"[^"]*"/.test(l) && !l.includes("//") && !l.trim().startsWith("*")).length
265
+ : lines.filter((l,i) => i < 200 && /'[^']*'/.test(l) && !l.includes("//") && !l.trim().startsWith("*")).test;
266
+ if (wrong > 5) {
267
+ const expected = conv.quoteStyle === "single" ? "comillas simples" : "comillas dobles";
268
+ warnings.push(`comillas: el proyecto usa ${expected} pero se encontraron ${wrong} líneas con el estilo opuesto`);
269
+ }
270
+ }
271
+
272
+ // ── Longitud de funciones ───────────────────────────────────────────────
273
+ if (conv.maxFunctionLines) {
274
+ const fnStarts = [];
275
+ lines.forEach((l, i) => {
276
+ if (/^\s*(export\s+)?(async\s+)?function\s+\w|=>\s*\{|^\s*(public|private|protected|async)\s+\w+\s*\(/.test(l)) {
277
+ fnStarts.push(i);
278
+ }
279
+ });
280
+
281
+ for (const start of fnStarts.slice(0, 20)) {
282
+ let depth = 0, end = start;
283
+ for (let i = start; i < Math.min(start + conv.maxFunctionLines * 2, lines.length); i++) {
284
+ depth += (lines[i].match(/\{/g) || []).length;
285
+ depth -= (lines[i].match(/\}/g) || []).length;
286
+ if (depth <= 0 && i > start) { end = i; break; }
287
+ }
288
+ const len = end - start;
289
+ if (len > conv.maxFunctionLines) {
290
+ warnings.push(`función en línea ${start + 1}: ${len} líneas > límite de ${conv.maxFunctionLines} (constitución)`);
291
+ }
292
+ }
293
+ }
294
+
295
+ // ── Patrones prohibidos (desde constitución) ────────────────────────────
296
+ for (const pattern of conv.prohibitedPatterns) {
297
+ if (content.includes(pattern)) {
298
+ errors.push(`patrón prohibido en constitución: \`${pattern}\` encontrado en el archivo`);
299
+ }
300
+ }
301
+
302
+ // ── console.log / print de debug en archivos no-test ───────────────────
303
+ const isTest = /\.(test|spec)\.|__tests__|test_/.test(filePath);
304
+ if (!isTest) {
305
+ if ([".ts",".js",".tsx",".jsx"].includes(ext)) {
306
+ const debugLines = lines.filter((l,i) => /console\.(log|warn|error|debug|info)\(/.test(l) && !l.trim().startsWith("//")).length;
307
+ if (debugLines > 2) {
308
+ warnings.push(`${debugLines} llamadas a console.log/warn/error en archivo no-test — ¿son logs de producción o debug olvidado?`);
309
+ }
310
+ }
311
+ if (ext === ".py") {
312
+ const printLines = lines.filter(l => /^\s*print\s*\(/.test(l) && !l.trim().startsWith("#")).length;
313
+ if (printLines > 2) {
314
+ warnings.push(`${printLines} llamadas a print() en archivo no-test — usa logging en lugar de print en producción`);
315
+ }
316
+ }
317
+ }
318
+
319
+ // ── TypeScript: any implícito ───────────────────────────────────────────
320
+ if (conv.noImplicitAny || conv.strictTypes) {
321
+ if ([".ts",".tsx"].includes(ext)) {
322
+ const anyCount = lines.filter(l => /:\s*any\b/.test(l) && !l.trim().startsWith("//")).length;
323
+ if (anyCount > 0) {
324
+ warnings.push(`${anyCount} usos de \`any\` en TypeScript estricto — reemplaza con tipos concretos`);
325
+ }
326
+ }
327
+ }
328
+
329
+ // ── Secretos hardcodeados ───────────────────────────────────────────────
330
+ const secretPatterns = [
331
+ { re: /password\s*=\s*['"][^'"]{4,}/i, label: "password hardcodeado" },
332
+ { re: /secret\s*=\s*['"][^'"]{8,}/i, label: "secret hardcodeado" },
333
+ { re: /api[_-]?key\s*=\s*['"][^'"]{8,}/i, label: "API key hardcodeada" },
334
+ { re: /sk-[a-zA-Z0-9]{20,}/, label: "OpenAI key" },
335
+ { re: /ghp_[a-zA-Z0-9]{36}/, label: "GitHub PAT" },
336
+ { re: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
337
+ { re: /BEGIN (RSA|EC|OPENSSH) PRIVATE KEY/, label: "clave privada" },
338
+ ];
339
+ for (const { re, label } of secretPatterns) {
340
+ if (re.test(content)) {
341
+ errors.push(`${label} detectado en el archivo — usa variables de entorno`);
342
+ }
343
+ }
344
+
345
+ // ── Archivos de test: verificar que tienen al menos una aserción ─────────
346
+ if (isTest) {
347
+ const hasAssertion = /expect\(|assert\.|assertEquals|pytest\.raises|#\[test\]|func Test/.test(content);
348
+ if (!hasAssertion) {
349
+ warnings.push("archivo de test sin aserciones detectadas — ¿el test verifica algo real?");
350
+ }
351
+ }
352
+
353
+ return { errors, warnings };
354
+ }
355
+
356
+ // ── Main ───────────────────────────────────────────────────────────────────
357
+
358
+ function main(raw) {
359
+ let event;
360
+ try { event = JSON.parse(raw); } catch { process.exit(0); }
361
+
362
+ const toolName = event?.tool_name ?? "";
363
+ if (!["Write", "Edit", "MultiEdit"].includes(toolName)) process.exit(0);
364
+
365
+ // Extraer ruta del archivo
366
+ const filePath = event?.tool_input?.file_path
367
+ ?? event?.tool_input?.path
368
+ ?? event?.tool_response?.file_path
369
+ ?? "";
370
+
371
+ if (!filePath) process.exit(0);
372
+
373
+ const ext = extname(filePath).toLowerCase();
374
+
375
+ // Solo validar archivos de código fuente
376
+ const CODE_EXTS = new Set([
377
+ ".ts",".tsx",".js",".jsx",".mjs",".cjs",
378
+ ".py",".rs",".go",".java",".kt",".cs",
379
+ ".rb",".php",".swift",".cpp",".c",".h",
380
+ ]);
381
+ if (!CODE_EXTS.has(ext)) process.exit(0);
382
+
383
+ const content = tryRead(filePath);
384
+ if (!content || content.length < 10) process.exit(0);
385
+
386
+ const root = findWorkspaceRoot(filePath);
387
+ const conv = detectConventions(root, ext);
388
+ const { errors, warnings } = validate(filePath, content, conv, ext);
389
+
390
+ if (errors.length === 0 && warnings.length === 0) process.exit(0);
391
+
392
+ const relPath = filePath.replace(root, "").replace(/^[/\\]/, "");
393
+ const sources = conv.sources.length ? `(fuentes: ${conv.sources.slice(0,3).join(", ")})` : "";
394
+
395
+ let msg = `\n── Validación de convenciones: ${relPath} ${sources}\n`;
396
+
397
+ if (errors.length > 0) {
398
+ msg += `\n🔴 VIOLACIONES BLOQUEANTES (corregir antes de continuar):\n`;
399
+ errors.forEach((e, i) => { msg += ` ${i+1}. ${e}\n`; });
400
+ }
401
+
402
+ if (warnings.length > 0) {
403
+ msg += `\n🟡 SUGERENCIAS (revisar antes del merge):\n`;
404
+ warnings.forEach((w, i) => { msg += ` ${i+1}. ${w}\n`; });
405
+ }
406
+
407
+ msg += "\n";
408
+ process.stderr.write(msg);
409
+
410
+ // Errores → bloquear; solo warnings → dejar pasar (Claude los ve y puede corregir)
411
+ process.exit(errors.length > 0 ? 2 : 0);
412
+ }
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pre-tool-guard.js — Hook PreToolUse global de Claude Code
4
+ *
5
+ * Bloquea operaciones destructivas o fuera del workspace antes
6
+ * de que Claude Code las ejecute. Se registra en settings.json
7
+ * bajo "hooks.PreToolUse".
8
+ *
9
+ * Protocolo de hooks de Claude Code:
10
+ * - Lee el evento JSON desde stdin
11
+ * - Exit 0 → permitir
12
+ * - Exit 2 → bloquear (Claude Code muestra el stderr al usuario)
13
+ * - Stderr → mensaje de error mostrado al usuario
14
+ */
15
+
16
+ import { createInterface } from "node:readline";
17
+
18
+ const rl = createInterface({ input: process.stdin, terminal: false });
19
+ let raw = "";
20
+ rl.on("line", (l) => (raw += l + "\n"));
21
+ rl.on("close", () => main(raw.trim()));
22
+
23
+ // ── Comandos prohibidos — bloqueo duro, sin confirmación ───────────────────
24
+ const PROHIBIDOS = [
25
+ // Eliminación masiva
26
+ /rm\s+-rf?\s+\/(?!\w)/, // rm -rf / (raíz)
27
+ /rm\s+-rf?\s+~\b/, // rm -rf ~
28
+ /rm\s+-rf?\s+\.\s*$/, // rm -rf . (cwd)
29
+ /rm\s+-rf?\s+\.\./, // rm -rf ..
30
+ /Remove-Item\s+.*-Recurse.*-Force\s+[/\\]/, // PowerShell rm raíz
31
+
32
+ // Git destructivo remoto
33
+ /git\s+push\s+.*--force(?!-with-lease)/, // push --force (no --force-with-lease)
34
+ /git\s+push\s+-f\b/,
35
+ /git\s+push\s+\w+\s+:\w+/, // git push origin :rama (borrar rama remota)
36
+
37
+ // Git destructivo local irreversible
38
+ /git\s+reset\s+--hard/,
39
+ /git\s+clean\s+.*-[xf]{1,3}d/,
40
+ /git\s+reflog\s+expire/,
41
+ /git\s+gc\s+.*--prune=now/,
42
+
43
+ // Base de datos
44
+ /DROP\s+DATABASE\b/i,
45
+ /DROP\s+SCHEMA\b/i,
46
+
47
+ // Credenciales en git config
48
+ /git\s+config\s+.*password/i,
49
+ /git\s+config\s+.*credential.*store/i,
50
+
51
+ // Operaciones fuera del workspace — acceso a rutas de sistema
52
+ /rm\s+.*\/etc\//,
53
+ /rm\s+.*\/usr\//,
54
+ /rm\s+.*\/bin\//,
55
+ /rm\s+.*C:\\Windows\\/i,
56
+ /rm\s+.*C:\\Program Files\\/i,
57
+
58
+ // Exposición de secrets
59
+ /cat\s+.*\.env(?!\.example|\.template)/,
60
+ /type\s+.*\.env(?!\.example|\.template)/i, // Windows
61
+ /Get-Content\s+.*\.env(?!\.example|\.template)/i,
62
+ ];
63
+
64
+ // ── Operaciones que requieren confirmación explícita ───────────────────────
65
+ // (Para estas Claude Code debe preguntar al usuario — no las bloqueamos aquí
66
+ // porque el hook PreToolUse no puede hacer interacción. Las marcamos en stderr
67
+ // con un aviso y exit 0 para que Claude Code muestre la advertencia antes de pedir
68
+ // permiso al usuario mediante el flujo normal de permisos.)
69
+ const ADVERTENCIAS = [
70
+ { re: /git\s+push\b/, msg: "git push — sube código al remoto" },
71
+ { re: /git\s+merge\b/, msg: "git merge — modifica historial" },
72
+ { re: /git\s+rebase\b/, msg: "git rebase — reescribe historial" },
73
+ { re: /git\s+reset\b/, msg: "git reset — descarta cambios" },
74
+ { re: /git\s+branch\s+-D\b/, msg: "git branch -D — borra rama local" },
75
+ { re: /DROP\s+TABLE\b/i, msg: "DROP TABLE — elimina tabla de BD" },
76
+ { re: /DELETE\s+FROM\b/i, msg: "DELETE FROM — elimina filas de BD" },
77
+ { re: /TRUNCATE\b/i, msg: "TRUNCATE — vacía tabla de BD" },
78
+ { re: /npm\s+publish\b/, msg: "npm publish — publica al registro público" },
79
+ { re: /terraform\s+apply\b/, msg: "terraform apply — modifica infraestructura real" },
80
+ { re: /terraform\s+destroy\b/, msg: "terraform destroy — destruye infraestructura" },
81
+ { re: /kubectl\s+delete\b/, msg: "kubectl delete — elimina recursos de k8s" },
82
+ { re: /helm\s+uninstall\b/, msg: "helm uninstall — elimina release de k8s" },
83
+ ];
84
+
85
+ // ── Patrones de secrets hardcodeados ──────────────────────────────────────
86
+ // Detecta si el comando intenta escribir un secret literal en un archivo
87
+ const SECRET_PATTERNS = [
88
+ /password\s*=\s*['"][^'"]{4,}/i,
89
+ /secret\s*=\s*['"][^'"]{8,}/i,
90
+ /api[_-]?key\s*=\s*['"][^'"]{8,}/i,
91
+ /token\s*=\s*['"][^'"]{10,}/i,
92
+ /sk-[a-zA-Z0-9]{20,}/, // OpenAI key
93
+ /xox[baprs]-[0-9]{10,}/, // Slack token
94
+ /ghp_[a-zA-Z0-9]{36}/, // GitHub PAT
95
+ /AKIA[0-9A-Z]{16}/, // AWS Access Key
96
+ /BEGIN (RSA|EC|OPENSSH) PRIVATE KEY/,
97
+ ];
98
+
99
+ function main(raw) {
100
+ let event;
101
+ try {
102
+ event = JSON.parse(raw);
103
+ } catch {
104
+ // Si no podemos parsear, dejamos pasar (no bloquear por error del hook)
105
+ process.exit(0);
106
+ }
107
+
108
+ const toolName = event?.tool_name ?? "";
109
+ const toolInput = event?.tool_input ?? {};
110
+
111
+ // Solo nos importan Bash y PowerShell
112
+ if (toolName !== "Bash" && toolName !== "PowerShell") {
113
+ process.exit(0);
114
+ }
115
+
116
+ const cmd = String(toolInput?.command ?? "").trim();
117
+ if (!cmd) process.exit(0);
118
+
119
+ // ── 1. Verificar prohibidos ─────────────────────────────────────────────
120
+ for (const re of PROHIBIDOS) {
121
+ if (re.test(cmd)) {
122
+ process.stderr.write(
123
+ `🚫 BLOQUEADO por guardia de seguridad SDD-ES\n` +
124
+ ` Comando: ${cmd.slice(0, 120)}\n` +
125
+ ` Razón: coincide con patrón prohibido (${re.source.slice(0, 60)})\n` +
126
+ ` Esta operación nunca debe ejecutarse automáticamente.\n`
127
+ );
128
+ process.exit(2);
129
+ }
130
+ }
131
+
132
+ // ── 2. Verificar secrets hardcodeados en el comando ────────────────────
133
+ for (const re of SECRET_PATTERNS) {
134
+ if (re.test(cmd)) {
135
+ process.stderr.write(
136
+ `🚫 BLOQUEADO — secret hardcodeado detectado en el comando\n` +
137
+ ` Patrón: ${re.source.slice(0, 60)}\n` +
138
+ ` Usa variables de entorno o un secret manager.\n`
139
+ );
140
+ process.exit(2);
141
+ }
142
+ }
143
+
144
+ // ── 3. Advertencias (no bloquean, pero se loguean) ─────────────────────
145
+ for (const { re, msg } of ADVERTENCIAS) {
146
+ if (re.test(cmd)) {
147
+ // Escribir a stderr — Claude Code lo muestra como contexto antes de pedir permiso
148
+ process.stderr.write(
149
+ `⚠️ Operación sensible detectada: ${msg}\n` +
150
+ ` Comando: ${cmd.slice(0, 120)}\n`
151
+ );
152
+ // Exit 0 — dejamos que el flujo normal de permisos de Claude Code actúe
153
+ process.exit(0);
154
+ }
155
+ }
156
+
157
+ // Todo bien
158
+ process.exit(0);
159
+ }