Especial: las skills de Claude Code y el harness del CLI

Por: Artiko
bifrostclaude-codeskillscliharnessgoai-agentes

Especial: las skills de Claude Code y el harness del CLI

Los catorce capítulos anteriores miraron a Bifrost desde afuera: cómo se instala, se configura y se usa como gateway. Este capítulo es distinto. Es un capítulo meta: abrimos el repositorio maximhq/bifrost y miramos cómo Maxim construye y opera Bifrost, porque ahí hay dos artefactos que valen un estudio propio:

  1. La carpeta .claude/skills/: diez skills de Claude Code que codifican, paso a paso, cómo se hace cada operación repetitiva del proyecto (investigar un issue, escribir tests, generar el changelog, auditar migraciones, revisar un PR). Es un caso real de cómo un equipo automatiza la parte mecánica de su ingeniería sin ceder el juicio.
  2. La carpeta cli/: el CLI de Bifrost, que —sorpresa— no es el gateway. Es un multiplexor de terminal que lanza CLIs de agentes de código (Claude Code, Codex, Gemini, Opencode) y los enruta a través de un gateway Bifrost, todo dentro de una TUI con pestañas. Su código es una clase magistral de cómo emular una terminal de forma testeable.

Ambas cosas comparten un tema: enseñarle a una herramienta a hacer bien una tarea repetible. Empecemos por las skills.

Todo lo de este capítulo sale de leer el código de la rama dev del repositorio público. Los detalles (nombres de tipos, constantes, rutas) corresponden a ese estado y pueden evolucionar.


Parte 1 · Las skills de Claude Code del proyecto

Qué es una skill (y qué no es)

Una skill de Claude Code no es un prompt bonito ni una lista de tips. Es un procedimiento determinista congelado: un archivo SKILL.md con pasos numerados, los comandos exactos a ejecutar (gh api graphql ..., python3 bundle.py --output /tmp/..., loops de paginación con cursor) y —crucialmente— las prohibiciones que evitan daño. Su valor no es “saber hacer un changelog”, sino codificar la forma exacta en que este repositorio lo hace, eliminando la variabilidad entre una corrida y la siguiente.

En Bifrost, cada skill vive en .claude/skills/<nombre>/SKILL.md (un único archivo por skill; los “scripts” que ejecuta van embebidos como bloques de bash/node/python dentro del propio SKILL.md). Se invocan como slash commands: /investigate-issue 1234, /review-pr, /api-validator --fix. El archivo raíz AGENTS.md cataloga el layout del monorepo Go (core → framework → plugins/* → transports), el workspace y todos los comandos make de test; las skills se apoyan constantemente en él.

Detalle revelador: AGENTS.md todavía dice que hay “4 skills”. Ya son 10. Ni la mejor documentación de agentes se libra de quedar desactualizada respecto al código.

Las diez skills

SkillFaseQué hace
investigate-issueTriageAnaliza un issue de GitHub, clasifica, busca en código, investiga docs externas y produce un plan accionable.
api-validatorAuditoríaCompara la superficie HTTP real (escaneando las rutas en Go) contra la OpenAPI documentada, derivando la autenticación efectiva.
harness-test-writerTestsConvierte un PR mergeado o un issue en casos de regresión en el provider harness (colección Postman + newman).
e2e-testTestsEscribe, corre, sincroniza y audita tests Playwright de la UI, con un catálogo de anti-patrones de aserción.
docs-writerDocsEscribe/actualiza docs MDX de Mintlify investigando el código real y validando cada ejemplo contra el schema.
changelog-writerReleaseGenera changelogs respetando la jerarquía de dependencias y bumpea versiones semver en cascada.
helm-updateReleaseSincroniza el Helm chart con config.schema.json y escribe el changelog del chart.
release-checklistReleaseAuditoría read-only de las migraciones de base de datos buscando deadlocks y trabajo que bloquea el boot.
review-prPRRevisa un PR con arquitectura multiagente find-then-verify (finders en paralelo + verificación adversarial).
resolve-pr-commentsPRResuelve interactivamente los comentarios de review sin resolver, con aprobación por comentario.

El mapa del ciclo de vida

Lo interesante no es cada skill aislada, sino que cubren todo el ciclo de vida del proyecto y se referencian entre sí. investigate-issue incluso delega hacia adelante (“usa /e2e-test para los tests de UI”). Este es el flujo:

flowchart TD
    A["Issue de GitHub"] --> B["investigate-issue<br/>triage + plan + research"]
    B --> C["Implementación<br/>con aprobación por cambio"]
    C --> D{"Superficie tocada"}
    D -->|"core / providers"| E["LLM tests y MCP tests<br/>(recomendados por investigate-issue)"]
    D -->|"rutas del gateway"| F["harness-test-writer<br/>provider-harness (Postman)"]
    D -->|"UI React"| G["e2e-test<br/>Playwright"]
    D -->|"superficie API"| H["api-validator<br/>OpenAPI vs código + auth"]
    C --> I["docs-writer<br/>MDX Mintlify"]
    C --> P["Pull Request"]
    P --> Q["review-pr<br/>find-then-verify"]
    Q --> R["resolve-pr-comments<br/>FIX / REPLY / SKIP"]
    R --> S["PR mergeado"]
    S --> T["release-checklist<br/>auditoría de migraciones"]
    T --> U["changelog-writer<br/>bump en cascada"]
    U --> V["helm-update<br/>chart + changelog helm"]
    V --> W["Release"]

Fijate en el anillo de tests estratificado: cada capa del gateway tiene su skill. api-validator audita el contrato OpenAPI; harness-test-writer prueba el gateway HTTP desde afuera; e2e-test prueba la UI; y los LLM/MCP tests en Go (que investigate-issue prescribe según la carpeta tocada) prueban el core. Y en el anillo de release, la responsabilidad sobre docs/ está repartida a propósito: changelog-writer nunca toca docs/, mientras que helm-update lo hace, con un handoff explícito sobre la sección ### Upcoming del README del chart.

Cuatro skills bajo la lupa

No hace falta desmenuzar las diez; cuatro alcanzan para ver las técnicas.

review-pr — arquitectura multiagente adversarial. Separa dos cosas que suelen mezclarse: generar candidatos y verificarlos. En la fase Find, lanza agentes finder independientes en paralelo, cada uno devolviendo hasta 6 candidatos {file, line, summary, failure_scenario} desde ángulos distintos (correctitud línea por línea, comportamiento removido, trazado cross-file). En la fase Verify, deduplica por mecanismo y corre un verificador por candidato que devuelve exactamente CONFIRMED, PLAUSIBLE o REFUTED. La regla de bloqueo es tajante: solo los CONFIRMED de correctitud pueden bloquear un merge; el resto son nits o follow-ups. Es un patrón para combatir a la vez los falsos negativos (fan-out de finders sin auto-censura) y los falsos positivos (verificación forzada).

harness-test-writer — inserción textual de un JSON de 1.5 MB. El provider harness es una colección Postman (tests/e2e/api/collections/provider-harness.json) que corre con newman. El problema: ese JSON no es estable byte a byte bajo JSON.stringify(JSON.parse(raw)) —reformatearlo produciría un diff monstruoso—. La solución codificada en la skill es quirúrgica: un script de Node añade el nuevo folder como texto justo antes del ] de cierre y verifica que el diff sea solo adiciones (item count == before + 1). Cada test lleva además un guard de infraestructura (if ([401,403,429,5xx].indexOf(code) !== -1) return;) para que el ruido de auth o rate limiting no produzca falsos fallos —pero con la excepción explícita de no ignorar un 400 cuando ese 400 es justamente la firma de la regresión.

changelog-writer — semver en cascada. Respeta la jerarquía core → framework → plugins → transports: si cambia core, todo lo que está debajo bumpea al menos un patch. Pregunta el tipo de bump por módulo con AskUserQuestion, presenta una tabla de versiones para confirmar, y acredita a los contribuidores externos consultando author_association vía gh api. Regla dura: ningún changelog queda vacío —un módulo que solo cambió por cascada recibe una entrada chore:.

release-checklist — un auditor que crece. Es read-only: escanea las migraciones de base de datos que cambian en un release (que en Bifrost están definidas en Go, no en .sql) buscando riesgos de deadlock y de trabajo que bloquea el arranque, y emite un reporte PASS/WARN/FAIL. Lo elegante es que declara una vez los “hechos del sistema de migraciones” (todas corren síncronas al boot, serializadas por un advisory lock con timeout de 5 minutos) y los reutiliza en cada check. Está diseñada para crecer: tiene una sección “Adding a New Check” con formato fijo y los checks son independientes —uno que falla no detiene a los demás—.

Ocho insights sobre ingeniería de skills

  1. Una skill es un procedimiento congelado, no un prompt. Codifica la forma exacta en que este repo hace la tarea, no el conocimiento genérico de la tarea.
  2. Los gates suelen ser negativos. Abundan las prohibiciones duras: “nunca edites openapi.json”, “nunca reformatees el JSON”, “nunca commitees/pushees”, “nunca toques docs/”, “read-only”. Codificar el no-hacer evita el daño irreversible.
  3. Determinismo por anclaje al código, no a la memoria. Regla recurrente: “escanea el código primero; no confíes en los docs ni en los tests como fuente de verdad”. api-validator incluso localiza bloques por símbolo (grep -n | cut | sed con ventanas generosas) para no desfasarse cuando el archivo crece.
  4. Auto-verificación embebida. Varias skills terminan re-validando su propio output: docs-writer valida cada config.json contra el schema; helm-update emite una tabla values → config key → ¿en schema?; harness-test-writer verifica item count == before + 1.
  5. Composición explícita. Las skills se referencian por nombre y se reparten fronteras a propósito, funcionando como un pipeline y no como diez herramientas sueltas.
  6. Progressive disclosure vía tablas de referencia. Cada skill incrusta el “conocimiento del dominio comprimido” que necesita (mapeo feature→directorio, clases de auth, anti-patrones P0–P6, señales de migración peligrosa) para no redescubrirlo cada vez.
  7. Human-in-the-loop como invariante. El patrón dominante es “audita/planifica → presenta y espera aprobación → ejecuta con aprobación por cambio”. Las skills son reversibles y supervisables, no autónomas.
  8. Automatizan lo mecánico, no la creatividad. Maxim no automatiza el juicio; automatiza la parte propensa a error: dónde vive cada cosa, qué comando exacto, qué no tocar. El juicio queda en el modelo, pero acotado por gates y checklists deterministas.

Parte 2 · El harness del CLI de Bifrost

Antes de nada: hay tres cosas llamadas “harness”

Es fácil confundirse, así que lo aclaramos de entrada. En el repositorio conviven tres “harnesses” distintos:

flowchart LR
    A["cli/internal/harness<br/>Lanzador de CLIs de agentes<br/>(FEATURE de producto)"]
    B["provider-harness.json<br/>Colección Postman + newman<br/>(tests del gateway HTTP)"]
    C["core/internal/llmtests<br/>Harness de tests en Go<br/>(tests del core)"]
    A -.->|"no relacionados"| B
    B -.->|"distintos"| C
  • La skill harness-test-writer de la Parte 1 escribe en B (la colección Postman).
  • Los tests de core corren en C.
  • Este apartado trata A: el harness del CLI, que es una feature del producto, no infraestructura de tests. Nada que ver con la skill.

Qué es el CLI de Bifrost

El CLI no levanta el gateway. Es un lanzador y multiplexor de CLIs de agentes de código que los arranca apuntando a un gateway Bifrost remoto vía variables de entorno. Dicho simple: es un “administrador de terminal con pestañas” (estilo tmux) que envuelve procesos de terceros —claude, codex, gemini, opencode— y hace que todo su tráfico LLM pase por Bifrost.

flowchart TD
    U["Usuario"] --> CLI["CLI de Bifrost<br/>(TUI con pestañas)"]
    CLI --> T1["Tab 1 · claude<br/>PTY propia"]
    CLI --> T2["Tab 2 · codex<br/>PTY propia"]
    CLI --> T3["Tab 3 · opencode<br/>PTY propia"]
    T1 --> GW["Gateway Bifrost (remoto)<br/>/anthropic · /openai · /genai"]
    T2 --> GW
    T3 --> GW
    GW --> P["Proveedores LLM<br/>(OpenAI, Anthropic, Google, ...)"]

El entrypoint (cli/main.go) parsea flags (-config, -no-resume, -worktree), tiene subcomandos update/version y delega a app.New(...).Run(ctx). El bucle en app/app.go carga estado (~/.bifrost/state.json) y config (~/.bifrost/config.json), chequea updates en background, resuelve un Profile (la base URL del gateway) con su virtual key, y entra en modo pestañas con runtime.RunTabbed(...). La conexión al proveedor se arma en runtime.go:BuildEnv como spec.BaseURL + Harness.BasePath (por ejemplo /anthropic) y se exporta al proceso hijo. El CLI consume el gateway (lista modelos por HTTP contra /v1/models con el header x-bf-vk); no lo hospeda.

El paquete harness: un registro declarativo

harness.go define un struct puramente declarativo que describe cómo lanzar y configurar cada CLI de agente:

type Harness struct {
    ID, Label, Binary, InstallPkg string
    BasePath     string // sufijo en el gateway: "/anthropic", "/openai", "/genai"
    BaseURLEnv   string // "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL", ...
    APIKeyEnv    string
    ModelEnv     string
    SupportsMCP, SupportsWorktree bool
    RunArgsForMod func(model string) []string
    PreLaunch         func(baseURL, apiKey, model string) (extraEnv []string, cleanup func(), err error)
    WriteNativeConfig func(baseURL, apiKey, model string) error
    NativeConfigPath  string
}

El registro map[string]Harness trae cuatro entradas: claude (Claude Code, con MCP y worktree), codex, gemini y opencode. Se instalan vía npm (npm install -g <InstallPkg>). Una función DetectVersion ejecuta cada binario con --version (timeout de 1200 ms) y —detalle de UX— el chooser detecta las versiones de todos los harnesses en paralelo con un sync.WaitGroup, para no encadenar esperas de subprocesos.

Config efímera vs. persistente: una decisión de diseño

native_config.go resuelve un problema sutil: que la configuración funcione también cuando el usuario lanza el CLI del agente por su cuenta, fuera de Bifrost. Hay dos mecanismos, y la distinción es enseñable:

  • PreLaunch = efímero. Genera archivos temporales antes de cada lanzamiento y devuelve un cleanup. Opencode lo usa: crea un OPENCODE_CONFIG temporal con un provider bifrost y lo borra al salir.
  • WriteNativeConfig = persistente. Mergea en el archivo real del usuario, de forma idempotente, preservando sus ajustes. Claude mergea en ~/.claude/settings.json el bloque env con ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY y —curiosidad— pinea el modelo en los tres tiers (ANTHROPIC_DEFAULT_SONNET_MODEL, _OPUS_MODEL, _HAIKU_MODEL) al mismo modelo elegido.

El caso Codex muestra por qué a veces hay que escribir el archivo nativo sí o sí: Codex lee ~/.codex/auth.json, que tiene precedencia sobre la variable OPENAI_API_KEY del entorno. Si Bifrost solo exportara la env var, un auth.json viejo la ensombrecería y el tráfico no pasaría por el gateway. Por eso Codex escribe auth.json y config.toml de forma persistente, con un mini-editor de TOML que preserva tablas, comentarios y orden.

El runtime: un emulador de terminal completo

Acá está el corazón “harness”. Para renderizar varias sesiones de agente en pestañas dentro de una sola terminal real, Bifrost implementa un emulador de terminal por etapas. Todo el multiplexado es solo Unix (//go:build !windows); en Windows degrada a sesión única, honestamente separado en archivos *_windows.go.

flowchart LR
    PTY["PTY del agente<br/>(bytes crudos)"] --> N["vtStreamNormalizer<br/>filtra/reescribe secuencias"]
    N --> VT["vt10x.Terminal<br/>grid de glifos"]
    VT --> SB["scrollback<br/>ring buffer 5000 filas"]
    VT --> COMP["compositor 30fps"]
    SB --> COMP
    COMP --> OUT["1 write atómico<br/>synchronized output"]
    OUT --> TERM["Terminal real"]

La etapa más instructiva es el normalizador (vtstream.go). El emulador subyacente (vt10x) no soporta todo lo que las TUIs modernas emiten, así que Bifrost pre-procesa los bytes:

  • Mantiene secuencias CSI partidas entre lecturas (un buffer pendingCSI), porque un read() puede cortar una secuencia por la mitad.
  • Descarta extensiones que vt10x malinterpretaría, como el protocolo de teclado Kitty (\x1b[>1u) o su query (\x1b[?u), que el emulador confundiría con DECRC y corrompería el cursor.
  • Reescribe los sub-parámetros SGR de estilo moderno (separados por :) a los que vt10x entiende (separados por ;): 38:2:cs:r:g:b → 38;2;r;g;b para truecolor.
  • Extrae fuera de banda datos que vt10x se traga, como la forma del cursor (DECSCUSR) y su visibilidad, para capturarlos en el instante exacto.

El scrollback (scrollback.go) es un ring buffer de 5000 filas (defaultScrollbackCap) que además deduplica filas en blanco consecutivas, para que una TUI que emite líneas vacías no infle la historia. El gestor de pestañas (tabmgr.go, ~3175 líneas) corre un render loop a ~30 fps (33 * time.Millisecond) que compone el contenido con la barra de pestañas en un solo write atómico usando synchronized output (\x1b[?2026h … l) para evitar tearing. Los prefijos de comando son Ctrl+B y Ctrl+G (tmux-safe), y Ctrl+Tab cicla.

Un truco elegante: el estado de actividad de cada agente se infiere del terminal, no del agente. hashVTScreen calcula un fingerprint FNV-64a de todo el grid; comparando hashes en ventanas temporales, el CLI decide qué emoji mostrar en la pestaña: (arrancando), 🔔 (campana en pestaña inactiva), 🧠 (la pantalla cambió recientemente sin input → “el agente está trabajando”) o (idle). Es una heurística barata para supervisar cualquier TUI de terceros sin cooperación del proceso hijo.

Cómo se testea un emulador de terminal de forma determinista

Aquí está, quizás, el mayor aprendizaje del CLI. Testear un emulador de terminal parece imposible (¿PTY reales? ¿timing? ¿subprocesos?), pero Bifrost lo hace con golden files de replay. El fixture testdata/opencode_scroll_replay.json —cuyo nombre confirma que el harness envuelve al CLI Opencode— contiene:

  • cols, rows,
  • chunks[]: trozos de bytes crudos de PTY, tal como llegarían fragmentados,
  • snapshots[]: para ciertos after_chunk, el screen[] (grid) esperado.

El test (replay_test.go) re-alimenta los chunks por el mismo pipeline de producción (vtStreamNormalizer.Normalize → vt10x.Write) y compara el grid contra los snapshots en checkpoints exactos. Sin PTY real, sin timing, sin subprocesos: los bugs de terminal (cursor, truecolor partido, alt-screen) quedan congelados como golden files. Los complementan tests unitarios dirigidos a bugs concretos, como TestNormalizerDropsKittyKeyboardProtocol o TestVTStreamNormalizerHandlesSplitTrueColorSGR, y tests de scrollback que arman un TabManager en memoria sin PTY ni proceso.

Patrones de ingeniería del CLI

  • Re-exec con execve (app/reexec_unix.go): tras auto-actualizarse, el proceso se reemplaza a sí mismo en vuelo con el binario nuevo vía syscall.Exec, sin relanzar la shell. Windows no puede hacerlo y pide reinicio manual, explícitamente.
  • Self-update con checksum obligatorio (update/selfupdate.go): descarga a temp, calcula SHA-256 con io.MultiWriter, rechaza el binario si no coincide, y hace un replace atómico (stage en el mismo directorio + rename).
  • Secretos que nunca tocan el disco en texto plano (secrets/secrets.go): la virtual key va al keyring del sistema (go-keyring, service bifrost-cli), y SaveConfig fuerza VirtualKey = "" antes de serializar el config.json. Incluso migra automáticamente claves de configs viejas al keyring.
  • Escritura atómica de config: temp file → SyncChmod 0o600Rename, para no dejar archivos corruptos ante un crash.
  • Producto “de dos caras” honesto vía build tags: experiencia rica multi-pestaña en Unix, fallback funcional a sesión única en Windows, con el mismo entrypoint y sin if runtime.GOOS disperso.
  • Integración MCP best-effort (mcp/mcp.go): para Claude, auto-registra el server MCP de Bifrost (claude mcp add --transport http bifrost <url>/mcp); para el resto, imprime instrucciones. Se conecta con lo visto en el capítulo 10 · MCP Gateway.

Seis insights sobre el harness del CLI

  1. Un multiplexor de terminal es un pipeline de bytes con estado, no magia: PTY crudo → normalizador → emulador VT → compositor → un write atómico. Al implementar cada etapa, Bifrost puede intervenir en el punto justo.
  2. Emular terminales es un campo minado: buena parte del código existe solo para lidiar con lo que vt10x malinterpreta (Kitty, SGR con :, DECSCUSR). Emular VT100 “de verdad” implica una larga cola de casos borde reales.
  3. Testear un emulador de terminal es posible y barato: fixtures JSON de chunks + snapshots del grid, alimentados por el código de producción. Los bugs de terminal se congelan como golden files.
  4. Separar config efímera de persistente tiene consecuencias reales: el caso Codex (auth.json ensombrece el env) muestra por qué a veces hay que escribir el archivo nativo del usuario sí o sí.
  5. Los secretos van al keyring, jamás al disco: patrón replicable para cualquier CLI que maneje credenciales.
  6. La actividad de un agente se infiere del terminal: hashVTScreen + ventanas temporales dan los emojis de estado sin cooperación del proceso hijo. Un truco reutilizable para supervisar TUIs de terceros.

Por qué este capítulo importa

Bifrost es un producto para orquestar LLMs, y estos dos artefactos muestran las dos caras de esa misma moneda aplicadas a la propia casa:

  • Las skills son ingeniería de repositorio codificada: automatizan la parte mecánica y propensa a error del desarrollo (dónde vive cada cosa, qué comando exacto, qué no tocar), dejando el juicio al modelo pero acotado por gates deterministas. Si mantenés un proyecto grande con agentes de IA, este es un modelo a copiar.
  • El harness del CLI cierra el círculo: le da a los usuarios de Bifrost una forma de correr sus propios agentes de código (Claude Code, Codex, Gemini, Opencode) a través del gateway, ganando de un plumazo los fallbacks, la governance, el caching y la observabilidad de todos los capítulos anteriores —esta vez para el consumo de LLM que hacen sus herramientas de desarrollo—. Es el mismo argumento del capítulo 14 · Despliegue en producción, llevado a la terminal.

Checklist

  • Entiendo que una skill de Claude Code es un procedimiento determinista con comandos exactos y prohibiciones, no un prompt.
  • Reconozco el mapa de skills del ciclo de vida: investigar → testear (por capa) → documentar → revisar → versionar → release.
  • Distingo los tres “harnesses” del repo: el lanzador del CLI (producto), el provider-harness Postman (tests del gateway) y los llmtests en Go (tests del core).
  • Sé que el CLI de Bifrost es un multiplexor que lanza CLIs de agentes y los enruta por el gateway; no es el gateway.
  • Comprendo la distinción PreLaunch (efímero) vs. WriteNativeConfig (persistente) y por qué Codex necesita lo segundo.
  • Vi cómo se testea un emulador de terminal con golden files de replay, sin PTY ni timing.
  • Me llevo patrones aplicables: re-exec, self-update con checksum, secretos en keyring y build tags para lo multiplataforma.

Con esto cerramos el recorrido por Bifrost, desde el primer chat completion hasta cómo se construye y se opera el propio proyecto. Si querés repasar cualquier pieza, volvé al índice del tutorial. ¡Felicitaciones por llegar hasta acá!