@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.
- package/CHANGELOG.md +228 -0
- package/LICENSE +191 -0
- package/README.md +156 -0
- package/assets/adapters/claude-code/commands/deploy-check.md +12 -0
- package/assets/adapters/claude-code/commands/new-feature.md +11 -0
- package/assets/adapters/claude-code/commands/plan.md +116 -0
- package/assets/adapters/claude-code/commands/review.md +219 -0
- package/assets/adapters/claude-code/commands/session-close.md +109 -0
- package/assets/adapters/claude-code/commands/session-start.md +59 -0
- package/assets/adapters/claude-code/commands/ship.md +133 -0
- package/assets/adapters/claude-code/commands/wiki-ingest.md +7 -0
- package/assets/adapters/claude-code/commands/wiki-lint.md +5 -0
- package/assets/adapters/claude-code/commands/wiki-query.md +7 -0
- package/assets/adapters/claude-code/commands/work.md +101 -0
- package/assets/adapters/claude-code/generate-claude-md.py +304 -0
- package/assets/adapters/codex/commands/plan.md +63 -0
- package/assets/adapters/codex/commands/review.md +53 -0
- package/assets/adapters/codex/commands/session-close.md +53 -0
- package/assets/adapters/codex/commands/session-start.md +49 -0
- package/assets/adapters/codex/commands/ship.md +53 -0
- package/assets/adapters/codex/commands/work.md +53 -0
- package/assets/adapters/codex/generate-codex-config.py +269 -0
- package/assets/adapters/codex/hooks/codex.yaml.tpl +43 -0
- package/assets/adapters/codex/hooks/forge-codex-finish.sh +158 -0
- package/assets/adapters/codex/hooks/forge-codex-start.sh +186 -0
- package/assets/adapters/kiro/generate-steering.py +367 -0
- package/assets/adapters/opencode/HOOKS.md +123 -0
- package/assets/adapters/opencode/commands/plan.md +119 -0
- package/assets/adapters/opencode/commands/review.md +164 -0
- package/assets/adapters/opencode/commands/session-close.md +111 -0
- package/assets/adapters/opencode/commands/session-start.md +62 -0
- package/assets/adapters/opencode/commands/ship.md +135 -0
- package/assets/adapters/opencode/commands/work.md +82 -0
- package/assets/adapters/opencode/generate-agents-md.py +262 -0
- package/assets/core/agents/backend-engineer.md +61 -0
- package/assets/core/agents/compliance-reviewer.md +83 -0
- package/assets/core/agents/docs-writer.md +77 -0
- package/assets/core/agents/frontend-engineer.md +70 -0
- package/assets/core/agents/orchestrator.md +104 -0
- package/assets/core/agents/security-auditor.md +54 -0
- package/assets/core/agents/test-engineer.md +57 -0
- package/assets/core/hooks/hooks-registry.yaml +48 -0
- package/assets/core/hooks/post-turn-check.sh +139 -0
- package/assets/core/hooks/pre-bash-check.py +202 -0
- package/assets/core/hooks/pre-edit-check.py +317 -0
- package/assets/core/hooks/session-start.sh +184 -0
- package/assets/core/schemas/project.schema.json +503 -0
- package/assets/core/skills/README.md +88 -0
- package/assets/core/skills/aitmpl-search/SKILL.md +74 -0
- package/assets/core/skills/browser-test/SKILL.md +177 -0
- package/assets/core/skills/db-migrate/SKILL.md +163 -0
- package/assets/core/skills/local2prod/SKILL.md +147 -0
- package/assets/core/skills/new-feature/SKILL.md +155 -0
- package/assets/core/skills/obsidian-sync/SKILL.md +152 -0
- package/assets/core/skills/phase-kickoff/SKILL.md +69 -0
- package/assets/core/skills/security-audit/SKILL.md +125 -0
- package/assets/core/skills/spec/SKILL.md +72 -0
- package/assets/core/skills/wiki-ingest/SKILL.md +183 -0
- package/assets/core/skills/wiki-lint/SKILL.md +109 -0
- package/assets/core/skills/wiki-query/SKILL.md +100 -0
- package/assets/core/templates/claude-md/architecture.rules +20 -0
- package/assets/core/templates/claude-md/global.md +30 -0
- package/assets/core/templates/claude-md/project.md +36 -0
- package/assets/core/templates/daily-note.md +38 -0
- package/assets/core/templates/spec-template.md +43 -0
- package/assets/core/workflows/sdd.md +69 -0
- package/assets/core/workflows/sprint.md +59 -0
- package/assets/forge.py +1265 -0
- package/assets/hooks/pre-commit +43 -0
- package/assets/manifest.json +274 -0
- package/assets/profiles/astro/README.md +24 -0
- package/assets/profiles/astro/agents/frontend-engineer.md +74 -0
- package/assets/profiles/django/agents/api-engineer.md +83 -0
- package/assets/profiles/expo/README.md +24 -0
- package/assets/profiles/expo/agents/mobile-engineer.md +69 -0
- package/assets/profiles/express/agents/api-engineer.md +60 -0
- package/assets/profiles/fastapi/README.md +32 -0
- package/assets/profiles/fastapi/agents/api-engineer.md +87 -0
- package/assets/profiles/go-gin/agents/api-engineer.md +98 -0
- package/assets/profiles/hono-drizzle/README.md +31 -0
- package/assets/profiles/hono-drizzle/agents/api-engineer.md +82 -0
- package/assets/profiles/laravel/README.md +32 -0
- package/assets/profiles/laravel/agents/api-engineer.md +114 -0
- package/assets/profiles/laravel/agents/fullstack-engineer.md +67 -0
- package/assets/profiles/laravel/agents/migration-specialist.md +420 -0
- package/assets/profiles/nestjs/agents/api-engineer.md +79 -0
- package/assets/profiles/nextjs-admin/README.md +32 -0
- package/assets/profiles/nextjs-admin/agents/admin-engineer.md +78 -0
- package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +51 -0
- package/assets/profiles/rails/agents/fullstack-engineer.md +61 -0
- package/assets/profiles/sveltekit/agents/frontend-engineer.md +96 -0
- package/assets/profiles/vuenuxt/agents/frontend-engineer.md +82 -0
- package/assets/profiles/wordpress/README.md +30 -0
- package/assets/profiles/wordpress/agents/divi-engineer.md +273 -0
- package/assets/profiles/wordpress/agents/elementor-engineer.md +310 -0
- package/assets/profiles/wordpress/agents/wp-engineer.md +216 -0
- package/assets/requirements.txt +2 -0
- package/assets/scripts/aitmpl-search.py +808 -0
- package/assets/scripts/forge-add-opportunities.py +92 -0
- package/assets/scripts/forge-audit.py +1061 -0
- package/assets/scripts/forge-generate-all.py +283 -0
- package/assets/scripts/forge-init.py +900 -0
- package/assets/scripts/forge-migrate-project-yaml.py +397 -0
- package/assets/scripts/forge-scaffold-profile.py +181 -0
- package/assets/scripts/forge-teardown.py +193 -0
- package/assets/scripts/forge-validate-project-yaml.py +457 -0
- package/assets/scripts/forge-wizard.py +1003 -0
- package/assets/scripts/setup-codex.sh +229 -0
- package/assets/scripts/team-install.sh +147 -0
- package/assets/scripts/token-stats.py +201 -0
- package/assets/templates/modes/enterprise.yaml.tpl +114 -0
- package/assets/templates/modes/multi-runtime.yaml.tpl +89 -0
- package/assets/templates/modes/new-stack.yaml.tpl +101 -0
- package/assets/templates/modes/startup.yaml.tpl +74 -0
- package/assets/templates/project.yaml.tpl +185 -0
- package/assets/templates/wiki/concepts/_template.md +22 -0
- package/assets/templates/wiki/entities/_template.md +19 -0
- package/assets/templates/wiki/index.md +32 -0
- package/assets/templates/wiki/log.md +6 -0
- package/assets/templates/wiki/sources/_template.md +25 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +64 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +21 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +58 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +27 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +22 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +20 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +49 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/python.d.ts +4 -0
- package/dist/lib/python.d.ts.map +1 -0
- package/dist/lib/python.js +46 -0
- package/dist/lib/python.js.map +1 -0
- 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()
|