@cristiancorreau/forge 2.1.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 (153) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/LICENSE +191 -0
  3. package/README.md +156 -0
  4. package/assets/adapters/claude-code/commands/deploy-check.md +12 -0
  5. package/assets/adapters/claude-code/commands/new-feature.md +11 -0
  6. package/assets/adapters/claude-code/commands/plan.md +116 -0
  7. package/assets/adapters/claude-code/commands/review.md +219 -0
  8. package/assets/adapters/claude-code/commands/session-close.md +109 -0
  9. package/assets/adapters/claude-code/commands/session-start.md +59 -0
  10. package/assets/adapters/claude-code/commands/ship.md +133 -0
  11. package/assets/adapters/claude-code/commands/wiki-ingest.md +7 -0
  12. package/assets/adapters/claude-code/commands/wiki-lint.md +5 -0
  13. package/assets/adapters/claude-code/commands/wiki-query.md +7 -0
  14. package/assets/adapters/claude-code/commands/work.md +101 -0
  15. package/assets/adapters/claude-code/generate-claude-md.py +304 -0
  16. package/assets/adapters/codex/commands/plan.md +63 -0
  17. package/assets/adapters/codex/commands/review.md +53 -0
  18. package/assets/adapters/codex/commands/session-close.md +53 -0
  19. package/assets/adapters/codex/commands/session-start.md +49 -0
  20. package/assets/adapters/codex/commands/ship.md +53 -0
  21. package/assets/adapters/codex/commands/work.md +53 -0
  22. package/assets/adapters/codex/generate-codex-config.py +269 -0
  23. package/assets/adapters/codex/hooks/codex.yaml.tpl +43 -0
  24. package/assets/adapters/codex/hooks/forge-codex-finish.sh +158 -0
  25. package/assets/adapters/codex/hooks/forge-codex-start.sh +186 -0
  26. package/assets/adapters/kiro/generate-steering.py +367 -0
  27. package/assets/adapters/opencode/HOOKS.md +123 -0
  28. package/assets/adapters/opencode/commands/plan.md +119 -0
  29. package/assets/adapters/opencode/commands/review.md +164 -0
  30. package/assets/adapters/opencode/commands/session-close.md +111 -0
  31. package/assets/adapters/opencode/commands/session-start.md +62 -0
  32. package/assets/adapters/opencode/commands/ship.md +135 -0
  33. package/assets/adapters/opencode/commands/work.md +82 -0
  34. package/assets/adapters/opencode/generate-agents-md.py +262 -0
  35. package/assets/core/agents/backend-engineer.md +61 -0
  36. package/assets/core/agents/compliance-reviewer.md +83 -0
  37. package/assets/core/agents/docs-writer.md +77 -0
  38. package/assets/core/agents/frontend-engineer.md +70 -0
  39. package/assets/core/agents/orchestrator.md +104 -0
  40. package/assets/core/agents/security-auditor.md +54 -0
  41. package/assets/core/agents/test-engineer.md +57 -0
  42. package/assets/core/hooks/hooks-registry.yaml +48 -0
  43. package/assets/core/hooks/post-turn-check.sh +139 -0
  44. package/assets/core/hooks/pre-bash-check.py +202 -0
  45. package/assets/core/hooks/pre-edit-check.py +317 -0
  46. package/assets/core/hooks/session-start.sh +184 -0
  47. package/assets/core/schemas/project.schema.json +503 -0
  48. package/assets/core/skills/README.md +88 -0
  49. package/assets/core/skills/aitmpl-search/SKILL.md +74 -0
  50. package/assets/core/skills/browser-test/SKILL.md +177 -0
  51. package/assets/core/skills/db-migrate/SKILL.md +163 -0
  52. package/assets/core/skills/local2prod/SKILL.md +147 -0
  53. package/assets/core/skills/new-feature/SKILL.md +155 -0
  54. package/assets/core/skills/obsidian-sync/SKILL.md +152 -0
  55. package/assets/core/skills/phase-kickoff/SKILL.md +69 -0
  56. package/assets/core/skills/security-audit/SKILL.md +125 -0
  57. package/assets/core/skills/spec/SKILL.md +72 -0
  58. package/assets/core/skills/wiki-ingest/SKILL.md +183 -0
  59. package/assets/core/skills/wiki-lint/SKILL.md +109 -0
  60. package/assets/core/skills/wiki-query/SKILL.md +100 -0
  61. package/assets/core/templates/claude-md/architecture.rules +20 -0
  62. package/assets/core/templates/claude-md/global.md +30 -0
  63. package/assets/core/templates/claude-md/project.md +36 -0
  64. package/assets/core/templates/daily-note.md +38 -0
  65. package/assets/core/templates/spec-template.md +43 -0
  66. package/assets/core/workflows/sdd.md +69 -0
  67. package/assets/core/workflows/sprint.md +59 -0
  68. package/assets/forge.py +1265 -0
  69. package/assets/hooks/pre-commit +43 -0
  70. package/assets/manifest.json +274 -0
  71. package/assets/profiles/astro/README.md +24 -0
  72. package/assets/profiles/astro/agents/frontend-engineer.md +74 -0
  73. package/assets/profiles/django/agents/api-engineer.md +83 -0
  74. package/assets/profiles/expo/README.md +24 -0
  75. package/assets/profiles/expo/agents/mobile-engineer.md +69 -0
  76. package/assets/profiles/express/agents/api-engineer.md +60 -0
  77. package/assets/profiles/fastapi/README.md +32 -0
  78. package/assets/profiles/fastapi/agents/api-engineer.md +87 -0
  79. package/assets/profiles/go-gin/agents/api-engineer.md +98 -0
  80. package/assets/profiles/hono-drizzle/README.md +31 -0
  81. package/assets/profiles/hono-drizzle/agents/api-engineer.md +82 -0
  82. package/assets/profiles/laravel/README.md +32 -0
  83. package/assets/profiles/laravel/agents/api-engineer.md +114 -0
  84. package/assets/profiles/laravel/agents/fullstack-engineer.md +67 -0
  85. package/assets/profiles/laravel/agents/migration-specialist.md +420 -0
  86. package/assets/profiles/nestjs/agents/api-engineer.md +79 -0
  87. package/assets/profiles/nextjs-admin/README.md +32 -0
  88. package/assets/profiles/nextjs-admin/agents/admin-engineer.md +78 -0
  89. package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +51 -0
  90. package/assets/profiles/rails/agents/fullstack-engineer.md +61 -0
  91. package/assets/profiles/sveltekit/agents/frontend-engineer.md +96 -0
  92. package/assets/profiles/vuenuxt/agents/frontend-engineer.md +82 -0
  93. package/assets/profiles/wordpress/README.md +30 -0
  94. package/assets/profiles/wordpress/agents/divi-engineer.md +273 -0
  95. package/assets/profiles/wordpress/agents/elementor-engineer.md +310 -0
  96. package/assets/profiles/wordpress/agents/wp-engineer.md +216 -0
  97. package/assets/requirements.txt +2 -0
  98. package/assets/scripts/aitmpl-search.py +808 -0
  99. package/assets/scripts/forge-add-opportunities.py +92 -0
  100. package/assets/scripts/forge-audit.py +1061 -0
  101. package/assets/scripts/forge-generate-all.py +283 -0
  102. package/assets/scripts/forge-init.py +900 -0
  103. package/assets/scripts/forge-migrate-project-yaml.py +397 -0
  104. package/assets/scripts/forge-scaffold-profile.py +181 -0
  105. package/assets/scripts/forge-teardown.py +193 -0
  106. package/assets/scripts/forge-validate-project-yaml.py +457 -0
  107. package/assets/scripts/forge-wizard.py +1003 -0
  108. package/assets/scripts/setup-codex.sh +229 -0
  109. package/assets/scripts/team-install.sh +147 -0
  110. package/assets/scripts/token-stats.py +201 -0
  111. package/assets/templates/modes/enterprise.yaml.tpl +114 -0
  112. package/assets/templates/modes/multi-runtime.yaml.tpl +89 -0
  113. package/assets/templates/modes/new-stack.yaml.tpl +101 -0
  114. package/assets/templates/modes/startup.yaml.tpl +74 -0
  115. package/assets/templates/project.yaml.tpl +185 -0
  116. package/assets/templates/wiki/concepts/_template.md +22 -0
  117. package/assets/templates/wiki/entities/_template.md +19 -0
  118. package/assets/templates/wiki/index.md +32 -0
  119. package/assets/templates/wiki/log.md +6 -0
  120. package/assets/templates/wiki/sources/_template.md +25 -0
  121. package/dist/cli.d.ts +3 -0
  122. package/dist/cli.d.ts.map +1 -0
  123. package/dist/cli.js +64 -0
  124. package/dist/cli.js.map +1 -0
  125. package/dist/commands/audit.d.ts +2 -0
  126. package/dist/commands/audit.d.ts.map +1 -0
  127. package/dist/commands/audit.js +21 -0
  128. package/dist/commands/audit.js.map +1 -0
  129. package/dist/commands/doctor.d.ts +2 -0
  130. package/dist/commands/doctor.d.ts.map +1 -0
  131. package/dist/commands/doctor.js +58 -0
  132. package/dist/commands/doctor.js.map +1 -0
  133. package/dist/commands/generate.d.ts +2 -0
  134. package/dist/commands/generate.d.ts.map +1 -0
  135. package/dist/commands/generate.js +27 -0
  136. package/dist/commands/generate.js.map +1 -0
  137. package/dist/commands/init.d.ts +2 -0
  138. package/dist/commands/init.d.ts.map +1 -0
  139. package/dist/commands/init.js +22 -0
  140. package/dist/commands/init.js.map +1 -0
  141. package/dist/commands/validate.d.ts +2 -0
  142. package/dist/commands/validate.d.ts.map +1 -0
  143. package/dist/commands/validate.js +20 -0
  144. package/dist/commands/validate.js.map +1 -0
  145. package/dist/lib/paths.d.ts +10 -0
  146. package/dist/lib/paths.d.ts.map +1 -0
  147. package/dist/lib/paths.js +49 -0
  148. package/dist/lib/paths.js.map +1 -0
  149. package/dist/lib/python.d.ts +4 -0
  150. package/dist/lib/python.d.ts.map +1 -0
  151. package/dist/lib/python.js +46 -0
  152. package/dist/lib/python.js.map +1 -0
  153. package/package.json +46 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: security-auditor
3
+ description: Audita el código por vulnerabilidades de seguridad. Foco en autenticación, autorización, inyección y dependencias. NO modifica código.
4
+ model: opus
5
+ tools: Read, Grep, Glob, Bash
6
+ tier: 1
7
+ standard_version: "1.0"
8
+ ---
9
+
10
+ # Security Auditor
11
+
12
+ Auditás el código por vulnerabilidades de seguridad. No modificás código — solo reportás hallazgos
13
+ con severidad y recomendación de fix.
14
+
15
+ ## Foco principal
16
+
17
+ - **Autenticación y autorización**: cada endpoint debe verificar ambas
18
+ - **Inyección**: SQL injection, command injection, SSTI, XSS
19
+ - **Secrets en código**: tokens, passwords, keys hardcodeados
20
+ - **Multi-tenancy**: que los datos de un tenant no sean accesibles desde otro
21
+ - **Dependencias**: versiones con CVEs conocidos
22
+
23
+ ## Proceso
24
+
25
+ 1. Revisar todos los endpoints/rutas del PR.
26
+ 2. Buscar patrones de riesgo (grep por strings críticos).
27
+ 3. Verificar autorización por recurso (no solo autenticación de sesión).
28
+ 4. Revisar manejo de errores — que no filtre información técnica al cliente.
29
+ 5. Buscar secrets hardcodeados (`grep -r "password\|secret\|token\|key" --include="*.ts"`).
30
+
31
+ ## Severidades
32
+
33
+ - **CRÍTICO**: Permite acceso no autorizado a datos de otro tenant, RCE, inyección SQL directa.
34
+ - **ALTO**: Bypass de autenticación, SSRF, deserialización insegura.
35
+ - **MEDIO**: XSS, CSRF sin protección en endpoints sensibles, verbose errors.
36
+ - **BAJO**: Headers de seguridad faltantes, dependencias desactualizadas sin CVE activo.
37
+
38
+ ## No hagas
39
+
40
+ - No modificás código. Solo reportás.
41
+ - No marcás como CRÍTICO algo que es solo teórico sin path de explotación.
42
+ - No ignorás findings por ser "solo del lado del cliente".
43
+
44
+ ## Forge v2 — Integración con el flujo
45
+
46
+ **Cuándo te invocan:**
47
+ - Como parte de `/review` en proyectos standard y enterprise
48
+ - Antes de `/ship` en proyectos con datos sensibles
49
+
50
+ **Checklist adicional para Forge v2:**
51
+ - ¿El PR agrega variables de entorno? Verificar que están documentadas en `.env.example`
52
+ - ¿Hay cambios en permisos de `settings.json`? Revisar que el allow-list es mínimo necesario
53
+ - ¿Los hooks de producción están activos? (pre-bash-check.py para mode=standard/enterprise)
54
+ - ¿El deploy pipeline de `/ship` incluye verificación de runtime logs?
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: test-engineer
3
+ description: Escribe y mantiene tests unitarios, integración y E2E. NO escribe código de producción.
4
+ model: sonnet
5
+ tools: Read, Grep, Glob, Bash, Edit, Write
6
+ tier: 1
7
+ standard_version: "1.0"
8
+ ---
9
+
10
+ # Test Engineer
11
+
12
+ Escribís y mantenés tests. No tocás código de producción — si el código necesita cambiar para ser
13
+ testeable, se lo pedís al agente que corresponda y esperás.
14
+
15
+ ## Tu trabajo
16
+
17
+ - Tests unitarios de lógica core (funciones, servicios, validaciones)
18
+ - Tests de integración (endpoints de API, flujos de DB)
19
+ - Tests E2E (flujos de usuario críticos)
20
+ - Coverage reports
21
+ - Fixtures y factories de datos de test
22
+
23
+ ## Reglas
24
+
25
+ - **No escribís código de producción.** Si algo no se puede testear, lo reportás.
26
+ - Tests determinísticos — sin `Math.random()` ni `Date.now()` sin mockear.
27
+ - Nombres descriptivos: `describe("cuando X") / it("debería Y")`.
28
+ - Tests de integración deben usar base de datos real, no mocks del ORM.
29
+ - Limpiar fixtures después de cada test (transacciones o truncate).
30
+ - Sin `console.log` en tests — el runner lo reportará como falla en CI.
31
+
32
+ ## Prioridades
33
+
34
+ 1. Casos edge y errores (los happy paths suelen estar implícitos)
35
+ 2. Lógica de negocio con consecuencias legales o de compliance
36
+ 3. Flujos de usuario críticos (login, pago, consent, DSAR)
37
+ 4. Regresiones de bugs conocidos
38
+
39
+ ## No hagas
40
+
41
+ - No skipees tests sin un comentario que explique por qué y cuándo se desbloquea.
42
+ - No mockees la base de datos en tests de integración.
43
+ - No testees implementación, testea comportamiento observable.
44
+ - No crees tests que solo pasan en tu máquina (evitar paths absolutos, fechas hardcodeadas).
45
+
46
+ ## Forge v2 — Reglas de testing
47
+
48
+ **Tu rol en el flujo SDD:**
49
+ - Los tests se escriben con la implementación, no al final
50
+ - Cada acceptance criterion de la spec debe tener al menos un test
51
+ - No marcar spec como "implementada" sin tests que verifiquen cada criterio
52
+
53
+ **Comandos relevantes:**
54
+ - `/review`: podés ser invocado como parte de un review multi-agente
55
+ - Los tests van en los mismos PRs que el código — no en PRs separados
56
+
57
+ **Scope:** Archivos de test, fixtures y configuración de testing. No modificar código de aplicación salvo para refactors que mejoren testabilidad (coordinar con backend/frontend engineer).
@@ -0,0 +1,48 @@
1
+ # Forge v2 — Hooks registry
2
+ # Define qué hooks se instalan según mode y stack del proyecto.
3
+
4
+ universal:
5
+ # Se instalan en TODOS los proyectos
6
+ - hook: pre-edit-check.py
7
+ event: PreToolUse
8
+ matcher: "Edit|Write"
9
+ description: "Branch guard, debug detection, secret detection"
10
+ - hook: post-turn-check.sh
11
+ event: Stop
12
+ description: "Typecheck/lint sobre archivos modificados"
13
+
14
+ standard:
15
+ # mode: standard + enterprise
16
+ - hook: pre-bash-check.py
17
+ event: PreToolUse
18
+ matcher: "Bash"
19
+ description: "Bloquea comandos destructivos en producción"
20
+
21
+ enterprise:
22
+ # mode: enterprise solamente (además de standard)
23
+ - hook: audit-log-append.py
24
+ event: PostToolUse
25
+ description: "Audit log inmutable para SOC2/compliance"
26
+
27
+ stack:
28
+ # Por stack (además de los universales)
29
+ supabase:
30
+ - hook: check-destructive-sql.py
31
+ event: PreToolUse
32
+ matcher: "Bash"
33
+ description: "Detecta SQL destructivo contra Supabase"
34
+ prisma:
35
+ - hook: prisma-safety.py
36
+ event: PreToolUse
37
+ matcher: "Bash"
38
+ description: "Bloquea migrate reset en producción"
39
+ nextjs-admin:
40
+ - hook: prisma-safety.py
41
+ event: PreToolUse
42
+ matcher: "Bash"
43
+ description: "Bloquea prisma migrate reset/fresh en producción (aplica si el profile usa Prisma como ORM)"
44
+ laravel:
45
+ - hook: composer-check.py
46
+ event: PreToolUse
47
+ matcher: "Bash"
48
+ description: "Verifica que composer install/update no instale paquetes de dev en producción; advierte sobre artisan migrate:fresh/reset"
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # Forge v2 — Stop hook: post-turn-check.sh
3
+ # Runs after each Claude turn. Detects modified files and runs type/syntax checks.
4
+ # Always exits 0 (never blocks).
5
+
6
+ set -euo pipefail
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Step 1 — Find modified files
10
+ # ---------------------------------------------------------------------------
11
+ MODIFIED=$(git diff --name-only HEAD 2>/dev/null || echo "")
12
+ STAGED=$(git diff --name-only --cached 2>/dev/null || echo "")
13
+ ALL_CHANGED="$MODIFIED $STAGED"
14
+
15
+ if [ -z "$(echo "$ALL_CHANGED" | tr -d '[:space:]')" ]; then
16
+ exit 0
17
+ fi
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Step 2 — Read project config
21
+ # ---------------------------------------------------------------------------
22
+ PKG_MGR=""
23
+ CUSTOM_CHECK=""
24
+
25
+ if [ -f "project.yaml" ]; then
26
+ PKG_MGR=$(python3 -c "
27
+ import yaml, sys
28
+ try:
29
+ d = yaml.safe_load(open('project.yaml'))
30
+ print(d.get('stack', {}).get('package_manager', ''))
31
+ except:
32
+ print('')
33
+ " 2>/dev/null || echo "")
34
+
35
+ CUSTOM_CHECK=$(python3 -c "
36
+ import yaml, sys
37
+ try:
38
+ d = yaml.safe_load(open('project.yaml'))
39
+ print(d.get('scripts', {}).get('check', ''))
40
+ except:
41
+ print('')
42
+ " 2>/dev/null || echo "")
43
+ fi
44
+
45
+ CHECK_OUTPUT=""
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Step 3 — Run checks
49
+ # ---------------------------------------------------------------------------
50
+
51
+ # Helper: check if any changed file matches a glob pattern
52
+ files_matching() {
53
+ local pattern="$1"
54
+ echo "$ALL_CHANGED" | tr ' ' '\n' | grep -E "$pattern" 2>/dev/null || true
55
+ }
56
+
57
+ if [ -n "$CUSTOM_CHECK" ]; then
58
+ # Run user-defined check command
59
+ CHECK_OUTPUT=$(eval "$CUSTOM_CHECK" 2>&1 | head -20 || true)
60
+
61
+ else
62
+ # Auto-detect by file type
63
+
64
+ # TypeScript / JavaScript
65
+ TS_FILES=$(files_matching '\.(ts|tsx)$')
66
+ if [ -n "$TS_FILES" ]; then
67
+ TSC_CMD=""
68
+ if [ -f "turbo.json" ] && command -v pnpm &>/dev/null; then
69
+ TSC_OUTPUT=$(pnpm turbo typecheck 2>&1 | head -20 || pnpm tsc --noEmit 2>&1 | head -20 || true)
70
+ elif command -v pnpm &>/dev/null; then
71
+ TSC_OUTPUT=$(pnpm tsc --noEmit 2>&1 | head -20 || true)
72
+ elif command -v npx &>/dev/null; then
73
+ TSC_OUTPUT=$(npx tsc --noEmit 2>&1 | head -20 || true)
74
+ else
75
+ TSC_OUTPUT=""
76
+ fi
77
+ if [ -n "${TSC_OUTPUT:-}" ]; then
78
+ CHECK_OUTPUT="${CHECK_OUTPUT:+$CHECK_OUTPUT$'\n'}[tsc] $TSC_OUTPUT"
79
+ fi
80
+ fi
81
+
82
+ # PHP
83
+ PHP_FILES=$(files_matching '\.php$')
84
+ if [ -n "$PHP_FILES" ] && [ -f "composer.json" ]; then
85
+ if command -v composer &>/dev/null; then
86
+ PHP_OUTPUT=$(composer validate --no-check-publish 2>&1 | head -10 || true)
87
+ if [ -n "$PHP_OUTPUT" ]; then
88
+ CHECK_OUTPUT="${CHECK_OUTPUT:+$CHECK_OUTPUT$'\n'}[composer] $PHP_OUTPUT"
89
+ fi
90
+ fi
91
+ fi
92
+
93
+ # Python
94
+ PY_FILES=$(files_matching '\.py$')
95
+ if [ -n "$PY_FILES" ]; then
96
+ PY_OUTPUT=""
97
+ while IFS= read -r f; do
98
+ [ -z "$f" ] && continue
99
+ [ -f "$f" ] || continue
100
+ RESULT=$(python3 -m py_compile "$f" 2>&1 || true)
101
+ if [ -n "$RESULT" ]; then
102
+ PY_OUTPUT="${PY_OUTPUT:+$PY_OUTPUT$'\n'}$f: $RESULT"
103
+ fi
104
+ done <<< "$PY_FILES"
105
+ if [ -n "$PY_OUTPUT" ]; then
106
+ CHECK_OUTPUT="${CHECK_OUTPUT:+$CHECK_OUTPUT$'\n'}[python] $PY_OUTPUT"
107
+ fi
108
+ fi
109
+
110
+ # Ruby
111
+ RB_FILES=$(files_matching '\.rb$')
112
+ if [ -n "$RB_FILES" ] && [ -f "Gemfile" ]; then
113
+ RB_OUTPUT=""
114
+ if command -v bundle &>/dev/null; then
115
+ while IFS= read -r f; do
116
+ [ -z "$f" ] && continue
117
+ [ -f "$f" ] || continue
118
+ RESULT=$(bundle exec ruby -c "$f" 2>&1 | head -10 || true)
119
+ if [ -n "$RESULT" ]; then
120
+ RB_OUTPUT="${RB_OUTPUT:+$RB_OUTPUT$'\n'}$f: $RESULT"
121
+ fi
122
+ done <<< "$RB_FILES"
123
+ fi
124
+ if [ -n "$RB_OUTPUT" ]; then
125
+ CHECK_OUTPUT="${CHECK_OUTPUT:+$CHECK_OUTPUT$'\n'}[ruby] $RB_OUTPUT"
126
+ fi
127
+ fi
128
+ fi
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Step 4 — Report
132
+ # ---------------------------------------------------------------------------
133
+ if [ -n "$CHECK_OUTPUT" ]; then
134
+ echo "── Forge post-turn check ─────────────────"
135
+ echo "$CHECK_OUTPUT"
136
+ echo "──────────────────────────────────────────"
137
+ fi
138
+
139
+ exit 0
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Forge v2 — PreToolUse hook: pre-bash-check.py
4
+ Bloquea comandos destructivos en contexto de producción.
5
+
6
+ Contexto crítico: el 2026-04-28, --force-reset fue ejecutado accidentalmente
7
+ contra la base de datos de producción de un proyecto real, borrando 225 usuarios
8
+ y 35 formularios (recuperados desde backup de Supabase Pro). Este hook existe
9
+ para que eso nunca vuelva a pasar.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+
18
+ DEBUG = os.environ.get("DEBUG", "") not in ("", "0", "false", "False")
19
+
20
+
21
+ def dbg(msg):
22
+ if DEBUG:
23
+ print(f"[forge-hook-debug] {msg}", flush=True)
24
+
25
+
26
+ def load_project_yaml():
27
+ """Walk up from cwd to find project.yaml. Returns dict or {}."""
28
+ try:
29
+ import yaml
30
+ path = os.getcwd()
31
+ for _ in range(6):
32
+ candidate = os.path.join(path, "project.yaml")
33
+ if os.path.isfile(candidate):
34
+ with open(candidate) as f:
35
+ data = yaml.safe_load(f)
36
+ return data if isinstance(data, dict) else {}
37
+ parent = os.path.dirname(path)
38
+ if parent == path:
39
+ break
40
+ path = parent
41
+ except Exception as e:
42
+ dbg(f"project.yaml load error: {e}")
43
+ return {}
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Patrones peligrosos hardcodeados
48
+ # ---------------------------------------------------------------------------
49
+
50
+ DANGEROUS_PATTERNS = [
51
+ (r"--force-reset", "--force-reset"),
52
+ (r"prisma\s+migrate\s+reset", "prisma migrate reset"),
53
+ (r"DROP\s+TABLE", "DROP TABLE"),
54
+ (r"TRUNCATE\s+", "TRUNCATE"),
55
+ (r"DELETE\s+FROM\s+\w+\s*;", "DELETE FROM sin WHERE"),
56
+ (r"DROP\s+DATABASE", "DROP DATABASE"),
57
+ (r"dropdb\s+", "dropdb"),
58
+ (r"rm\s+-rf\s+/", "rm -rf /"),
59
+ (r"git\s+push\s+--force(?!\s+--with-lease)", "git push --force sin --with-lease"),
60
+ ]
61
+
62
+
63
+ def _match_dangerous(command: str) -> tuple[bool, str]:
64
+ """Retorna (matched, pattern_label). Verifica patrones hardcodeados."""
65
+ for pattern, label in DANGEROUS_PATTERNS:
66
+ if re.search(pattern, command, re.IGNORECASE):
67
+ return True, label
68
+ return False, ""
69
+
70
+
71
+ def _match_project_forbidden(command: str, project: dict) -> tuple[bool, str]:
72
+ """Verifica patrones forbidden_in_production del project.yaml."""
73
+ try:
74
+ rules = project.get("rules", {})
75
+ forbidden = rules.get("forbidden_in_production", [])
76
+ if not isinstance(forbidden, list):
77
+ return False, ""
78
+ for pattern in forbidden:
79
+ if re.search(pattern, command):
80
+ return True, pattern
81
+ except Exception as e:
82
+ dbg(f"project.yaml forbidden_in_production error: {e}")
83
+ return False, ""
84
+
85
+
86
+ def _is_production_context(command: str, project: dict) -> bool:
87
+ """
88
+ Retorna True si el comando se ejecuta en contexto de producción:
89
+ - La URL de producción aparece en el comando, o
90
+ - Variables de entorno PROD_* / PRODUCTION_* están seteadas en el proceso.
91
+ """
92
+ # URL de producción del project.yaml
93
+ deploy = project.get("deploy", {})
94
+ prod_url = deploy.get("production_url", "") or ""
95
+ project_id = deploy.get("project_id", "") or ""
96
+
97
+ if prod_url and prod_url in command:
98
+ dbg(f"production context: URL '{prod_url}' encontrada en comando")
99
+ return True
100
+ if project_id and project_id in command:
101
+ dbg(f"production context: project_id '{project_id}' encontrado en comando")
102
+ return True
103
+
104
+ # Variables de entorno que sugieren contexto de producción
105
+ prod_env_patterns = re.compile(r"^(PROD_|PRODUCTION_|PROD$|PRODUCTION$)", re.IGNORECASE)
106
+ for key, value in os.environ.items():
107
+ if prod_env_patterns.match(key) and value:
108
+ dbg(f"production context: env var '{key}' activa")
109
+ return True
110
+
111
+ return False
112
+
113
+
114
+ def _extract_snippet(command: str, max_len: int = 120) -> str:
115
+ """Retorna un extracto legible del comando."""
116
+ command = command.strip()
117
+ if len(command) <= max_len:
118
+ return command
119
+ return command[:max_len] + "..."
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Main
124
+ # ---------------------------------------------------------------------------
125
+
126
+ def main():
127
+ try:
128
+ raw = sys.stdin.read()
129
+ if not raw.strip():
130
+ dbg("empty stdin, allowing")
131
+ sys.exit(0)
132
+ data = json.loads(raw)
133
+ except Exception as e:
134
+ dbg(f"stdin parse error: {e}")
135
+ sys.exit(0)
136
+
137
+ try:
138
+ tool_name = data.get("tool_name", "")
139
+ if tool_name != "Bash":
140
+ sys.exit(0)
141
+
142
+ tool_input = data.get("tool_input", {})
143
+ command = tool_input.get("command", "")
144
+ if not command:
145
+ sys.exit(0)
146
+
147
+ dbg(f"command: {command[:200]!r}")
148
+
149
+ project = load_project_yaml()
150
+
151
+ # Verificar patrones peligrosos hardcodeados
152
+ matched, pattern_label = _match_dangerous(command)
153
+
154
+ # Verificar patrones custom del project.yaml
155
+ if not matched:
156
+ matched, pattern_label = _match_project_forbidden(command, project)
157
+
158
+ if not matched:
159
+ sys.exit(0)
160
+
161
+ # Hay un patrón peligroso — ¿es contexto de producción?
162
+ in_production = _is_production_context(command, project)
163
+
164
+ snippet = _extract_snippet(command)
165
+
166
+ if in_production:
167
+ # BLOQUEAR
168
+ print(
169
+ f"forge: BLOQUEADO — comando destructivo detectado en contexto de producción.\n"
170
+ f"\n"
171
+ f" Comando: {snippet}\n"
172
+ f" Patrón: {pattern_label}\n"
173
+ f"\n"
174
+ f" Si necesitás ejecutar esto, hacelo MANUALMENTE en la terminal\n"
175
+ f" con plena consciencia de que afecta producción.\n"
176
+ f"\n"
177
+ f" Lección del 2026-04-28: --force-reset borró 225 usuarios y\n"
178
+ f" 35 formularios en producción. Este hook existe por eso.",
179
+ flush=True,
180
+ )
181
+ sys.exit(2)
182
+ else:
183
+ # ADVERTIR — no bloquear
184
+ print(
185
+ f"forge: ADVERTENCIA — comando potencialmente destructivo detectado.\n"
186
+ f"\n"
187
+ f" Comando: {snippet}\n"
188
+ f" Patrón: {pattern_label}\n"
189
+ f"\n"
190
+ f" Si esto apunta a producción, cancela y ejecuta manualmente.\n"
191
+ f" Confirmá que el contexto es seguro antes de continuar.",
192
+ flush=True,
193
+ )
194
+ sys.exit(0)
195
+
196
+ except Exception as e:
197
+ dbg(f"unexpected error: {e}")
198
+ sys.exit(0)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()