Agente necesita documentación: escribe repo para personas y máquinas

1. Descripción general

Los agentes de codificación se están convirtiendo gradualmente en un “miembro” del equipo: leen código, corrigen bugs, escriben tests y abren pull requests. El problema es que el agente no recuerda nada entre sesiones. Existen herramientas que proporcionan funcionalidad de “memory”, pero esas memorias suelen estar vinculadas a cada persona, son difíciles de revisar y no siguen las versiones del código.

Este artículo explica por qué la documentación dentro del repositorio es más confiable que la memoria del agente. También guía cómo escribir un repositorio Spring Boot de modo que las personas nuevas en el equipo y los agentes trabajen correctamente desde el primer momento. Después de leer, sabrás cómo escribir el archivo de guía AGENTS.md, consolidar todas las verificaciones en un único comando, convertir convenciones en tests, y documentar las decisiones de diseño.

2. Por qué la memoria no es suficiente

Compara dos formas en que un agente “sabe” que la capa domain no debe depender de Spring:

  • Memoria: la última vez se lo dijiste y guardó una nota en algún lugar.
  • Repositorio: la regla está en AGENTS.md y hay un test que falla si alguien la viola.

El primer enfoque tiene varias debilidades evidentes:

  • La nota existe solo en la máquina o cuenta de una persona. Los compañeros que usan un agente diferente no la tienen.
  • Nadie revisa esa nota. Si el agente “recuerda mal”, es muy difícil detectarlo.
  • La nota no sigue las ramas. Cuando la regla cambia en main, la memoria antigua persiste.

El segundo enfoque está versionado, se revisa mediante pull request y es correcto para cada commit. Lo más importante es que también ayuda a las personas. Lo que el agente necesita para hacer bien casi coincide con lo que un desarrollador nuevo necesita en su primera semana.

Por eso este tema está recibiendo atención. El agente no expone un problema nuevo. Revela el costo de la documentación pobre, porque el agente no puede ir y preguntar “¿y esto cómo funciona?” como lo haría una persona nueva.

3. Archivo de guía para el agente: AGENTS.md

Muchas herramientas de agentes leen un archivo Markdown en la raíz del repositorio para obtener instrucciones. El nombre del archivo depende de la herramienta, pero AGENTS.md es una convención común. Si tu herramienta usa otro nombre, puedes crear un symlink en lugar de duplicar el contenido.

3.1. Qué debe contener

# AGENTS.md

## Proyecto
Servicio de pedidos (Spring Boot 3, Java 21). Módulos principales: `order`, `payment`, `customer`.

## Comandos
- Build + todas las verificaciones: `./scripts/verify.sh`
- Ejecutar un test: `./mvnw -q test -Dtest=DiscountPolicyTest`
- Ejecutar la app localmente: `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`

## Convenciones obligatorias
- `..domain..` no importa `org.springframework..` (tiene test: `ArchitectureTest`).
- Las monedas usan `BigDecimal`, no `double`.
- Todo cambio en el comportamiento de negocio debe tener un test en `src/test/java/.../domain`.

## No hacer
- No edites archivos en `src/main/resources/db/migration` ya merged; crea una migration nueva.
- No hagas commit de `application-local.yml`.

## Leer más
- Decisiones de arquitectura: `docs/adr/`
- Flujo de pagos: `docs/payment-flow.md`

Cada sección tiene un propósito específico:

  • Comandos: el agente ejecuta comandos muy a menudo. Un comando incorrecto lo hará perder muchas iteraciones. Escribe comandos exactos, que se puedan copiar y pegar.
  • Convenciones obligatorias: cada línea debe indicar cómo verificar (nombre del test, nombre de la regla de lint). Las convenciones sin un mecanismo de verificación se olvidan fácilmente.
  • No hacer: enumera las acciones que tienen consecuencias difíciles de revertir.
  • Leer más: apunta a la documentación detallada en lugar de copiarla aquí.

3.2. Mantén it conciso y apunta a la fuente de verdad

El agente carga este archivo en el contexto en cada sesión. Cuanto más largo sea el archivo, más contexto consume y más probable es que contenga información obsoleta.

Regla general: no reescribas lo que el código o la compilación ya explica claramente. No enumeres cada dependencia porque pom.xml ya las tiene. No expliques cada endpoint porque la especificación OpenAPI ya los tiene. Solo documenta lo que no se puede inferir del código.

Nota: Nunca pongas secretos, tokens o URLs internas sensibles en AGENTS.md. Este archivo lo leen herramientas externas y permanece en el historial de git para siempre.

4. Un único comando de verificación

El agente funciona mejor cuando tiene un bucle de retroalimentación claro: edita código, ejecuta comando, ve si pasa o falla. Si tienes que ejecutar cuatro comandos diferentes en orden para una verificación completa, el agente se saltará pasos. Las personas también.

#!/usr/bin/env bash
# scripts/verify.sh — ejecuta igual que CI. Exit code diferente de 0 significa que no está listo para merge.
set -euo pipefail

echo ">> Compilar + unit test + test de arquitectura"
./mvnw -B -q verify

echo ">> Verificar que no quede ningún TODO-AGENT sin resolver"
if grep -rn "TODO-AGENT" src/; then
  echo "Aún hay TODO-AGENT sin procesar" >&2
  exit 1
fi

echo "OK: listo para crear un pull request"

Explicación:

  • set -euo pipefail detiene el script inmediatamente si un comando falla. Gracias a eso, el agente recibe un código de salida diferente de 0 en lugar de un output largo seguido de un “OK” engañoso al final.
  • -B (modo batch) desactiva el output coloreado y la barra de progreso. Un log más limpio facilita que el agente lea los errores.
  • El paso grep es un ejemplo de convención personalizada del equipo: se permite que el agente marque partes incompletas con TODO-AGENT, pero no se puede dejar ninguna cuando se abre un PR.

Cuando se ejecuta con éxito, verás:

>> Compilar + unit test + test de arquitectura
>> Verificar que no quede ningún TODO-AGENT sin resolver
OK: listo para crear un pull request

Lo más importante: CI debe llamar exactamente a este script. Si CI ejecuta un conjunto diferente de comandos, verify.sh gradualmente se desviará de la realidad y se convertirá en documentación incorrecta.

5. Convierte convenciones en tests con ArchUnit

Una frase como “domain no depende de Spring” en la documentación solo es un consejo. Si la conviertes en test, se convierte en ley. ArchUnit te permite escribir reglas de arquitectura como tests de JUnit.

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.3.0</version>
    <scope>test</scope>
</dependency>
package com.nonla.shop;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packages = "com.nonla.shop", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static final ArchRule domainKhongPhuThuocSpring = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("org.springframework..")
            .because("domain debe poder probarse sin el contexto de Spring, ver docs/adr/0003-domain-puro.md");

    @ArchTest
    static final ArchRule webKhongGoiThangRepository = noClasses()
            .that().resideInAPackage("..web..")
            .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
            .because("los controllers deben pasar a través del service para mantener la transacción en un solo lugar");
}

El punto más valioso está en .because(...). Cuando se viola la regla, el mensaje de error contiene la razón y la ruta a la ADR. Supongamos que alguien añade @Component a DiscountPolicy en el paquete domain, la salida será algo como:

Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'..domain..' should depend on classes that reside in a package 'org.springframework..',
because domain debe poder probarse sin el contexto de Spring, ver docs/adr/0003-domain-puro.md'
was violated (1 times):
Class <com.nonla.shop.order.domain.DiscountPolicy> is annotated with <org.springframework.stereotype.Component> ...

Un agente que lee este mensaje sabrá inmediatamente qué hacer: quitar la anotación y luego registrar el bean en una clase @Configuration en la capa infra. Un buen mensaje de error es documentación que aparece en el momento exacto que el lector la necesita.

6. Los tests son documentación de negocio

Las reglas de negocio son lo que el agente más fácilmente puede hacer mal. No puede adivinar que “el descuento de primer pedido tiene un máximo de 50.000đ”. Deja que el código y el test lo digan.

package com.nonla.shop.order.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;

public record Order(String customerId, BigDecimal subtotal, boolean firstOrder) {}

class DiscountPolicy {

    static final BigDecimal FIRST_ORDER_RATE = new BigDecimal("0.10");
    static final BigDecimal FIRST_ORDER_CAP = new BigDecimal("50000");

    BigDecimal discountFor(Order order) {
        if (!order.firstOrder()) {
            return BigDecimal.ZERO;
        }
        BigDecimal raw = order.subtotal().multiply(FIRST_ORDER_RATE)
                .setScale(0, RoundingMode.DOWN);
        return raw.min(FIRST_ORDER_CAP);
    }
}

Los tests que acompañan deben nombrarse según el comportamiento, no según el nombre del método:

package com.nonla.shop.order.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("Descuentos de pedidos")
class DiscountPolicyTest {

    private final DiscountPolicy policy = new DiscountPolicy();

    @Test
    @DisplayName("El primer pedido obtiene un descuento del 10% del total")
    void firstOrderGetsTenPercent() {
        var order = new Order("c-1", new BigDecimal("200000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("20000");
    }

    @Test
    @DisplayName("El descuento del primer pedido no excede 50.000đ")
    void firstOrderDiscountIsCapped() {
        var order = new Order("c-1", new BigDecimal("2000000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("50000");
    }

    @Test
    @DisplayName("Desde el segundo pedido en adelante no hay descuento")
    void repeatOrderGetsNoDiscount() {
        var order = new Order("c-1", new BigDecimal("2000000"), false);

        assertThat(policy.discountFor(order)).isZero();
    }
}

Explicación:

  • @DisplayName convierte el informe de prueba en una especificación legible. Las personas de producto también lo entienden.
  • isEqualByComparingTo compara BigDecimal por valor, así que 20000 y 20000.00 se consideran iguales. Usar isEqualTo aquí es un error común porque también compara la escala.
  • Si se pide al agente que “aumente el descuento a 15%”, verá inmediatamente qué test necesita cambiar. Si accidentalmente deja fuera el tope, el segundo test fallará.

Cuando se ejecuta, el informe se vería así:

Descuentos de pedidos
  ✔ El primer pedido obtiene un descuento del 10% del total
  ✔ El descuento del primer pedido no excede 50.000đ
  ✔ Desde el segundo pedido en adelante no hay descuento

7. Documenta el “por qué”: ADR y package-info

El código dice qué hace el sistema. Los tests dicen qué debe hacer. Pero nada explica el por qué se eligió ese enfoque. Sin el “por qué”, el agente tiende a “limpiar” lo que parece innecesario pero en realidad tiene razón de ser.

Architecture Decision Record (ADR) es un archivo corto para cada decisión:

# 0003. La capa domain no depende de Spring

Estado: Aceptado

## Contexto
Antes, los tests de domain tenían que iniciar el contexto de Spring, lo que era lento y frágil.

## Decisión
El paquete `..domain..` solo usa JDK. Los beans se declaran en `..infra.config`.

## Consecuencias
- Los tests de domain se ejecutan con JUnit puro.
- Se requiere escribir clases de configuración adicionales. Se verifica con `ArchitectureTest`.

Para notas vinculadas a un paquete, package-info.java es el lugar apropiado porque el agente las ve al abrir el directorio:

/**
 * Reglas de negocio de pedidos. Solo usa JDK, no usa Spring (ADR 0003).
 * Todo dinero es BigDecimal, en VND, redondeado hacia abajo al dong.
 */
package com.nonla.shop.order.domain;

8. Errores comunes y cuándo no hacerlo

Documentación duplicada. La misma regla se documenta en README.md, AGENTS.md y wiki. Después de unos meses, los tres dicen cosas diferentes. Elige una fuente de verdad y apunta a ella desde los demás lugares.

Instrucciones que contradicen CI. AGENTS.md dice “ejecuta mvn test”, pero CI también ejecuta tests de integración. El agente reportará “listo” mientras el PR sigue en rojo.

Escribir para el agente con instrucciones vagas. La frase “Por favor escribe código limpio” no ayuda. La frase “Los métodos públicos en ..service.. deben tener test” es útil, y mejor aún si es verificable.

Poner todo en un archivo. Un AGENTS.md de miles de líneas diluye el contexto. La información importante se pierde.

Nota: No todo repositorio necesita este conjunto completo. Un script de un solo uso, un prototipo a punto de descartar, o un repositorio de solo cientos de líneas merece un README.md con comandos de ejecución. ArchUnit también solo vale la pena cuando tienes al menos algunas capas o módulos con límites que realmente necesitas mantener. Escribir documentación para lo que no existe aún solo crea más cosas que mantener.

En cambio, la memoria del agente aún tiene su lugar. Es apropiada para preferencias personales como el tono al explicar o el editor que usas. Esas cosas no pertenecen al repositorio. Las reglas del proyecto siempre deben estar en el repositorio.

9. Conclusión

El agente no necesita mejor memoria, necesita un repositorio más comprensible. Has visto cómo:

  • Escribir AGENTS.md conciso que contenga comandos exactos y convenciones verificables.
  • Consolidar todos los pasos de verificación en scripts/verify.sh y hacer que CI use el mismo script.
  • Convertir convenciones de arquitectura en tests de ArchUnit, con mensajes de error que señalen la razón.
  • Usar tests con @DisplayName como especificación de negocio.
  • Documentar el “por qué” con ADR y package-info.java.

Todo lo anterior es útil para las personas primero. El agente es solo la razón por la que no podemos posponer esto más. Comienza poco a poco: escribe verify.sh y las primeras cinco líneas de AGENTS.md esta semana.

ai-agentdocumentationarchunittestingspring-boot