Cap 24: Claude Code en CI/CD y Tool Design

Por: Artiko
claude-codecicdgithub-actionsgitlab-cimcptool-designcertificacion

Claude Code en CI/CD y Tool Design

Este tutorial cubre dos dominios del examen Claude Certified Architect – Foundations:


1. Modo No-Interactivo

Claude Code fue diseñado para correr en terminales interactivas, pero en CI/CD necesitamos que opere de forma autónoma.

Flag -p (—print)

El flag -p activa el modo no-interactivo: Claude ejecuta el prompt y termina. No espera input del usuario.

# Modo interactivo (por defecto, espera input)
claude "revisa este código"

# Modo no-interactivo: ejecuta y termina
claude -p "revisa este código y reporta issues"

Output formats

Por defecto, Claude imprime texto en formato legible para humanos. En CI necesitamos machine-parseable:

# JSON estructurado para parsear con jq o scripts
claude -p "analiza el PR" --output-format json

# Forzar un schema específico en el JSON de respuesta
claude -p "analiza el PR" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"issues":{"type":"array"},"severity":{"type":"string"}}}'

Exit codes

Claude Code usa exit codes estándar en modo -p:

Exit codeSignificado
0Éxito
1Error de ejecución
2Error de configuración / API key inválida

Estos exit codes permiten a tu pipeline tomar decisiones con if [ $? -ne 0 ].


2. Session Isolation: Por qué importa en CI/CD

Un principio crítico: Claude no puede revisar efectivamente sus propios cambios en la misma sesión.

El problema

Cuando Claude genera código en una sesión y luego se le pide que lo revise, el modelo tiene el contexto completo de su propio razonamiento. Esto crea un bias de confirmación: tiende a validar lo que generó en lugar de detectar errores.

sequenceDiagram
    participant CI as CI Pipeline
    participant S1 as Sesión 1 (Generate)
    participant S2 as Sesión 2 (Review)
    participant Repo as Repositorio

    CI->>S1: "Genera tests para auth.ts"
    S1->>Repo: Escribe tests/auth.test.ts
    CI->>S2: "Revisa tests/auth.test.ts (sin contexto previo)"
    S2->>CI: Review objetivo con issues reales

La solución: sesión nueva por cada review

Cada paso del pipeline debe iniciar una sesión completamente nueva. Nunca reutilices una sesión para generar Y revisar.

flowchart LR
    A[PR abierto] --> B[Sesión 1: Generar review]
    B --> C[Parsear JSON output]
    C --> D{Issues críticos?}
    D -->|Sí| E[Fail pipeline]
    D -->|No| F[Comentar en PR]
    F --> G[Merge permitido]

3. GitHub Actions: Code Review Automático

Workflow completo para revisar PRs automáticamente con Claude Code.

# .github/workflows/claude-review.yml
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Get changed files
        id: changed
        run: |
          FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | \
            grep -E '\.(ts|tsx|js|jsx|py|go|rs)$' | \
            head -20)
          echo "files=$FILES" >> $GITHUB_OUTPUT

      - name: Run Claude Review
        id: review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- ${{ steps.changed.outputs.files }})

          OUTPUT=$(claude -p \
            --output-format json \
            "Revisa este diff de código. Responde en JSON con: {\"summary\": string, \"issues\": [{\"file\": string, \"line\": number, \"severity\": \"critical|warning|info\", \"message\": string}], \"approved\": boolean}

          DIFF:
          $DIFF" 2>&1)

          echo "output=$OUTPUT" >> $GITHUB_OUTPUT

          # Fallar si hay issues críticos
          CRITICAL=$(echo "$OUTPUT" | jq '[.issues[] | select(.severity == "critical")] | length')
          if [ "$CRITICAL" -gt "0" ]; then
            echo "❌ Se encontraron $CRITICAL issues críticos"
            exit 1
          fi

      - name: Comment on PR
        if: always()
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          OUTPUT='${{ steps.review.outputs.output }}'
          SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "Sin resumen"')
          ISSUES=$(echo "$OUTPUT" | jq -r '.issues[] | "- **\(.severity)** `\(.file):\(.line)` — \(.message)"' 2>/dev/null || echo "Sin issues")

          COMMENT="## 🤖 Claude Code Review

          $SUMMARY

          ### Issues encontrados

          $ISSUES"

          gh pr comment ${{ github.event.pull_request.number }} \
            --body "$COMMENT"

Notas importantes del workflow

  1. fetch-depth: 0 es necesario para que git diff funcione correctamente entre branches.
  2. permissions: pull-requests: write permite que el workflow comente en el PR.
  3. El output de Claude se parsea con jq — por eso necesitamos --output-format json.
  4. head -20 limita los archivos a revisar para no exceder el context window.

4. GitLab CI: Equivalente

# .gitlab-ci.yml
stages:
  - review
  - test

variables:
  ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY  # Configurar en GitLab CI/CD Variables

claude-review:
  stage: review
  image: node:20-alpine
  only:
    - merge_requests
  script:
    - npm install -g @anthropic-ai/claude-code
    - |
      DIFF=$(git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD)

      claude -p \
        --output-format json \
        "Revisa este diff. JSON: {\"issues\": [{\"severity\": \"critical|warning\", \"message\": string}], \"approved\": boolean}

      DIFF: $DIFF" > review-output.json

      CRITICAL=$(jq '[.issues[] | select(.severity == "critical")] | length' review-output.json)
      if [ "$CRITICAL" -gt "0" ]; then
        echo "Issues críticos encontrados:"
        jq '.issues[] | select(.severity == "critical")' review-output.json
        exit 1
      fi
  artifacts:
    paths:
      - review-output.json
    expire_in: 7 days
    when: always
  allow_failure: false

Variables de entorno en GitLab

Configurar en Settings → CI/CD → Variables:

VariableTipoDescripción
ANTHROPIC_API_KEYSecretAPI key de Anthropic
CLAUDE_MAX_TOKENSVariableLímite de tokens por request

5. CLAUDE.md para CI/CD

Cuando Claude corre en CI, el CLAUDE.md debe ser más explícito que para uso interactivo. Claude no puede pedirte aclaraciones.

Qué incluir en CLAUDE.md para CI

# CLAUDE.md — CI/CD Configuration

## Modo de operación
Estás corriendo en un pipeline CI/CD automatizado. No hay usuario presente.
Nunca pidas aclaraciones. Si falta información, usa valores por defecto documentados aquí.

## Output format requerido
SIEMPRE responde en JSON válido con esta estructura exacta:
{
  "summary": "string — resumen de máximo 200 caracteres",
  "issues": [
    {
      "file": "ruta/relativa/al/archivo.ts",
      "line": 42,
      "severity": "critical | warning | info",
      "category": "security | performance | correctness | style",
      "message": "descripción del issue"
    }
  ],
  "approved": true | false,
  "metrics": {
    "files_reviewed": 0,
    "total_issues": 0
  }
}

## Criterios de review

### Critical (falla el pipeline):
- SQL injection o XSS potencial
- Secrets hardcodeados (API keys, passwords)
- Funciones async sin manejo de errores
- Imports de módulos inexistentes

### Warning (comentario, no falla):
- Variables declaradas pero no usadas
- Console.log en código de producción
- Funciones con más de 50 líneas

### Info (informativo):
- Sugerencias de refactoring
- Oportunidades de optimización

## Testing standards
- Framework: Vitest
- Fixtures disponibles en: tests/fixtures/
- Mocks en: tests/mocks/
- Cobertura mínima: 80%

## Umbrales que fallan el pipeline
- Más de 0 issues de tipo "critical"
- Más de 10 issues de tipo "warning"
- Cobertura de tests por debajo del 80%

6. Generación de Tests en CI

Patrón para generar tests automáticamente cuando se agregan nuevas funciones:

flowchart TD
    A[Nueva función detectada en PR] --> B[Sesión: Generar tests]
    B --> C[Guardar tests generados]
    C --> D[Ejecutar tests]
    D --> E{Tests pasan?}
    E -->|Sí| F[Commit tests al PR]
    E -->|No| G[Sesión: Corregir tests]
    G --> D
    E -->|Fallo 3+ veces| H[Fallar pipeline con reporte]

Script de generación de tests

#!/bin/bash
# scripts/generate-tests.sh

MAX_RETRIES=3
RETRY=0

generate_and_run() {
  local source_file=$1
  local test_file="${source_file%.ts}.test.ts"

  # Sesión nueva: generar tests
  claude -p \
    --output-format json \
    "Lee el archivo $source_file y genera tests completos en Vitest.
    Responde con JSON: {\"test_code\": \"...\", \"imports_needed\": [\"...\"]}.
    Los tests deben ser ejecutables sin modificaciones.
    Usa los fixtures en tests/fixtures/ si aplica.
    $(cat $source_file)" > /tmp/test-output.json

  TEST_CODE=$(jq -r '.test_code' /tmp/test-output.json)
  echo "$TEST_CODE" > "$test_file"

  # Ejecutar tests
  if npx vitest run "$test_file" --reporter=json > /tmp/test-results.json 2>&1; then
    echo "✅ Tests generados y pasando: $test_file"
    return 0
  else
    return 1
  fi
}

SOURCE_FILE=$1
while [ $RETRY -lt $MAX_RETRIES ]; do
  if generate_and_run "$SOURCE_FILE"; then
    exit 0
  fi
  RETRY=$((RETRY + 1))
  echo "Intento $RETRY/$MAX_RETRIES falló"
done

echo "❌ No se pudieron generar tests válidos después de $MAX_RETRIES intentos"
exit 1

7. Tool Design — Domain 2

Esta sección es crítica para el examen. El diseño de herramientas determina si Claude las usará correctamente.

El problema con descripciones vagas

Claude selecciona herramientas basándose en sus descripciones. Una descripción vaga produce selecciones incorrectas.

MALO:

{
  "name": "analyze_content",
  "description": "Analyzes content",
  "input_schema": {
    "type": "object",
    "properties": {
      "data": { "type": "string" }
    }
  }
}

Problemas: ¿Qué tipo de contenido? ¿Qué devuelve? ¿Cuándo NO usar esta herramienta?

BUENO:

{
  "name": "analyze_invoice_pdf",
  "description": "Extrae datos estructurados de facturas en formato PDF. Input: PDF codificado en base64. Devuelve: vendor_name, total_amount, line_items[], invoice_date, currency. Úsala cuando el usuario suba una factura para procesar. NO la uses para páginas web, imágenes de texto genéricas o documentos que no sean facturas.",
  "input_schema": {
    "type": "object",
    "properties": {
      "pdf_base64": {
        "type": "string",
        "description": "PDF codificado en base64. Máximo 10MB."
      },
      "extract_line_items": {
        "type": "boolean",
        "description": "Si true, extrae cada línea de la factura. Default: true."
      }
    },
    "required": ["pdf_base64"]
  }
}

Reglas para descripciones efectivas

Toda buena descripción de herramienta incluye:

  1. Qué hace exactamente — verbo concreto, no genérico
  2. Formato exacto del input — tipo, encoding, límites
  3. Formato exacto del output — campos, tipos, valores posibles
  4. Cuándo USAR esta herramienta — trigger conditions
  5. Cuándo NO USAR esta herramienta — boundaries explícitos
  6. Ejemplos de queries que deben rutear a esta herramienta

Comparación completa

// MALO: Claude no sabe cómo usarla
const badTool = {
  name: "search",
  description: "Searches for information",
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string" }
    }
  }
}

// BUENO: Claude selecciona correctamente
const goodTool = {
  name: "search_product_catalog",
  description: `Busca productos en el catálogo interno por nombre, SKU o categoría.
Devuelve: [{id, name, sku, price, stock, category}].
Úsala cuando el usuario pregunte por disponibilidad, precio o características de productos.
NO la uses para buscar información general, noticias o datos externos.
Queries que van aquí: "¿tienen el modelo X?", "precio de SKU-123", "productos de la categoría Y".`,
  input_schema: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "Término de búsqueda: nombre parcial, SKU completo, o nombre de categoría"
      },
      max_results: {
        type: "number",
        description: "Máximo de resultados. Default: 10, máximo: 50."
      }
    },
    required: ["query"]
  }
}

8. Error Responses Estructuradas en MCP Tools

Las herramientas MCP deben devolver errores que permitan a Claude decidir si reintentar o no.

Estructura recomendada

interface ToolError {
  isError: true;
  errorCategory: "transient" | "validation" | "business" | "permission";
  isRetryable: boolean;
  message: string;
  retryAfterMs?: number;  // Solo si isRetryable: true
}

Cuándo usar cada categoría

// TRANSIENT: error temporal, Claude debe reintentar
function handleTimeout(): ToolError {
  return {
    isError: true,
    errorCategory: "transient",
    isRetryable: true,
    message: "El servicio no respondió a tiempo. Reintenta en unos segundos.",
    retryAfterMs: 3000
  };
}

// VALIDATION: input incorrecto, Claude debe corregir el input
function handleInvalidInput(field: string): ToolError {
  return {
    isError: true,
    errorCategory: "validation",
    isRetryable: false,
    message: `El campo '${field}' tiene formato inválido. Formato esperado: YYYY-MM-DD.`
  };
}

// BUSINESS: dato genuinamente ausente, NO reintentar
function handleNotFound(id: string): ToolError {
  return {
    isError: true,
    errorCategory: "business",
    isRetryable: false,
    message: `El producto con ID '${id}' no existe en el catálogo.`
  };
}

// PERMISSION: sin acceso, NO reintentar
function handleUnauthorized(): ToolError {
  return {
    isError: true,
    errorCategory: "permission",
    isRetryable: false,
    message: "Sin permisos para acceder a datos financieros. Contacta al administrador."
  };
}

Distinción crítica: timeout vs vacío válido

// ❌ INCORRECTO: reintentar cuando el resultado es vacío válido
async function searchProducts_bad(query: string) {
  const results = await db.search(query);
  if (results.length === 0) {
    return {
      isError: true,
      errorCategory: "transient",
      isRetryable: true,  // ← INCORRECTO: no hay productos, no es un error
      message: "No se encontraron resultados"
    };
  }
  return results;
}

// ✅ CORRECTO: vacío es un resultado válido
async function searchProducts_good(query: string) {
  try {
    const results = await db.search(query);
    return { results, total: results.length };  // Devolver array vacío es válido
  } catch (err) {
    if (err.code === 'TIMEOUT') {
      return {
        isError: true,
        errorCategory: "transient",
        isRetryable: true,
        message: "Base de datos no disponible temporalmente."
      };
    }
    throw err;
  }
}

9. Distribución de Herramientas entre Agentes

Un principio fundamental: demasiadas herramientas disminuye la confiabilidad.

Por qué menos herramientas es mejor

Cuando un agente tiene acceso a herramientas fuera de su especialización:

flowchart TD
    subgraph Incorrecto["❌ Incorrecto: Todas las tools en todos los agentes"]
        A1[Research Agent] --> T1[web_search]
        A1 --> T2[write_file]
        A1 --> T3[execute_sql]
        A1 --> T4[send_email]
        A2[Synthesis Agent] --> T1
        A2 --> T2
        A2 --> T3
        A2 --> T4
    end

    subgraph Correcto["✅ Correcto: Tools especializadas por agente"]
        B1[Research Agent] --> U1[web_search]
        B1 --> U2[fetch_url]
        B2[Synthesis Agent] --> U3[write_file]
        B2 --> U4[verify_fact]
        B3[Delivery Agent] --> U5[send_email]
        B3 --> U6[create_ticket]
    end

tool_choice: controlar cuándo Claude usa herramientas

import anthropic

client = anthropic.Anthropic()

# tool_choice: "any" — Claude SIEMPRE debe llamar una tool
# Útil cuando necesitas output estructurado garantizado
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=[search_tool, write_tool],
    tool_choice={"type": "any"},  # Garantiza que siempre use una tool
    messages=[{"role": "user", "content": "Analiza el repositorio"}]
)

# tool_choice: "auto" — Claude decide si usar herramientas
# Comportamiento por defecto
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=[search_tool, write_tool],
    tool_choice={"type": "auto"},
    messages=[{"role": "user", "content": "¿Qué hace esta función?"}]
)

# tool_choice: "tool" — Claude DEBE usar esta herramienta específica
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=[format_output_tool],
    tool_choice={"type": "tool", "name": "format_output"},
    messages=[{"role": "user", "content": "Dame el reporte final"}]
)

Cuándo dar acceso cross-role

En algunos casos, un agente necesita una herramienta de otro dominio:

// Synthesis Agent normalmente NO tiene web_search
// PERO puede tener verify_fact (versión limitada de search)
const synthesisAgentTools = [
  write_document_tool,
  format_output_tool,
  // Acceso de solo lectura para verificar datos, no para investigar
  {
    name: "verify_fact",
    description: `Verifica si un dato específico es correcto consultando fuentes confiables.
    Solo úsala para confirmar hechos que ya tienes, no para investigar nuevos temas.
    Input: el dato a verificar. Output: {confirmed: boolean, source: string}.
    NO uses esto para buscar información nueva — usa el Research Agent para eso.`
  }
]

10. Testing de Tool Selection

Cómo verificar que tus descripciones funcionan correctamente.

Test con requests ambiguos

import anthropic
import json

client = anthropic.Anthropic()

# Herramientas con descripciones a testear
tools = [search_product_tool, search_knowledge_base_tool, search_web_tool]

# Test cases: queries ambiguos que deben rutear a la tool correcta
test_cases = [
    {
        "query": "¿tienen el modelo MacBook Pro M3?",
        "expected_tool": "search_product_catalog"
    },
    {
        "query": "¿cómo configuro el adaptador?",
        "expected_tool": "search_knowledge_base"
    },
    {
        "query": "últimas noticias sobre Apple",
        "expected_tool": "search_web"
    }
]

def test_tool_selection():
    failures = []

    for case in test_cases:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=256,
            tools=tools,
            tool_choice={"type": "any"},
            messages=[{"role": "user", "content": case["query"]}]
        )

        selected_tool = response.content[0].name
        if selected_tool != case["expected_tool"]:
            failures.append({
                "query": case["query"],
                "expected": case["expected_tool"],
                "got": selected_tool
            })

    if failures:
        print("❌ Tool selection failures:")
        print(json.dumps(failures, indent=2))
        return False

    print("✅ Todas las tool selections correctas")
    return True

test_tool_selection()

Iterar sobre descripciones

Si una herramienta se selecciona incorrectamente, el proceso de mejora es:

flowchart LR
    A[Ejecutar test suite] --> B{Algún fallo?}
    B -->|No| C[Descripciones correctas ✅]
    B -->|Sí| D[Identificar query problemática]
    D --> E[Analizar por qué Claude eligió mal]
    E --> F[Mejorar descripción de la tool incorrecta]
    F --> G[Agregar boundary explícito en ambas tools]
    G --> A

Keywords que causan confusión

Evita keywords genéricas en descripciones que se solapan con otras herramientas:

// PROBLEMA: "buscar" aparece en ambas tools
const tool1 = { description: "Busca información en el catálogo" }
const tool2 = { description: "Busca información en la base de conocimiento" }

// SOLUCIÓN: diferenciadores explícitos
const tool1Fixed = {
  description: `Consulta el inventario de productos por SKU, nombre o categoría.
  SOLO para preguntas sobre disponibilidad, precio y stock.
  Queries: "¿tienen X?", "precio de Y", "stock de Z"`
}
const tool2Fixed = {
  description: `Consulta artículos de soporte, manuales y guías de configuración.
  SOLO para preguntas sobre cómo usar productos, troubleshooting o configuración.
  Queries: "cómo configurar X", "error al instalar Y", "pasos para Z"`
}

11. MCP Servers en Workflows CI/CD

Configurar MCP servers para CI

Los MCP servers en CI necesitan credenciales via variables de entorno. Usa el archivo .mcp.json del proyecto (no el global del usuario).

// .mcp.json (en raíz del proyecto, commiteado al repo)
{
  "mcpServers": {
    "database": {
      "command": "npx",
      "args": ["-y", "@company/mcp-database"],
      "env": {
        "DB_HOST": "${DB_HOST}",
        "DB_PORT": "${DB_PORT}",
        "DB_NAME": "${DB_NAME}",
        "DB_USER": "${DB_USER}",
        "DB_PASSWORD": "${DB_PASSWORD}"
      }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}

GitHub Actions con MCP

# .github/workflows/claude-with-mcp.yml
- name: Run Claude with MCP access
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    DB_HOST: ${{ secrets.DB_HOST }}
    DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
    DB_USER: ${{ vars.DB_USER }}
    DB_NAME: ${{ vars.DB_NAME }}
    DB_PORT: "5432"
    GITHUB_TOKEN: ${{ github.token }}
  run: |
    claude -p \
      --mcp-config .mcp.json \
      "Verifica que todas las migraciones de base de datos sean reversibles y
      no rompan las queries existentes. Devuelve JSON con {safe: boolean, issues: []}."

Scoping: por qué usar .mcp.json del proyecto

flowchart TD
    A["~/.claude/settings.json (global)"] --> B[Disponible para todos los proyectos]
    C[".mcp.json (proyecto)"] --> D[Solo disponible en este pipeline]

    style A fill:#ff6b6b,color:#fff
    style C fill:#51cf66,color:#fff

    B --> E[❌ Expone tools de producción en proyectos de test]
    D --> F[✅ Solo tools relevantes para este CI/CD]

Usar .mcp.json en el proyecto garantiza que el CI solo tiene acceso a los backends necesarios, no a todos los MCP servers configurados globalmente en la máquina del desarrollador.


Resumen: Puntos Clave para el Examen

ConceptoRegla
Modo no-interactivoFlag -p, --output-format json
Session isolationNueva sesión para generate y para review
CLAUDE.md en CIOutput format explícito, criterios categóricos, umbrales
Tool descriptionsInput, output, cuándo usar, cuándo NO usar
Error responsesisRetryable: false para datos genuinamente ausentes
Distribución de toolsMenos tools por agente = mayor confiabilidad
tool_choice: "any"Garantiza que Claude siempre llame una tool
.mcp.json en proyectoScope correcto para CI/CD

12. Manejo Seguro de Contenido en CI

El problema: caracteres especiales rompen el JSON

Cuando se pasa un diff como string directo en el prompt, los caracteres especiales (", \n, `) corrompen el JSON que Claude intenta parsear:

# ❌ INCORRECTO: $DIFF sin escapar puede contener comillas, barras, etc.
OUTPUT=$(claude -p \
  --output-format json \
  "Revisa este diff: $DIFF")
# Error: unexpected token in JSON at position 47

La solución: jq -Rs .

jq -Rs . lee stdin como raw string y lo convierte en un string JSON correctamente escapado:

# ✅ CORRECTO: escapar el diff antes de interpolarlo
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
DIFF_ESCAPED=$(echo "$DIFF" | jq -Rs .)

# Construir el prompt con jq para garantizar JSON válido
PROMPT=$(jq -n --argjson diff "$DIFF_ESCAPED" \
  '"Revisa este diff de código. Responde en JSON con issues y severidad.\n\nDIFF:\n" + $diff')

OUTPUT=$(claude -p --output-format json "$PROMPT")

Workflow corregido (paso Run Claude Review)

      - name: Run Claude Review
        id: review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          DIFF=$(git diff origin/${{ github.base_ref }}...HEAD \
            -- ${{ steps.changed.outputs.files }})

          # Escapar contenido arbitrario para uso seguro en JSON
          DIFF_ESCAPED=$(echo "$DIFF" | jq -Rs .)

          PROMPT=$(jq -n --argjson diff "$DIFF_ESCAPED" \
            '"Revisa este diff. JSON: {\"summary\":string,\"issues\":[{\"file\":string,\"line\":number,\"severity\":\"critical|warning|info\",\"message\":string}],\"approved\":boolean}\n\nDIFF:\n" + $diff')

          OUTPUT=$(claude -p --output-format json "$PROMPT" 2>&1)
          echo "output=$OUTPUT" >> $GITHUB_OUTPUT

          CRITICAL=$(echo "$OUTPUT" | jq '[.issues[] | select(.severity == "critical")] | length')
          if [ "$CRITICAL" -gt "0" ]; then
            echo "Se encontraron $CRITICAL issues críticos"
            exit 1
          fi

Regla general: cualquier contenido externo (diff, archivos, logs) que se interpole en un prompt debe pasar por jq -Rs . antes de usarse.


13. Testing de Tool Selection Reliability

Una descripción de herramienta puede parecer correcta pero fallar con queries ambiguas del mundo real. La única forma de saberlo es con tests.

Suite de tests en TypeScript

// tests/tool-selection.test.ts
import Anthropic from "@anthropic-ai/sdk";
import { expect, test, describe } from "vitest";

const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "search_customer",
    description: `Busca un cliente por nombre, email o ID de cliente.
Devuelve: {id, name, email, plan, created_at}.
Úsala cuando pregunten por datos de un cliente específico.
NO la uses para buscar órdenes o productos.
Queries: "datos del cliente X", "email de Juan", "cliente ID-123".`,
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Nombre, email o ID del cliente" }
      },
      required: ["query"]
    }
  },
  {
    name: "lookup_order",
    description: `Consulta una orden de compra por número de orden o cliente.
Devuelve: {order_id, status, items[], total, created_at}.
Úsala cuando pregunten por el estado o contenido de una compra.
NO la uses para datos de clientes ni información de productos.
Queries: "estado de la orden #456", "compras de María", "orden de ayer".`,
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Número de orden o nombre del cliente" }
      },
      required: ["query"]
    }
  },
  {
    name: "get_product",
    description: `Obtiene información de un producto por nombre, SKU o categoría.
Devuelve: {sku, name, price, stock, description}.
Úsala cuando pregunten por disponibilidad, precio o características de productos.
NO la uses para clientes ni órdenes.
Queries: "precio del SKU-789", "¿tienen laptops?", "características del modelo X".`,
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Nombre, SKU o categoría del producto" }
      },
      required: ["query"]
    }
  }
];

const testCases = [
  { query: "¿cuál es el email de Ana García?",         expected: "search_customer" },
  { query: "¿llegó mi pedido del martes?",             expected: "lookup_order"    },
  { query: "¿tienen el modelo Pro en stock?",          expected: "get_product"     },
  { query: "cliente con ID C-9921",                    expected: "search_customer" },
  { query: "estado de la orden #ORD-4455",             expected: "lookup_order"    },
];

describe("tool selection reliability", () => {
  test.each(testCases)("'$query' → $expected", async ({ query, expected }) => {
    const response = await client.messages.create({
      model: "claude-opus-4-5",
      max_tokens: 256,
      tools,
      tool_choice: { type: "any" },
      messages: [{ role: "user", content: query }]
    });

    const selected = (response.content[0] as Anthropic.ToolUseBlock).name;
    expect(selected).toBe(expected);
  });
});

Matriz de resultados: esperado vs obtenido

Ejecutar npx vitest run tests/tool-selection.test.ts produce una tabla clara:

 FAIL  tests/tool-selection.test.ts
  tool selection reliability
    ✓ '¿cuál es el email de Ana García?' → search_customer
    ✗ '¿llegó mi pedido del martes?' → lookup_order
        expected: 'lookup_order'
        received: 'search_customer'   ← "mi pedido" se confunde con cliente
    ✓ '¿tienen el modelo Pro en stock?' → get_product
    ✓ 'cliente con ID C-9921' → search_customer
    ✓ 'estado de la orden #ORD-4455' → lookup_order

Ciclo de iteración

flowchart LR
    A[Ejecutar test suite] --> B{Algún fallo?}
    B -->|No| C[Descripciones confiables]
    B -->|Sí| D[Identificar query fallida]
    D --> E[Leer descripción de la tool incorrecta]
    E --> F[Agregar boundary explícito:\nNO la uses cuando...]
    F --> G[Agregar query de ejemplo a ambas tools]
    G --> A

Regla práctica: si dos tools comparten un keyword genérico (cliente, buscar, datos), agrega ese keyword como boundary negativo en la que NO debe responder.


14. Análisis de Cobertura Diferencial en CI

En lugar de medir cobertura total (que puede ocultar gaps en código nuevo), se analiza específicamente qué líneas del diff no tienen cobertura.

Flujo completo

flowchart TD
    A[PR con cambios] --> B[Ejecutar tests con coverage\nnpx vitest --coverage]
    B --> C[Generar lcov.info]
    C --> D[Extraer diff del PR]
    D --> E[Claude: analizar qué líneas del diff\nno aparecen en lcov.info]
    E --> F{Gaps críticos?}
    F -->|Sí| G[Fail pipeline\ncon lista de funciones sin test]
    F -->|No| H[Pass + reporte informativo]

Script de análisis

#!/bin/bash
# scripts/coverage-diff-analysis.sh

# 1. Generar coverage report
npx vitest run --coverage --reporter=lcov
LCOV_FILE="coverage/lcov.info"

# 2. Obtener funciones modificadas en el diff
DIFF=$(git diff origin/$BASE_BRANCH...HEAD)
CHANGED_FUNCTIONS=$(echo "$DIFF" | grep "^+" | grep -E "^\\+.*(function |const .+ = |=> \{)" | \
  sed 's/^+//')

# 3. Enviar a Claude para análisis cruzado
LCOV_CONTENT=$(cat "$LCOV_FILE" | head -500)  # primeras 500 líneas son suficientes

ANALYSIS=$(claude -p --output-format json \
  "Analiza qué funciones del DIFF no están cubiertas en el LCOV report.
   Responde JSON: {\"covered\": [\"fn1\"], \"uncovered\": [\"fn2\"], \"coverage_pct\": 85}

   DIFF (funciones modificadas):
   $(echo "$CHANGED_FUNCTIONS" | jq -Rs .)

   LCOV REPORT (extracto):
   $(echo "$LCOV_CONTENT" | jq -Rs .)")

UNCOVERED=$(echo "$ANALYSIS" | jq '.uncovered | length')
echo "$ANALYSIS" | jq .

if [ "$UNCOVERED" -gt "3" ]; then
  echo "Más de 3 funciones modificadas sin cobertura de test"
  exit 1
fi

Prompt específico para coverage gap analysis

Analiza el siguiente diff de código y el reporte de cobertura lcov.
Tu tarea: identificar qué funciones o bloques NUEVOS o MODIFICADOS en el diff
no tienen líneas correspondientes en el lcov report.

Reglas:
- Solo reporta funciones que aparecen en el diff (no las que ya existían)
- Una función está "cubierta" si su número de línea aparece en el lcov con DA:<line>,<count> donde count > 0
- Ignora comentarios y líneas de declaración de tipos

Responde con JSON:
{
  "covered": ["nombre de función o descripción"],
  "uncovered": ["nombre de función o descripción"],
  "coverage_pct": 0-100,
  "risk_assessment": "low|medium|high"
}

Integración en GitHub Actions

      - name: Coverage diff analysis
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          BASE_BRANCH: ${{ github.base_ref }}
        run: |
          npx vitest run --coverage --reporter=lcov --silent
          bash scripts/coverage-diff-analysis.sh

Resumen: Puntos Clave para el Examen

ConceptoRegla
Modo no-interactivoFlag -p, --output-format json
Session isolationNueva sesión para generate y para review
CLAUDE.md en CIOutput format explícito, criterios categóricos, umbrales
Tool descriptionsInput, output, cuándo usar, cuándo NO usar
Error responsesisRetryable: false para datos genuinamente ausentes
Distribución de toolsMenos tools por agente = mayor confiabilidad
tool_choice: "any"Garantiza que Claude siempre llame una tool
.mcp.json en proyectoScope correcto para CI/CD
Escaping en CIjq -Rs . para contenido externo en prompts
Tool selection testsSuite TypeScript con queries ambiguas + matriz esperado/obtenido
Coverage diferencialAnalizar solo líneas del diff, no cobertura total

Siguiente: Tips de Productividad