@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.
Files changed (36) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +2 -1
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/init.js +1 -1
  5. package/dist/lib/paths.d.ts +1 -2
  6. package/dist/lib/paths.d.ts.map +1 -1
  7. package/dist/lib/paths.js +12 -16
  8. package/dist/lib/paths.js.map +1 -1
  9. package/dist/version.d.ts +1 -1
  10. package/dist/version.js +1 -1
  11. package/package.json +1 -1
  12. package/assets/adapters/claude-code/generate-claude-md.py +0 -304
  13. package/assets/adapters/codex/generate-codex-config.py +0 -269
  14. package/assets/adapters/kiro/generate-steering.py +0 -367
  15. package/assets/adapters/opencode/generate-agents-md.py +0 -262
  16. package/assets/core/hooks/pre-bash-check.py +0 -202
  17. package/assets/core/hooks/pre-edit-check.py +0 -317
  18. package/assets/forge.py +0 -1265
  19. package/assets/requirements.txt +0 -2
  20. package/assets/scripts/aitmpl-search.py +0 -808
  21. package/assets/scripts/forge-add-opportunities.py +0 -92
  22. package/assets/scripts/forge-audit.py +0 -1061
  23. package/assets/scripts/forge-generate-all.py +0 -283
  24. package/assets/scripts/forge-init.py +0 -900
  25. package/assets/scripts/forge-migrate-project-yaml.py +0 -397
  26. package/assets/scripts/forge-scaffold-profile.py +0 -181
  27. package/assets/scripts/forge-teardown.py +0 -193
  28. package/assets/scripts/forge-validate-project-yaml.py +0 -457
  29. package/assets/scripts/forge-wizard.py +0 -1003
  30. package/assets/scripts/setup-codex.sh +0 -229
  31. package/assets/scripts/team-install.sh +0 -147
  32. package/assets/scripts/token-stats.py +0 -201
  33. package/dist/lib/python.d.ts +0 -4
  34. package/dist/lib/python.d.ts.map +0 -1
  35. package/dist/lib/python.js +0 -46
  36. 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()