@cristiancorreau/forge 2.9.5 → 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/assets/adapters/claude-code/commands/new-feature.md +10 -5
- package/assets/adapters/claude-code/commands/plan.md +61 -36
- package/assets/adapters/claude-code/commands/session-start.md +1 -1
- package/assets/adapters/claude-code/commands/ship.md +6 -4
- package/assets/adapters/claude-code/commands/work.md +8 -6
- package/assets/core/skills/README.md +2 -2
- package/assets/core/skills/aitmpl-search/SKILL.md +7 -19
- package/assets/core/skills/local2prod/SKILL.md +1 -1
- package/assets/core/skills/new-feature/SKILL.md +1 -1
- package/assets/core/skills/phase-kickoff/SKILL.md +2 -0
- package/assets/core/skills/spec/SKILL.md +2 -0
- package/assets/core/skills/wiki-ingest/SKILL.md +7 -7
- package/assets/core/skills/wiki-lint/SKILL.md +4 -4
- package/assets/core/skills/wiki-query/SKILL.md +3 -3
- 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 +2 -2
- 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,367 +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-steering.py — Genera archivos .kiro/steering/ para Kiro IDE.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 .agentic/adapters/kiro/generate-steering.py
|
|
9
|
-
|
|
10
|
-
Lee project.yaml en la raíz y genera los steering files de Kiro:
|
|
11
|
-
.kiro/steering/product.md ← descripción del producto y stack
|
|
12
|
-
.kiro/steering/structure.md ← estructura del proyecto, workflow SDD y flujo de sesión Forge v2
|
|
13
|
-
.kiro/steering/agents.md ← roster de agentes y sus responsabilidades
|
|
14
|
-
.kiro/steering/commands.md ← 6 comandos Forge para contexto del agente Kiro
|
|
15
|
-
.kiro/steering/compliance.md ← reglas de compliance (si hay frameworks activos)
|
|
16
|
-
.kiro/hooks/pre-edit-branch-guard.json ← hook que bloquea ediciones directas en main/master
|
|
17
|
-
|
|
18
|
-
Kiro usa estos archivos como contexto persistente en todas las conversaciones.
|
|
19
|
-
|
|
20
|
-
Requiere: pyyaml
|
|
21
|
-
"""
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
import yaml
|
|
29
|
-
except ImportError:
|
|
30
|
-
print("ERROR: pyyaml requerido. pip install pyyaml", file=sys.stderr)
|
|
31
|
-
sys.exit(1)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def find_project_root() -> Path:
|
|
35
|
-
here = Path.cwd()
|
|
36
|
-
for p in [here] + list(here.parents):
|
|
37
|
-
if (p / "project.yaml").exists():
|
|
38
|
-
return p
|
|
39
|
-
raise FileNotFoundError("No se encontró project.yaml")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def find_forge_dir() -> Path:
|
|
43
|
-
root = find_project_root()
|
|
44
|
-
for candidate in [root / ".agentic", root / "forge", Path(__file__).parent.parent.parent]:
|
|
45
|
-
if (candidate / "core").exists():
|
|
46
|
-
return candidate
|
|
47
|
-
raise FileNotFoundError("No se encontró el directorio forge con core/")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def write_file(path: Path, content: str, force: bool = False) -> str:
|
|
51
|
-
if path.exists() and not force:
|
|
52
|
-
return "KEEP"
|
|
53
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
-
path.write_text(content)
|
|
55
|
-
return "OK"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def generate_product_md(config: dict) -> str:
|
|
59
|
-
proj = config.get("project", {})
|
|
60
|
-
stack = config.get("stack", {})
|
|
61
|
-
team = config.get("team", {})
|
|
62
|
-
|
|
63
|
-
return f"""# {proj.get('name', 'Mi Proyecto')}
|
|
64
|
-
|
|
65
|
-
{proj.get('description', '')}
|
|
66
|
-
|
|
67
|
-
## Stack
|
|
68
|
-
|
|
69
|
-
- **Lenguaje:** {proj.get('language', 'typescript')}
|
|
70
|
-
- **Backend:** {stack.get('backend') or 'N/A'}
|
|
71
|
-
- **Frontend:** {stack.get('frontend') or 'N/A'}
|
|
72
|
-
- **Base de datos:** {stack.get('database') or 'N/A'}
|
|
73
|
-
- **Cache:** {stack.get('cache') or 'N/A'}
|
|
74
|
-
- **Testing:** {', '.join(stack.get('testing', []))}
|
|
75
|
-
|
|
76
|
-
## Estado del proyecto
|
|
77
|
-
|
|
78
|
-
- **Status:** {proj.get('status', 'active')}
|
|
79
|
-
- **Equipo:** {team.get('name', 'Equipo Principal')}
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def generate_structure_md(config: dict) -> str:
|
|
84
|
-
proj = config.get("project", {})
|
|
85
|
-
paths = config.get("paths", {})
|
|
86
|
-
specs_path = paths.get("specs", "docs/specs")
|
|
87
|
-
language = proj.get("language", "typescript")
|
|
88
|
-
|
|
89
|
-
cmd_map = {
|
|
90
|
-
"typescript": ("pnpm dev", "pnpm test", "pnpm lint"),
|
|
91
|
-
"python": ("uvicorn main:app --reload # o python manage.py runserver", "pytest", "ruff check ."),
|
|
92
|
-
"ruby": ("rails server", "bundle exec rspec", "rubocop"),
|
|
93
|
-
"go": ("go run ./cmd/...", "go test ./...", "golangci-lint run"),
|
|
94
|
-
"php": ("php artisan serve", "vendor/bin/phpunit", "vendor/bin/phpstan analyse"),
|
|
95
|
-
}
|
|
96
|
-
dev_cmd, test_cmd, lint_cmd = cmd_map.get(language, ("# ver docs del proyecto",) * 3)
|
|
97
|
-
|
|
98
|
-
return f"""# Estructura del proyecto
|
|
99
|
-
|
|
100
|
-
## Workflow: Spec-Driven Development (SDD)
|
|
101
|
-
|
|
102
|
-
Antes de implementar cualquier feature:
|
|
103
|
-
|
|
104
|
-
1. Verificar que existe una spec en `{specs_path}/`.
|
|
105
|
-
2. Si no existe, crear la spec y obtener aprobación antes de codificar.
|
|
106
|
-
3. Implementar con tests junto al código, no al final.
|
|
107
|
-
4. Actualizar la spec con decisiones tomadas durante la implementación.
|
|
108
|
-
|
|
109
|
-
## Flujo de sesión — Forge v2
|
|
110
|
-
|
|
111
|
-
Cada sesión de trabajo sigue este orden:
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
session-start → Leer AGENTS.md + specs relevantes. Confirmar rama activa.
|
|
115
|
-
plan → Descomponer la tarea. Identificar spec. Presentar opciones si hay decisiones abiertas.
|
|
116
|
-
work → Implementar con tests. Un agente por scope. Actualizar spec si hay desvíos.
|
|
117
|
-
review → Lint + tests verdes. Compliance review si toca PII o auth.
|
|
118
|
-
ship → PR abierto, CI verde. No mergear sin revisión si >5 archivos del mismo módulo.
|
|
119
|
-
session-close → Reportar cambios, decisiones tomadas y próximos pasos.
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Comandos frecuentes
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
{dev_cmd} # Desarrollo
|
|
126
|
-
{test_cmd} # Tests
|
|
127
|
-
{lint_cmd} # Lint
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## Reglas no-negociables
|
|
131
|
-
|
|
132
|
-
- Sin hardcodear tokens, passwords ni secrets.
|
|
133
|
-
- Parámetros preparados en todas las queries SQL — nunca concatenar input del usuario.
|
|
134
|
-
- PII nunca en logs de stdout.
|
|
135
|
-
- Auth + authz verificados en cada endpoint.
|
|
136
|
-
- Sin force push a main/master.
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def generate_agents_md(config: dict) -> str:
|
|
141
|
-
agents_cfg = config.get("agents", {})
|
|
142
|
-
compliance_cfg = config.get("compliance", {})
|
|
143
|
-
|
|
144
|
-
active = agents_cfg.get("active", [])
|
|
145
|
-
compliance = agents_cfg.get("compliance", [])
|
|
146
|
-
specialized = agents_cfg.get("specialized", [])
|
|
147
|
-
frameworks = compliance_cfg.get("frameworks", [])
|
|
148
|
-
|
|
149
|
-
if frameworks and "compliance-reviewer" not in active + compliance:
|
|
150
|
-
compliance = list(set(compliance + ["compliance-reviewer"]))
|
|
151
|
-
|
|
152
|
-
role_descriptions = {
|
|
153
|
-
"orchestrator": "Coordina al team. Descompone tareas, delega y sintetiza. No implementa código directamente.",
|
|
154
|
-
"backend-engineer": "Backend — API, base de datos, lógica de negocio.",
|
|
155
|
-
"frontend-engineer": "Frontend — UI, componentes, integración con API.",
|
|
156
|
-
"api-engineer": "API — framework HTTP + ORM + lógica de negocio.",
|
|
157
|
-
"admin-engineer": "Admin dashboard — UI de gestión interna.",
|
|
158
|
-
"mobile-engineer": "Apps móviles.",
|
|
159
|
-
"fullstack-engineer": "Features full-stack (backend + frontend).",
|
|
160
|
-
"test-engineer": "Testing — unitario, integración, E2E.",
|
|
161
|
-
"docs-writer": "Documentación — specs, ADRs, READMEs.",
|
|
162
|
-
"compliance-reviewer": "Revisa cambios contra marcos regulatorios activos. Tiene poder de veto.",
|
|
163
|
-
"security-auditor": "Auditoría de seguridad — auth, authz, OWASP Top 10.",
|
|
164
|
-
"scanner-engineer": "Scanner de cookies y trackers.",
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
lines = [
|
|
168
|
-
"# Roster de agentes",
|
|
169
|
-
"",
|
|
170
|
-
"## Reglas operativas",
|
|
171
|
-
"",
|
|
172
|
-
"- El orchestrator coordina; los demás agentes no se comunican directamente entre sí.",
|
|
173
|
-
"- Cada agente respeta su scope — no sale del directorio que le corresponde.",
|
|
174
|
-
"- Specs en `docs/specs/` antes de implementar. Sin spec, sin código.",
|
|
175
|
-
"",
|
|
176
|
-
]
|
|
177
|
-
|
|
178
|
-
if active:
|
|
179
|
-
lines += ["## Agentes activos", ""]
|
|
180
|
-
for name in active:
|
|
181
|
-
desc = role_descriptions.get(name, "Agente de implementación")
|
|
182
|
-
lines.append(f"- **`{name}`** — {desc}")
|
|
183
|
-
lines.append("")
|
|
184
|
-
|
|
185
|
-
if compliance:
|
|
186
|
-
lines += ["## Agentes de revisión", ""]
|
|
187
|
-
for name in compliance:
|
|
188
|
-
desc = role_descriptions.get(name, "Agente revisor")
|
|
189
|
-
lines.append(f"- **`{name}`** — {desc}")
|
|
190
|
-
lines.append("")
|
|
191
|
-
|
|
192
|
-
if specialized:
|
|
193
|
-
lines += ["## Agentes especializados del proyecto", ""]
|
|
194
|
-
for name in specialized:
|
|
195
|
-
desc = role_descriptions.get(name, "Agente especializado del proyecto")
|
|
196
|
-
lines.append(f"- **`{name}`** — {desc}")
|
|
197
|
-
lines.append("")
|
|
198
|
-
|
|
199
|
-
return "\n".join(lines)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def generate_commands_md(config: dict) -> str:
|
|
203
|
-
paths = config.get("paths", {})
|
|
204
|
-
specs_path = paths.get("specs", "docs/specs")
|
|
205
|
-
|
|
206
|
-
return f"""# Forge commands — referencia para el agente
|
|
207
|
-
|
|
208
|
-
Este proyecto usa Forge (framework de agentic development). Los siguientes comandos
|
|
209
|
-
son los flujos de trabajo centrales que el agente debe seguir cuando el usuario los invoca.
|
|
210
|
-
|
|
211
|
-
## new-feature
|
|
212
|
-
|
|
213
|
-
Inicia la implementación de una nueva feature siguiendo SDD.
|
|
214
|
-
|
|
215
|
-
1. Si no existe spec para la feature en `{specs_path}/`, crearla primero.
|
|
216
|
-
2. Leer la spec antes de proponer cualquier implementación.
|
|
217
|
-
3. Proponer el plan y esperar aprobación explícita.
|
|
218
|
-
4. Implementar con tests junto a la implementación, no al final.
|
|
219
|
-
5. Al terminar, actualizar la spec con las decisiones tomadas.
|
|
220
|
-
|
|
221
|
-
## review
|
|
222
|
-
|
|
223
|
-
Revisa el estado actual de la rama activa.
|
|
224
|
-
|
|
225
|
-
1. Listar archivos modificados (`git diff --name-only main`).
|
|
226
|
-
2. Ejecutar tests y lint. Reportar resultados.
|
|
227
|
-
3. Verificar que los cambios corresponden a una spec aprobada.
|
|
228
|
-
4. Identificar items pendientes antes de poder hacer merge.
|
|
229
|
-
|
|
230
|
-
## deploy-check
|
|
231
|
-
|
|
232
|
-
Verifica que el deploy de producción está sano después de un merge.
|
|
233
|
-
|
|
234
|
-
1. Confirmar que el build de CI pasó.
|
|
235
|
-
2. Revisar runtime logs (errores 5xx, excepciones no manejadas).
|
|
236
|
-
3. Smoke test de los endpoints principales.
|
|
237
|
-
4. Reportar estado: OK | DEGRADED | DOWN.
|
|
238
|
-
|
|
239
|
-
## wiki-ingest
|
|
240
|
-
|
|
241
|
-
Indexa documentación externa para que el agente pueda consultarla.
|
|
242
|
-
|
|
243
|
-
1. Recibir URL o path a la documentación.
|
|
244
|
-
2. Parsear y fragmentar el contenido.
|
|
245
|
-
3. Almacenar en el directorio de wiki del proyecto.
|
|
246
|
-
4. Confirmar qué secciones quedaron disponibles.
|
|
247
|
-
|
|
248
|
-
## wiki-query
|
|
249
|
-
|
|
250
|
-
Consulta la wiki indexada para responder preguntas técnicas.
|
|
251
|
-
|
|
252
|
-
1. Buscar en los fragmentos indexados con la consulta del usuario.
|
|
253
|
-
2. Sintetizar la respuesta citando las fuentes.
|
|
254
|
-
3. Si no hay resultado en la wiki, indicarlo explícitamente — no inventar.
|
|
255
|
-
|
|
256
|
-
## wiki-lint
|
|
257
|
-
|
|
258
|
-
Verifica la calidad y consistencia de la wiki indexada.
|
|
259
|
-
|
|
260
|
-
1. Detectar fragmentos desactualizados (referencias a versiones antiguas).
|
|
261
|
-
2. Identificar contradicciones entre documentos.
|
|
262
|
-
3. Reportar items que requieren revisión manual.
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def generate_branch_guard_hook() -> str:
|
|
267
|
-
"""Genera el hook JSON de Kiro que advierte al editar en main/master."""
|
|
268
|
-
import json
|
|
269
|
-
hook = {
|
|
270
|
-
"name": "branch-guard",
|
|
271
|
-
"description": "Warns when editing files directly on main or master branch.",
|
|
272
|
-
"event": "onBeforeEdit",
|
|
273
|
-
"condition": {
|
|
274
|
-
"type": "gitBranch",
|
|
275
|
-
"branches": ["main", "master"]
|
|
276
|
-
},
|
|
277
|
-
"action": {
|
|
278
|
-
"type": "agentPrompt",
|
|
279
|
-
"prompt": (
|
|
280
|
-
"STOP: You are about to edit files on the '{{branch}}' branch. "
|
|
281
|
-
"Direct commits to main/master are not allowed in this project. "
|
|
282
|
-
"Create a feature branch first: `git checkout -b feat/<name>`. "
|
|
283
|
-
"Do not proceed with the edit until the user confirms or switches branch."
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return json.dumps(hook, indent=2, ensure_ascii=False)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def generate_compliance_md(config: dict) -> str | None:
|
|
291
|
-
compliance_cfg = config.get("compliance", {})
|
|
292
|
-
frameworks = compliance_cfg.get("frameworks", [])
|
|
293
|
-
if not frameworks:
|
|
294
|
-
return None
|
|
295
|
-
|
|
296
|
-
return f"""# Compliance activo
|
|
297
|
-
|
|
298
|
-
Este proyecto opera bajo los siguientes marcos regulatorios:
|
|
299
|
-
{chr(10).join(f'- **{f.upper()}**' for f in frameworks)}
|
|
300
|
-
|
|
301
|
-
## Reglas no-negociables
|
|
302
|
-
|
|
303
|
-
- Sin datos personales en logs de stdout.
|
|
304
|
-
- Consentimiento explícito antes de cualquier tracker no esencial.
|
|
305
|
-
- Logs de auditoría append-only (sin UPDATE/DELETE sobre tablas de eventos).
|
|
306
|
-
- Derechos del titular implementados: acceso, rectificación, supresión, oposición, portabilidad.
|
|
307
|
-
- TLS 1.2+ para toda comunicación externa.
|
|
308
|
-
|
|
309
|
-
## Cuándo involucrar al compliance-reviewer
|
|
310
|
-
|
|
311
|
-
- Cualquier cambio que toque datos de usuarios o consentimientos.
|
|
312
|
-
- Nuevos endpoints que reciban PII.
|
|
313
|
-
- Cambios en la lógica de logs de auditoría.
|
|
314
|
-
- Implementación de derechos del titular (DSAR).
|
|
315
|
-
|
|
316
|
-
> Nota: el agente compliance-reviewer opera como primer filtro técnico.
|
|
317
|
-
> No sustituye revisión legal profesional para proyectos con obligaciones regulatorias reales.
|
|
318
|
-
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def main():
|
|
322
|
-
force = "--force" in sys.argv
|
|
323
|
-
|
|
324
|
-
try:
|
|
325
|
-
root = find_project_root()
|
|
326
|
-
forge = find_forge_dir()
|
|
327
|
-
except FileNotFoundError as e:
|
|
328
|
-
print(f"ERROR: {e}", file=sys.stderr)
|
|
329
|
-
sys.exit(1)
|
|
330
|
-
|
|
331
|
-
with open(root / "project.yaml") as f:
|
|
332
|
-
config = yaml.safe_load(f)
|
|
333
|
-
|
|
334
|
-
steering_dir = root / ".kiro" / "steering"
|
|
335
|
-
steering_dir.mkdir(parents=True, exist_ok=True)
|
|
336
|
-
|
|
337
|
-
files = {
|
|
338
|
-
"product.md": generate_product_md(config),
|
|
339
|
-
"structure.md": generate_structure_md(config),
|
|
340
|
-
"agents.md": generate_agents_md(config),
|
|
341
|
-
"commands.md": generate_commands_md(config),
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
compliance_content = generate_compliance_md(config)
|
|
345
|
-
if compliance_content:
|
|
346
|
-
files["compliance.md"] = compliance_content
|
|
347
|
-
|
|
348
|
-
for filename, content in files.items():
|
|
349
|
-
path = steering_dir / filename
|
|
350
|
-
status = write_file(path, content, force=force)
|
|
351
|
-
icon = "[OK] " if status == "OK" else "[KEEP]"
|
|
352
|
-
print(f" {icon} .kiro/steering/{filename}" + (" — ya existe (--force para sobreescribir)" if status == "KEEP" else ""))
|
|
353
|
-
|
|
354
|
-
# Hooks
|
|
355
|
-
hooks_dir = root / ".kiro" / "hooks"
|
|
356
|
-
hook_path = hooks_dir / "pre-edit-branch-guard.json"
|
|
357
|
-
hook_status = write_file(hook_path, generate_branch_guard_hook(), force=force)
|
|
358
|
-
hook_icon = "[OK] " if hook_status == "OK" else "[KEEP]"
|
|
359
|
-
print(f" {hook_icon} .kiro/hooks/pre-edit-branch-guard.json" + (" — ya existe (--force para sobreescribir)" if hook_status == "KEEP" else ""))
|
|
360
|
-
|
|
361
|
-
print(f"\nKiro steering files en {steering_dir.relative_to(root)}/")
|
|
362
|
-
if not force:
|
|
363
|
-
print(" Usar --force para sobreescribir archivos existentes.")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if __name__ == "__main__":
|
|
367
|
-
main()
|
|
@@ -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()
|