@cristiancorreau/forge 2.9.6 → 2.9.7
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/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/lib/paths.d.ts +1 -2
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +12 -16
- package/dist/lib/paths.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/assets/adapters/claude-code/generate-claude-md.py +0 -304
- package/assets/adapters/codex/generate-codex-config.py +0 -269
- package/assets/adapters/kiro/generate-steering.py +0 -367
- package/assets/adapters/opencode/generate-agents-md.py +0 -262
- package/assets/core/hooks/pre-bash-check.py +0 -202
- package/assets/core/hooks/pre-edit-check.py +0 -317
- package/assets/forge.py +0 -1265
- package/assets/requirements.txt +0 -2
- package/assets/scripts/aitmpl-search.py +0 -808
- package/assets/scripts/forge-add-opportunities.py +0 -92
- package/assets/scripts/forge-audit.py +0 -1061
- package/assets/scripts/forge-generate-all.py +0 -283
- package/assets/scripts/forge-init.py +0 -900
- package/assets/scripts/forge-migrate-project-yaml.py +0 -397
- package/assets/scripts/forge-scaffold-profile.py +0 -181
- package/assets/scripts/forge-teardown.py +0 -193
- package/assets/scripts/forge-validate-project-yaml.py +0 -457
- package/assets/scripts/forge-wizard.py +0 -1003
- package/assets/scripts/setup-codex.sh +0 -229
- package/assets/scripts/team-install.sh +0 -147
- package/assets/scripts/token-stats.py +0 -201
- package/dist/lib/python.d.ts +0 -4
- package/dist/lib/python.d.ts.map +0 -1
- package/dist/lib/python.js +0 -46
- package/dist/lib/python.js.map +0 -1
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# Copyright 2026 Cristian Correa — Apache License 2.0
|
|
3
|
-
# https://github.com/cristiancorreau/forge
|
|
4
|
-
"""
|
|
5
|
-
generate-agents-md.py — Genera AGENTS.md para OpenCode / Codex.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 .agentic/adapters/opencode/generate-agents-md.py
|
|
9
|
-
|
|
10
|
-
Lee project.yaml en la raíz y genera AGENTS.md con el roster completo del equipo.
|
|
11
|
-
OpenCode y Codex usan AGENTS.md como contexto de sistema para los agentes.
|
|
12
|
-
|
|
13
|
-
Requiere: pyyaml
|
|
14
|
-
"""
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
try:
|
|
19
|
-
import yaml
|
|
20
|
-
except ImportError:
|
|
21
|
-
print("ERROR: pyyaml requerido. pip install pyyaml", file=sys.stderr)
|
|
22
|
-
sys.exit(1)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def find_project_root() -> Path:
|
|
26
|
-
here = Path.cwd()
|
|
27
|
-
for p in [here] + list(here.parents):
|
|
28
|
-
if (p / "project.yaml").exists():
|
|
29
|
-
return p
|
|
30
|
-
raise FileNotFoundError("No se encontró project.yaml")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def find_forge_dir() -> Path:
|
|
34
|
-
root = find_project_root()
|
|
35
|
-
for candidate in [root / ".agentic", root / "forge", Path(__file__).parent.parent.parent]:
|
|
36
|
-
if (candidate / "core").exists():
|
|
37
|
-
return candidate
|
|
38
|
-
raise FileNotFoundError("No se encontró el directorio forge con core/")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def read_agent_description(forge: Path, name: str, profiles: list[str]) -> str:
|
|
42
|
-
"""Lee el frontmatter description del agente desde forge (profiles > core)."""
|
|
43
|
-
for profile in profiles:
|
|
44
|
-
p = forge / "profiles" / profile / "agents" / f"{name}.md"
|
|
45
|
-
if p.exists():
|
|
46
|
-
content = p.read_text()
|
|
47
|
-
for line in content.splitlines():
|
|
48
|
-
if line.startswith("description:"):
|
|
49
|
-
return line.split(":", 1)[1].strip().strip('"')
|
|
50
|
-
p = forge / "core" / "agents" / f"{name}.md"
|
|
51
|
-
if p.exists():
|
|
52
|
-
content = p.read_text()
|
|
53
|
-
for line in content.splitlines():
|
|
54
|
-
if line.startswith("description:"):
|
|
55
|
-
return line.split(":", 1)[1].strip().strip('"')
|
|
56
|
-
return "Agente de implementación"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _guardrail_section(config: dict) -> list[str]:
|
|
60
|
-
"""Genera la sección de guardrails embebidos (equivalente a hooks de Claude Code)."""
|
|
61
|
-
proj = config.get("project", {})
|
|
62
|
-
mode = proj.get("mode", "startup")
|
|
63
|
-
|
|
64
|
-
lines = [
|
|
65
|
-
"## Guardrails (comportamiento no-negociable)",
|
|
66
|
-
"",
|
|
67
|
-
"Estas reglas se aplican siempre, en cualquier tarea, sin excepción.",
|
|
68
|
-
"",
|
|
69
|
-
"### Branch guard",
|
|
70
|
-
"",
|
|
71
|
-
"NUNCA editar código cuando la rama actual sea `main`, `master` o `develop`.",
|
|
72
|
-
"Antes de cualquier edición de archivo, verificar la rama con `git branch --show-current`.",
|
|
73
|
-
"Si la rama es main/master/develop: detener y pedir al usuario que cree una rama de feature.",
|
|
74
|
-
"Excepción: archivos de documentación (*.md) pueden editarse en main si el usuario lo indica explícitamente.",
|
|
75
|
-
"",
|
|
76
|
-
"### Detección de debug",
|
|
77
|
-
"",
|
|
78
|
-
"Antes de hacer commit, verificar que no haya en el código a commitear:",
|
|
79
|
-
"- `console.log(` en JS/TS (excepto archivos de logging)",
|
|
80
|
-
"- `print(` en Python que no sea logging de producción",
|
|
81
|
-
"- `debugger;` en JS/TS",
|
|
82
|
-
"- `binding.pry` en Ruby",
|
|
83
|
-
"- `dd(` o `dump(` en PHP",
|
|
84
|
-
"",
|
|
85
|
-
"Si se detectan estos patrones: reportar la línea exacta y pedir confirmación antes de continuar.",
|
|
86
|
-
"",
|
|
87
|
-
"### Producción safety",
|
|
88
|
-
"",
|
|
89
|
-
"Nunca ejecutar sin confirmación explícita del usuario:",
|
|
90
|
-
"- `DROP TABLE`, `DROP DATABASE`, `TRUNCATE` en bases de datos de producción",
|
|
91
|
-
"- `rm -rf` en directorios que no sean temporales o de build",
|
|
92
|
-
"- `git push --force` a main/master",
|
|
93
|
-
"- Deploy a producción sin haber ejecutado `/review` primero",
|
|
94
|
-
"",
|
|
95
|
-
"### SQL injection",
|
|
96
|
-
"",
|
|
97
|
-
"Nunca concatenar input del usuario en strings SQL.",
|
|
98
|
-
"Siempre usar parámetros preparados o el ORM del proyecto.",
|
|
99
|
-
"",
|
|
100
|
-
"### Secrets",
|
|
101
|
-
"",
|
|
102
|
-
"Nunca hardcodear tokens, passwords, API keys o certificados en archivos que van a git.",
|
|
103
|
-
"Usar variables de entorno y documentarlas en `.env.example`.",
|
|
104
|
-
"",
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
if mode in ("standard", "enterprise"):
|
|
108
|
-
lines += [
|
|
109
|
-
"### Compliance (mode: " + mode + ")",
|
|
110
|
-
"",
|
|
111
|
-
"Verificar en cada PR que toque datos de usuarios:",
|
|
112
|
-
"- PII nunca en logs de stdout sin enmascarar",
|
|
113
|
-
"- Consentimiento explícito antes de cualquier tracker no esencial",
|
|
114
|
-
"- Logs de auditoría append-only (sin UPDATE/DELETE sobre eventos ya registrados)",
|
|
115
|
-
"",
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
return lines
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _forge_v2_commands_section() -> list[str]:
|
|
122
|
-
"""Genera la sección de comandos Forge v2 SDD para OpenCode."""
|
|
123
|
-
return [
|
|
124
|
-
"## Comandos Forge v2 (flujo SDD)",
|
|
125
|
-
"",
|
|
126
|
-
"Este proyecto usa el flujo Spec-Driven Development de Forge v2.",
|
|
127
|
-
"Los comandos disponibles en `.opencode/commands/` son:",
|
|
128
|
-
"",
|
|
129
|
-
"| Comando | Cuándo usarlo |",
|
|
130
|
-
"|---------|--------------|",
|
|
131
|
-
"| `/session-start` | Al comenzar una sesión de trabajo — detecta branch, PRs abiertos y estado del repo |",
|
|
132
|
-
"| `/plan` | Para crear o revisar una spec en `docs/specs/` antes de implementar |",
|
|
133
|
-
"| `/work` | Para implementar una spec en estado `ready` — ejecuta en serie en la sesión actual |",
|
|
134
|
-
"| `/review` | Para hacer code review con veredicto APPROVED/CHANGES_REQUESTED/BLOCKED |",
|
|
135
|
-
"| `/ship` | Para hacer deploy a producción — requiere review aprobado y git limpio |",
|
|
136
|
-
"| `/session-close` | Al terminar una sesión — commit, daily note, RELEASE-NOTES y PR |",
|
|
137
|
-
"",
|
|
138
|
-
"**Flujo estándar:** `/session-start` → `/plan` → `/work` → `/review` → `/ship` → `/session-close`",
|
|
139
|
-
"",
|
|
140
|
-
"**Regla fundamental:** Sin spec en `docs/specs/` con estado `ready`, no se ejecuta `/work`.",
|
|
141
|
-
"",
|
|
142
|
-
]
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def generate_agents_md(config: dict, forge: Path) -> str:
|
|
146
|
-
proj = config.get("project", {})
|
|
147
|
-
agents_cfg = config.get("agents", {})
|
|
148
|
-
compliance_cfg = config.get("compliance", {})
|
|
149
|
-
stack = config.get("stack", {})
|
|
150
|
-
paths = config.get("paths", {})
|
|
151
|
-
|
|
152
|
-
name = proj.get("name", "Mi Proyecto")
|
|
153
|
-
active = agents_cfg.get("active", [])
|
|
154
|
-
compliance = agents_cfg.get("compliance", [])
|
|
155
|
-
specialized = agents_cfg.get("specialized", [])
|
|
156
|
-
profiles = agents_cfg.get("profiles", [])
|
|
157
|
-
frameworks = compliance_cfg.get("frameworks", [])
|
|
158
|
-
specs_path = paths.get("specs", "docs/specs")
|
|
159
|
-
|
|
160
|
-
# Compliance-reviewer automático si hay frameworks
|
|
161
|
-
if frameworks and "compliance-reviewer" not in active + compliance:
|
|
162
|
-
compliance = list(set(compliance + ["compliance-reviewer"]))
|
|
163
|
-
|
|
164
|
-
lines = [
|
|
165
|
-
f"# AGENTS.md — {name}",
|
|
166
|
-
"",
|
|
167
|
-
f"> Generado por forge (adapter OpenCode/Codex).",
|
|
168
|
-
f"> Fuente de verdad: `project.yaml`. Re-ejecutar `generate-agents-md.py` al cambiar agentes.",
|
|
169
|
-
"",
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
# Sección de comandos Forge v2 al inicio
|
|
173
|
-
lines += _forge_v2_commands_section()
|
|
174
|
-
|
|
175
|
-
lines += [
|
|
176
|
-
"## Stack del proyecto",
|
|
177
|
-
"",
|
|
178
|
-
f"- **Backend:** {stack.get('backend') or 'N/A'}",
|
|
179
|
-
f"- **Frontend:** {stack.get('frontend') or 'N/A'}",
|
|
180
|
-
f"- **Base de datos:** {stack.get('database') or 'N/A'}",
|
|
181
|
-
f"- **Testing:** {', '.join(stack.get('testing', []))}",
|
|
182
|
-
"",
|
|
183
|
-
"## Reglas globales (todos los agentes)",
|
|
184
|
-
"",
|
|
185
|
-
"- Specs en `" + specs_path + "/` primero — sin spec, sin código.",
|
|
186
|
-
"- Cada agente respeta su scope — no modifica archivos fuera de su dominio.",
|
|
187
|
-
"- Sin hardcodear tokens, passwords ni secrets.",
|
|
188
|
-
"- Parámetros preparados en todas las queries SQL.",
|
|
189
|
-
"- PII nunca en logs de stdout.",
|
|
190
|
-
"",
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
# Guardrails embebidos (equivalente a hooks de Claude Code)
|
|
194
|
-
lines += _guardrail_section(config)
|
|
195
|
-
|
|
196
|
-
lines += [
|
|
197
|
-
"## Roster de agentes",
|
|
198
|
-
"",
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
if active:
|
|
202
|
-
lines += ["### Agentes activos", ""]
|
|
203
|
-
for agent in active:
|
|
204
|
-
desc = read_agent_description(forge, agent, profiles)
|
|
205
|
-
lines.append(f"#### `{agent}`")
|
|
206
|
-
lines.append(f"{desc}")
|
|
207
|
-
lines.append("")
|
|
208
|
-
|
|
209
|
-
if compliance:
|
|
210
|
-
lines += ["### Agentes de compliance y revisión", ""]
|
|
211
|
-
for agent in compliance:
|
|
212
|
-
desc = read_agent_description(forge, agent, profiles)
|
|
213
|
-
lines.append(f"#### `{agent}`")
|
|
214
|
-
lines.append(f"{desc}")
|
|
215
|
-
lines.append("")
|
|
216
|
-
|
|
217
|
-
if specialized:
|
|
218
|
-
lines += ["### Agentes especializados del proyecto", ""]
|
|
219
|
-
for agent in specialized:
|
|
220
|
-
desc = read_agent_description(forge, agent, profiles)
|
|
221
|
-
lines.append(f"#### `{agent}`")
|
|
222
|
-
lines.append(f"{desc}")
|
|
223
|
-
lines.append("")
|
|
224
|
-
|
|
225
|
-
if frameworks:
|
|
226
|
-
lines += [
|
|
227
|
-
"## Compliance activo",
|
|
228
|
-
"",
|
|
229
|
-
f"Marcos regulatorios: {', '.join(f.upper() for f in frameworks)}",
|
|
230
|
-
"",
|
|
231
|
-
"Incluir `compliance-reviewer` en toda tarea que toque:",
|
|
232
|
-
"- Datos de usuarios o consentimientos",
|
|
233
|
-
"- Logs de auditoría",
|
|
234
|
-
"- Endpoints de derechos del titular (DSAR)",
|
|
235
|
-
"",
|
|
236
|
-
]
|
|
237
|
-
|
|
238
|
-
return "\n".join(lines)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def main():
|
|
242
|
-
try:
|
|
243
|
-
root = find_project_root()
|
|
244
|
-
forge = find_forge_dir()
|
|
245
|
-
except FileNotFoundError as e:
|
|
246
|
-
print(f"ERROR: {e}", file=sys.stderr)
|
|
247
|
-
sys.exit(1)
|
|
248
|
-
|
|
249
|
-
with open(root / "project.yaml") as f:
|
|
250
|
-
config = yaml.safe_load(f)
|
|
251
|
-
|
|
252
|
-
content = generate_agents_md(config, forge)
|
|
253
|
-
output_path = root / "AGENTS.md"
|
|
254
|
-
|
|
255
|
-
with open(output_path, "w") as f:
|
|
256
|
-
f.write(content)
|
|
257
|
-
|
|
258
|
-
print(f"AGENTS.md generado en {output_path}")
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if __name__ == "__main__":
|
|
262
|
-
main()
|
|
@@ -1,202 +0,0 @@
|
|
|
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()
|