Una fuente de verdad, tres harnesses que la leen
El repositorio corre igual en Claude Code, OpenCode y Codex (CLI y Desktop). Existe una sola copia de cada instrucción y de cada skill. Donde los harnesses difieren de verdad —formato de MCP, API de hook— cada uno conserva un adaptador fino, versionado y verificado. Nada más está duplicado.
CLAUDE.md: @AGENTS.mdNingún nombre de archivo lo descubren los tres
Claude Code carga CLAUDE.md. OpenCode y Codex cargan
AGENTS.md. Mantener dos documentos completos y compararlos en CI no elimina
el drift: lo detecta tarde, después de que alguien ya trabajó con la copia vieja.
Por eso la relación se invirtió.
AGENTS.md es la única entrada al cuerpo de instrucciones y
CLAUDE.md quedó en once bytes. Ese @ es la sintaxis de import
documentada de Claude Code: trae el archivo al contexto de la sesión. Se eligió por
encima de un symlink justamente porque un checkout de Windows lo trata igual que uno de
macOS o Linux.
Ese cuerpo además 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: que agrega el hook, lo nombra. Los tres hosts siguen leyendo el
mismo archivo de entrada; lo que cambió es cuánto carga siempre. El mecanismo:
Progressive disclosure.
# CLAUDE.md — el archivo entero, sin recortar
@AGENTS.md
Escribir prosa operativa dentro de CLAUDE.md es drift estructural:
bun run agents:compat:check falla si el archivo deja de ser exactamente
el shim.
Qué consume cada harness, superficie por superficie
Cinco superficies por tres harnesses. Lo marcado como nativo lo descubre el harness por su cuenta; lo marcado como generado es salida de un script y no se edita a mano.
| Superficie | Claude Code | OpenCode | Codex CLI + Desktop |
|---|---|---|---|
| Instrucciones |
generadoCLAUDE.md
un import: @AGENTS.md
|
nativoAGENTS.md |
nativoAGENTS.md |
| Skills |
generado.claude/skills
symlink en POSIX, junction en Windows
|
nativo.agents/skills/ |
nativo.agents/skills/ |
| Comandos |
ninguno
skill + modo: /project-context data
|
ninguno skill y modo, en prosa | ninguno skill y modo, en prosa |
| Hook |
.claude/settings.json
UserPromptSubmit, SessionStart,
PostToolUse
|
.opencode/plugins/
plugin de proyecto
|
.codex/hooks.json
UserPromptSubmit, SessionStart +
commandWindows
|
| MCP | .mcp.jsonJSON |
opencode.jsoncJSONC |
.codex/config.tomlTOML |
Codex Desktop no agrega convención
CLI y Desktop consumen exactamente la misma configuración del repositorio. Desktop agrega interfaz, no un segundo contrato ni un directorio extra.
Los adaptadores son delgados a propósito
Solo dos superficies tienen adaptador real: el hook y el MCP. Ahí los harnesses difieren en la API, no en la intención. Instrucciones y skills son la misma fuente vista desde tres lugares, y archivos de comando no hay.
Lo generado nunca se edita a mano
Hay un solo store de skills, .agents/skills/, versionado, con todas las
skills adentro (el índice es .agents/skills/REGISTRY.md). Todo lo demás que
un harness necesita ver son punteros hacia ahí. Si un puntero se rompe, se regenera;
nunca se parchea. De lo generado, solo .claude/skills queda fuera del
repositorio (está en .gitignore); CLAUDE.md sí se commitea.
| Artefacto generado | Su fuente | Se repara con |
|---|---|---|
.claude/skills |
.agents/skills/symlink o junction
|
bun run agents:compat |
CLAUDE.md |
AGENTS.mdun import de una línea |
bun run agents:compat |
La reparación del alias tiene una salvaguarda: si .claude/skills es un
directorio real con contenido propio, se niega a borrarlo y nombra el conflicto. La
única excepción es la forma que escribe la CLI de skills —un directorio cuyas entradas
son todas punteros hacia .agents/skills— porque ahí no hay nada que
perder.
Una skill, un modo
Antes cada comando llevaba el workflow entero escrito adentro.
adapt-framework era el más largo. Eso era instrucción duplicada que ningún
harness fuera de Claude podía descubrir, y que se desincronizaba de su skill en cuanto
alguien editaba una de las dos copias. Ahora no queda ni el archivo: invocás la skill
por su nombre y le decís el modo. Las mediciones de ese antes y después, con su fecha,
están en .context/ADR/ADR-0006-forensic-measurements-ledger.md.
La forma es la misma en los tres harnesses. Cambia solo cómo la escribís:
# Claude Code: el slash de la skill, el modo como primer argumento /project-context data /test-automation explain <target> # OpenCode y Codex: lo mismo, en prosa carga project-context, modo data
La regla del primer token: el primer token de $ARGUMENTS que coincide con
un modo de la skill es el modo, y el resto se reenvía sin tocar. Si ninguno
coincide, la skill pregunta antes de hacer nada.
| Invocación | Skill | Modo |
|---|---|---|
/test-automation explain <target> |
test-automation |
explainsellado, solo lectura |
| … |
los modos de cada skill están en su SKILL.md; el catálogo es
.agents/skills/REGISTRY.md
|
|
Nada que mantener por host
Claude Code llega a la skill por su slash, a través de .claude/skills.
OpenCode y Codex la cargan por nombre. En ninguno hay un archivo intermedio que se
pueda desincronizar.
Un comando no tapa una skill
Un comando del proyecto con el mismo nombre que una skill la taparía.
agents:compat:check lo rechaza y bun run agents:compat lo
mueve a .backups/shadowing-commands/, donde se puede recuperar.
Un emisor, tres adaptadores
Lo que el hook agrega a cada prompt vive una sola vez, en
.agents/hooks/personality-reinject.mjs. El archivo exporta funciones y
además escribe a stdout cuando se lo ejecuta como script. Esas dos formas
de consumirlo son las que necesitan los tres harnesses. El contrato de salida de
AGENTS.md no va en el emisor: el harness lo carga una vez por sesión, así
que no se reinyecta en cada prompt.
Por prompt, en este orden, el emisor escribe:
-
Una línea
AGENT IDENTITY:con worktree, sesión y harness, de donde salen los trailers forenses de cada commit. -
Una línea
ORCA:que nombra/orca-orchestration, solo si el binario de orquestación está instalado. -
Como mucho un aviso de setup: falta
.enven el checkout, o es un worktree enlazado que nunca corrióbun run worktree:provision. -
Líneas
ROUTE: read <archivo>con las secciones de instrucciones que el prompt necesita y la sesión todavía no leyó, más una líneaROUTE-OPTIONAL:si sobran. Si nada nuevo coincide, no hay líneas de ruta.
El validador de contrato rechaza, con nombre y línea:
- Rutas absolutas personales dentro de cualquier comando de hook, que romperían en la máquina de cualquier otra persona.
-
Un emisor compartido que trate
CLAUDE.mdcomo el archivo canónico, que es exactamente la inversión que este release deshizo. -
Un plugin de OpenCode que reemplace
output.systemen vez de mutarlo, lo que descartaría el system prompt del harness. - La reaparición de los archivos de hook duplicados que se eliminaron.
El adaptador de Codex además carga commandWindows para PowerShell y
resuelve la raíz del repositorio con git rev-parse --show-toplevel, en vez
de asumir el directorio de trabajo.
Las rutas se rearman solas. Claude Code y Codex registran dos grupos
SessionStart, con matcher compact y con clear: al
compactar o limpiar el contexto, las secciones ya ruteadas dejan de contar como leídas y
el emisor las vuelve a pedir en el prompt siguiente. OpenCode 1 hace lo mismo con
experimental.session.compacting. Solo Claude Code corre además el emisor en
PostToolUse: ahí no escribe líneas nuevas, solo una vez por prompt una
línea ROUTE-PENDING: si una sección ruteada sigue sin leerse.
Hay un segundo hook, separado de este: .agents/hooks/doc-contracts.mjs.
Claude Code y Codex lo corren después de cada edición de archivo
(PostToolUse). Cuando la edición cae dentro de una región marcada con
LINT.IfChange, agrega una línea DOCS: con las páginas que
describen ese código, una vez por sesión. El que bloquea es el gate de pre-push y de CI;
el hook solo adelanta el aviso al momento de editar. OpenCode no lo registra. Vale solo
en el repo del boilerplate (ADR-0016).
Los trailers de commit son forenses, no atribución
Todo commit que escribe una sesión de agente termina con dos líneas copiadas de la línea
AGENT IDENTITY:, en cualquier host. Dicen qué checkout y qué sesión
produjeron el cambio, para encontrar la transcripción correcta en un bisect o en un
post-mortem. No nombran ninguna herramienta.
docs: rewrite the harnesses page Worktree: <nombre|primary> Session: <etiqueta>
Obligatorio
Worktree: y después Session:, al final y sin nada debajo.
Si un valor no se pudo resolver se escribe unknown, nunca se adivina ni
se omite la clave.
Prohibido
Claude-Session: o cualquier trailer con marca de un harness, un
Co-Authored-By: de una IA y cualquier línea "Generated with". En Claude
Code, .claude/settings.json deja attribution en blanco.
Cómo se controla
.husky/commit-msg llama a .husky/framework-gates.sh, que
corre scripts/check-commit-trailers.ts. Avisa y nunca bloquea: un
commit humano, un merge o un arreglo de emergencia siempre pasan.
La regla es la Critical Rule #3 de AGENTS.md; el procedimiento de etiquetas
y valores por harness vive en la skill /git-flow-master.
Un solo set de servidores, tres formatos, paridad semántica
Los servidores que declara .mcp.json existen en las configuraciones de los
harnesses que usa el proyecto (las tres en el boilerplate). La comparación no puede ser
textual: los archivos ni siquiera comparten lenguaje.
Un solo harness alcanza. La lista harnesses: de
.agents/project.yaml dice cuáles usa el proyecto, y cada gate
(agents:compat:check, setup:doctor, el instalador,
bun sync) revisa solo esos. Sin la lista, o en null, se
detectan por los archivos presentes; una lista explícita manda. El boilerplate mismo
revisa siempre los tres. Decisión:
.context/ADR/ADR-0012-declared-harnesses.md.
Las credenciales llegan igual en los tres hosts: cada servidor que necesita valores de
.env arranca detrás del mismo loader,
varlock run --inject vars --filter A,B -- <server>, que lee
.env (o el gestor de secretos que nombre el esquema) al arrancar el
servidor y le pasa solo los nombres de su --filter. Las configuraciones
commiteadas llevan nombres, nunca valores, y ese --filter es justo el
conjunto de dependencias que compara la paridad
(.context/ADR/ADR-0011-mcp-env-loader.md).
Falla silenciosa. En ningún host una credencial vacía avisa al arrancar: el
servidor sube sin la variable y muere en su primera llamada autenticada. Un 401, un
403 o una tool que falla sin explicación es la señal: parar, nombrar la variable,
corregir .env y reiniciar la sesión (Critical Rule #10). El entorno se
lee cuando el harness lanza los servidores y no se refresca a mitad de sesión.
Si venís de la versión anterior
Un proyecto creado cuando las instrucciones vivían en CLAUDE.md y las
skills en .claude/skills/ recibe una migración preflight la primera vez que
corre bun sync, antes de que se sincronice un solo componente, porque
sincronizar primero sobre una estructura vieja es exactamente cómo se pierde trabajo. La
migración se ve dentro de esa misma corrida, como un aviso en la página local o en la
terminal si corrés con --headless.
Nada se borra
Todo se mueve o se respalda. Lo que no se puede mover a su lugar canónico queda en
.template/pre-agents-migration/, con su contenido intacto.
Es idempotente
Correrla dos veces no hace nada la segunda vez. Un proyecto ya migrado pasa de largo sin escribir.
Lo que todavía no está cerrado
Lo que conviene saber antes de apoyarse en esto, dicho como límites y no como notas al pie.
-
El adaptador de OpenCode cubre dos generaciones del plugin.
OpenCode 1 entra por
server()y usaexperimental.chat.system.transform, una API oficial pero marcada como experimental por el proyecto. OpenCode 2 entra porsetup(ctx)y registra un hook de sesióncontext; como no le llega el texto del prompt, no recibe líneasROUTE:y se queda con la tabla del router. Claude y Codex están sobre APIs de hook estables. Hay que re-verificar al actualizar OpenCode. - El junction de Windows está cubierto por tests unitarios de construcción de ruta y de comando, no por una corrida en una máquina Windows.
-
Codex necesita que el repositorio esté marcado como confiable. La configuración
de proyecto bajo
.codex/y sus hooks no cargan si no lo está.bun run setup:doctorreporta esa confianza por separado de la corrección de los archivos, porque es estado de runtime que no se puede verificar leyendo el disco. -
Una variable que exportaste en tu shell gana sobre
.env. El harness se abre tal cual (claude,opencode,codexo su app de escritorio) y cada MCP lee.envpor su cuenta, pero un valor viejo exportado en el perfil de tu shell tapa el del archivo en todo lo que arranques desde ahí.bun run vars:env:checkte dice cuál, sin mostrar el valor.