Um CLI de backlog que vive dentro do repositório
Por que coloquei meu backlog de arquiteto em SQLite local, isolado por projeto, e não em Linear, Notion ou arquivo Markdown.
Backlog de arquiteto que orquestra vários projetos costuma viver em três
lugares errados ao mesmo tempo: um TODO.md que ninguém atualiza, uma
issue do GitHub sem prioridade e o chat com o agente de IA, que reinicia
do zero toda manhã. Cada abertura de repositório recomeça a arqueologia —
e reidratar esse contexto num prompt custa tokens que ninguém mede.
Eu tinha mais de 20 repositórios ativos misturando NuageIT, CNPJ próprio e
experimentos. Não quis montar um Jira ou ClickUp privado. Também não quis mais um
arquivo Markdown para fingir que gerencio. Escrevi um CLI em Rust,
local-backlog, com um único binário que serve todos os repos e mantém
tenancy estrita por projeto.
A dor concreta
Opero em três modos ao mesmo tempo. Em projetos de consultoria faço skeleton walking — desenho arquitetura, scaffolding, ADRs, pipeline — e depois entrego a execução para juniors e plenos do time. Em produtos já estáveis, acompanho a equipe que mantém. E, em paralelo, toco minhas próprias consultorias-solo, muitas com apoio de agentes de IA (Claude Code, Codex, Gemini para mapas).
A pilha de repositórios não é toda minha de desenvolver. Mas é toda minha de lembrar. Em cada projeto eu preciso recuperar: onde parei na revisão de ADR, qual decisão ficou pendente com o tech lead, qual dívida técnica eu registrei na última passada, qual plano de fase dois eu prometi escrever. Isso não é backlog de time — time tem Jira, ClickUp, Linear ou quadro compartilhado. Isso é anotação de quem orquestra várias frentes e precisa voltar ao contexto sem reler três canais de Slack.
O problema não é o número de tasks. É que nenhum formato solto sobrevive
a um restart de agente ou a uma semana longe do repo. Toda volta
começa com “me explica o que tínhamos em andamento aqui” — comigo
mesmo ou com o agente. E aí eu copio TODO.md, colo issues, resumo
decisões de ontem. É o equivalente editorial de rebuildar cache do zero
a cada requisição.
Dois backlogs que não se misturam
Em time maduro, existem dois backlogs sobre o mesmo código. O backlog de produto — épicos, features, prioridades que PO ou GP discutem em refinement — é público e coletivo. O backlog de engenharia — dívida técnica, refatorações pendentes, ideias que ainda não viram épico, riscos arquiteturais notados em review — costuma ser canal lateral entre tech leads, quando não mora só na cabeça de quem lidera.
A fronteira não é invenção minha. Martin Fowler formaliza parte no Technical Debt Quadrant: decisão vira registro antes de virar trabalho. Will Larson, em Staff Engineer, trata o “technical roadmap” como artefato separado do product roadmap — trabalho que Staff e Tech Lead carregam mesmo quando nenhum board pede.
O ponto vale além de liderança formal. Todo sênior opera em duas camadas: o que está no quadro da sprint e o que carrega de contexto técnico próprio. Em alguns lugares você tem poder de promover item da sua camada direto para o backlog do projeto. Em outros, leva para PO ou GP numa cerimônia. Em ambos os casos o material precisa estar registrado em lugar que responda a filtros — por projeto, prioridade, horizonte.
E é aí que entra a cadência. Dia, semana, quinzena, mês, trimestre,
ano. Cada horizonte pede uma lente do mesmo backlog: hoje é
status=doing; planejamento mensal é query por priority e tag;
retrospectiva trimestral lê o timeline determinístico de eventos. Sem
filtro estruturado, horizonte colapsa em documento-narrativa que
ninguém relê.
local-backlog é essa segunda camada materializada em disco. Não
compete com Jira ou ClickUp — alimenta o refinement.
Três custos
Antes de escrever o CLI, listei alternativas. Nenhuma é ruim em geral — cada uma falha num eixo específico pro meu caso.
- Custo: mensalidade + latência de contexto-switch pro browser. - Boa UI, ruim como fonte programável para agente. - Silo externo: agente precisa de API key, rate limit, auth. - Backup depende do fornecedor.
- Custo: zero em ferramenta, alto em disciplina. - Sem schema: prioridade vira prefixo
[P1], tag vira emoji, ordenação vira ordem de digitação. - Filtrar por “só bugs em aberto” égrep+ leitura humana. - Export pra LLM é colar o arquivo inteiro.
A terceira opção — SQLite local com CLI próprio — tem o custo de escrever
a ferramenta. E, no meu caso, de aprender Rust no processo. Paga-se uma
vez; depois o arquivo ~/.local-backlog/backlog.db é só dado, versionável
via dotfiles, consultável por SQL quando o CLI não cobrir um relatório
excepcional.
A escolha
local-backlog é um binário único chamado backlog. Todo estado vive em
~/.local-backlog/ (override via LOCAL_BACKLOG_HOME). Nada no repositório,
nada de .local-backlog.db poluindo git status. O vínculo entre pasta e
projeto mora num registry global; backlog init numa pasta nova registra
aquele caminho como tenant.
O Quickstart é literalmente isso:
cd ~/code/my-project
backlog init --yes
backlog add "Refactor auth middleware" \
--type feature --tag security --priority 50
backlog list
backlog list --format json
backlog show 1
backlog done 1
backlog export --format markdown
Todo comando acima resolve o tenant a partir da CWD. Não existe --all-projects
em comandos de dados. Entrar no diretório é escolher o escopo.
Decisões que importam
Tenancy estrita, reforçada por trigger
A ADR-0001 registra a escolha: cada query de dados filtra por
project_id inferido do CWD, e triggers SQL em task_tags, task_links e
parent_id bloqueiam inserts/updates cross-project. Tag #auth em dois
repos não colide nem compartilha registro — tags.(project_id, name) é
único por tenant.
Poderia ter feito só filtro na aplicação. Não fiz. Um WHERE esquecido
silencia tenancy; um trigger falha alto. É defesa em profundidade barata
num projeto que eu mesmo mantenho e onde um vazamento entre repos é
exatamente o tipo de erro que eu cometeria num refactor apressado.
tasks atômica, tudo mais em satélites
A ADR-0002 define tasks com o núcleo mínimo (id, title, status,
priority, type, parent_id, timestamps). O resto — tags, atributos EAV,
links tipados (blocks, relates, duplicates), eventos append-only —
vive em tabelas satélites.
A armadilha: EAV seduz. Dá vontade de jogar tudo em task_attributes(key, value) e nunca mais fazer migration. A regra que escrevi pra mim mesmo é
explícita — chave EAV vira coluna em tasks só quando aparece em ≥80%
das tasks ativas, ou quando vira filtro recorrente. Promoção é decisão
consciente, não automática.
Output contract
A ADR-0004 é a que mais me salvou de retrabalho: dados vão pra
stdout, mensagens (logs, prompts, erros) vão pra stderr, e todo
comando de leitura aceita --format=table|json desde o dia 1. JSON com
envelope { "schema_version": N, "data": ... }. Nenhum println! cru em
subcomando — tudo passa por um helper em src/output.rs.
O trade-off: disciplina chata na primeira semana, pipe confiável pra
sempre. backlog list --format json | jq '.data[] | select(.priority > 40)'
não quebra quando eu ligar --verbose.
IA como consumidor, não como dono
O ponto que justifica o projeto inteiro é backlog export --format json.
Saída estruturada, filtros por status/tag/tipo, ordenação determinística
(priority → updated_at → id). Dois runs contra um banco inalterado
produzem bytes idênticos — então eu posso colocar um snapshot em tests/
e diffar contra ele.
{
"schema_version": 1,
"project": { "id": 1, "name": "proj", "root_path": "...", "archived_at": null },
"tasks": [
{
"id": 42,
"title": "refactor auth middleware",
"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": []
}
]
}
Na prática, o workflow virou: agente começa a sessão com backlog export --format markdown --status todo,doing coladinho no contexto inicial.
Fim de sessão, se a task mudou, o próprio agente roda backlog done ou
backlog edit. A IA consome e atualiza, não reinterpreta. O dono do
estado continua sendo o SQLite.
Antes, cada sessão começava com três ou quatro minutos de “me explica o que temos em andamento” e um parágrafo colado de três fontes diferentes. Hoje começa com um comando, saída determinística, e o agente entra no mesmo estado da sessão anterior. É menos reidratação por prompt, é também menos variância entre o que eu lembro e o que a ferramenta afirma.
O que eu não fiz
- Sync entre máquinas. O plano é copiar
~/.local-backlog/via dotfiles +git. Sem merge de conflitos reais, sem CRDT, sem servidor. Em duas máquinas ativas eu aceito resolver manualmente — em quatro, já seria problema. - UI web. Não existe. Talvez nunca exista. Terminal é o ambiente;
backlog show 42ebacklog events 42cobrem o que eu preciso. - Multi-usuário. O modelo assume um humano. O time tem Jira,
ClickUp ou Linear;
local-backlogé anotação pessoal de quem orquestra, não quadro compartilhado. Adicionarauthor_idseria migration aditiva, mas não há caso de uso.
Sem múltiplas frentes rodando em paralelo, um TODO.md disciplinado
resolve. A ferramenta paga o custo de existir quando a dor de recuperar
contexto já é diária.
Código
Código em github.com/OliveiraCleidson/local-backlog.
Os ADRs canônicos vivem em docs/adr/pt-BR/, com traduções em en e
es-AR no mesmo commit.
Discussão
Este blog não tem comentários. Para debater o conteúdo: