Cap 24: Claude Code en CI/CD y Tool Design
Claude Code en CI/CD y Tool Design
Este tutorial cubre dos dominios del examen Claude Certified Architect – Foundations:
- Domain 3: Claude Code en pipelines CI/CD
- Domain 2: Diseño de herramientas (Tool Design)
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 code | Significado |
|---|---|
| 0 | Éxito |
| 1 | Error de ejecución |
| 2 | Error 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
fetch-depth: 0es necesario para quegit difffuncione correctamente entre branches.permissions: pull-requests: writepermite que el workflow comente en el PR.- El output de Claude se parsea con
jq— por eso necesitamos--output-format json. head -20limita 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:
| Variable | Tipo | Descripción |
|---|---|---|
ANTHROPIC_API_KEY | Secret | API key de Anthropic |
CLAUDE_MAX_TOKENS | Variable | Lí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:
- Qué hace exactamente — verbo concreto, no genérico
- Formato exacto del input — tipo, encoding, límites
- Formato exacto del output — campos, tipos, valores posibles
- Cuándo USAR esta herramienta — trigger conditions
- Cuándo NO USAR esta herramienta — boundaries explícitos
- 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:
- Selecciona herramientas incorrectas para tareas ambiguas
- Produce resultados inconsistentes
- Es más difícil de debuggear y testear
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
| Concepto | Regla |
|---|---|
| Modo no-interactivo | Flag -p, --output-format json |
| Session isolation | Nueva sesión para generate y para review |
| CLAUDE.md en CI | Output format explícito, criterios categóricos, umbrales |
| Tool descriptions | Input, output, cuándo usar, cuándo NO usar |
| Error responses | isRetryable: false para datos genuinamente ausentes |
| Distribución de tools | Menos tools por agente = mayor confiabilidad |
tool_choice: "any" | Garantiza que Claude siempre llame una tool |
.mcp.json en proyecto | Scope 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
| Concepto | Regla |
|---|---|
| Modo no-interactivo | Flag -p, --output-format json |
| Session isolation | Nueva sesión para generate y para review |
| CLAUDE.md en CI | Output format explícito, criterios categóricos, umbrales |
| Tool descriptions | Input, output, cuándo usar, cuándo NO usar |
| Error responses | isRetryable: false para datos genuinamente ausentes |
| Distribución de tools | Menos tools por agente = mayor confiabilidad |
tool_choice: "any" | Garantiza que Claude siempre llame una tool |
.mcp.json en proyecto | Scope correcto para CI/CD |
| Escaping en CI | jq -Rs . para contenido externo en prompts |
| Tool selection tests | Suite TypeScript con queries ambiguas + matriz esperado/obtenido |
| Coverage diferencial | Analizar solo líneas del diff, no cobertura total |
Siguiente: Tips de Productividad