Un CLI de backlog que vive adentro del repo
Por qué metí mi backlog de arquitecto en un SQLite local, aislado por proyecto, e ignoré Linear, Notion o los archivos Markdown.
El backlog de un arquitecto que orquesta varios proyectos suele vivir en tres lugares equivocados al mismo tiempo: un TODO.md que nadie actualiza, un issue de GitHub perdido sin prioridad y el chat con el agente de IA, que se resetea de cero cada mañana. Cada vez que abrís un repo, la arqueología arranca de nuevo — y rehidratar ese contexto en un prompt se fuma tokens que ni te das cuenta.
Tenía más de 20 repos activos mezclando NuageIT, mi propio laburo y experimentos. No me quería armar un Jira o ClickUp privado. Tampoco quería otro archivo Markdown para caretear que gestiono algo. Escribí un CLI en Rust, local-backlog, con un único binario que sirve a todos los repos y mantiene un aislamiento estricto por proyecto (tenancy).
El dolor concreto
Opero en tres modos al mismo tiempo. En proyectos de consultoría hago skeleton walking — diseño arquitectura, scaffolding, ADRs, pipelines — y después le entrego la ejecución a los juniors y semiseniors del equipo. En productos ya estables, acompaño al equipo que mantiene. Y, en paralelo, llevo mis propias consultorías solo, muchas con apoyo de agentes de IA (Claude Code, Codex, Gemini).
La pila de repos no es toda mía para desarrollar. Pero es toda mía para recordar. En cada proyecto necesito recuperar: dónde dejé la revisión de ADR, qué decisión quedó pendiente con el tech lead, qué deuda técnica registré en la última pasada o qué plan de fase dos prometí escribir. Esto no es un backlog de equipo — el equipo tiene Jira, ClickUp, Linear o un tablero compartido. Esto son las anotaciones de quien orquesta varios frentes y necesita volver al contexto sin releer tres canales de Slack.
El problema no es el número de tareas. Es que ningún formato suelto sobrevive a un reinicio del agente o a una semana lejos del repo. Cada vuelta empieza con “explicame qué teníamos en marcha acá” — conmigo mismo o con el agente. Y ahí copio el TODO.md, pego issues, resumo decisiones de ayer. Es el equivalente editorial de reconstruir la caché de cero en cada petición.
Dos backlogs que no se mezclan
En un equipo maduro, existen dos backlogs sobre el mismo código. El backlog de producto — épicos, features, prioridades que el PO o el PM discuten en el refinement — es público y colectivo. El backlog de ingeniería — deuda técnica, refactorizaciones pendientes, ideas que todavía no son épicos, riesgos arquitectónicos notados en reviews — suele ser un canal lateral entre tech leads, cuando no vive solo en la cabeza de quien lidera.
La frontera no es invento mío. Martin Fowler formaliza parte en el Technical Debt Quadrant: una decisión se convierte en registro antes de ser laburo. Will Larson, en Staff Engineer, trata el “technical roadmap” como un artefacto separado del product roadmap — laburo que el Staff y el Tech Lead llevan encima aunque ningún tablero lo pida.
El punto vale más allá del liderazgo formal. Todo senior opera en dos capas: lo que está en el tablero del sprint y lo que carga de contexto técnico propio. En algunos lugares tenés el poder de promover un ítem de tu capa directo al backlog del proyecto. En otros, se lo llevás al PO o PM en una ceremonia. En ambos casos, el material tiene que estar registrado en un lugar que responda a filtros — por proyecto, prioridad, horizonte.
Y ahí es donde entra la cadencia. Día, semana, quincena, mes, trimestre, año. Cada horizonte pide una lente del mismo backlog: hoy es status=doing; el planeamiento mensual es una query por priority y tag; la retrospectiva trimestral lee el timeline determinístico de eventos. Sin un filtro estructurado, el horizonte colapsa en un documento narrativo que nadie vuelve a leer.
local-backlog es esta segunda capa materializada en el disco. No compite con Jira o ClickUp — alimenta el refinement.
Tres costos
Antes de escribir el CLI, listé alternativas. Ninguna es mala en general — cada una falla en un eje específico para mi caso.
- Costo: cuota mensual + latencia de cambio de contexto al browser. - Buena UI, mala como fuente programable para un agente. - Silo externo: el agente necesita API key, rate limit, auth. - El backup depende del proveedor.
- Costo: cero en herramientas, alto en disciplina. - Sin esquema: la prioridad pasa a ser un
prefijo
[P1], el tag un emoji, el orden es el orden de tipeo. - Filtrar por “solo bugs abiertos” esgrep+ lectura humana. - Exportar para LLM es pegar el archivo entero.
La tercera opción — SQLite local con CLI propio — tiene el costo de escribir la herramienta. Y, en mi caso, de aprender Rust en el proceso. Se paga una vez; después el archivo ~/.local-backlog/backlog.db es solo datos, versionable vía dotfiles, consultable por SQL cuando el CLI no cubra un reporte excepcional.
La elección
local-backlog es un binario único llamado backlog. Todo el estado vive en ~/.local-backlog/ (se puede cambiar vía LOCAL_BACKLOG_HOME). Nada en el repo, nada de .local-backlog.db ensuciando el git status. El vínculo entre la carpeta y el proyecto vive en un registro global; backlog init en una carpeta nueva registra ese path como tenant.
El Quickstart es literalmente esto:
cd ~/code/mi-proyecto
backlog init --yes
backlog add "Refactorizar middleware de auth" \
--type feature --tag security --priority 50
backlog list
backlog list --format json
backlog show 1
backlog done 1
backlog export --format markdown
Todo comando de arriba resuelve el tenant a partir del CWD. No existe --all-projects en comandos de datos. Entrar al directorio es elegir el scope.
Decisiones que importan
Tenancy estricta, reforzada por triggers
La ADR-0001 registra la elección: cada query de datos filtra por project_id inferido del CWD, y triggers SQL en task_tags, task_links y parent_id bloquean inserts/updates cross-project. El tag #auth en dos repos no colisiona ni comparte registro — tags.(project_id, name) es único por tenant.
Podría haber hecho solo el filtro en la aplicación. No lo hice. Un WHERE olvidado rompe la tenancy en silencio; un trigger falla fuerte. Es defensa en profundidad barata en un proyecto que mantengo yo mismo y donde una fuga entre repos es exactamente el tipo de error que cometería en un refactor apurado.
tasks atómica, todo lo demás en satélites
La ADR-0002 define tasks con el núcleo mínimo (id, title, status, priority, type, parent_id, timestamps). El resto — tags, atributos EAV, links tipados (blocks, relates, duplicates), eventos append-only — vive en tablas satélites.
La trampa: el EAV seduce. Te dan ganas de mandar todo a task_attributes(key, value) y no hacer nunca más una migración. La regla que me escribí es explícita — una clave EAV pasa a ser columna en tasks solo cuando aparece en ≥80% de las tareas activas o cuando se vuelve un filtro recurrente. El ascenso es una decisión consciente, no automática.
Output contract
La ADR-0004 es la que más me salvó de laburo extra: los datos van al stdout, los mensajes (logs, prompts, errores) van al stderr, y todo comando de lectura acepta --format=table|json desde el primer día. JSON con un sobre { "schema_version": N, "data": ... }. Nada de println! crudos en los subcomandos — todo pasa por un helper en src/output.rs.
El trade-off: disciplina densa en la primera semana, pipes confiables para siempre. backlog list --format json | jq '.data[] | select(.priority > 40)' no se rompe cuando activo el --verbose.
La IA como consumidor, no como dueño
El punto que justifica todo el proyecto es backlog export --format json. Salida estructurada, filtros por status/tag/tipo, ordenación determinística (priority → updated_at → id). Dos ejecuciones contra una base de datos sin cambios producen bytes idénticos — así que puedo colocar un snapshot en tests/ y hacer un diff contra eso.
{
"schema_version": 1,
"project": { "id": 1, "name": "proy", "root_path": "...", "archived_at": null },
"tasks": [
{
"id": 42,
"title": "refactorizar middleware de auth",
"status": "doing",
"priority": 50,
"type": "feature",
"tags": ["security", "debt"],
"attributes": [{ "key": "jira", "value": "ABC-123" }],
"links_out": [{ "from_id": 42, "to_id": 17, "kind": "blocks" }],
"events": []
}
]
}
En la práctica, el workflow pasó a ser: el agente comienza la sesión con backlog export --format markdown --status todo,doing pegado en el contexto inicial. Al final de la sesión, si la tarea cambió, el propio agente corre backlog done o backlog edit. La IA consume y actualiza, no se pone a reinterpretar. El dueño del estado continúa siendo SQLite.
Antes, cada sesión empezaba con tres o cuatro minutos de “explicame qué tenemos en marcha” y un párrafo pegado de tres fuentes distintas. Hoy empieza con un comando, salida determinística, y el agente entra en el mismo estado de la sesión anterior. Es menos rehidratación por prompt y también menos variación entre lo que yo recuerdo y lo que la herramienta afirma.
Lo que no hice
- Sync entre máquinas. El plan es copiar
~/.local-backlog/vía dotfiles +git. Sin merge de conflictos reales, sin CRDT, sin servidor. En dos máquinas activas acepto resolverlo a mano — en cuatro, ya sería un problema. - UI web. No existe. Capaz que nunca exista. Terminal es el ambiente;
backlog show 42ybacklog events 42cubren lo que necesito. - Multiusuario. El modelo asume un humano. El equipo tiene Jira, ClickUp o Linear;
local-backlogson anotaciones personales de quien orquesta, no un tablero compartido. Agregarauthor_idsería una migración aditiva, pero no hay caso de uso.
Sin múltiples frentes corriendo en paralelo, un TODO.md disciplinado alcanza. La herramienta paga el costo de existir cuando el dolor de recuperar el contexto ya es diario.
Código
El código está en github.com/OliveiraCleidson/local-backlog. Las ADRs canónicas viven en docs/adr/pt-BR/, con traducciones en en y es-AR en el mismo commit.
Discusión
Este blog no tiene comentarios. Para debatir este post: