agentic-dev-core · progressive disclosure
El agente no necesita todas las reglas del repo en cada turno. Necesita siempre las que lo
atan, y el resto cuando el pedido lo pide. Por eso AGENTS.md es un núcleo chico
que carga en cada sesión, cada tema vive en su propio archivo bajo
.agents/instructions/, y un router fijo, reforzado por un hook, dice cuál leer y
cuándo.
Antes, AGENTS.md era un solo archivo que todas las sesiones cargaban entero,
hiciera falta o no. Tenía dos costos, y el segundo no se veía.
Una charla sobre un color pagaba la política de git, el caché de Jira y el contrato multi-harness. Las secciones condicionales cargaban siempre, aunque la mayoría de las sesiones no las usara.
Codex junta los AGENTS.md del proyecto en un solo presupuesto,
project_doc_max_bytes, y corta el texto en el byte que lo cruza, con apenas
un aviso en el log. El archivo viejo lo pasaba largo: en Codex, todo lo que venía después
del corte nunca llegaba al modelo.
En Claude Code un @archivo se expande entero al arrancar, y ni OpenCode ni
Codex expanden imports. Lo único perezoso que comparten los tres hosts es que el modelo
lea un archivo porque una instrucción se lo pidió, las skills, y un hook que inyecta texto
en cada pedido. El diseño usa esas tres cosas.
Cada capa carga en un momento distinto. L0 está siempre; L1 entra a pedido, una sección por
tema; L2 es lo que ya existía, las references/ de cada skill.
Las flechas son lecturas a pedido: L0 nunca importa una sección. Las que muestra el dibujo son ejemplos; la lista real es la carpeta.
| Capa | Archivo | Cuándo carga |
|---|---|---|
| L0 | AGENTS.md |
cada sesión, en cada host. Claude Code llega por CLAUDE.md, que es
exactamente @AGENTS.md más un salto de línea
|
| L1 | un archivo por sección en .agents/instructions/ |
a pedido: el router de L0 dice cuándo leer cada uno, y el hook lo recuerda con una
línea ROUTE:
|
| L2 | references/ de cada skill |
a pedido, nombradas por la sección o la skill que las necesita |
references/ que cita.
Una tabla en L0, entre <!-- router:start --> y
<!-- router:end -->, con tres columnas: Kind,
Load y Also. Las filas son tipos de pedido, no
features, así que la tabla no crece con el proyecto: crece la sección y sus
triggers:. Lo de fijo lo hace cumplir un candado: el comentario
<!-- router:lock <huella> <ADR-NNNN> --> guarda la huella de
la tabla y el ADR que la decidió, y cualquier cambio de fila falla hasta que un ADR lo cubra.
| Columna | Qué dice | Ejemplo (fila de git) |
|---|---|---|
| Kind | el tipo de pedido que dispara la fila | git: branch, commit, push, PR, merge, conflict |
| Load |
los archivos a leer, como rutas entre backticks; el primero es el
ancla de la fila
|
.agents/instructions/agent-git.md |
| Also | la skill o el archivo que se carga junto | git-flow-master |
AGENTS.md; este
deck no la copia.
Cinco líneas en la cabecera de L0 convierten el router en obligación. Sin ellas, la tabla sería una sugerencia.
ROUTE: que inyecta el hook es obligatoria y gana
sobre el criterio propio del modelo: se lee su archivo antes de actuar. Una
ROUTE-OPTIONAL: no obliga (se lee uno de sus archivos solo si la tarea lo
pide) y una ROUTE-PENDING: nombra un archivo obligatorio que sigue sin leerse.
AGENTS.md. Una regla que solo tiene este proyecto va a
.agents/instructions/agent-project.md.
El router en L0 depende de que el modelo lo consulte. El hook no: clasifica cada pedido con
código y deja escrito qué leer. Es el mismo emisor que ya ponía el contrato de salida y la
línea AGENT IDENTITY:.
| Regla | Qué hace |
|---|---|
| Ancla |
Una fila se dispara cuando el pedido coincide con su primer archivo de
Load; entonces se rutean todos los archivos de la fila. Así un archivo
que comparten dos filas no arrastra los de la otra.
|
| Disparadores |
Cada sección declara en su frontmatter triggers: (regex sin distinguir
mayúsculas, en inglés y en español) y paths: (prefijos del repo). Ajustar
el ruteo es editar esas listas, nunca L0.
|
| Alcance |
Una línea ROUTE-SCOPE: en el pedido reemplaza al pedido para clasificar:
cada ítem es un id de sección (se rutea directo) o palabras (se
clasifican); none no rutea nada. Sin ella, un pedido que trae el preámbulo
de un orquestador se clasifica solo por el bloque de tarea que sigue a su marcador. Las
rutas absolutas se neutralizan antes: el nombre de una carpeta no es una intención.
|
| Ranking y tope |
Las filas disparadas se ordenan por la fuerza de su ancla (un id del
alcance, después hits de paths:, después triggers distintos, después la
coincidencia más temprana). Solo las primeras MAX_ROUTED_SECTIONS
secciones reciben un ROUTE: obligatorio; el resto va a una sola línea
ROUTE-OPTIONAL:. Medido: un agente lee una ruta sola mucho más que una
de cinco en el mismo turno (ADR-0018).
|
| Recordatorio (Claude Code) |
Un hook PostToolUse corre el mismo emisor: la primera herramienta que no
lee ninguna sección ruteada, mientras alguna sigue sin leerse, recibe UNA línea
ROUTE-PENDING:. No se repite en el turno y calla para los subagentes.
|
| Dedupe por sesión | Un archivo de estado en el temp del sistema, por checkout y por sesión, recuerda qué se ruteó. Un pedido que no necesita nada nuevo no agrega nada. |
Re-armado tras compactar o /clear |
Un SessionStart con matcher: "compact" o
matcher: "clear" vuelve a correr el emisor, que borra el estado y no
imprime nada: lo que la compactación o el /clear sacó del contexto se
vuelve a rutear la próxima vez que haga falta.
|
| Host | Cómo llega la línea |
|---|---|
| Claude Code |
.claude/settings.json: UserPromptSubmit ejecuta el emisor;
SessionStart con compact o clear lo re-arma;
PostToolUse sin matcher recuerda una ruta sin leer.
|
| Codex |
.codex/hooks.json: lo mismo, solo en un proyecto de confianza (uno sin
confianza no carga ni AGENTS.md ni hooks).
|
| OpenCode 1 |
.opencode/plugins/personality-reinject.js clasifica en
chat.message, empuja la línea al system prompt y re-arma solo al compactar (no
registra nada para /clear).
|
| OpenCode 2 | Solo router: ningún hook documentado le pasa el texto del pedido, así que manda el router de L0 con el LOAD PROTOCOL. Es una degradación declarada. |
Una regla que obliga tiene que estar donde el agente siempre lee. Pero su fundamento, sus casos borde y su mecánica no necesitan ocupar cada turno.
Cada regla conserva su frase que ata, palabra por palabra, con el mismo número y nombre. Ninguna regla pierde fuerza: lo que obliga sigue cargando en cada sesión.
.agents/instructions/agent-critical-rules.md tiene cada regla bajo
## N. NOMBRE, con su fundamento y sus casos borde. El router tiene una fila
para leerlo cuando el agente está por romper una regla, duda o le preguntan por ella.
Cambiar una regla es tocar su texto completo en agent-critical-rules.md y su
frase en AGENTS.md, que tiene que seguir textual.
bun run instructions:check compara las dos.
package.json y .agents/project.yaml son los dos archivos de datos
que más se consultan. Las filas del router que los nombran hacen dos cosas a la vez.
| Host | Qué pasa |
|---|---|
| Claude Code |
La fila escribe @package.json y @.agents/project.yaml como
texto plano, sin backticks: Claude Code expande un import al arrancar y
salta los que están dentro de código. Los dos archivos entran a la sesión enteros.
|
| OpenCode y Codex | No expanden imports. Leen la instrucción reforzada de la misma fila ("whenever any of these apply, read ...") y abren el archivo cuando la condición se cumple. |
.context/ADR/ADR-0009-progressive-disclosure-of-instructions.md.
bun run instructions:check rechaza cualquier otro @ suelto en L0:
la lista permitida es ALLOWED_IMPORTS en
scripts/lint-instructions.ts.
L0 guarda lo que tiene que atar en cada turno, lo pida o no el pedido. Todo lo demás vive en una sección.
Sin ellos ninguna sección es alcanzable.
Lo que prohíbe tiene que estar donde el agente siempre lee.
Butler, PM Voice, Visual Mapping, el reparto de capas y cómo se le pide al humano que decida. El comportamiento tiene que ser determinista, así que no se recorta.
Subagentes y conversación principal, el briefing de siete componentes, el protocolo de error. Gobierna cada despacho, sea cual sea el pedido.
Valen en cada turno y ocupan poco.
Las secciones son doctrina compartida: bun sync las mantiene al día con el
boilerplate. agent-project.md es lo propio de cada proyecto y ninguna sincronización
lo toca; un proyecto que no lo tiene lo recibe una sola vez desde
.agents/instructions/agent-project.md.template. La regla de oro: el texto va al
archivo dueño, nunca de vuelta a AGENTS.md.
| Quiero cambiar | Dónde |
|---|---|
| el detalle de un tema | su archivo en .agents/instructions/ |
| cuándo se rutea una sección | sus triggers: y paths: en el frontmatter |
| una regla crítica |
su texto en agent-critical-rules.md y su frase textual en
AGENTS.md
|
| una regla que solo tiene este proyecto | .agents/instructions/agent-project.md |
| una línea con NEVER o MUST en una sección |
que nombre quién la sostiene cuando la sección no está cargada (una regla crítica, las
Compact Rules de una skill) o que se declare local con
<!-- binds-in-section: <motivo> -->
|
CLAUDE.md |
nada: es el shim generado (Critical Rule #15) |
bun run instructions:check revisa presupuesto, router,
frontmatter, frases de las reglas, quién sostiene cada NEVER o MUST, e imports. Corre dentro
de bun run repo:check. Además sostiene tres candados
(.context/ADR/ADR-0014-instructions-maintenance-locks.md): el router fijado a su
ADR (--accept-router ADR-NNNN registra una decisión nueva), la evaluación del
clasificador contra los prompts etiquetados en cada corrida, y que cada sección venga
completa (frontmatter, fila en el router, prompts propios). La otra mitad la mide
bun run instructions:audit: de las rutas que inyectó el hook, cuántas leyó de
verdad el agente. La guía de edición completa está en
.agents/instructions/README.md.
L0 tiene un techo porque Codex corta sin avisar. Los valores son constantes con nombre en
scripts/lint-instructions.ts, fijadas por
.context/ADR/ADR-0009-progressive-disclosure-of-instructions.md: cambiar una
es una decisión nueva, con su ADR.
| Nivel | Constante | Si se pasa |
|---|---|---|
| Objetivo | L0_TARGET |
aviso: hay detalle que podría vivir en una sección |
| Techo | L0_BUDGET |
error: el L0 del boilerplate no entra |
| Con agregados del proyecto | L0_PROJECT_BUDGET |
error: el texto del proyecto va a agent-project.md o a una context skill |
| Tope de Codex | CODEX_PROJECT_DOC_MAX_BYTES |
error siempre, digan lo que digan los otros: es el valor por defecto de project_doc_max_bytes |
bun run instructions:check.
Dos preguntas distintas: si el clasificador rutea lo que tiene que rutear, y si el modelo después lee lo que le rutearon.
| Qué se mide | Dónde |
|---|---|
| Recall, recall obligatorio y precisión del clasificador |
Pedidos etiquetados a mano, en inglés y en español, en
cli/lib/fixtures/instruction-router-eval.json; los umbrales son su campo
targets y los verifica cli/lib/instruction-router.test.ts
dentro de bun run test. El recall obligatorio cuenta solo las líneas
ROUTE: (que el tope no haya empujado una sección esperada a la opcional) y
la precisión también: una línea opcional no obliga a nada.
|
| Que no tarde | el mismo test exige que rutear un pedido quede muy por debajo de 50 ms |
| Que hook y lint lean igual |
scripts/lib/hook-router-parity.test.ts ata el parser del hook al de
scripts/lib/instructions.ts
|
| Que el modelo obedezca |
auditoría de transcripciones: cada ROUTE: tendría que ir seguido de una
lectura del archivo ruteado antes de la primera acción; también cuenta cuántas
lecturas siguieron a un ROUTE-PENDING:
|
triggers: de la sección, y el pedido entra al set etiquetado para que no vuelva
a pasar. Etiquetar de nuevo para esconder un falso positivo no vale.