@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,397 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
forge-migrate-project-yaml.py — Migra un project.yaml de v1 a v2 de Forge.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python3 .agentic/scripts/forge-migrate-project-yaml.py
|
|
7
|
-
python3 .agentic/scripts/forge-migrate-project-yaml.py --dry-run
|
|
8
|
-
python3 .agentic/scripts/forge-migrate-project-yaml.py --backup
|
|
9
|
-
|
|
10
|
-
Detecta si el project.yaml es v1 (sin secciones 'deploy' estructurado, 'rules', 'mcp', 'github')
|
|
11
|
-
y agrega las secciones nuevas preservando todos los valores existentes.
|
|
12
|
-
|
|
13
|
-
Exit codes:
|
|
14
|
-
0 — migración exitosa (o ya era v2)
|
|
15
|
-
1 — error
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import sys
|
|
19
|
-
import os
|
|
20
|
-
import re
|
|
21
|
-
import shutil
|
|
22
|
-
import argparse
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import Any, Optional, Tuple
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# ---------------------------------------------------------------------------
|
|
28
|
-
# Búsqueda de project.yaml
|
|
29
|
-
# ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
def find_project_yaml(start: Path) -> Optional[Path]:
|
|
32
|
-
current = start.resolve()
|
|
33
|
-
while True:
|
|
34
|
-
candidate = current / "project.yaml"
|
|
35
|
-
if candidate.exists():
|
|
36
|
-
return candidate
|
|
37
|
-
parent = current.parent
|
|
38
|
-
if parent == current:
|
|
39
|
-
return None
|
|
40
|
-
current = parent
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
# Carga YAML
|
|
45
|
-
# ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
def load_yaml_raw(path: Path) -> Tuple[dict, str]:
|
|
48
|
-
"""
|
|
49
|
-
Carga el YAML y retorna (data, raw_text).
|
|
50
|
-
Requiere pyyaml.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
import yaml
|
|
54
|
-
except ImportError:
|
|
55
|
-
print("ERROR: pyyaml no está instalado. Instalarlo con: pip install pyyaml")
|
|
56
|
-
sys.exit(1)
|
|
57
|
-
|
|
58
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
59
|
-
raw = f.read()
|
|
60
|
-
|
|
61
|
-
data = yaml.safe_load(raw)
|
|
62
|
-
return (data if data is not None else {}), raw
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# ---------------------------------------------------------------------------
|
|
66
|
-
# Detección de versión
|
|
67
|
-
# ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
def detect_version(data: dict) -> str:
|
|
70
|
-
"""
|
|
71
|
-
Detecta si el project.yaml es v1 o v2.
|
|
72
|
-
Criterio: si tiene 'rules' O 'mcp' O 'github' O 'project.mode' → es v2.
|
|
73
|
-
"""
|
|
74
|
-
if "rules" in data or "mcp" in data or "github" in data:
|
|
75
|
-
return "2"
|
|
76
|
-
project = data.get("project", {}) or {}
|
|
77
|
-
if "mode" in project:
|
|
78
|
-
return "2"
|
|
79
|
-
return "1"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# ---------------------------------------------------------------------------
|
|
83
|
-
# Generador del bloque v2 en YAML comentado
|
|
84
|
-
# ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
V2_ADDITIONS = """\
|
|
87
|
-
# ---------------------------------------------------------------------------
|
|
88
|
-
# schema_version: "2"
|
|
89
|
-
# Las siguientes secciones fueron agregadas por forge-migrate-project-yaml.py
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
# nuevo en v2: modo operativo del proyecto
|
|
93
|
-
# Si ya existe project.mode, puedes ignorar este comentario.
|
|
94
|
-
|
|
95
|
-
agents:
|
|
96
|
-
# nuevo en v2: mapeo rol → modelo específico de Claude
|
|
97
|
-
by_role:
|
|
98
|
-
orchestrator: null # ej: claude-opus-4-7
|
|
99
|
-
senior-backend: null # ej: claude-sonnet-4-6
|
|
100
|
-
|
|
101
|
-
deploy:
|
|
102
|
-
# nuevo en v2
|
|
103
|
-
provider: null # vercel | railway | fly | aws | github-actions | custom | null
|
|
104
|
-
project_id: null # ID del proyecto en la plataforma (ej: prj_xxx en Vercel)
|
|
105
|
-
production_url: null # https://mi-proyecto.vercel.app
|
|
106
|
-
smoke_tests: [] # Tests de humo post-deploy
|
|
107
|
-
# Ejemplo:
|
|
108
|
-
# smoke_tests:
|
|
109
|
-
# - url: /api/health
|
|
110
|
-
# expect_status: 200
|
|
111
|
-
# expect_json:
|
|
112
|
-
# status: ok
|
|
113
|
-
# - url: https://mi-proyecto.vercel.app
|
|
114
|
-
# expect_status: 200
|
|
115
|
-
|
|
116
|
-
mcp:
|
|
117
|
-
# nuevo en v2: servidores MCP del proyecto
|
|
118
|
-
servers: []
|
|
119
|
-
# Ejemplo:
|
|
120
|
-
# servers:
|
|
121
|
-
# - name: supabase
|
|
122
|
-
# auto_approve:
|
|
123
|
-
# - list_tables
|
|
124
|
-
# - execute_sql
|
|
125
|
-
|
|
126
|
-
github:
|
|
127
|
-
# nuevo en v2: integración con GitHub Projects
|
|
128
|
-
project:
|
|
129
|
-
number: null # Número del GitHub Project
|
|
130
|
-
owner: null # usuario u organización
|
|
131
|
-
repo: null # nombre del repositorio
|
|
132
|
-
status_field_id: null # ID del campo Status
|
|
133
|
-
status_in_progress: null # ej: "In Progress"
|
|
134
|
-
status_done: null # ej: "Done"
|
|
135
|
-
|
|
136
|
-
rules:
|
|
137
|
-
# nuevo en v2: guardrails del proyecto
|
|
138
|
-
forbidden_in_production:
|
|
139
|
-
- "console.log" # eliminar logs de debug en producción
|
|
140
|
-
- "TODO:" # no dejar TODOs sin resolver
|
|
141
|
-
- "FIXME:"
|
|
142
|
-
required_review_before_ship: false
|
|
143
|
-
require_spec_before_implementation: false
|
|
144
|
-
conventional_commits: true
|
|
145
|
-
forbidden_patterns: [] # regex evaluadas por el hook pre-edit-check
|
|
146
|
-
# Ejemplo:
|
|
147
|
-
# forbidden_patterns:
|
|
148
|
-
# - "process\\.env\\.[A-Z_]+\\s*=\\s*['\\\"][^'\\\"]+['\\\"]" # hardcoded env values
|
|
149
|
-
|
|
150
|
-
scripts:
|
|
151
|
-
# nuevo en v2: comando ejecutado por post-turn-check.sh
|
|
152
|
-
check: null # ej: "pnpm typecheck && pnpm lint"
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def build_v2_yaml(data: dict, raw: str) -> str:
|
|
157
|
-
"""
|
|
158
|
-
Construye el YAML v2 completo.
|
|
159
|
-
Estrategia: agrega un encabezado de schema_version y las secciones nuevas al final,
|
|
160
|
-
preservando el contenido original.
|
|
161
|
-
|
|
162
|
-
Para las secciones que ya existen en el original (como 'deploy' con solo 'provider' y 'branch'),
|
|
163
|
-
se hace un merge inteligente para no duplicar.
|
|
164
|
-
"""
|
|
165
|
-
try:
|
|
166
|
-
import yaml
|
|
167
|
-
except ImportError:
|
|
168
|
-
print("ERROR: pyyaml no está instalado.")
|
|
169
|
-
sys.exit(1)
|
|
170
|
-
|
|
171
|
-
# Secciones que ya existen en el YAML original
|
|
172
|
-
existing_sections = set(data.keys())
|
|
173
|
-
|
|
174
|
-
lines_to_add = []
|
|
175
|
-
|
|
176
|
-
# Encabezado
|
|
177
|
-
lines_to_add.append("# ---------------------------------------------------------------------------")
|
|
178
|
-
lines_to_add.append('# schema_version: "2"')
|
|
179
|
-
lines_to_add.append("# Las siguientes secciones fueron agregadas por forge-migrate-project-yaml.py")
|
|
180
|
-
lines_to_add.append("# ---------------------------------------------------------------------------")
|
|
181
|
-
lines_to_add.append("")
|
|
182
|
-
|
|
183
|
-
# project.mode — agregar si no existe
|
|
184
|
-
project = data.get("project", {}) or {}
|
|
185
|
-
needs_project_mode = "mode" not in project
|
|
186
|
-
|
|
187
|
-
# agents.by_role — agregar si agents existe pero no tiene by_role
|
|
188
|
-
agents = data.get("agents", {}) or {}
|
|
189
|
-
needs_agents_by_role = "agents" in existing_sections and "by_role" not in agents
|
|
190
|
-
|
|
191
|
-
# Secciones completamente nuevas
|
|
192
|
-
needs_deploy_new_fields = True # siempre agregar campos nuevos de deploy
|
|
193
|
-
needs_mcp = "mcp" not in existing_sections
|
|
194
|
-
needs_github = "github" not in existing_sections
|
|
195
|
-
needs_rules = "rules" not in existing_sections
|
|
196
|
-
needs_scripts = "scripts" not in existing_sections
|
|
197
|
-
|
|
198
|
-
# --- project.mode patch ---
|
|
199
|
-
if needs_project_mode:
|
|
200
|
-
lines_to_add.append("# ACCIÓN REQUERIDA: Agrega 'mode' a la sección project arriba.")
|
|
201
|
-
lines_to_add.append("# Valores: startup | standard | enterprise")
|
|
202
|
-
lines_to_add.append("# Ejemplo:")
|
|
203
|
-
lines_to_add.append("# project:")
|
|
204
|
-
lines_to_add.append('# mode: "standard"')
|
|
205
|
-
lines_to_add.append("")
|
|
206
|
-
|
|
207
|
-
# --- agents.by_role ---
|
|
208
|
-
if needs_agents_by_role:
|
|
209
|
-
lines_to_add.append("# nuevo en v2 — agregar bajo la sección agents existente:")
|
|
210
|
-
lines_to_add.append("# agents:")
|
|
211
|
-
lines_to_add.append("# by_role:")
|
|
212
|
-
lines_to_add.append("# orchestrator: claude-opus-4-7")
|
|
213
|
-
lines_to_add.append("# senior-backend: claude-sonnet-4-6")
|
|
214
|
-
lines_to_add.append("")
|
|
215
|
-
|
|
216
|
-
# --- deploy (campos nuevos de v2) ---
|
|
217
|
-
deploy = data.get("deploy", {}) or {}
|
|
218
|
-
existing_deploy_keys = set(deploy.keys()) if isinstance(deploy, dict) else set()
|
|
219
|
-
deploy_new_fields = []
|
|
220
|
-
|
|
221
|
-
if "project_id" not in existing_deploy_keys:
|
|
222
|
-
deploy_new_fields.append(" project_id: null # ID del proyecto en la plataforma (ej: prj_xxx en Vercel)")
|
|
223
|
-
if "production_url" not in existing_deploy_keys:
|
|
224
|
-
deploy_new_fields.append(" production_url: null # https://mi-proyecto.vercel.app")
|
|
225
|
-
if "smoke_tests" not in existing_deploy_keys:
|
|
226
|
-
deploy_new_fields.append(" smoke_tests: [] # Tests de humo post-deploy")
|
|
227
|
-
deploy_new_fields.append(" # Ejemplo de smoke test:")
|
|
228
|
-
deploy_new_fields.append(" # smoke_tests:")
|
|
229
|
-
deploy_new_fields.append(" # - url: /api/health")
|
|
230
|
-
deploy_new_fields.append(" # expect_status: 200")
|
|
231
|
-
deploy_new_fields.append(" # expect_json:")
|
|
232
|
-
deploy_new_fields.append(" # status: ok")
|
|
233
|
-
|
|
234
|
-
if deploy_new_fields:
|
|
235
|
-
if "deploy" in existing_sections:
|
|
236
|
-
lines_to_add.append("# nuevo en v2 — campos adicionales para la sección deploy existente:")
|
|
237
|
-
lines_to_add.append("# (agregar manualmente bajo la sección deploy arriba)")
|
|
238
|
-
for line in deploy_new_fields:
|
|
239
|
-
lines_to_add.append("# " + line.lstrip())
|
|
240
|
-
else:
|
|
241
|
-
lines_to_add.append("")
|
|
242
|
-
lines_to_add.append("deploy: # nuevo en v2")
|
|
243
|
-
if "provider" not in existing_deploy_keys:
|
|
244
|
-
lines_to_add.append(" provider: null # vercel | railway | fly | aws | github-actions | custom | null")
|
|
245
|
-
lines_to_add.extend(deploy_new_fields)
|
|
246
|
-
lines_to_add.append("")
|
|
247
|
-
|
|
248
|
-
# --- mcp ---
|
|
249
|
-
if needs_mcp:
|
|
250
|
-
lines_to_add.append("mcp: # nuevo en v2")
|
|
251
|
-
lines_to_add.append(" servers: []")
|
|
252
|
-
lines_to_add.append(" # Ejemplo:")
|
|
253
|
-
lines_to_add.append(" # servers:")
|
|
254
|
-
lines_to_add.append(" # - name: supabase")
|
|
255
|
-
lines_to_add.append(" # auto_approve:")
|
|
256
|
-
lines_to_add.append(" # - list_tables")
|
|
257
|
-
lines_to_add.append(" # - execute_sql")
|
|
258
|
-
lines_to_add.append("")
|
|
259
|
-
|
|
260
|
-
# --- github ---
|
|
261
|
-
if needs_github:
|
|
262
|
-
lines_to_add.append("github: # nuevo en v2")
|
|
263
|
-
lines_to_add.append(" project:")
|
|
264
|
-
lines_to_add.append(" number: null # Número del GitHub Project")
|
|
265
|
-
lines_to_add.append(" owner: null # usuario u organización")
|
|
266
|
-
lines_to_add.append(" repo: null # nombre del repositorio")
|
|
267
|
-
lines_to_add.append(" status_field_id: null # ID del campo Status")
|
|
268
|
-
lines_to_add.append(' status_in_progress: null # ej: "In Progress"')
|
|
269
|
-
lines_to_add.append(' status_done: null # ej: "Done"')
|
|
270
|
-
lines_to_add.append("")
|
|
271
|
-
|
|
272
|
-
# --- rules ---
|
|
273
|
-
if needs_rules:
|
|
274
|
-
lines_to_add.append("rules: # nuevo en v2")
|
|
275
|
-
lines_to_add.append(" forbidden_in_production:")
|
|
276
|
-
lines_to_add.append(' - "console.log"')
|
|
277
|
-
lines_to_add.append(' - "TODO:"')
|
|
278
|
-
lines_to_add.append(' - "FIXME:"')
|
|
279
|
-
lines_to_add.append(" required_review_before_ship: false")
|
|
280
|
-
lines_to_add.append(" require_spec_before_implementation: false")
|
|
281
|
-
lines_to_add.append(" conventional_commits: true")
|
|
282
|
-
lines_to_add.append(" forbidden_patterns: []")
|
|
283
|
-
lines_to_add.append("")
|
|
284
|
-
|
|
285
|
-
# --- scripts ---
|
|
286
|
-
if needs_scripts:
|
|
287
|
-
lines_to_add.append("scripts: # nuevo en v2")
|
|
288
|
-
lines_to_add.append(' check: null # ej: "pnpm typecheck && pnpm lint"')
|
|
289
|
-
lines_to_add.append("")
|
|
290
|
-
|
|
291
|
-
if not lines_to_add or all(l == "" for l in lines_to_add):
|
|
292
|
-
return raw # nada que agregar
|
|
293
|
-
|
|
294
|
-
# Construir resultado final
|
|
295
|
-
additions = "\n".join(lines_to_add)
|
|
296
|
-
result = raw.rstrip("\n") + "\n\n" + additions
|
|
297
|
-
return result
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# ---------------------------------------------------------------------------
|
|
301
|
-
# Diff simple
|
|
302
|
-
# ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
def show_diff(original: str, new: str):
|
|
305
|
-
"""Muestra un diff básico entre el contenido original y el nuevo."""
|
|
306
|
-
orig_lines = original.splitlines(keepends=True)
|
|
307
|
-
new_lines = new.splitlines(keepends=True)
|
|
308
|
-
|
|
309
|
-
import difflib
|
|
310
|
-
diff = difflib.unified_diff(
|
|
311
|
-
orig_lines, new_lines,
|
|
312
|
-
fromfile="project.yaml (v1)",
|
|
313
|
-
tofile="project.yaml (v2)",
|
|
314
|
-
lineterm=""
|
|
315
|
-
)
|
|
316
|
-
diff_text = "".join(diff)
|
|
317
|
-
if diff_text:
|
|
318
|
-
print(diff_text)
|
|
319
|
-
else:
|
|
320
|
-
print("(sin cambios)")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
# ---------------------------------------------------------------------------
|
|
324
|
-
# Main
|
|
325
|
-
# ---------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
def main():
|
|
328
|
-
parser = argparse.ArgumentParser(
|
|
329
|
-
description="Migra project.yaml de v1 a v2 de Forge"
|
|
330
|
-
)
|
|
331
|
-
parser.add_argument(
|
|
332
|
-
"--dry-run",
|
|
333
|
-
action="store_true",
|
|
334
|
-
help="Muestra el diff sin escribir cambios"
|
|
335
|
-
)
|
|
336
|
-
parser.add_argument(
|
|
337
|
-
"--backup",
|
|
338
|
-
action="store_true",
|
|
339
|
-
help="Crea project.yaml.bak antes de modificar"
|
|
340
|
-
)
|
|
341
|
-
args = parser.parse_args()
|
|
342
|
-
|
|
343
|
-
# Buscar project.yaml
|
|
344
|
-
project_yaml_path = find_project_yaml(Path.cwd())
|
|
345
|
-
if project_yaml_path is None:
|
|
346
|
-
print("ERROR: No se encontró project.yaml en el directorio actual ni superiores")
|
|
347
|
-
sys.exit(1)
|
|
348
|
-
|
|
349
|
-
print(f"Leyendo: {project_yaml_path}")
|
|
350
|
-
|
|
351
|
-
# Cargar
|
|
352
|
-
data, raw = load_yaml_raw(project_yaml_path)
|
|
353
|
-
|
|
354
|
-
# Detectar versión
|
|
355
|
-
version = detect_version(data)
|
|
356
|
-
if version == "2":
|
|
357
|
-
print("El project.yaml ya está en v2 (contiene secciones 'rules', 'mcp', 'github' o 'project.mode').")
|
|
358
|
-
print("No se requiere migración.")
|
|
359
|
-
sys.exit(0)
|
|
360
|
-
|
|
361
|
-
print(f"Versión detectada: v{version} → migrando a v2...")
|
|
362
|
-
print()
|
|
363
|
-
|
|
364
|
-
# Construir nuevo contenido
|
|
365
|
-
new_content = build_v2_yaml(data, raw)
|
|
366
|
-
|
|
367
|
-
if args.dry_run:
|
|
368
|
-
print("--- DRY RUN: mostrando diff (no se escribirán cambios) ---")
|
|
369
|
-
print()
|
|
370
|
-
show_diff(raw, new_content)
|
|
371
|
-
sys.exit(0)
|
|
372
|
-
|
|
373
|
-
# Backup
|
|
374
|
-
if args.backup:
|
|
375
|
-
backup_path = project_yaml_path.with_suffix(".yaml.bak")
|
|
376
|
-
shutil.copy2(project_yaml_path, backup_path)
|
|
377
|
-
print(f"Backup creado: {backup_path}")
|
|
378
|
-
|
|
379
|
-
# Escribir
|
|
380
|
-
with open(project_yaml_path, "w", encoding="utf-8") as f:
|
|
381
|
-
f.write(new_content)
|
|
382
|
-
|
|
383
|
-
print(f"Migración completada: {project_yaml_path}")
|
|
384
|
-
print()
|
|
385
|
-
print("Próximos pasos:")
|
|
386
|
-
print(" 1. Revisa el archivo y completa los campos null con valores reales")
|
|
387
|
-
print(" 2. Agrega project.mode (startup | standard | enterprise) si no está")
|
|
388
|
-
print(" 3. Valida con: python3 .agentic/scripts/forge-validate-project-yaml.py")
|
|
389
|
-
print()
|
|
390
|
-
|
|
391
|
-
# Mostrar diff informativo
|
|
392
|
-
print("--- Cambios aplicados ---")
|
|
393
|
-
show_diff(raw, new_content)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if __name__ == "__main__":
|
|
397
|
-
main()
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
forge-scaffold-profile.py — Crea un profile Tier 2 para un stack no cubierto por forge.
|
|
4
|
-
|
|
5
|
-
Uso:
|
|
6
|
-
python3 .agentic/scripts/forge-scaffold-profile.py --name django --engineer api-engineer
|
|
7
|
-
python3 .agentic/scripts/forge-scaffold-profile.py \\
|
|
8
|
-
--name django --engineer api-engineer \\
|
|
9
|
-
--description "Backend Django con DRF" \\
|
|
10
|
-
--stack-details "Django 4.2 + PostgreSQL + Django REST Framework"
|
|
11
|
-
"""
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import argparse
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Optional
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _find_forge_dir() -> Path:
|
|
21
|
-
here = Path(__file__).resolve().parent
|
|
22
|
-
for candidate in [here.parent, here.parent.parent]:
|
|
23
|
-
if (candidate / "core").exists() and (candidate / "profiles").exists():
|
|
24
|
-
return candidate
|
|
25
|
-
for p in [Path.cwd()] + list(Path.cwd().parents):
|
|
26
|
-
for sub in [p / ".agentic", p / "forge"]:
|
|
27
|
-
if (sub / "core").exists() and (sub / "profiles").exists():
|
|
28
|
-
return sub
|
|
29
|
-
raise FileNotFoundError(
|
|
30
|
-
"No se encontró el directorio forge con core/ y profiles/. "
|
|
31
|
-
"Ejecutar desde el repositorio de forge o desde un proyecto que lo tenga instalado."
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _agent_md(
|
|
36
|
-
name: str,
|
|
37
|
-
engineer: str,
|
|
38
|
-
description: str,
|
|
39
|
-
stack_details: str,
|
|
40
|
-
) -> str:
|
|
41
|
-
slug_title = name.replace("-", " ").title()
|
|
42
|
-
eng_title = engineer.replace("-", " ").title()
|
|
43
|
-
|
|
44
|
-
desc_line = (
|
|
45
|
-
description
|
|
46
|
-
if description
|
|
47
|
-
else f"Implementa el backend del proyecto usando {slug_title}. NO trabaja fuera del directorio definido en project.yaml."
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
stack_block = (
|
|
51
|
-
stack_details
|
|
52
|
-
if stack_details
|
|
53
|
-
else f"- **Framework:** {slug_title}\n- **Lenguaje:** (especificar)\n- **ORM/DB:** (especificar)\n- **Tests:** (especificar)"
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
return f"""\
|
|
57
|
-
---
|
|
58
|
-
name: {engineer}
|
|
59
|
-
description: {desc_line}
|
|
60
|
-
model: sonnet
|
|
61
|
-
tools: Read, Grep, Glob, Bash, Edit, Write
|
|
62
|
-
tier: 2
|
|
63
|
-
profile: {name}
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
# {eng_title} — {slug_title}
|
|
67
|
-
|
|
68
|
-
Implementás el backend del proyecto con {slug_title}. Tu scope es el directorio
|
|
69
|
-
definido en el `CLAUDE.md` del proyecto. Leé ese archivo antes de empezar.
|
|
70
|
-
|
|
71
|
-
## Stack
|
|
72
|
-
|
|
73
|
-
{stack_block}
|
|
74
|
-
|
|
75
|
-
## Tu trabajo
|
|
76
|
-
|
|
77
|
-
- Implementar endpoints, modelos y migraciones según las specs en `docs/specs/`.
|
|
78
|
-
- Escribir tests unitarios y de integración para toda la lógica nueva.
|
|
79
|
-
- Correr el linter, typecheck y tests antes de reportar al orchestrator.
|
|
80
|
-
- Proponer un plan antes de codificar cuando la tarea afecte >3 archivos.
|
|
81
|
-
|
|
82
|
-
## Reglas
|
|
83
|
-
|
|
84
|
-
- **Logs de auditoría son append-only.** NUNCA `UPDATE` ni `DELETE` sobre tablas de eventos.
|
|
85
|
-
- **PII nunca en logs.** Solo IDs o indicadores no reversibles.
|
|
86
|
-
- **Parámetros preparados siempre:** nunca interpolar input del usuario en SQL.
|
|
87
|
-
- **Auth + authz en cada endpoint:** verificar sesión Y permisos por recurso.
|
|
88
|
-
- **Migraciones reversibles:** toda migración tiene su operación inversa documentada.
|
|
89
|
-
- Sin spec en `docs/specs/` → no empieces. Pedí que se cree primero.
|
|
90
|
-
|
|
91
|
-
## Workflow
|
|
92
|
-
|
|
93
|
-
1. Leer el `CLAUDE.md` del proyecto y la spec de la feature activa.
|
|
94
|
-
2. Revisar el data model si la tarea toca schema.
|
|
95
|
-
3. Si la tarea toca compliance, informar al compliance-reviewer antes de implementar.
|
|
96
|
-
4. Implementar con tests (TDD para lógica core, integración para endpoints).
|
|
97
|
-
5. Correr tests + linter + typecheck.
|
|
98
|
-
6. Reportar al orchestrator: qué se hizo, qué archivos se tocaron, qué falta.
|
|
99
|
-
|
|
100
|
-
## No hagas
|
|
101
|
-
|
|
102
|
-
- No salgas del directorio de API/backend del proyecto.
|
|
103
|
-
- No implementes lógica de UI ni de frontend.
|
|
104
|
-
- No modifiques specs ni documentación de arquitectura sin aprobación.
|
|
105
|
-
- No mergees ni crees PRs directamente.
|
|
106
|
-
- No uses queries SQL raw con interpolación de strings.
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def main() -> None:
|
|
111
|
-
parser = argparse.ArgumentParser(
|
|
112
|
-
description="Crea un profile Tier 2 para un stack no cubierto por forge."
|
|
113
|
-
)
|
|
114
|
-
parser.add_argument("--name", required=True, metavar="SLUG", help="Nombre del profile (ej: django)")
|
|
115
|
-
parser.add_argument("--engineer", required=True, metavar="AGENT", help="Nombre del agente (ej: api-engineer)")
|
|
116
|
-
parser.add_argument("--description", default="", metavar="DESC", help="Descripción breve del agente")
|
|
117
|
-
parser.add_argument("--stack-details", default="", metavar="DETAILS", help="Detalles del stack (tecnologías, versiones)")
|
|
118
|
-
|
|
119
|
-
args = parser.parse_args()
|
|
120
|
-
|
|
121
|
-
name: str = args.name.strip().lower()
|
|
122
|
-
engineer: str = args.engineer.strip().lower()
|
|
123
|
-
|
|
124
|
-
if not name or not engineer:
|
|
125
|
-
print("Error: --name y --engineer son obligatorios y no pueden estar vacíos.", file=sys.stderr)
|
|
126
|
-
sys.exit(1)
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
forge = _find_forge_dir()
|
|
130
|
-
except FileNotFoundError as e:
|
|
131
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
132
|
-
sys.exit(1)
|
|
133
|
-
|
|
134
|
-
profile_dir = forge / "profiles" / name / "agents"
|
|
135
|
-
agent_file = profile_dir / f"{engineer}.md"
|
|
136
|
-
|
|
137
|
-
if agent_file.exists():
|
|
138
|
-
print(f"Error: {agent_file} ya existe. Editar manualmente si querés actualizarlo.", file=sys.stderr)
|
|
139
|
-
sys.exit(1)
|
|
140
|
-
|
|
141
|
-
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
-
|
|
143
|
-
content = _agent_md(
|
|
144
|
-
name=name,
|
|
145
|
-
engineer=engineer,
|
|
146
|
-
description=args.description.strip(),
|
|
147
|
-
stack_details=args.stack_details.strip(),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
with open(agent_file, "w") as f:
|
|
151
|
-
f.write(content)
|
|
152
|
-
|
|
153
|
-
print(f"Profile creado: {agent_file.relative_to(forge.parent) if forge.parent != forge else agent_file}")
|
|
154
|
-
print()
|
|
155
|
-
print("Próximos pasos:")
|
|
156
|
-
print()
|
|
157
|
-
print(f" 1. Revisar y completar el agente:")
|
|
158
|
-
print(f" {agent_file}")
|
|
159
|
-
print()
|
|
160
|
-
print(f" 2. Documentar el profile en docs/agent-standard.md:")
|
|
161
|
-
print(f" Agregar una fila en la tabla Tier 2:")
|
|
162
|
-
print(f" | `{name}` | `{engineer}` |")
|
|
163
|
-
print()
|
|
164
|
-
print(f" 3. Activar el profile en project.yaml del proyecto:")
|
|
165
|
-
print(f" agents:")
|
|
166
|
-
print(f" profiles:")
|
|
167
|
-
print(f" - {name}")
|
|
168
|
-
print()
|
|
169
|
-
print(f" 4. Instalar el agente:")
|
|
170
|
-
print(f" python3 .agentic/scripts/forge-init.py --tool claude-code")
|
|
171
|
-
print()
|
|
172
|
-
print(f" 5. Agregar tests en tests/test_profiles.py para el nuevo profile.")
|
|
173
|
-
print()
|
|
174
|
-
# forge-init.py gestiona profiles dinámicamente desde el filesystem, no tiene un
|
|
175
|
-
# registro estático de nombres — no es necesario editar su código para que funcione.
|
|
176
|
-
print("Nota: forge-init.py detecta profiles automáticamente desde forge/profiles/.")
|
|
177
|
-
print("No es necesario modificar ese script.")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if __name__ == "__main__":
|
|
181
|
-
main()
|