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.

· 7 min de leitura

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.

Jira / ClickUp / Linear / Notion SaaS
  • 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.
TODO.md por repo Markdown
  • 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_atid). 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 42 e backlog events 42 cobrem 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. Adicionar author_id seria 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: