Harness engineering: todo lo que rodea al modelo

El modelo razona; el harness decide qué lee, qué puede tocar y qué se le impide romper. Esta página explica de qué piezas está hecho un harness y cómo este repo sostiene uno solo para tres herramientas distintas sin que se desincronicen.

FundamentosClaude CodeOpenCodeCodex

Qué es un harness

Un modelo de lenguaje, solo, recibe texto y devuelve texto. Para que lea un repositorio, ejecute un comando o cree un issue en Jira necesita una capa que le entregue contexto, le dé herramientas y ponga límites. Esa capa es el harness. Claude Code, OpenCode y Codex son harnesses: programas que envuelven al modelo, arman lo que ve en cada turno y ejecutan lo que pide.

Harness engineering es el trabajo de diseñar y mantener esa capa para un proyecto concreto. En la práctica consiste en escribir archivos: instrucciones, skills, hooks, configuración de MCP y scripts de verificación. Con el mismo modelo, dos proyectos obtienen resultados muy distintos según lo que cada uno le pone alrededor.

Las seis piezas

Cada harness usa nombres propios, pero las piezas se repiten. Esta tabla las ordena por la pregunta que responde cada una.

Pieza Pregunta que responde Cuándo actúa
Instrucciones Qué reglas valen siempre en este repo? Se cargan al abrir la sesión
Skills Cómo se hace esta tarea concreta? Bajo demanda, cuando la tarea coincide con su trigger
Hooks Qué tiene que pasar sí o sí en cierto momento? En eventos del harness (por ejemplo, antes de procesar cada prompt)
MCP A qué sistemas externos puede llegar el agente? Cuando el modelo llama a una tool del servidor
Memoria Qué aprendimos en sesiones anteriores? Entre sesiones; sobrevive al cierre de la ventana de contexto
Gates Sigue todo lo anterior siendo coherente? Antes de un commit, antes de un push y en CI

Las cinco primeras le hablan al modelo. La sexta le habla al repositorio: los gates impiden que las otras piezas se contradigan con el paso de los commits. Un harness sin gates se degrada en silencio, porque nadie se entera de que una skill cita un archivo que ya no existe hasta que un agente lo busca y falla.

La diferencia entre instrucciones y hooks es de garantía. Una instrucción es un pedido que el modelo puede diluir en una sesión larga. Un hook es código que el harness ejecuta cada vez, lo recuerde el modelo o no. Si una regla tiene que cumplirse siempre, va en un hook o en un gate.

Una fuente, adaptadores finos

Este repo funciona en tres harnesses. Un proyecto puede declarar solo los que usa en harnesses: (.agents/project.yaml): los gates revisan solo esos, y el boilerplate siempre revisa los tres (.context/ADR/ADR-0012-declared-harnesses.md, .agents/instructions/agent-harnesses.md). La tentación obvia sería mantener tres copias de todo (un CLAUDE.md con las reglas, otras en la configuración de OpenCode, otras para Codex). Cada copia empezaría igual y a la tercera semana diría algo distinto.

La alternativa es tener un solo cuerpo de instrucciones y skills, y dejar en cada host un adaptador mínimo que solo resuelva lo que ese host hace distinto: el formato del archivo de MCP y la API de hooks. AGENTS.md es un formato abierto que OpenCode y Codex leen de forma nativa; Claude Code lee CLAUDE.md, así que en este repo ese archivo contiene una sola línea, @AGENTS.md, que importa el original.

Ese cuerpo está en capas. AGENTS.md carga en cada sesión y guarda solo lo que ata en cada turno, más un router; cada tema vive en su propio archivo bajo .agents/instructions/ y se lee cuando el router, o una línea ROUTE: del hook, lo nombra. Así ningún host paga en cada sesión lo que no usa, y Codex, que corta las instrucciones del proyecto en su tope de bytes, recibe el archivo entero. El mecanismo está en el deck Progressive disclosure.

Una fuente, tres hosts Arriba, la fuente única: AGENTS.md, .agents/skills y el emisor de hooks. En el medio, una franja de gates (skills:check, agents:compat:check, docs:check, vars). Abajo, tres adaptadores finos que llevan esa fuente a Claude Code, OpenCode y Codex. Fuente única (versionada) AGENTS.md · .agents/skills/ · .agents/hooks/ GATES skills:check · skills:registry:check · agents:compat:check · docs:check · vars:check CLAUDE.md → @AGENTS.md .claude/settings.json opencode.jsonc .opencode/plugins/ .codex/config.toml .codex/hooks.json adaptadores finos: solo MCP y API de hooks Claude Code OpenCode Codex
Todo lo que el modelo lee sale de la fuente de arriba. Los adaptadores de abajo no contienen reglas propias, y los gates del medio fallan si alguno empieza a tenerlas.

Gates contra el drift

Drift es la distancia que se abre entre dos cosas que deberían decir lo mismo: una skill y el archivo que cita, .env.example y el manifiesto de variables, el .mcp.json y su equivalente en Codex. Nadie lo introduce a propósito. Aparece porque alguien edita un lado y se olvida del otro, y el agente sigue leyendo la versión vieja con total confianza.

Un gate es un script que compara los dos lados y termina con código de salida distinto de cero si no coinciden. Lo que lo vuelve útil es dónde corre: en el hook de pre-commit, en el de pre-push y en CI. Así el drift se detecta en el commit que lo crea, cuando arreglarlo cuesta un minuto.

Generado contra versionado

Hay dos clases de archivo en un harness y conviene distinguirlas a simple vista. Los versionados son la fuente: se editan a mano y se revisan en un PR. Los generados son salida de un script: se regeneran, y editarlos a mano es trabajo perdido, porque la próxima regeneración lo pisa (o peor, un gate lo rechaza y no sabes por qué).

La regla práctica: antes de editar un archivo, pregunta de dónde sale. Si sale de un script, edita la entrada del script y vuelve a correrlo.

Así lo hace este repo

La sección 4.5, en .agents/instructions/agent-harnesses.md (el router de AGENTS.md la nombra), describe el contrato completo. Este es el mapa por superficie:

Superficie Claude Code OpenCode Codex
Instrucciones CLAUDE.md (generado, importa AGENTS.md), más las secciones de .agents/instructions/ a pedido AGENTS.md, más las secciones a pedido AGENTS.md, más las secciones a pedido
Skills .claude/skills (alias generado) .agents/skills/ .agents/skills/
Hook .claude/settings.json .opencode/plugins/personality-reinject.js .codex/hooks.json
MCP .mcp.json opencode.jsonc .codex/config.toml

Un emisor, tres adaptadores

El texto del hook vive una sola vez, en .agents/hooks/personality-reinject.mjs. En cada prompt agrega una línea AGENT IDENTITY: (worktree, sesión y harness, que luego terminan en los trailers de cada commit), avisa si falta el .env y clasifica el pedido contra el router de AGENTS.md: por cada sección que el pedido necesita y la sesión todavía no leyó, suma una línea ROUTE: read <archivo>, con un tope de pocas por pedido: el resto va en una sola línea ROUTE-OPTIONAL: que no obliga. Un aviso de tarea del harness (un prompt que empieza con <task-notification>) no se rutea. Claude Code y Codex lo ejecutan como hook de comando en el evento UserPromptSubmit, y un SessionStart con matcher compact o clear re-arma las rutas después de compactar o de un /clear. En Claude Code, un PostToolUse corre el mismo archivo y, si el agente usa una herramienta sin haber leído lo ruteado, le recuerda una sola vez qué falta (ROUTE-PENDING:). OpenCode no tiene hooks de comando, así que su plugin importa las mismas funciones y las empuja al prompt de sistema; en OpenCode 2, que no le pasa el texto del pedido, queda solo el router.

Hay un segundo hook, aparte y mucho más chico: .agents/hooks/doc-contracts.mjs. Claude Code y Codex lo corren después de cada edición de archivo (evento PostToolUse). Si la edición cae dentro de una región marcada con LINT.IfChange, suma una línea DOCS: con las páginas que describen ese código, una vez por sesión. Es el aviso; el gate que lo hace cumplir corre en el pre-push y en CI (ver la tabla de abajo). OpenCode no lo registra y depende solo del gate. Vale únicamente en el repo del boilerplate: en un proyecto queda en silencio (ADR-0016).

Una skill, un modo

No hay archivos de comando. Una skill se invoca por su nombre más un modo: en Claude Code, /project-context api o /test-automation explain <target>; en OpenCode y Codex, nombrando la skill y el modo en prosa. El primer token de $ARGUMENTS que coincide con un modo es el modo, y el resto se reenvía; si ninguno coincide, la skill pregunta.

Un comando del proyecto con el mismo nombre que una skill la taparía, así que agents:compat:check lo rechaza y bun run agents:compat lo mueve a .backups/shadowing-commands/, donde se puede recuperar.

La cadena de repo:check

bun run repo:check encadena los gates del repo; la cadena exacta y su orden son los del script repo:check en package.json (regla 11 de AGENTS.md: se lee ahí, nunca de una copia).

Los que cuidan el harness en sí son estos:

Script Archivo Qué detecta
skills:check scripts/lint-skills.ts Frontmatter roto, categorías fuera del vocabulario, tiers incoherentes con cli/install.ts y rutas citadas en una skill que ya no existen
skills:registry:check scripts/build-skill-registry.ts Que .agents/skills/REGISTRY.md (las reglas compactas que se copian en cada briefing de subagente) esté al día con las skills
agents:compat:check scripts/agent-compatibility.ts El shim de CLAUDE.md, el alias de skills, los comandos del proyecto que tapan una skill, los adaptadores de hooks y la paridad de servidores MCP entre los formatos de los harnesses en uso
vars:check / vars:env:check scripts/lint-vars.ts / scripts/check-vars.ts Variables de proyecto mal usadas, y que .env.example y cli/lib/variables-manifest.ts coincidan
instructions:check scripts/lint-instructions.ts El cuerpo de instrucciones: presupuesto de AGENTS.md, el ROUTER bloqueado detrás de un ADR, el eval del router con pisos de recall y precisión en cada corrida y que cada sección nueva llegue completa
vars:schema:check scripts/env-schema.ts Que .env.core.schema esté al día con el manifiesto y que toda clave con nombre de secreto (TOKEN, SECRET, PASSWORD, API_KEY...) esté marcada @sensitive
docs:check scripts/lint-docs.ts Links relativos rotos (también los links markdown de los .md de .agents/, .context/ y .claude/), rutas citadas en docs/ que no existen y páginas sin <title> o descripción. Corre en cada push y en CI
contratos de docs scripts/lint-doc-contracts.ts Que un cambio dentro de una región LINT.IfChange llegue con todas las páginas que nombra su LINT.ThenChange, o con un commit que diga Docs-Checked: <label> <motivo>. Avisa en el commit, bloquea en el push y en CI. Solo en el repo del boilerplate (ADR-0016)

Aparte de los gates, bun run instructions:audit mide en las transcripciones locales de Claude Code cuántas de las secciones que el hook ruteó leyó de verdad el agente (solo nombres y conteos, nunca contenido).

No todos corren en todos lados. Qué corre en cada hook está en .husky/framework-gates.sh: el pre-push corre un subconjunto amplio, el pre-commit corre unos pocos siempre y el resto solo cuando el commit toca los archivos que vigilan. repo:check es la pasada completa, la que conviene correr antes de abrir un PR.

Errores comunes

Editar un archivo generado. CLAUDE.md y .agents/skills/REGISTRY.md son salida de scripts. Escribir reglas en CLAUDE.md rompe agents:compat:check. Edita la fuente y corre bun run agents:compat o bun run skills:registry.

Confiar en que el MCP avisa si falta una variable. Si una variable de .env no está definida, el loader de .env arranca el servidor sin ella en los tres harnesses, y el servidor muere más tarde, en su primera llamada autenticada. Ninguno falla al arrancar. Un 401 o una tool que falla sin explicación casi siempre es eso: corrige el .env y reinicia la sesión, porque las variables se leen cuando el harness levanta los servidores.

Exportar una variable del proyecto en tu shell. El harness se abre tal cual (claude, opencode, codex o su app de escritorio) y cada servidor MCP lee .env por su cuenta. Pero una variable que exportaste tú en el perfil de tu shell gana sobre el archivo en todo lo que arranques desde esa terminal, y un valor viejo ahí tapa el .env corregido sin avisar. bun run vars:env:check te dice cuál (solo el nombre y el largo): quítala del perfil o abre una terminal limpia.

Para profundizar