Planos · agentic-dev-boilerplate

Multi-harness · cómo se comporta el repo

Una fuente, tres harnesses que la leen

El repo corre en Claude Code, OpenCode y Codex (CLI y Desktop). Cada instrucción y cada skill existe una sola vez. Donde los tres harnesses difieren de verdad (formato de MCP, API de hooks) cada uno tiene un adaptador fino, versionado o generado, y un check lo compara contra la fuente.

Claude Code OpenCode Codex CLI + Desktop fuente de verdad: .agents/instructions/agent-harnesses.md §5.5

01Qué lee cada harness

OpenCode y Codex leen AGENTS.md y .agents/skills/ de forma nativa, así que ahí no hay adaptador. Las secciones de .agents/instructions/ no las carga ningún host por su cuenta: el router de AGENTS.md dice cuándo leer cada una, igual en los tres. Claude Code necesita dos superficies generadas para llegar al mismo contenido. El hook y el MCP tienen un archivo por host porque cada uno habla un formato distinto.

Superficie Claude Code OpenCode Codex CLI + Desktop
Instrucciones CLAUDE.md → @AGENTS.md generado AGENTS.md nativo AGENTS.md nativo
Secciones de instrucciones .agents/instructions/ a pedido, por el router de L0 .agents/instructions/ a pedido .agents/instructions/ a pedido
Skills .claude/skills alias generado .agents/skills/ nativo .agents/skills/ nativo
Comandos ninguno: /<skill> <modo> ninguno: skill y modo en prosa ninguno: skill y modo en prosa
Hook .claude/settings.json → UserPromptSubmit .opencode/plugins/personality-reinject.js .codex/hooks.json → UserPromptSubmit
MCP .mcp.json opencode.jsonc .codex/config.toml
Capas AGENTS.md es el núcleo que carga siempre: la frase que ata de cada regla crítica, la capa comportamental, el núcleo de orquestación y un router fijo. El resto vive en una sección por tema, que el agente lee cuando el router o una línea ROUTE: del hook la nombra. Para Claude Code, el router además importa package.json y .agents/project.yaml; OpenCode y Codex no expanden imports y siguen la instrucción reforzada de la misma fila. Cómo funciona, en detalle: Las instrucciones, en capas.
Uno alcanza 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-0013-declared-harnesses.md.

02Lo generado no se edita a mano

Se edita la fuente y se regenera la salida con bun run agents:compat. Es la Critical Rule #15: escribir prosa operativa en CLAUDE.md o tocar el alias es drift estructural, y el check lo marca.

Fuente · commiteada

Lo que se edita

AGENTS.md, .agents/instructions/, .agents/skills/, .agents/hooks/

→
Comando

Regenerar

bun run agents:compat

→
Salida

Lo que se lee

CLAUDE.md: shim de una línea, commiteado. .claude/skills: symlink POSIX o junction de Windows, ignorado por git.

Por qué CLAUDE.md es un import documentado y no un symlink porque un archivo de una línea (@AGENTS.md más un salto de línea) sobrevive a un checkout en Windows. Nunca lleva prosa propia: todo lo que diga va en AGENTS.md o en su sección de .agents/instructions/.

03Una skill se invoca por nombre y modo

No hay archivos de comando ni capa de alias: el boilerplate no trae .claude/commands/ ni .opencode/commands/. La skill es la única forma de invocación en los tres hosts, y una skill con varios modos los lista en su propia sección ## Mode routing.

Host Cómo se pide
Claude Code /project-context data, el slash de la skill a través de .claude/skills
OpenCode "cargá project-context, modo data"
Codex "cargá project-context, modo data"

El primer token de los argumentos que coincide con un modo es el modo, y el resto se reenvía. Si ninguno coincide, la skill lo deduce de la frase o pregunta.

Rechazo Un proyecto puede tener sus propios comandos, pero no uno que se llame igual que una skill: Claude Code registraría los dos con el mismo slash y ganaría el cuerpo del comando. bun run agents:compat:check falla con ese caso y bun run agents:compat mueve el archivo a .backups/shadowing-commands/, recuperable y nunca borrado. La decisión está en .context/ADR/ADR-0006-skill-plus-mode-invocation.md.

04Un emisor, tres adaptadores

.agents/hooks/personality-reinject.mjs tiene el texto que se reinyecta en cada prompt. Existe en ese archivo y en ningún otro; cada host llega a él por un adaptador. Emite, en este orden:

  1. El OUTPUT CONTRACT de AGENTS.md §2, para que la voz no se diluya a medida que se llena la ventana de contexto.
  2. La línea AGENT IDENTITY: con el worktree, la etiqueta de sesión y el harness. Es metadato forense: de ahí salen los trailers de cada commit.
  3. La línea ORCA:, solo cuando el binario orca es alcanzable. Una máquina sin Orca nunca oye hablar de él.
  4. Como mucho un aviso de setup: falta .env en el checkout, o es un worktree enlazado que nunca corrió bun run worktree:provision.
  5. Las líneas ROUTE: read <archivo>: las secciones de instrucciones que el prompt necesita y la sesión todavía no leyó, clasificadas con el router de AGENTS.md y los triggers: de cada sección. Un SessionStart con compact o clear las re-arma en Claude Code y Codex; OpenCode 1 rutea desde chat.message y OpenCode 2 se queda con el router.
AGENT IDENTITY: worktree=<nombre|primary> session=<etiqueta> harness=<claude-code|codex|opencode>
Claude Code

.claude/settings.json

UserPromptSubmit corre node "$CLAUDE_PROJECT_DIR/.agents/hooks/personality-reinject.mjs" con un timeout corto. Es el único host que además acepta un título de sesión propuesto por el hook.

OpenCode

.opencode/plugins/personality-reinject.js

Un plugin fino importa agentContextLines y empuja las mismas líneas al system prompt, en el lugar y sin duplicar. Un solo export por defecto { id, setup, server } carga en las dos generaciones de plugins de OpenCode.

OpenCode no le pasa un nombre de sesión al plugin, solo el id, así que la etiqueta queda como el id crudo.

Codex

.codex/hooks.json

UserPromptSubmit resuelve la raíz con git rev-parse --show-toplevel y corre el mismo emisor, con una variante commandWindows en PowerShell.

Solo carga en un repositorio que Codex marcó como confiable.

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-0017).

05Los 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 as a behaviour 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 y includeCoAuthoredBy en false.

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; la decisión está en .context/ADR/ADR-0004-harness-agnostic-commit-trailers.md.

06MCP: un conjunto declarado, tres formatos

El conjunto canónico de servidores es el que declara .mcp.json (sin Claude, el archivo del primer harness declarado). Cada uno tiene que existir también en la configuración de los otros harnesses en uso (opencode.jsonc, .codex/config.toml), con las mismas dependencias de .env y los mismos valores literales. Los tres archivos declaran solo servidores locales.

Host Archivo Cómo referencia una credencial Si la credencial falta
Claude Code .mcp.json el cargador de .env, con --filter = sus variables el servidor recibe la variable vacía o sin definir y muere en su primera llamada autenticada
OpenCode opencode.jsonc el mismo cargador, el mismo filtro igual: falla en la primera llamada autenticada
Codex .codex/config.toml el mismo cargador, el mismo filtro, más startup_timeout_sec igual en un servidor stdio; solo un servidor remoto sin su bearer_token_env_var es error duro que nombra el servidor
Falla silenciosa En ningún host una credencial vacía avisa al arrancar. 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 #9). El entorno se lee cuando el harness lanza los servidores y no se refresca a mitad de sesión.

La paridad se compara por significado, no por texto

bun run agents:compat:check normaliza JSON, JSONC y TOML a una forma común (transporte, comando, argumentos, URL, variables de entorno, habilitado) y compara eso; un servidor detrás del cargador se lee como su propio comando, con su --filter como dependencias de .env. Un servidor presente en un solo host falla nombrando el servidor y el host. Los servidores que trae el boilerplate (KNOWN_MCP_IDS en cli/lib/agent-compatibility-contracts.ts) pasan además un chequeo de forma estricto por host; cualquier otro, solo el genérico. Un proyecto puede sacar o agregar servidores sin pelear con el check.

Búsqueda web La búsqueda web no está en ningún archivo MCP del proyecto. Vive a nivel de harness, en la config de usuario de cada máquina: Exa primero, Tavily segundo. Las skills declaran una capacidad (library-docs, web-search, db…) y el agente la resuelve por el sufijo del nombre de la tool, sea cual sea el prefijo del servidor. Si ninguna tool la provee, el agente para y dice qué capacidad falta y cómo habilitarla. Detalle en .agents/skills/agentic-dev-core/references/mcp-capabilities.md y .context/ADR/ADR-0005-harness-level-mcps-and-capabilities.md.

07Cómo llegan las credenciales a cada arranque

Un harness lanza sus servidores MCP antes de que corra cualquier hook, así que las credenciales tienen que estar disponibles en el momento del arranque. Las lee cada servidor por su cuenta, con el mismo cargador en los tres hosts, se haya abierto la sesión como sea.

Cada servidor MCP

El cargador de .env

Cada servidor stdio que necesita valores arranca con bunx -p varlock@<pin> varlock run --no-redact-stdout --inject vars --filter <sus variables> -- <comando real>. Lee .env (o el gestor de secretos que nombra el schema) al arrancar y le pasa al servidor solo las variables de su filtro.

Sin línea de comando

No hace falta nada

La app de escritorio o un worker supervisado lanzado de forma nativa no tienen comando que envolver ni entorno de proceso, y el cargador lee .env igual. No se copia ningún valor a otro archivo.

Desde la terminal

claude · opencode · codex

El binario pelado, sin script envoltorio: nada carga .env en el proceso del agente, así que ningún secreto llega al shell de la IA. Un comando que necesita un valor lo corre por el cargador en un subproceso, bunx varlock run -- sh -c '<comando>'. Si un servidor se comporta como si .env no existiera, bun run vars:env:check nombra la variable que el shell exporta con otro valor (bajo varlock gana el heredado).

Qué retira bun run harness:env

Copia vieja Qué hace
igual a .env la borra: .env ya tiene el valor
distinta de .env, o una variable que .env no declara la mueve a .auth/harness-env-backup/<VAR> (modo 0600) y la nombra: pasás el valor correcto a .env y después borrás ese directorio
leída todavía por una config vieja la deja hasta que esa config pase al cargador
Después de cada cambio en .env Reiniciar la sesión. En OpenCode también su servicio de fondo (opencode service restart), porque cachea la config resuelta por directorio. Las copias en texto plano que escribía una instalación vieja (el bloque env de .claude/settings.local.json, .auth/opencode/<VAR>) ya no las lee nadie; bun run harness:env:check sale con 1 mientras quede una.

08Cómo se verifica y qué no se puede verificar

Comando Qué hace
bun run agents:compat Regenera el shim y el alias y mueve aparte los comandos que tapan una skill. También lo corren bun sync y bun setup.
bun run agents:compat:check El gate del contrato: bytes del shim, destino del alias, ningún comando con nombre de skill, adaptadores de hook, paridad MCP y que eslint.config.js conecte cada bloque que exporta eslint.config.base.js. Es parte de bun run repo:check.
bun run setup:doctor Diagnóstico por superficie (instrucciones, alias, comandos, hooks, MCP, lint), la línea de estado del alias, las copias en texto plano de credenciales MCP que quedan, de dónde salen los secretos y la confianza de Codex como aviso.
bun setup Detecta los tres harnesses y pregunta cuáles configurar (INSTALL_AGENTS=claude-code,opencode,codex para un run sin preguntas), guarda la elección en harnesses: y ofrece borrar los archivos de los que quedaron afuera (por defecto los conserva). Retira las copias en texto plano de una instalación vieja y, después de escribir .env, avisa que hay que reiniciar la sesión.

Lo que conviene saber de cada host