Laboratorio IA · Día 4 BCP · NTT DATA · Train-the-Trainers — inicio de la guía

BCP · NTT DATA · Train-the-Trainers IA · Día 4

Agentic AI en la práctica

S1 y S2 — generación: leen un feature del backlog vía MCP y construyen el módulo Web con Login (Backend Java Quarkus · Frontend Angular · SQLite). Roles: Designer → Backend Dev → Frontend Dev → Tech Lead Reviewer. S3 y S4 — refactorización: clonan un repositorio real, lo analizan y abren un Pull Request. El hilo conductor no es el artefacto, sino la capa de ingeniería de agentes que se agrega en cada sesión.

4 sesiones8 h netas≥ 80 % Gate G2 al cierreFlujo progresivo — cada sesión consume el artefacto de la anterior

LLM
Ollama local · localhost:11434/v1
Modelo:
qwen2.5-coder:14b-instruct-q4_K_M · num_ctx 32768

Elige tu camino antes de empezar

LLM — Ollama local

Local · sin internet

Pre-req: Ollama instalado y modelo descargado. Verificar con ollama list. Importante: Ollama usa 4k de contexto por defecto y las specs lo rompen — fijar OLLAMA_NUM_CTX=32768 antes de arrancar el servidor.
# Descargar modelo (una sola vez)
ollama pull qwen2.5-coder:14b-instruct-q4_K_M

OPENAI_API_BASE=http://localhost:11434/v1
OPENAI_API_KEY=ollama
OPENAI_MODEL_NAME=qwen2.5-coder:14b-instruct-q4_K_M

Terminal 1 Obligatoria

Servidor MCP Local (Podman o Docker)

Para que las sesiones S1 (CrewAI) y S2 (LangGraph) lean el backlog en vivo desde GitHub Projects, debes mantener abierta esta terminal de fondo durante el laboratorio:

# PowerShell — Ejecutar en 00_support/mcp-local
cd 00_support\mcp-local

# Con Podman (entorno oficial del lab):
.\start-mcp.ps1 -Owner "NTTData-Academy" -Repo "bcp-ai-lab-day-04-enterprise"

# O con Docker Desktop (máquina personal):
.\start-mcp.ps1 -Owner "NTTData-Academy" -Repo "bcp-ai-lab-day-04-enterprise" -Engine docker

Expone el servicio en http://localhost:8081 (Tasks/Backlog). Consulta GUIA_ESTUDIANTE.md para el paso a paso completo.

Mañana

S1 AM–Duración: 2 h

CrewAI — Agentes por roles

Instanciar un crew de 4 agentes (Chapter, Backend Quarkus, Frontend Angular, Líder Técnico) que colaboran para especificar e implementar el módulo de Login de la web BCP.

Ver de manera interactiva · S1 CrewAI (se abre en una pestaña nueva)Visualizador interactivo del pipeline

Materiales

  • Python 3.11+ instalado
  • uv o pip disponible en terminal
  • VS Code + Copilot Enterprise activo
  • Ollama corriendo localmente con qwen2.5-coder:14b-instruct-q4_K_M y OLLAMA_NUM_CTX=32768
  • Variables de entorno configuradas en .env (ver paso 3)
  • Repo de práctica clonado localmente
  • Rama feature/login creada desde main

El LLM es Ollama local — mismo código en las 4 sesiones, solo cambia el .env por sesión.

Pasos

Paso 00: Elegir camino e instalar dependencias

Según el camino elegido en la sección superior:
# 🌟 Opción 1 (Recomendada): Un solo .venv en la raíz para las 4 sesiones
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements-all.txt

# Opción 2: Entorno virtual aislado solo para S1
cd s1_crewai
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
💡 Si usas el Camino C (este repositorio), los archivos ya están creados. Salta directo al Step 09 para ejecutar.

Paso 01: Activar el entorno virtual en tu terminal

En la terminal integrada de VS Code:
# Windows (PowerShell)
.\.venv\Scripts\activate
# macOS / Linux
# source .venv/bin/activate

Paso 02: Instalar CrewAI y el conector LLM (si no usaste requirements-all.txt)

pip install crewai langchain-openai
Verificar con pip show crewai.

Paso 03: Configurar el endpoint LLM en .env

Crear .env en la raíz del repo con el endpoint de Ollama:
# ── LLM: Ollama local ─────────────────────────
OPENAI_API_BASE=http://localhost:11434/v1
OPENAI_API_KEY=ollama
OPENAI_MODEL_NAME=qwen2.5-coder:14b-instruct-q4_K_M
# Requiere: ollama pull qwen2.5-coder:14b-instruct-q4_K_M
# y OLLAMA_NUM_CTX=32768 antes de arrancar el servidor

# ── MCP Gestión de Proyectos (GitHub Projects → backlog) ──
PROJECTS_MCP_URL=http://localhost:8081
PROJECTS_MCP_API_KEY=prj-xxxxxxxxxxxxxxxxxxxx
GITHUB_OWNER=bcp-enterprise
GITHUB_REPO=ai-lab-day04

# ── MCP Git/Backend (commits, push, PRs) ─────────────
GIT_MCP_URL=http://localhost:8082
GIT_MCP_API_KEY=git-xxxxxxxxxxxxxxxxxxxx

# ── Camino C: el .env.example ya está en s1_crewai/ ─
# copy .env.example .env   (y completar con los valores de A o B)
Agregar .env al .gitignore — la key nunca va al repo.

Paso 04: Crear mcp_projects.py dentro de la sesión — cliente MCP con Streamable HTTP

S1 sólo necesita el MCP Proyectos (puerto 8081), que lista proyectos, backlog e issues desde GitHub Projects — el pipeline termina en disco, sin PR. El MCP Git (puerto 8082) aparece en S3, que sí abre el Pull Request. Ambos usan Streamable HTTP — JSON-RPC 2.0 enviado como POST a /mcp, con la respuesta llegando como SSE en el mismo cuerpo. El cliente hace primero un handshake initialize, recibe el Mcp-Session-Id, y luego llama tools/call. Los errores de ejecución de una tool vienen como isError: true dentro de la respuesta exitosa — no como el campo error de JSON-RPC. Importante: estos módulos van dentro de s1_crewai/, no en la raíz del repo.
# s1_crewai/mcp_projects.py — cliente MCP de proyectos · Streamable HTTP
import os, re, json, threading, requests

_URL = os.getenv("PROJECTS_MCP_URL", "http://localhost:8081")
_KEY = os.getenv("PROJECTS_MCP_API_KEY", "")
_rpc_id = 0; _id_lock = threading.Lock()
_session_id = None; _initialized = False; _init_lock = threading.Lock()

def _next_id():
    global _rpc_id
    with _id_lock: _rpc_id += 1; return _rpc_id

def _headers():
    hdrs = {"Authorization": f"Bearer {_KEY}",
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream"}
    if _session_id: hdrs["Mcp-Session-Id"] = _session_id
    return hdrs

def _parse_sse_body(resp, msg_id):
    """Lee el stream SSE del POST y retorna el mensaje JSON-RPC que coincide con msg_id."""
    data_buf = []
    for raw in resp.iter_lines(decode_unicode=True):
        if raw is None: continue
        if raw.startswith("data:"): data_buf.append(raw[5:].strip())
        elif raw == "":
            if not data_buf: continue
            payload = json.loads("\n".join(data_buf)); data_buf = []
            if isinstance(payload, dict) and payload.get("id") == msg_id:
                return payload
    raise RuntimeError("[MCP Proyectos] Stream SSE cerrado sin respuesta")

def _post_mcp(payload, timeout):
    resp = requests.post(f"{_URL}/mcp", headers=_headers(),
                         json=payload, stream=True, timeout=timeout)
    resp.raise_for_status()
    sid = resp.headers.get("Mcp-Session-Id")
    if "text/event-stream" in resp.headers.get("Content-Type", ""):
        result = _parse_sse_body(resp, payload.get("id"))
    elif resp.content: result = resp.json()
    else: result = {}
    return result, sid

def _ensure_session(timeout=20):
    """Handshake MCP: initialize → notifications/initialized (una sola vez)."""
    global _session_id, _initialized
    with _init_lock:
        if _initialized: return
        init = {"jsonrpc": "2.0", "id": _next_id(), "method": "initialize",
                "params": {"protocolVersion": "2025-03-26", "capabilities": {},
                            "clientInfo": {"name": "s1-crewai", "version": "1.0.0"}}}
        result, sid = _post_mcp(init, timeout)
        if sid: _session_id = sid
        try: _post_mcp({"jsonrpc":"2.0","method":"notifications/initialized","params":{}}, timeout)
        except: pass
        _initialized = True

def _sse_call(method, params, timeout=20):
    _ensure_session(timeout)
    msg_id = _next_id()
    result, sid = _post_mcp({"jsonrpc":"2.0","id":msg_id,"method":method,"params":params}, timeout)
    if sid: global _session_id; _session_id = sid
    if "error" in result: raise RuntimeError(f"[MCP Proyectos] Error RPC: {result['error']}")
    return result.get("result", {})

def call_tool(tool, params):
    """Llama una herramienta del MCP. Detecta isError dentro de la respuesta exitosa."""
    result  = _sse_call("tools/call", {"name": tool, "arguments": params})
    content = result.get("content", [])
    text = content[0]["text"] if content and content[0].get("type") == "text" else None
    if result.get("isError"): raise RuntimeError(f"[MCP Proyectos] Tool '{tool}' falló: {text or result}")
    if text is not None:
        try: return json.loads(text)
        except: return {"raw": text}
    return result

_OWNER = os.getenv("GITHUB_OWNER", "")
_REPO  = os.getenv("GITHUB_REPO",  "")

def list_tools(): return _sse_call("tools/list", {}).get("tools", [])

def list_projects():
    """Lista GitHub Projects (v2) visibles para el MCP."""
    result = call_tool("manage_project", {"action":"list","status":"all","limit":20})
    raw = result if isinstance(result, list) else result.get("projects", result.get("items", []))
    return [{"id": p.get("id"), "title": p.get("title","(sin título)"),
             "closed": bool(p.get("closed"))} for p in raw]

def list_project_items(project_id, limit=100):
    """Lista el backlog de un GitHub Project — intenta filter_items, cae a list_items."""
    for params in [{"action":"filter_items","projectId":project_id,"filter":{}},
                   {"action":"filter_items","projectId":project_id,"filterQuery":""},
                   {"action":"list_items",  "projectId":project_id,"limit":limit}]:
        try:
            result = call_tool("manage_project", params)
            raw = (result if isinstance(result, list)
                   else result.get("structuredContent",{}).get("items")
                   or result.get("items", []))
            return [_normalize_project_item(it) for it in raw]
        except RuntimeError: continue
    raise RuntimeError("[MCP Proyectos] No se pudo listar items del proyecto")

def get_feature_from_item(item):
    """Resuelve el feature brief completo a partir de un item del backlog.
       Si el item es un Draft Issue sin número, usa los datos del propio item."""
    number = item.get("number")
    if number:
        r = call_tool("manage_issues", {"action":"get","owner":_OWNER,
                                         "repo":_REPO,"issueNumber":number})
        issue = r.get("issue", r) if isinstance(r, dict) else r
        body  = issue.get("body") or ""
        return {"title": issue.get("title",""), "description": body,
                "acceptance_criteria": _extract_acceptance_criteria(body)}
    return {"title": item.get("title","(sin título)"),
            "description": item.get("description",""),
            "acceptance_criteria": item.get("acceptance_criteria", [])}

Paso 05: Seleccionar proyecto y feature del backlog — MCP lee GitHub Projects en tiempo real

Al arrancar, main.py llama list_projects() para listar los GitHub Projects disponibles. Si hay más de uno, presenta un menú y espera selección. Luego llama list_project_items(project_id) para obtener el backlog real, muestra otro menú, y al elegir llama get_feature_from_item(item) para obtener título, descripción y criterios de aceptación del issue o draft.
# s1_crewai/main.py — selección de proyecto y feature (extracto)
from mcp_projects import list_tools as proj_tools, list_projects, list_project_items, get_feature_from_item

# 1. Autenticación MCP y descubrimiento de herramientas
print("[MCP Proyectos] Autenticando...")
proj_tools()   # dispara el handshake initialize → session
print("[MCP Proyectos] Herramientas cargadas correctamente.")

# 2. Listar proyectos y pedir selección
projects = list_projects()
if len(projects) == 1:
    selected_project = projects[0]
else:
    for i, p in enumerate(projects, 1):
        icon = "⚫" if p.get("closed") else "🟢"
        print(f"  {i}. {icon} {p['title']}")
    sel = input(f"Selecciona el número de proyecto [1-{len(projects)}]: ")
    selected_project = projects[int(sel) - 1]

# 3. Listar backlog del proyecto seleccionado
backlog = list_project_items(selected_project["id"])
for i, item in enumerate(backlog, 1):
    icon = "⚫" if item.get("state","").lower() == "closed" else "🟢"
    num  = f"#{item['number']} — " if item.get("number") else ""
    print(f"  {i}. {icon} {num}{item['title']}")

sel = input(f"Selecciona el número de feature [1-{len(backlog)}]: ")
selected_item = backlog[int(sel) - 1]

# 4. Cargar el brief completo del feature
feature = get_feature_from_item(selected_item)
print(f"[MCP Proyectos] Feature cargado: {feature['title']}")
print(f"[MCP Proyectos] Criterios: {feature['acceptance_criteria']}")

# 5. Inyectar brief + HEX_PROMPT_PREFIX en el backstory del Designer
from hex_archetype import HEX_PROMPT_PREFIX
designer.backstory += HEX_PROMPT_PREFIX + (
    f"\n\n## Feature brief\n**{feature['title']}**\n\n"
    f"{feature['description']}\n\n"
    f"**Criterios:** {', '.join(feature['acceptance_criteria'])}"
)
# Nota: chapter_agent = designer es un alias disponible por compatibilidad
Verificar conectividad: curl -s -X POST http://localhost:8081/mcp -H "Content-Type: application/json" -d "{}" debe responder con SSE o JSON-RPC. Si falla, revisar que el contenedor Docker/Podman del MCP esté corriendo.

Paso 06: Crear crew/agents.py — definir los 4 agentes del caso Login

Los cuatro roles se mapean a agentes CrewAI: Technical Designer define la especificación técnica, Backend Developer implementa el microservicio Quarkus, Frontend Developer implementa los componentes Angular 17, y el Tech Lead Reviewer emite el veredicto final. Camino C: archivo en s1_crewai/crew/agents.py.
# crew/agents.py
from crewai import Agent
from crew.llm_config import llm   # LLM ya configurado con .env

# ── Agente 1: Technical Designer (Chapter) ───────────────
designer = Agent(
    role="Technical Designer",
    goal=(
        "Analizar el feature brief recibido y producir una especificación "
        "técnica completa: arquitectura, componentes, contrato de API y "
        "criterios de aceptación medibles — todo alineado a los estándares "
        "de seguridad y calidad del BCP."
    ),
    backstory=(
        "Eres un arquitecto de software senior con 10 años de experiencia en "
        "banca digital peruana. Conoces los estándares PCI-DSS y las normas "
        "de la SBS, y aplicas los patrones de diseño más robustos."
    ),
    llm=llm,
    verbose=True,
    allow_delegation=False,
)

# ── Agente 2: Backend Developer (Java Quarkus) ───────────
backend_dev = Agent(
    role="Backend Developer",
    goal=(
        "Implementar el backend del feature siguiendo la especificación del "
        "Designer. Entregar código limpio, tipado, con manejo de errores y "
        "comentarios que el Frontend y el Reviewer puedan entender y validar."
    ),
    backstory=(
        "Eres un desarrollador backend senior especializado en Java Quarkus 3.x "
        "y microservicios bancarios. Aplicas principios SOLID, implementas "
        "seguridad con BCrypt (salt factor ≥ 12) y JWT."
    ),
    llm=llm,
    verbose=True,
    allow_delegation=False,
)

# ── Agente 3: Frontend Developer (Angular 17) ────────────
frontend_dev = Agent(
    role="Frontend Developer",
    goal=(
        "Diseñar e implementar la interfaz Angular 17 del feature para los "
        "usuarios finales del BCP. Entregar componentes, servicios y guards "
        "listos para producción, con UX clara y segura."
    ),
    backstory=(
        "Eres un desarrollador frontend Angular 17 especializado en "
        "aplicaciones financieras del BCP. Priorizas seguridad (AuthGuard, "
        "interceptors de token), accesibilidad y claridad de la información."
    ),
    llm=llm,
    verbose=True,
    allow_delegation=False,
)

# ── Agente 4: Tech Lead Reviewer ─────────────────────────
reviewer = Agent(
    role="Tech Lead Reviewer",
    goal=(
        "Validar que los outputs del Designer, Backend y Frontend son "
        "coherentes entre sí, cumplen los criterios de aceptación y no "
        "presentan riesgos de seguridad. Emitir un veredicto claro: "
        "VEREDICTO: APROBADO o VEREDICTO: RECHAZADO con observaciones accionables."
    ),
    backstory=(
        "Eres el tech lead del equipo BCP con experiencia en auditorías de "
        "código en entornos regulados. Revisas que la arquitectura propuesta "
        "se refleje en el código y que todo cumpla con las políticas del banco."
    ),
    llm=llm,
    verbose=True,
    allow_delegation=False,
)

# Aliases para compatibilidad: chapter_agent = designer, backend_agent = backend_dev, etc.
Verificar conectividad: curl -s -X POST http://localhost:8081/mcp -H "Content-Type: application/json" -d "{}" debe responder con SSE o JSON-RPC. Si falla, revisar que el contenedor Docker del MCP de proyectos esté corriendo.

Paso 07: Crear crew/tasks.py — una task por agente

Cada Task tiene description (qué hacer), expected_output (contrato del output) y el agente asignado. Las tasks se ejecutan en orden — el output de cada una alimenta a la siguiente. Camino C: archivo listo en s1_crewai/crew/tasks.py.
# crew/tasks.py
from crewai import Task
from crew.agents import chapter_agent, backend_agent, frontend_agent, lt_agent

FEATURE_BRIEF = (
    "Web BCP con módulo de Login. "
    "Stack: Backend Java Quarkus · Frontend Angular · DB SQLite. "
    "Endpoint principal: POST /api/auth/login "
    "Body: {username, password} → Response: {token, expires_in, role}. "
    "Seguridad obligatoria: hash bcrypt (factor 12), JWT firmado (exp 8h), "
    "rate-limiting 5 intentos/min, HTTPS en todos los endpoints."
)

t_story = Task(
    description=(
        f"Analiza el brief:\n{FEATURE_BRIEF}\n\n"
        "Produce el user story con: título, descripción como usuario, "
        "y ≥ 3 criterios de aceptación en formato Dado/Cuando/Entonces."
    ),
    expected_output=(
        "User story completo: título + como [rol] quiero [acción] para [beneficio]. "
        "Mínimo 3 criterios de aceptación Dado/Cuando/Entonces. "
        "Incluir criterio de seguridad explícito."
    ),
    agent=chapter_agent,
)

t_backend = Task(
    description=(
        "Basándote en el user story del Chapter, diseña la spec técnica "
        "del backend Quarkus: estructura del proyecto, contrato JSON del endpoint, "
        "esquema SQLite de la tabla users, y pseudocódigo del AuthService."
    ),
    expected_output=(
        "Spec con: estructura de carpetas Quarkus, contrato JSON del POST /api/auth/login, "
        "DDL de la tabla users (con columna password_hash), "
        "y pseudocódigo del método authenticate() con bcrypt."
    ),
    agent=backend_agent,
)

t_frontend = Task(
    description=(
        "Basándote en la spec del backend, especifica el módulo Angular: "
        "LoginComponent (reactive form), AuthService (HttpClient + JWT), "
        "AuthGuard (CanActivate) e HttpInterceptor (Bearer token)."
    ),
    expected_output=(
        "Spec de 4 artefactos Angular con interfaces TypeScript: "
        "LoginComponent (FormGroup + validadores), AuthService (login/logout), "
        "AuthGuard (canActivate → boolean), AuthInterceptor (headers)."
    ),
    agent=frontend_agent,
)

t_review = Task(
    description=(
        "Revisa los 3 outputs anteriores: cobertura de criterios del user story, "
        "coherencia entre contrato JSON y el AuthService Angular, "
        "y riesgos de seguridad (passwords en texto plano, JWT sin expiración). "
        "Emitir: VEREDICTO: APROBADO / RECHAZADO con observaciones."
    ),
    expected_output=(
        "Reporte con: VEREDICTO explícito, tabla de cobertura de criterios (✓/✗), "
        "lista de riesgos encontrados y descripción del PR lista para GitHub."
    ),
    agent=lt_agent,
)

Paso 08: Crear main.py — HITL loop con checkpoint por paso

# s1_crewai/main.py — loop HITL por paso con checkpoint (extracto)
from crewai import Crew, Process

STEPS = [
    ("designer",    "Designer — Especificación técnica",    chapter_agent,  "user_story.md",    False),
    ("backend_dev", "Backend Dev — Implementación Java",    backend_agent,  "backend_spec.md",  True),
    ("frontend_dev","Frontend Dev — Componentes Angular",   frontend_agent, "frontend_spec.md", True),
    ("reviewer",    "Líder Técnico — Revisión y veredicto", lt_agent,       "lt_review.md",     True),
]

checkpoint  = load_checkpoint()    # carga output/.checkpoint.json
steps_state = checkpoint.get("steps", {})

for step_key, step_label, agent, spec_file, do_code in STEPS:

    if steps_state.get(step_key, {}).get("status") == "approved":
        print(f"✅ [{step_label}] ya aprobado (checkpoint). Omitiendo.")
        continue

    while True:                     # bucle de reintentos
        t_story, t_backend, t_frontend, t_review = build_tasks(feature)
        task = {"designer":t_story,"backend_dev":t_backend,
                "frontend_dev":t_frontend,"reviewer":t_review}[step_key]

        ctx = build_context_note(approved_outputs)
        if ctx: task.description += ctx

        output = run_step(agent, task)   # mini-Crew de un solo agente

        decision = hitl_approve(step_label, output)  # s / n / r

        if decision == "s":
            save_spec(spec_file, output)        # guarda specs/*.md
            if do_code: extract_and_save_code(output)  # extrae .java / .ts
            approved_outputs[step_key] = output
            steps_state[step_key] = {"status": "approved", "output": output}
            save_checkpoint({"steps": steps_state, "feature_title": feature["title"]})
            break
        elif decision == "n":
            steps_state[step_key] = {"status": "rejected"}
            save_checkpoint({"steps": steps_state, "feature_title": feature["title"]})
            raise SystemExit(0)     # abortar — el checkpoint permite retomar
        # "r" → reintentar sin break

Paso 09: Ejecutar el crew con HITL

cd s1_crewai
python main.py
El script presenta menús de selección de proyecto y feature, luego ejecuta cada agente uno por uno mostrando un preview del output y pidiendo aprobación [s / n / r]. Si se interrumpe, al volver a correr retoma desde el último paso sin aprobar gracias al checkpoint en output/.checkpoint.json. Si hay error de LLM, verificar las variables de entorno del paso 3.

Qué obtengo

Terminal — output en consola
[MCP Proyectos] Autenticando...
[MCP Proyectos] Herramientas cargadas correctamente.

[MCP Proyectos] Proyectos disponibles:
  1. 🟢 BCP Login Module — Sprint 1

[MCP Proyectos] Backlog de 'BCP Login Module — Sprint 1':
  1. 🟢 #12 — Login web con autenticación JWT
  2. 🟢 Mejoras de seguridad (Draft)

Selecciona el número de feature [1-2]: 1
[MCP Proyectos] Feature cargado: Login web con autenticación JWT
[MCP Proyectos] Criterios: ['token JWT devuelto', 'bloqueo tras 5 intentos', ...]

════════════════════════════════════════════════════════════
S1 CrewAI — HITL — Feature: Login web con autenticación JWT
Pasos: Designer → Backend → Frontend → Líder Técnico
════════════════════════════════════════════════════════════

────────────────────────────────────────────────────────────
▶ PASO: Designer — Especificación técnica
────────────────────────────────────────────────────────────
[Agente Chapter corre mini-Crew...]

────────────────────────────────────────────────────────────
▶ OUTPUT — Designer — Especificación técnica
────────────────────────────────────────────────────────────
Como usuario BCP quiero iniciar sesión con mis credenciales...
Criterio 1 — Dado credenciales válidas: token JWT devuelto
Criterio 2 — Dado 5 intentos fallidos: cuenta bloqueada 15 min
────────────────────────────────────────────────────────────

¿Aprobar el paso 'Designer — Especificación técnica'? [s=sí / n=rechazar / r=reintentar]: s
✅ Designer — Especificación técnica aprobado. Guardando artefactos...
  ✅ specs/user_story.md

▶ PASO: Backend Dev — Implementación Java
...
✅ Backend Dev aprobado.
  ✅ specs/backend_spec.md
  💾 backend-quarkus/src/main/java/pe/bcp/login/service/AuthService.java

▶ PASO: Frontend Dev — Componentes Angular
...
✅ Frontend Dev aprobado.
  ✅ specs/frontend_spec.md
  💾 frontend-angular/src/app/auth/login.component.ts

▶ PASO: Líder Técnico — Revisión y veredicto
...
  ✓ Cobertura: 3/3 criterios cubiertos
  ✓ VEREDICTO: APROBADO
  ✅ specs/lt_review.md

════════════════════════════════════════════════════════════
✅ TODOS LOS PASOS APROBADOS
════════════════════════════════════════════════════════════
  output/specs/            → user_story.md · backend_spec.md · frontend_spec.md · lt_review.md
  output/backend-quarkus/  → proyecto Quarkus (pom.xml, application.properties, src/...)
  output/frontend-angular/ → proyecto Angular (package.json, angular.json, src/...)
Estructura generada
s1_crewai/
├── crew/
│   ├── llm_config.py    ← crewai.LLM + .env (Ollama local)
│   ├── agents.py        ← designer, backend_dev, frontend_dev, reviewer
│   └── tasks.py         ← build_tasks(feature) → Task × 4 dinámicas
├── main.py              ← HITL loop: MCP → selección → mini-Crew × 4 → checkpoint
├── mcp_projects.py      ← Streamable HTTP → MCP Proyectos :8081
├── hex_archetype.py     ← HEX_PROMPT_PREFIX inyectado en backstory del Designer
├── output/
│   ├── .checkpoint.json ← estado de cada paso (approved / rejected)
│   ├── specs/           ← user_story.md · backend_spec.md · frontend_spec.md · lt_review.md
│   ├── backend-quarkus/ ← pom.xml · application.properties · User.java · AuthService.java …
│   ├── frontend-angular/← package.json · angular.json · login.component.ts · models.ts …
│   └── db/              ← schema.sql · seed.sql
├── .env.example         ← plantilla versionada
└── requirements.txt

Conceptos demostrados

  • MCP Streamable HTTP — JSON-RPC 2.0 en un único POST a /mcp; la respuesta llega como SSE en el mismo body. El handshake initialize se hace una sola vez; los errores de tool vienen como isError: true y no como error JSON-RPC.
  • Mini-Crew por paso — en lugar de un crew monolítico, cada agente corre en su propio Crew([agent], [task]). Esto permite pausar y pedir aprobación humana entre pasos sin perder el contexto.
  • HITL loop — tras cada paso el humano puede aprobar (s), rechazar (n) o reintentar (r). Solo al aprobar se persisten los artefactos y el checkpoint.
  • Checkpoint — output/.checkpoint.json registra el estado de cada paso. Al volver a correr el script, los pasos ya aprobados se omiten inyectando su output como contexto para los restantes.
  • extract_and_save_code(): regex extrae bloques ```java / ```ts / ```sql del output del LLM → los guarda como archivos reales en output/
  • Evidencia: screenshot del log en consola mostrando selección de feature + aprobaciones HITL + link al PR feature/login → main en GitHub

S2 AM–Duración: 2 h

LangGraph — Workflow stateful

Convertir el flujo Login en un StateGraph de 4 nodos (designer → backend_dev → frontend_dev → reviewer) con interrupt_before en cada uno: el grafo se pausa antes de ejecutar cada nodo y espera la decisión del humano. Al cerrar, valida la arquitectura hexagonal del backend generado.

Ver de manera interactiva · S2 LangGraph (se abre en una pestaña nueva)Visualizador interactivo del workflow stateful

Materiales

  • LangGraph + conector LLM (instalar en el paso 1)
  • Mismo .env de S1 — Ollama local
  • Crew de S1 funcionando como referencia
  • Diagrama del graph (provisto por el trainer)

El crew de S1 no se reemplaza — el graph es una capa distinta sobre la misma lógica.

Pasos

Paso 00: Leer feature brief desde GitHub Project MCP

El estado inicial del graph se puebla con el brief del issue en vez de estar hardcodeado. Mismo módulo mcp_projects.py creado en S1.
# s2_langgraph/main.py — al inicio, antes de compilar el graph
import os
from mcp_projects import list_tools, list_projects, list_project_items, get_feature_from_item

# 1. Autenticación MCP y descubrimiento de tools
tools = list_tools()
print("[MCP Proyectos] Herramientas cargadas correctamente.")

# 2. Selección de proyecto y feature desde GitHub Projects
projects = list_projects()
selected_project = projects[0]  # O mediante menú interactivo
backlog = list_project_items(selected_project["id"])
feature = get_feature_from_item(backlog[0])
print(f"[MCP Proyectos] Feature cargado: {feature['title']}")

# 3. El estado inicial del graph cumple con WorkflowState
initial_state = {
    "feature_title":   feature["title"],
    "feature_brief":   feature["description"],
    "acceptance":      feature["acceptance_criteria"],
    "designer_output": "",
    "backend_output":  "",
    "frontend_output": "",
    "reviewer_output": "",
}
El nodo designer del graph recibirá state["feature_brief"] y state["acceptance"] como contexto dinámico del backlog.

Paso 01: Activar entorno e instalar dependencias de S2

# Si usas el .venv global de la raíz, solo actívalo:
.\.venv\Scripts\activate

# Si usas venv aislado para S2:
cd s2_langgraph
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
# El graph ya está en graph/ — ir directo al Step 05 para ejecutar
💡 En este repositorio, todos los archivos de S2 ya están creados. Configura tu .env y pasa directo a ejecutar.

Paso 02: Crear graph/state.py — definir el State

# graph/state.py
from typing import TypedDict

class WorkflowState(TypedDict):
    # Datos del feature leido del backlog (MCP Proyectos)
    feature_title:   str
    feature_brief:   str
    acceptance:      list

    # Outputs acumulados de cada agente
    designer_output: str
    backend_output:  str
    frontend_output: str
    reviewer_output: str

Paso 03: Crear graph/nodes.py — un nodo por rol

Cada función recibe el state y retorna un dict con el campo actualizado. Inicializar el LLM con el mismo .env:
# graph/nodes.py  — un nodo por rol
import time
from langchain_core.messages import SystemMessage, HumanMessage
from config.llm import llm

_MAX_RETRIES = 5
_CTX_LIMIT   = 3000   # chars de contexto entre nodos

def _invoke(system: str, user: str) -> str:
    # Retry exponencial ante 502/503/504/timeout del endpoint LLM
    for attempt in range(_MAX_RETRIES):
        try:
            r = llm.invoke([SystemMessage(content=system),
                            HumanMessage(content=user)])
            return r.content
        except Exception as e:
            retriable = any(c in str(e).lower() for c in
                            ["504", "503", "502", "500", "timeout", "connection"])
            if not retriable or attempt == _MAX_RETRIES - 1:
                raise
            time.sleep(12.0 * (2 ** attempt))

def _ctx(text: str, max_chars: int = _CTX_LIMIT) -> str:
    # Trunca conservando inicio y fin para no inflar el prompt
    if not text or len(text) <= max_chars:
        return text
    half = max_chars // 2
    return text[:half] + "
[... omitido ...]
" + text[-half:]

def designer_node(state: dict) -> dict:
    # Lee el feature del state, NO un brief hardcodeado
    user = (f"Feature: **{state['feature_title']}**
{state['feature_brief']}

"
            f"Criterios:
{_ac_text(state['acceptance'])}

"
            "Especificación técnica: resumen, componentes, contrato de API, "
            "reglas de negocio y criterios Dado/Cuando/Entonces. Máx 600 palabras.")
    return {"designer_output": _invoke(SYS_DESIGNER, user)}

def backend_dev_node(state: dict) -> dict:
    ctx  = _ctx(state.get("designer_output", ""), max_chars=2000)
    user = (f"Especificación del Designer:
{ctx}

---

"
            f"Implementa el backend Quarkus 3.x para **{state['feature_title']}**.
"
            "Entrega cada archivo bajo su propio encabezado:
"
            "### `AuthResource.java`, `AuthService.java`, `User.java`,
"
            "### `LoginRequest.java`, `AuthResponse.java`, `schema.sql`")
    return {"backend_output": _invoke(SYS_BACKEND, user)}

def frontend_dev_node(state: dict) -> dict:
    # Recibe contexto del Designer Y del Backend
    ctx = "

---

".join(filter(None, [
        _ctx(state.get("designer_output", ""), 1500),
        _ctx(state.get("backend_output",  ""), 2000)]))
    user = (f"{ctx}

---

Implementa Angular 17 para "
            f"**{state['feature_title']}**: `models.ts`, `auth.service.ts`, "
            "`auth.guard.ts`, `login.component.ts/.html`, `auth.interceptor.ts`")
    return {"frontend_output": _invoke(SYS_FRONTEND, user)}

def reviewer_node(state: dict) -> dict:
    ctx = "

---

".join(filter(None, [
        _ctx(state.get("designer_output", ""), 1000),
        _ctx(state.get("backend_output",  ""), 1500),
        _ctx(state.get("frontend_output", ""), 1500)]))
    user = (f"{ctx}

Revisa los outputs. Checklist SÍ/NO:
"
            "1. Backend implementa las reglas del Designer
"
            "2. Contrato de API consistente entre los tres
"
            f"3. Criterios cubiertos:
{_ac_text(state['acceptance'])}
"
            "4. Sin riesgos de seguridad evidentes
"
            "5. Compatible con Quarkus 3.x y Angular 17

"
            "Concluye con **VEREDICTO: APROBADO** o **VEREDICTO: RECHAZADO**.")
    return {"reviewer_output": _invoke(SYS_REVIEWER, user)}
Cada nodo lee el feature desde el state — nada hardcodeado. El contexto se encadena: Designer → Backend → Frontend → Reviewer, truncado a _CTX_LIMIT para no desbordar el prompt. El mismo código funciona contra Ollama local — solo cambia el .env por sesión.

Paso 04: Crear graph/graph.py — ensamblar el graph

# graph/graph.py  — 4 nodos, flujo lineal
import sqlite3
from pathlib import Path
from langgraph.graph import StateGraph, END
from graph.state import WorkflowState
from graph.nodes import (designer_node, backend_dev_node,
                         frontend_dev_node, reviewer_node)

# Checkpointer: SqliteSaver si esta disponible, sino MemorySaver
_DB = Path(__file__).parent.parent / ".langgraph_checkpoint.db"
try:
    from langgraph_checkpoint_sqlite import SqliteSaver
    checkpointer = SqliteSaver(sqlite3.connect(str(_DB), check_same_thread=False))
    USING_SQLITE = True
except ModuleNotFoundError:
    from langgraph.checkpoint.memory import MemorySaver
    checkpointer = MemorySaver()      # main.py persiste en .checkpoint.json
    USING_SQLITE = False

builder = StateGraph(WorkflowState)
builder.add_node("designer",     designer_node)
builder.add_node("backend_dev",  backend_dev_node)
builder.add_node("frontend_dev", frontend_dev_node)
builder.add_node("reviewer",     reviewer_node)

builder.set_entry_point("designer")
builder.add_edge("designer",     "backend_dev")
builder.add_edge("backend_dev",  "frontend_dev")
builder.add_edge("frontend_dev", "reviewer")
builder.add_edge("reviewer",     END)

# HITL: pausa ANTES de ejecutar cada nodo, no solo al final
graph = builder.compile(
    checkpointer=checkpointer,
    interrupt_before=["designer", "backend_dev", "frontend_dev", "reviewer"],
)

Paso 05: Ejecutar el graph con thread_id fijo

CONFIG = {"configurable": {"thread_id": "s2-langgraph-crew-01"}}

# El state inicial se puebla con el feature leido del MCP
initial_state = {
    "feature_title":   feature["title"],
    "feature_brief":   feature["description"],
    "acceptance":      feature["acceptance_criteria"],
    "designer_output": "", "backend_output":  "",
    "frontend_output": "", "reviewer_output": "",
}

# Arranca y se detiene ANTES del primer nodo (designer)
graph.invoke(initial_state, CONFIG)

# Un ciclo por nodo: avanzar, inspeccionar, decidir
for step in ["designer", "backend_dev", "frontend_dev", "reviewer"]:
    input(f"¿Ejecutar {step}? [s/n]: ")
    graph.invoke(None, CONFIG)              # corre ese nodo y vuelve a pausar
    snap   = graph.get_state(CONFIG)
    output = snap.values[f"{step.replace('_dev', '')}_output"]
    print(output[:1000])
    # [s]=guardar  [r]=reintentar (update_state)  [n]=cancelar
El graph se pausa antes de cada nodo, no solo al final: cada graph.invoke(None, CONFIG) ejecuta exactamente un nodo y vuelve a interrumpir. El mismo thread_id reanuda el state guardado. Con [r] el nodo se re-ejecuta y el resultado se inyecta con graph.update_state(..., as_node=step).

Paso 06: Verificar el checkpoint

# Ver el state guardado
snapshot = graph.get_state(config)
print(snapshot.values)
El state debe mostrar los outputs de los nodos anteriores sin re-ejecutarlos.

Paso 07: Validar la arquitectura hexagonal

S2 no se queda en generar: valida. El prefijo HEX_PROMPT_PREFIX se concatena al system prompt de los nodos backend y frontend, el prompt del backend pide los 11 archivos por capa con su package, y FILE_MAP los extrae sobre output/hex/. Al cerrar el flujo:
# main.py — cierre del flujo
from hex_archetype import HEX_LAYERS, scaffold, validate

scaffold(HEX_DIR, feature["title"])   # completa capas vacias + ARCHITECTURE.md
hex_result = validate(HEX_DIR)        # estructural + semantica
write_hex_report(hex_result)          # -> output/hex_report.md
La validación tiene dos niveles: estructural — cada una de las 7 capas debe contener al menos un archivo real; y semántica — ni domain/ ni application/ pueden importar de infrastructure/. Si el LLM genera un LoginService que importa UserRepositoryJpa en vez del puerto, el reporte lo marca como violación con el archivo exacto.
# Verificar el cableado sin gastar tokens (no llama al LLM ni al MCP)
cd s2_langgraph
python test_hex_wiring.py

1. FILE_MAP: 11 archivos en capas hexagonales validas  OK
2. Las 7 capas de HEX_LAYERS tienen destino  OK
3. Backend limpio: APROBADO  OK
3. Backend con violacion: RECHAZADO  OK

Qué obtengo

Terminal — output real de ejecución
[MCP Proyectos] Autenticando...
[MCP Proyectos] Herramientas cargadas correctamente.

[MCP Proyectos] Proyecto asignado: Project Test

[MCP Proyectos] Backlog de 'Project Test':
  1. 🟢 feature de creación de login
  2. 🟢 Bloqueo y desbloqueo temporal de tarjeta

Selecciona el número de feature [1-2]: 1
[MCP Proyectos] Feature seleccionado: feature de creación de login

============================================================
S2 LangGraph — mismo flujo CrewAI con LangGraph
Feature: feature de creación de login
============================================================

────────────────────────────────────────────────────────────
[CHECKPOINT] Sesión anterior encontrada.
  Feature:      feature de creación de login
  Próximo paso: Backend Developer (Implementación Quarkus)
    ✅ Designer (Especificación técnica)
    ⏳ Backend Developer (Implementación Quarkus)
    ⬜ Frontend Developer (Implementación Angular)
    ⬜ Tech Lead Reviewer (Veredicto)
────────────────────────────────────────────────────────────
¿Reanudar desde ese punto? [s/N]: s

[RESUME] Reanudando desde: Backend Developer

[HITL] Próximo paso: Backend Developer (Implementación Quarkus)
¿Ejecutar Backend Developer? [s/n]: s

[OUTPUT] Backend Developer (Implementación Quarkus):
### `AuthResource.java` · ### `AuthService.java` · ...
[Output completo: 14174 caracteres]

¿Qué haces? [s=guardar / r=reintentar / n=cancelar]: s
  ✅ output/backend_code.md
  💾 output/backend-quarkus/src/main/java/pe/bcp/auth/AuthResource.java
  💾 output/backend-quarkus/src/main/java/pe/bcp/auth/AuthService.java
  💾 output/backend-quarkus/src/main/java/pe/bcp/auth/model/User.java
  💾 output/backend-quarkus/src/main/java/pe/bcp/auth/dto/LoginRequest.java
  💾 output/backend-quarkus/src/main/java/pe/bcp/auth/dto/AuthResponse.java
  💾 output/backend-quarkus/src/main/resources/application.properties
  💾 output/db/schema.sql
  ✅ 7 archivo(s) de código extraído(s).

[HITL] Próximo paso: Frontend Developer (Implementación Angular)
¿Ejecutar Frontend Developer? [s/n]: s

¿Qué haces? [s=guardar / r=reintentar / n=cancelar]: s
  ✅ output/frontend_code.md
  💾 output/frontend-angular/src/app/auth/models.ts
  💾 output/frontend-angular/src/app/auth/auth.service.ts
  💾 output/frontend-angular/src/app/auth/auth.guard.ts
  💾 output/frontend-angular/src/app/auth/login.component.ts
  💾 output/frontend-angular/src/app/auth/login.component.html
  💾 output/frontend-angular/src/app/core/auth.interceptor.ts
  ✅ 6 archivo(s) de código extraído(s).

[HITL] Próximo paso: Tech Lead Reviewer (Veredicto)
¿Ejecutar Tech Lead Reviewer? [s/n]: s

[OUTPUT] Tech Lead Reviewer:
1. Backend implementa todas las reglas del Designer: NO
2. Contrato de API consistente entre los tres outputs: NO
3. Criterios de aceptación cubiertos: NO
...
  ✅ output/reviewer_report.md

============================================================
✅ Flujo S2 LangGraph completado.
Archivos guardados en: output/
============================================================
Estructura generada
s2_langgraph/
├── output/
│   ├── backend_code.md
│   ├── frontend_code.md
│   ├── reviewer_report.md
│   ├── .checkpoint.json    ← resume por paso
│   ├── backend-quarkus/src/main/java/pe/bcp/auth/
│   │   ├── AuthResource.java
│   │   ├── AuthService.java
│   │   ├── model/User.java
│   │   ├── dto/LoginRequest.java
│   │   ├── dto/AuthResponse.java
│   │   └── resources/application.properties
│   ├── db/
│   │   └── schema.sql
│   └── frontend-angular/src/app/
│       ├── auth/models.ts
│       ├── auth/auth.service.ts
│       ├── auth/auth.guard.ts
│       ├── auth/login.component.ts
│       ├── auth/login.component.html
│       └── core/auth.interceptor.ts
├── graph/
│   ├── state.py  ← WorkflowState (TypedDict)
│   ├── nodes.py  ← designer/backend/frontend/reviewer _node()
│   └── graph.py  ← StateGraph + checkpoint
├── mcp_projects.py
├── hex_archetype.py   ← scaffold() + validate()
├── test_hex_wiring.py
└── main.py

Concepto demostrado

  • Mismo flujo HITL que S1 (s=guardar / r=reintentar / n=cancelar) ahora implementado con LangGraph StateGraph — cada nodo es un agente, el state se pasa entre nodos
  • Checkpoint por paso: .checkpoint.json persiste el avance; al reiniciar el script el usuario puede reanudar desde el paso pendiente sin repetir los aprobados
  • Extracción de código real: regex detecta bloques ### path/file.ext en el output del LLM y guarda cada archivo en su ruta correspondiente dentro de output/
  • El Tech Lead Reviewer evalúa coherencia Backend + Frontend + Designer; emite veredicto con checklist accionable — el humano decide si reintentar o cancelar con HITL

Almuerzo · 12:15 – 13:15

Tarde

S3 PM–Duración: 2 h

Harness Engineering

Un solo pipeline, tres capas. El graph tiene los 5 nodos que hacen el trabajo (analyze → chapter → backend → frontend → reviewer) y es el único lugar donde vive el LLM. El harness es su puerta de entrada: carga constitution.md en state["context"], entrega el control nodo a nodo y mide compliance. Y main.py orquesta lo que el graph no hace — clona el repo, pide aprobación humana, extrae archivos y abre el PR. compare() corre el mismo pipeline SIN y CON constitución para cuantificar la diferencia.

Ver de manera interactiva · S3 Harness (se abre en una pestaña nueva)Visualizador interactivo del LangGraph + constitution compare()

Materiales

  • Python 3.11+ con venv activo
  • VS Code
  • Endpoint LLM configurado en .env (Ollama local — mismo de S1/S2)
  • Repositorio GitHub configurado en .env (GITHUB_OWNER, GITHUB_REPO, GIT_MCP_URL)
  • gh CLI autenticado (gh auth login) — fallback si el MCP Git no puede crear el PR
  • Camino C: usar s3_harness/ del ZIP — ya incluye graph/, harness/, main.py y constitution.md

El harness inyecta constitution.md como system context enriqueciendo el input del LangGraph graph — sin re-entrenar el modelo, el comportamiento cambia cuantitativamente en los 5 checks de seguridad.

Pasos

Paso 00: Instalar dependencias y revisar estructura

S3 usa LangGraph para el pipeline multi-nodo. Instalar y verificar que el graph arranca.
# Instalar dependencias de s3_harness
cd s3_harness
copy .env.example .env   # completar con creds
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
# requirements.txt incluye:
# langgraph>=0.2.0
# langchain-openai>=0.2.0
# python-dotenv>=1.0.0
# requests>=2.32.0

# Verificar que el graph importa correctamente
python -c "from graph.graph import graph; print('Graph OK:', graph)"

Paso 01: ¿Qué es un nodo? — el contrato (state) -> dict

Un nodo es una función común. Recibe el estado completo del pipeline y devuelve sólo los campos que modifica; LangGraph mergea ese dict en el estado y se lo pasa al siguiente. No devuelve el estado entero, no muta el que recibe, y no sabe quién corre antes ni después — por eso se pueden reordenar, saltar o reintentar de a uno.
# graph/nodes.py — el LLM vive aca, y en ningun otro lado
def analyze_node(state: WorkflowState) -> dict:
    prompt = (
        f"Eres un Tech Lead senior. Analiza el repo '{state['repo_name']}':"
        f"{_ctx(state['snapshot'], 8000)}"
        "Identifica problemas reales de calidad..."
    )
    return {"out_analysis": _invoke(prompt)}   # solo el campo que escribe

# Un nodo puede saltarse a si mismo devolviendo un dict vacio
def frontend_node(state: WorkflowState) -> dict:
    if state.get("project_type") not in ("frontend", "ambos"):
        return {}                              # no toca el estado
    ...
Los cinco nodos de S3 son los cinco roles del equipo: analyze detecta problemas, chapter arma el plan, backend y frontend implementan, reviewer emite veredicto. Cada uno decide cómo usar state["context"] (la constitución que carga el harness): analyze la ignora a propósito para no sesgar el diagnóstico, chapter acota el scope, los devs restringen el código, y el reviewer la convierte en checklist.

Paso 02: Revisar graph/graph.py — HITL por nodo + retry automático

Dos mecanismos de control que no son lo mismo. interrupt_before es el HITL: el grafo se detiene antes de cada nodo y devuelve el control, así que cada invoke(None, cfg) ejecuta exactamente uno. should_retry es el retry automático: si el reviewer no aprueba, la arista condicional vuelve a backend sin preguntar. El humano confirma el resultado de cada ronda, pero no decide si hay ronda.
# graph/graph.py (extracto)
MAX_RETRIES = 3
NODES = ["analyze", "chapter", "backend", "frontend", "reviewer"]

def should_retry(state: WorkflowState) -> str:
    if not state.get("approved", False) and state.get("retry_count", 0) < MAX_RETRIES:
        return "backend"   # otra ronda con el feedback del reviewer
    return END

builder = StateGraph(WorkflowState)
builder.add_node("analyze",  analyze_node)
builder.add_node("chapter",  chapter_node)
builder.add_node("backend",  backend_node)
builder.add_node("frontend", frontend_node)
builder.add_node("reviewer", reviewer_node)

builder.set_entry_point("analyze")
builder.add_edge("analyze",  "chapter")
builder.add_edge("chapter",  "backend")
builder.add_edge("backend",  "frontend")
builder.add_edge("frontend", "reviewer")
builder.add_conditional_edges("reviewer", should_retry,
                              {"backend": "backend", END: END})

graph = builder.compile(
    checkpointer=MemorySaver(),
    interrupt_before=NODES,   # sin esto se pierde el HITL
)
Verificar sin gastar tokens: python test_graph_hitl.py comprueba que un invoke ejecute exactamente un nodo, que el retry vuelva a backend, y que project_type omita el nodo que no corresponde.

Paso 03: Revisar harness/context/constitution.md — las 6 reglas

El harness la carga una sola vez y la deja en state["context"]. No se re-entrena el modelo: cambia el contexto, y con eso el comportamiento.
# harness/context/constitution.md
R1: Contratos preservados (POST /auth/login, /api/tasks, /api/summary, /api/requirements).
R2: Secretos fuera del codigo (AUTH_TOKEN_SECRET por entorno). Nada sensible en logs.
R3: SQL parametrizado y contrasenas hasheadas (bcrypt/argon2/PBKDF2).
R4: Token firmado (HMAC/JWT) con exp real, validado por quien lo recibe.
R5: Errores genericos sin stack traces. Sin [innerHTML] dinamico en Angular.
R6: Caracterizacion antes del cambio, cobertura >= 85%, cita #issue y H-xx.
# + artefactos de TU repo (Dias 1-3): REGLAS.md, docs/ENDPOINTS.md, openspec/specs/...

Paso 04: Revisar harness/harness.py — la única puerta de entrada

main.py no instancia modelos ni arma prompts: siembra el estado aquí y hace avanzar el pipeline. run_with_harness() carga instructions + constitution + policies y los deja en context; run_without_harness() lo deja vacío — esa es justamente la diferencia que mide compare().
# harness/harness.py (extracto)
def run_with_harness(repo_name, snapshot, project_type, thread_id) -> dict:
    # Siembra el estado. NO ejecuta ningun nodo todavia.
    return _start(repo_name, snapshot, project_type, thread_id, load_context())

def step(thread_id) -> tuple[str | None, dict]:
    # Ejecuta EXACTAMENTE un nodo y devuelve (nombre, estado)
    nodo = next_node(thread_id)
    if nodo is None:
        return None, state(thread_id)
    graph.invoke(None, _config(thread_id))
    return nodo, state(thread_id)

def retry_node(thread_id, nodo) -> dict:
    # El [r] del HITL: reejecuta y reinyecta la salida
    salida = _NODE_FNS[nodo](graph.get_state(cfg).values)
    graph.update_state(cfg, salida, as_node=nodo)
    return state(thread_id)
resume_with_harness() resuelve el reinicio: el MemorySaver vive en proceso, así que al volver a arrancar se resiembra el estado y se adelanta con update_state(as_node=...) usando los artefactos del checkpoint — sin reejecutar ningún nodo ya aprobado.

Paso 05: Revisar check_security_compliance() — 5 checks deterministas

Evalúa el texto generado con cinco patrones verificables. Vive en el harness y main.py la importa de ahí: antes estaba duplicada byte a byte en los dos archivos. Resultado esperado: SIN harness 1-2/5, CON harness 5/5.
# harness/harness.py
def check_security_compliance(response_text: str) -> dict[str, bool]:
    t = (response_text or "").lower()
    return {
        "bcrypt_factor_12":       ("bcrypt" in t) and any(x in t for x in ("factor 12", " 12")),
        "jwt_exp_8h":             any(x in t for x in ("jwt", "token")) and "28800" in t,
        "rate_limit_5_intentos":  any(x in t for x in ("rate", "5 intentos", "429")),
        "https_enforced":         any(x in t for x in ("https", "tls", "ssl")),
        "no_plaintext_passwords": "hash" in t and "plaintext" not in t,
    }

Paso 06: Revisar main.py — orquesta, no genera

Tras la alineación, main.py no importa ChatOpenAI ni arma un solo prompt. Se ocupa de lo que el graph no hace: clonar el repo, leer el snapshot, detectar el tipo de proyecto, pedir aprobación, extraer los archivos del output, commitear y abrir el PR.
# main.py — el bucle completo
run_with_harness(repo_name=f"{GITHUB_OWNER}/{GITHUB_REPO}",
                 snapshot=snapshot, project_type=project_type,
                 thread_id=THREAD_ID)

while next_node(THREAD_ID) is not None:
    nodo_corrido, estado = step(THREAD_ID)     # un nodo por vuelta
    salida = _hitl_node(nodo_corrido, estado)  # [s/r/n]
    if not salida:
        continue                               # omitido por project_type
    # persistir artefacto, extraer archivos, checkpoint...

final = harness_state(THREAD_ID)
compliance = check_security_compliance(
    " ".join([final["out_backend"], final["out_frontend"], final["out_reviewer"]])
)
El [r] del HITL llama a retry_node(), que reejecuta ese nodo. Es distinto del retry automático del reviewer: ése lo decide el veredicto dentro del graph. Verificar con python test_harness_flow.py.

Paso 07: Conventional Commits — _derive_commit_type() y _branch_name()

El tipo de commit y el nombre de rama se derivan automáticamente del plan de refactorización — nunca hardcodeados.
# main.py — Conventional Commits
commit_type = _derive_commit_type(plan, review)
# Analiza keywords en plan + review_out
# → 'feat' | 'fix' | 'refactor' | 'chore'

branch = _branch_name(commit_type, repo_slug, description)
# → "refactor/mi-repo/mejorar-auth-20260830"

commit_msg = _commit_message(commit_type, scope, description)
# → "refactor(auth): mejorar manejo de sesiones JWT"

_git_branch_push(repo_dir, branch, commit_msg)
# configura git user, crea rama, git add -A, git commit, git push

Paso 08: Push con git, PR con MCP Git (fallback a gh)

Tres tramos con tecnologías distintas. El push es git plano por subprocess: _git_branch_push() reescribe el remote con GITHUB_TOKEN para autenticar, crea la rama, commitea con mensaje Conventional y pushea. El PR lo intenta primero mcp_git.create_pr() contra el MCP en :8082, probando varias tools (create_pull_request, open_pull_request…). Si el MCP no expone ninguna, o si falla con el bug GraphQL org-vs-user, cae a gh pr create. El body del PR incluye el reporte del reviewer, el plan del chapter y el score de compliance. Ten gh auth login hecho también para S3: es lo único que salva el paso si el MCP del lab arrastra ese bug.
# main.py — creación de PR vía MCP Git
from mcp_git import create_pr

pr_url = create_pr(
    title=f"{commit_type}({scope}): {description}",
    body=(
        f"## Plan de refactorización\n{plan[:500]}...\n\n"
        f"## Reporte del Reviewer\n{review}\n\n"
        f"## ⚖️ Compliance con Constitución — Score {_state['compliance_score']}\n"
        f"| Regla | Estado |\n|-------|--------|\n"
        f"... (generado por check_security_compliance())\n\n"
        f"> Verificado por `check_security_compliance()` integrado en el harness."
    ),
    head=branch,
    base=os.getenv("GIT_HEAD_BRANCH", "main"),
)
print(f"[MCP Git] PR creado: {pr_url}")

# Checkpoint guardado en output/.checkpoint.json
# Claves: project_type, analysis, plan, backend_out, frontend_out,
#         head_branch, base_branch, commit_type, commit_msg, next_step

Qué obtengo

Terminal — output real de ejecución (main.py Option C)
============================================================
S3 Harness — Code Refactoring Pipeline
Repo: NTTData-Academy/workshop-requirements-api-template
Branch: feature/equipo-01/workshop-refactor → main
============================================================

[MCP Git] Conectando... Herramientas disponibles.

[HARNESS] Cargando constitución...
  [HARNESS] Constitución cargada (2823 chars).
[HARNESS] Pipeline ejecutará CON constitución inyectada.

[REPO] Descargando NTTData-Academy/workshop-requirements-api-template...
[REPO] Snapshot generado: 37 archivos leídos.

[DETECCIÓN] Tipo de proyecto detectado: BACKEND
¿Tipo de proyecto? [backend/frontend/ambos] (Enter=backend):
  → Usando: BACKEND

¿Iniciar pipeline de refactorización? [s/N]: s

[PASO 1/4] Análisis de código...
  - Secreto JWT hardcodeado en tests/test_api.py
  - Autenticación inconsistente: base64 vs jwt real
  - Tests desactualizados respecto al contrato real
  ✅ output/analysis.md  · 💾 Checkpoint (next_step=chapter)

[PASO 2/4] Chapter Lead — Plan...
  > Nota de scope BCP: archivos no pertenecen a detection/
  > Requiere aprobación humana: merge a main/master
  ✅ output/chapter_plan.md · 💾 Checkpoint (next_step=backend)

[PASO 3/4] Backend Developer...
  ✅ output/backend_refactor.md (12 archivos extraídos)
  💾 output/app/config.py · app/security.py · app/api/dependencies.py
  💾 Checkpoint (next_step=reviewer)

[PASO 4/4] Tech Lead Reviewer...
  ❌ RECHAZADO — diff truncado, sin tests, frontend vacío
  [Y] scope respetado   [Y] no PII   [N] regulatorio   [Y] aprobación humana
  ✅ output/reviewer_report.md

[HARNESS] Verificando compliance con la constitución...
[HARNESS] Score: 1/5
  ❌ bcrypt_factor_12
  ✅ jwt_exp_8h
  ❌ rate_limit_5_intentos
  ❌ https_enforced
  ❌ no_plaintext_passwords
  ✅ evidence/compliance_report.json · 💾 Checkpoint (next_step=push)

¿Subir código y crear PR? [s/N]: s

[GIT] Tipo  : fix
[GIT] Rama  : fix/workshop-requirements-api-temp/1-backend-20260911
[GIT] Commit: fix(workshop-requirements-api): Seguridad / secretos hardcodeados
  ✅ Push → origin/fix/workshop-requirements-api-temp/1-backend-20260911

[MCP Git] ✅ PR creado:
  https://github.com/NTTData-Academy/workshop-requirements-api-template/pull/12
  🗑️  Checkpoint eliminado.

============================================================
✅ S3 Harness completado.
  PR: https://github.com/NTTData-Academy/workshop-requirements-api-template/pull/12
============================================================
Estructura generada
s3_harness/
├── output/
│   ├── analysis.md
│   ├── chapter_plan.md
│   ├── backend_refactor.md   ← 12 archivos extraídos
│   ├── reviewer_report.md
│   ├── .checkpoint.json      ← eliminado tras PR
│   └── app/                  ← archivos refactorizados
│       ├── config.py
│       ├── security.py
│       ├── api/dependencies.py
│       ├── api/routes/tasks.py
│       ├── api/routes/requirements.py
│       ├── schemas/tasks.py
│       ├── schemas/requirements.py
│       ├── repositories/tasks_repository.py
│       ├── services/tasks_service.py
│       └── main.py
├── evidence/
│   ├── compliance_report.json  ← Score 1/5 (jwt_exp_8h ✅)
│   ├── run_without_harness.json / .txt
│   └── run_with_harness.json / .txt
├── harness/
│   ├── harness.py
│   └── context/
│       └── constitution.md  ← 2823 chars cargados
├── graph/
│   ├── graph.py · nodes.py · state.py
├── main.py
└── mcp_git.py

Conceptos demostrados

  • Harness como puerta de entrada: run_with_harness() carga instructions + constitution + policies en state["context"] y cada nodo decide cómo aplicarlo; el pipeline refactoriza código real del repo NTTData-Academy/workshop-requirements-api-template
  • Compliance score real: check_security_compliance() post-reviewer detectó 1/5 reglas cumplidas (jwt_exp_8h ✅) — demostración honesta de que el harness detecta gaps
  • El Tech Lead Reviewer emitió RECHAZADO con checklist de compliance de constitución por regla ([Y]/[N]); el output truncado y la ausencia de tests fueron los motivos concretos
  • Pipeline completo: 37 archivos de snapshot → análisis → plan (con nota de scope BCP) → 12 archivos refactorizados → PR #12 (se abre en una pestaña nueva) en rama fix/workshop-requirements-api-temp/1-backend-20260911

Pausa · 15:15 – 15:30

S4 PM–Duración: 2 h

Guardrails + Review Loop multi-agente

La misma arquitectura de S3, con gobierno encima. Mismas tres capas (main.py → harness/ → graph/), mismos 5 nodos sobre el repositorio clonado. Lo que S4 agrega: un guardrail de entrada que valida el snapshot antes de gastar tokens, un review loop donde el developer corrige con las observaciones textuales del reviewer, un guardrail de salida que impide abrir un PR con un output inválido, y una suite de 10 evals deterministas.

Ver de manera interactiva · S4 Guardrails (se abre en una pestaña nueva)Visualizador interactivo del pipeline completo

Materiales

  • Repositorio GitHub configurado en .env (GITHUB_OWNER + GITHUB_REPO)
  • gh CLI instalado y autenticado (gh auth login)
  • MCP Git corriendo en GIT_MCP_URL (Streamable HTTP)
  • Camino C: ejecutar python main.py en s4_guardrails/ — el pipeline completo corre en un solo comando

S4 usa LangGraph igual que S3: main.py no instancia modelos ni arma prompts — siembra el estado en el harness y avanza nodo a nodo con step(). El LLM vive sólo en graph/nodes.py. Retoma desde output/.checkpoint.json si se interrumpe, restaurando también la ronda de review y el feedback pendiente. No usa mcp_projects: clona el repositorio directamente.

Pasos

Paso 00: Configurar variables de entorno

Copiar .env.example a .env y completar los valores. S4 no usa PROJECTS_MCP_URL — clona el repo directamente.
# s4_guardrails/.env
OPENAI_API_BASE=http://localhost:11434/v1
OPENAI_API_KEY=ollama
OPENAI_MODEL_NAME=qwen2.5-coder:14b-instruct-q4_K_M

GIT_MCP_URL=http://localhost:8082      # MCP Git Streamable HTTP
GITHUB_OWNER=bcp-enterprise
GITHUB_REPO=ai-lab-day04
GIT_HEAD_BRANCH=feature/guardrails-s4
GIT_BASE_BRANCH=main
MAX_REVIEW_ROUNDS=3                    # máx. rondas reviewer→dev

Paso 01: Clone + Snapshot del repositorio

El pipeline clona el repo en output/repo/ y genera un snapshot de archivos como contexto para el LLM.
# Internamente en main.py — paso CLONE
import subprocess, pathlib

repo_dir = OUTPUT_DIR / "repo"
subprocess.run(
    ["git", "clone",
     f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}.git",
     str(repo_dir)],
    check=True
)

# Paso SNAPSHOT — lista de archivos para el LLM
files = [str(p.relative_to(repo_dir))
         for p in repo_dir.rglob("*") if p.is_file()]
snapshot = "\n".join(files[:200])
(OUTPUT_DIR / "snapshot.txt").write_text(snapshot)

Paso 02: Analyze: Arquitecto analiza el repositorio

El LLM actúa como Arquitecto y genera un informe de análisis del repo clonado. El output se guarda en output/analysis_r1.md.
# Paso ANALYZE en main.py
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url=OPENAI_API_BASE,
    api_key=OPENAI_API_KEY,
    model=OPENAI_MODEL_NAME,
)

architect_prompt = f"""Eres un Arquitecto de Software Senior.
Analiza la siguiente lista de archivos del repositorio y genera
un informe: stack tecnológico detectado, capas del proyecto,
puntos de mejora y recomendaciones de implementación.

Snapshot del repo:
{snapshot}
"""
analysis = llm.invoke(architect_prompt).content
(OUTPUT_DIR / "analysis_r1.md").write_text(analysis)

Paso 03: Confirm Type + Chapter Leader

El usuario confirma si el proyecto es backend, frontend o ambos. El Chapter Leader genera el brief de implementación.
# Paso CONFIRM TYPE
project_type = input(
    "Tipo de proyecto [backend/frontend/ambos]: "
).strip().lower() or "ambos"

# Paso CHAPTER
chapter_prompt = f"""Eres el Chapter Leader técnico.
Con base en el análisis del repo y el tipo '{project_type}',
define el brief de implementación: objetivos, criterios de aceptación,
restricciones técnicas y patrones a aplicar.

Análisis:
{analysis[:2000]}
"""
chapter = llm.invoke(chapter_prompt).content
(OUTPUT_DIR / "chapter.md").write_text(chapter)

Paso 04: Review Loop — Developer ↔ Tech Lead Reviewer

El corazón de S4. El developer genera el código; el reviewer lo evalúa. Si hay observaciones, el developer recibe el feedback y reintenta en la siguiente ronda. El loop termina cuando el reviewer aprueba sin observaciones o se alcanza MAX_REVIEW_ROUNDS.
# Review loop en main.py (simplificado)
review_round   = 0
review_feedback = ""

while True:
    review_round += 1
    rnd = f"Ronda {review_round}/{MAX_REVIEW_ROUNDS}"

    # Backend Developer
    feedback_section = (
        f"\nFeedback del reviewer (ronda anterior):\n{review_feedback}"
        if review_feedback else ""
    )
    backend_out = llm.invoke(
        f"[Backend Dev · {rnd}] Implementa el módulo backend.\n"
        f"Brief:\n{chapter}\n{feedback_section}"
    ).content
    (OUTPUT_DIR / f"backend_refactor_r{review_round}.md").write_text(backend_out)

    # Frontend Developer
    frontend_out = llm.invoke(
        f"[Frontend Dev · {rnd}] Implementa el módulo frontend.\n"
        f"Brief:\n{chapter}\n{feedback_section}"
    ).content
    (OUTPUT_DIR / f"frontend_refactor_r{review_round}.md").write_text(frontend_out)

    # Tech Lead Reviewer
    reviewer_out = llm.invoke(
        f"[Tech Lead Reviewer · {rnd}] Evalúa el código.\n"
        f"Backend:\n{backend_out[:1500]}\n"
        f"Frontend:\n{frontend_out[:1500]}\n"
        "Responde con APROBADO, APROBADO CON OBSERVACIONES o RECHAZADO."
    ).content
    (OUTPUT_DIR / f"reviewer_r{review_round}.md").write_text(reviewer_out)

    verdict = _parse_verdict(reviewer_out)

    if verdict == "aprobado":
        print(f"  🎉 [{rnd}] APROBADO — sin observaciones")
        break

    if review_round >= MAX_REVIEW_ROUNDS:
        opt = input("  ¿Hacer push de todas formas? [s/n]: ")
        if opt != "s": sys.exit(0)
        break

    opt = input(f"  Reviewer: {verdict}. ¿Reintentar? [s/n]: ")
    if opt != "s": break
    review_feedback = reviewer_out     # pasa a la siguiente ronda

Paso 05: _parse_verdict() — lógica de evaluación

Función que interpreta la respuesta del reviewer de forma conservadora: si no dice claramente "APROBADO" retorna observaciones.
# En main.py
def _parse_verdict(reviewer_out: str) -> str:
    text = reviewer_out.lower()
    if "rechazado" in text or "rejected" in text:
        return "rechazado"
    if ("aprobado con observaciones" in text
            or "approved with observations" in text
            or ("aprobado" in text and "observaci" in text)):
        return "observaciones"
    if "aprobado" in text or "approved" in text:
        return "aprobado"
    return "observaciones"   # conservador por defecto

Paso 06: Git push + PR vía gh CLI

Una vez aprobado, el pipeline hace push usando gh auth setup-git (sin exponer el token en la URL) y crea el PR con gh pr create --body-file.
# Paso GIT PUSH en main.py (_git_branch_push)
subprocess.run(["gh", "auth", "setup-git"], cwd=repo_dir)
subprocess.run(["git", "checkout", "-b", GIT_HEAD_BRANCH], cwd=repo_dir)
subprocess.run(["git", "add", "-A"], cwd=repo_dir)
subprocess.run(["git", "commit", "-m",
    f"feat(s4): refactor ronda {review_round} — {verdict}"],
    cwd=repo_dir)
subprocess.run(["git", "push", "--set-upstream", "origin",
    GIT_HEAD_BRANCH], cwd=repo_dir)

# Paso PR CREATE en main.py (_gh_create_pr)
body_file = OUTPUT_DIR / ".pr_body.md"
body_file.write_text(pr_body)
subprocess.run([
    "gh", "pr", "create",
    "--title", f"feat(s4): refactor guardrails ({review_round} rondas)",
    "--body-file", str(body_file),
    "--head",  GIT_HEAD_BRANCH,
    "--base",  GIT_BASE_BRANCH,
    "--repo",  f"{GITHUB_OWNER}/{GITHUB_REPO}",
])
body_file.unlink(missing_ok=True)

Paso 07: Ejecutar el pipeline completo

cd s4_guardrails
python main.py

# Salida esperada:
# [CLONE]    Clonando bcp-enterprise/ai-lab-day04 …
# [SNAPSHOT] 47 archivos indexados → output/snapshot.txt
# [ANALYZE]  Arquitecto analizando repositorio …
# [CHAPTER]  Chapter Leader generando brief …
# [BACKEND Dev · Ronda 1/3]  Implementando backend …
# [FRONTEND Dev · Ronda 1/3] Implementando frontend …
# [REVIEWER · Ronda 1/3]     Evaluando …
# → APROBADO CON OBSERVACIONES  ¿Reintentar? [s/n]: s
# [BACKEND Dev · Ronda 2/3]  Incorporando feedback …
# [REVIEWER · Ronda 2/3]     Evaluando …
# 🎉 APROBADO — sin observaciones
# [PUSH]     gh auth setup-git → git push origin feature/guardrails-s4
# [PR]       gh pr create … → https://github.com/…/pull/99

Paso 08: Guardrails de entrada/salida + evals deterministas

Dos barreras deterministas alrededor del pipeline. La de entrada valida el snapshot del repositorio antes del primer nodo: si el código no se pudo leer, o trae secretos, se corta ahí y no se paga ni una llamada al modelo. La de salida revisa el estado final antes del push.
# guardrails/input_check.py — corta antes de gastar tokens
MIN_SNAPSHOT_CHARS, MAX_SNAPSHOT_CHARS = 200, 200_000

SECRET_PATTERNS = [
    (r"sk-[A-Za-z0-9]{20,}",          "API key de OpenAI"),
    (r"ghp_[A-Za-z0-9]{20,}",         "token de GitHub"),
    (r"-----BEGIN .*PRIVATE KEY",     "clave privada"),
]

def validate_snapshot(snapshot) -> ValidationResult:
    # vacio -> tamano -> secretos -> PII; el primero que falla corta
    ...
    return ValidationResult(True, f"OK · {len(snapshot)} chars")
Cada regla de salida lleva un código para trazabilidad: OUT-01 los campos obligatorios (out_analysis, out_plan, out_reviewer) con al menos 50 chars; OUT-02 el reviewer emitió un veredicto legible; OUT-03 los devs emitieron bloques ### ruta/archivo.ext — sin eso el PR saldría vacío; OUT-04 sin secretos. Aparte, OUT-HEX no aplica aquí: S4 refactoriza código ajeno, que no tiene por qué ser hexagonal. Esa validación vive en S2, que genera desde cero.
# Correr la suite completa
cd s4_guardrails
python evals/eval_basic.py     # 10 evals sobre el repo real

# Evals de contrato: peticiones HTTP REALES contra los servicios
# Requiere BACKEND_BASE_URL y API_BASE_URL levantados
python evals/contract_evals.py # AC-01..AC-06
Los guardrails protegen al pipeline real. validate_snapshot() corre antes del primer nodo; validate_output() revisa el estado final antes del push. La regla OUT-03 es la más práctica: si los developers no emiten bloques ### ruta/archivo.ext, no se extrae ningún archivo y el PR saldría vacío — el guardrail lo detecta antes de publicar.

Qué obtengo

Terminal — output real de ejecución (3 rondas)
============================================================
S4 Guardrails — Code Analysis & Refactoring Pipeline
Repo: NTTData-Academy/workshop-requirements-api-template
============================================================

[02:48:16] Clonando NTTData-Academy/workshop-requirements-api-template...
  💾 Checkpoint guardado (next_step=analyze)

[02:48:26] Leyendo código... Snapshot: 15 archivos leídos.

[EJECUTANDO] Analyzer...
  - Secreto hardcodeado: LEGACY_SECRET = "WORKSHOP_SECRET_2026"
  - Autenticación Base64 insegura (no JWT real)
  - SQL por concatenación, uploads sin controles
  - Lógica mezclada en app/main.py
  [Output completo: 24095 chars]
¿Continuar? [s]iguiente / [r]eejecutar / [n]o: s
  ✅ output/analysis.md · 💾 next_step=confirm_type

  Tipo detectado: BACKEND
  ¿Confirmar? (Enter = backend): → BACKEND · 💾 next_step=chapter

[02:49:46] Chapter Lead...
  - Eliminar auth Base64 y secretos hardcodeados
  - Separar responsabilidades de app/main.py
  - Pydantic schemas, SQL parametrizado, upload seguro
  - CI con ruff/mypy/bandit/pip-audit/pytest ≥85%
  [Output completo: 27077 chars]
¿Continuar? s → ✅ output/chapter_plan.md · 💾 next_step=backend

[02:50:54] Backend Developer [Ronda 1/3]...
  [Output: 39145 chars — 13 archivos extraídos]
¿Continuar? s
  💾 app/__init__.py · app/config.py · app/database.py
  💾 app/schemas.py · app/security.py · app/repositories.py
  💾 app/services.py · app/dependencies.py · app/main.py
  💾 app/routers/__init__.py · app/routers/requirements.py
  💾 tests/test_api.py
  ✅ output/backend_refactor_r1.md · 💾 next_step=reviewer

[02:52:42] Tech Lead Reviewer [Ronda 1/3]...
  ✅/⚠️  Separación de responsabilidades
  ✅/⚠️  Secretos hardcodeados (falta validar robustez)
  ✅/⚠️  Autorización por recurso (faltan tests negativos)
  ✅/⚠️  Upload seguro (riesgo si se lee completo en memoria)
  [Output completo: 8917 chars]
¿Continuar? s → ✅ output/reviewer_report_r1.md
  ⚠️  Ronda 1/3: RECHAZADO — ¿Reintentar? s

[02:53:49] Backend Developer [Ronda 2/3]...
  [Output: 41379 chars — 15 archivos extraídos]
¿Continuar? s
  💾 .github/workflows/ci.yml · requirements-dev.txt
  💾 app/security.py · app/routers/uploads.py
  💾 tests/test_security_regression.py · ...
  ✅ output/backend_refactor_r2.md · 💾 next_step=reviewer

[02:55:18] Tech Lead Reviewer [Ronda 2/3]...
  ✅ Separación, Base64 eliminado, CI con ruff/mypy/bandit
  ✅ Tests negativos: uploads, acceso cruzado
  ⚠️  Mínimos no completamente cerrados en CI
  ⚠️  Configuración de secretos: robustecer
  [Output completo: 7924 chars]
¿Continuar? s → ✅ output/reviewer_report_r2.md
  ⚠️  Ronda 2/3: RECHAZADO — ¿Reintentar? s

[02:56:51] Backend Developer [Ronda 3/3]...
  [Output: 45684 chars — 19 archivos extraídos]
¿Continuar? s
  💾 requirements.txt · requirements-dev.txt · pyproject.toml
  💾 app/__init__.py · app/config.py · app/db.py
  💾 app/models.py · app/security.py · app/repositories.py
  💾 app/services.py · app/dependencies.py · app/main.py
  💾 app/routers/__init__.py · app/routers/uploads.py
  💾 tests/conftest.py · tests/test_security.py
  💾 tests/test_tasks.py · tests/test_uploads.py
  ✅ output/backend_refactor_r3.md · 💾 next_step=reviewer

[02:58:37] Tech Lead Reviewer [Ronda 3/3]...
  ✅ Separación en config/security/db/repos/services/routers
  ✅ Auth Base64 eliminada, Pydantic schemas, SQL parametrizado
  ✅ Upload valida ownership y recurso asociado
  ✅ ruff + mypy + bandit + pip-audit + pytest-cov
  ⚠️  CI y cobertura: criterio no completamente cerrado
  [Output completo: 5919 chars]
¿Continuar? s → ✅ output/reviewer_report_r3.md
  ⚠️  Ronda 3/3: APROBADO CON OBSERVACIONES
  Límite de rondas alcanzado. ¿Push de todas formas? [s/n]: s

[08:05:48] Preparando commit y push...
[GIT] Tipo    : feat
[GIT] Rondas  : 3  ·  Veredicto: OBSERVACIONES
[GIT] Rama    : feature/equipo-01/workshop-refactor
[GIT] Mensaje : feat(workshop-requirement): Autenticación casera basada en Base64
  ✅ Rama creada: feature/equipo-01/workshop-refactor
  ✅ Commit y Push → origin/feature/equipo-01/workshop-refactor

[gh] gh pr create --title feat(workshop-requirement): guardrails refactoring ...
✅ PR creado: https://github.com/NTTData-Academy/workshop-requirements-api-template/pull/11
  🗑️  Checkpoint eliminado.

============================================================
✅ Pipeline completado.
============================================================
Estructura generada
s4_guardrails/
├── output/
│   ├── analysis.md
│   ├── chapter_plan.md
│   ├── backend_refactor_r1.md  ← 13 archivos (Ronda 1)
│   ├── backend_refactor_r2.md  ← 15 archivos (Ronda 2)
│   ├── backend_refactor_r3.md  ← 19 archivos (Ronda 3)
│   ├── reviewer_report_r1.md   ← RECHAZADO
│   ├── reviewer_report_r2.md   ← RECHAZADO
│   ├── reviewer_report_r3.md   ← APROBADO CON OBS.
│   └── .checkpoint.json        ← eliminado tras PR
├── _repos/
│   └── workshop-requirements-api-template/
│       ├── app/__init__.py · app/config.py · app/db.py
│       ├── app/models.py · app/security.py
│       ├── app/repositories.py · app/services.py
│       ├── app/dependencies.py · app/main.py
│       ├── app/routers/__init__.py · app/routers/uploads.py
│       ├── tests/conftest.py · tests/test_security.py
│       ├── tests/test_tasks.py · tests/test_uploads.py
│       ├── requirements.txt · requirements-dev.txt
│       ├── pyproject.toml
│       └── .github/workflows/ci.yml
├── main.py
└── mcp_git.py

Flujo completo del Día 4

  • S1 CrewAI HITL → S2 LangGraph + checkpoint → S3 Harness constitution + compliance score → S4 Review Loop 3 rondas con feedback iterativo → PR #11 (se abre en una pestaña nueva) en rama feature/equipo-01/workshop-refactor
  • Review Loop real: 3 rondas ejecutadas — el developer incorporó feedback del reviewer en cada ronda; archivos extraídos crecieron de 13 → 15 → 19 a medida que el código evolucionó
  • El veredicto final fue APROBADO CON OBSERVACIONES al agotar las 3 rondas; el pipeline preguntó antes de hacer push — el humano decide siempre
  • gh pr create --body-file: evita problemas con caracteres especiales y no expone el token en la URL de git

Gate G2

Caso práctico individual — Pipeline multi-agente con Review Loop

El participante extiende el pipeline S4 para un nuevo módulo (p.ej. recuperación de contraseña, registro de usuario): configura el repo, ajusta los prompts del developer y del reviewer, verifica que el Review Loop itere correctamente con el feedback, y confirma que el PR queda abierto con el código aprobado. Evaluado en 5 dimensiones iguales.

Rúbrica de evaluación del Gate G2
DimensiónPeso
Pipeline corre end-to-end sin errores20 %
Review Loop itera con feedback real del reviewer20 %
Checkpoint funcional (retoma tras interrupción)20 %
PR abierto con gh pr create y body descriptivo20 %
Prompts del developer incorporan feedback del reviewer20 %

Umbral de aprobación: ≥ 80 %  ·  Modalidad: ejecución en vivo + revisión con el trainer · 30 min