O usuário mais frequente do meu CLI não sou eu

lql é um CLI em Rust para gerenciar issues do Linear. Escrevi porque nenhuma das alternativas existentes funcionava para meu caso de uso real: um agente de IA que gerencia issues de forma autônoma.

Por que tive que escrever meu próprio CLI

O MCP do Linear foi a primeira tentativa. A ideia é elegante: um servidor MCP que expõe a API do Linear diretamente ao agente. Na prática, era lento, instável, e o agente tinha que construir queries GraphQL do zero a cada chamada. Cada chamada era uma oportunidade de inventar um campo que não existe. Desinstalei em duas semanas.

O CLI da comunidade (linear, do schpet) foi a segunda tentativa. Projetado para humanos: menus interativos, seleção com setas, prompts de confirmação. Um agente não consegue navegar menus interativos. Próximo.

O “agente” do Linear. Em março de 2026, o Linear anunciou seu próprio agente com IA. Parece perfeito até você ler as letrinhas miúdas: só funciona dentro da interface web do Linear. Não pode ser chamado do terminal, não tem API, não se integra com nada externo. É um chatbot colado na própria UI. Se seu fluxo de trabalho é “o agente que programa também gerencia as issues”, o agente do Linear não resolve nada.

Design baseado em dados: a primeira análise

Antes de escrever o lql, parseei 165 sessões do Claude Code buscando cada erro ao interagir com o Linear. O resultado: mais de 500 erros, 370 tentativas, e uma estimativa conservadora de 700.000 tokens por mês desperdiçados. --sort esquecido 40 vezes. --state "Todo" em vez de --state unstarted, 12 vezes. --no-interactive ausente e o CLI travado esperando input do teclado, 64 vezes. (Os detalhes completos estão no post original.)

Com esses dados projetei a interface do lql. Não adivinhei o que um agente precisava — medi. Daí saíram as decisões fundamentais: output compacto em TOON (~25 tokens por issue em vez de JSON verboso), aliases para flags que os LLMs confundem (--status--state), normalização de valores (Todounstarted, urgent1), e mensagens de erro que sugerem o comando correto em vez de se limitarem a dizer “flag desconhecida”.

Sem saber, estava aplicando a Lei de Postel. Mas isso descobri depois.

A segunda análise: um mês com lql em produção

O lql está há um mês em produção. O Claude Code o chama entre 30 e 50 vezes por dia — criando issues, atualizando estados, linkando dependências, consultando detalhes. Eu não o chamo nunca. Não me interessa. Não quero gerenciar issues manualmente; para isso tenho um agente. Se algum dia eu tiver que executar lql eu mesmo, algo deu muito errado.

O único usuário do lql é um LLM. Isso muda todo o design.

Então repeti o exercício: parseei os logs de sessão do Claude Code buscando erros ao chamar lql.

MétricaValor
Sessões analisadas200
chamadas de lql1.324
Erros (is_error: true)210
Taxa de erro15,9%

Os 15,9% não incluem chamadas canceladas por paralelismo (quando um tool call falha e o Claude cancela os demais desse batch). Só erros reais do CLI.

Classificação de erros

Nem todos os erros são iguais. Alguns revelam convenções que faltam; outros revelam operações que deveriam existir.

ErroFrequênciaExemplo real
Label não encontrada20--label tokamak (não existe nesse team)
--title como flag em create8lql create --title "Epic: ..." --team PROD
show/get em vez de view6lql show PROD-911
Args de relate em ordem incorreta12lql relate PROD-834 PROD-833 blocked-by
update --team (mover issue)15+ tentativaslql update PRIV-32 --team PROD
relates em vez de related2lql relate PROD-912 relates PROD-910
--body em comment1lql comment PROD-926 --body "texto"
--comments em view1lql view PROD-824 --comments

O resto eram erros da API do Linear, problemas de autenticação com 1Password, ou erros de shell (quoting quebrado em heredocs longos).

O que os erros revelam

Os LLMs não leem --help. Adivinham por intuição semântica.

Quando um desenvolvedor não sabe como funciona um comando, executa lql view --help. Quando o Claude não sabe, adivinha. E adivinha bem 84% do tempo — mas os 16% restantes revelam seus vieses.

lql show é mais intuitivo que lql view. A maioria das ferramentas usa show: kubectl get, docker inspect, git show. O Claude não consulta a documentação do lql para escolher o verbo. Usa o que lhe parece mais natural dados os milhares de CLIs que viu em seus dados de treinamento.

A solução não é documentar melhor. É aceitar o sinônimo:

#[command(alias = "show", alias = "get")]
View(ViewOpts),

Uma linha. Seis erros eliminados.

Os agentes preferem flags nomeadas a argumentos posicionais

Em lql create, o título é um argumento posicional:

lql create "Meu título" --team PROD

O Claude, em 8 ocasiões, escreveu assim:

lql create --title "Meu título" --team PROD

--title não existia como flag. Para um humano isso é óbvio — você lê o --help e vê que <TITLE> é posicional. Para um LLM, as flags nomeadas são mais seguras porque não dependem da posição.

A correção: aceitar ambos.

pub struct CreateOpts {
    pub title: Option<String>,

    #[arg(long = "title", hide = true)]
    pub title_flag: Option<String>,
    // ...
}

A flag --title está oculta no --help (os humanos não precisam), mas funciona.

Se você pode detectar o erro, corrija em vez de rejeitar

O caso mais revelador. lql relate espera três argumentos posicionais em ordem estrita:

lql relate <FROM> <RELATION_TYPE> <TO>

O Claude escreveu isso 12 vezes:

lql relate PROD-834 PROD-833 blocked-by

A ordem natural para um LLM é FROM TO TYPE — “relacione isso com isso, desta forma”. A ordem do CLI é FROM TYPE TO — “daqui, tipo de relação, para lá”.

A filosofia POSIX diz: rejeite a entrada incorreta com um erro descritivo. A filosofia AX diz: se você pode detectar que o segundo argumento é um ID de issue e o terceiro é um tipo de relação, reordene-os automaticamente.

pub fn normalize_args(args: &[String]) -> Option<Vec<String>> {
    if args.len() < 5 { return None; }
    if args[1] == "relate"
        && looks_like_issue_id(&args[2])
        && looks_like_issue_id(&args[3])
        && !looks_like_issue_id(&args[4])
    {
        let mut fixed = args.to_vec();
        fixed.swap(3, 4);
        eprintln!(
            "ℹ Reordenado: relate {} {} {} → relate {} {} {}",
            args[2], args[3], args[4], fixed[2], fixed[3], fixed[4]
        );
        return Some(fixed);
    }
    None
}

A detecção é determinística: um ID de issue tem o formato TEAM-123 (maiúsculas, hífen, número). Um tipo de relação não. Não há ambiguidade possível.

É emitida uma nota para stderr (ℹ Reordenado: ...) para que fique registro da correção. Se alguma vez a heurística falhar, o usuário pode rastrear o que aconteceu.

Se uma operação é tentada repetidamente, deveria existir

O agente tentou lql update PRIV-32 --team PROD mais de 15 vezes ao longo de múltiplas sessões. Mover uma issue de um team para outro é uma operação legítima do Linear que o lql simplesmente não implementava.

Não era um erro de interface. Era uma feature que faltava. Os dados tornaram isso visível.

Adicionar --team ao update exigiu 3 linhas no parser do clap e uma chamada adicional a meta.find_team() na lógica de atualização. A API do Linear já suportava teamId na mutação issueUpdate.

O que NÃO se corrige com tolerância

É preciso ser honesto sobre os limites. Dos 210 erros:

  • 20 eram labels que não existem. O Claude inventava labels como tokamak ou improvement que não estão nesse team do Linear. Isso não se corrige com alias — exige que o agente consulte os labels disponíveis antes de criar. O lql já retorna sugestões fuzzy (“Closest: …”), mas o Claude nem sempre tenta novamente.

  • 18 eram erros da API (labels de team incorreto, queries GraphQL inválidas em raw). Esses são erros do agente, não do CLI.

  • 7 eram de autenticação (1Password fora do ar ou sessão expirada). Infraestrutura, não interface.

A tolerância de interface cobre talvez 60% dos erros. O resto exige que o agente seja mais disciplinado ou que a ferramenta valide mais coisas antes de enviar para a API.

Lei de Postel aplicada a CLIs

Jon Postel escreveu em 1980: “Be conservative in what you send, be liberal in what you accept” (RFC 761). É o princípio de robustez do TCP. Todo protocolo de internet que funciona o aplica.

Ninguém o aplica a CLIs. A ortodoxia POSIX é o contrário: rejeite qualquer input que não coincida exatamente com a especificação, retorne um erro descritivo, e deixe o usuário corrigir. Quando seu usuário é um humano que lê o erro e ajusta, funciona. Quando seu usuário é um LLM que tenta novamente com uma variação aleatória, é uma perda de tempo e tokens.

Experiência Agêntica é a Lei de Postel aplicada a argumentos de CLI. Não é uma ideia nova. É um princípio de 1980 que nunca foi aplicado neste contexto.

Dos dados emergem cinco regras concretas:

  1. Aceite sinônimos naturais. Se o verbo existe em CLIs populares (show, get, display), aceite-o como alias. Não custa nada e elimina erros de vocabulário.

  2. Aceite flags nomeadas além de posicionais. Os LLMs preferem --title "X" a colocar "X" na posição correta. Oculte-as no --help se não quiser confundir os humanos.

  3. Reordene antes de rejeitar. Se os argumentos são de tipos distinguíveis (ID de issue vs. enum string), a ordem incorreta pode ser detectada e corrigida automaticamente.

  4. Normalize variantes próximas. relatesrelated, blockedbyblocked-by. A distância de edição é 1. O custo de aceitar é zero. O custo de rejeitar é um erro e uma nova tentativa.

  5. Se é tentado >3 vezes, provavelmente deveria existir. Os logs de sessão são uma mina de dados sobre features que faltam. Um agente não insiste em uma operação absurda 15 vezes. Se insiste, a operação faz sentido e a ferramenta não a suporta.

O meta-ângulo

Usei o Claude para parsear os logs do Claude, classificar os erros do Claude ao usar minha ferramenta, e então implementar os fixes. A ferramenta se adapta ao seu usuário mais frequente com dados desse mesmo usuário.

Todo o código é público. O commit com as correções é 34f1f08. Os dados podem ser reproduzidos parseando os JSONL em ~/.claude/projects/.

Como fazer você mesmo

  1. Parse os logs. Os arquivos JSONL do Claude Code estão em ~/.claude/projects/<project>/. Cada tool_result com is_error: true é um dado. O formato é o mesmo para qualquer ferramenta, não só para CLIs.

  2. Classifique antes de corrigir. Nem todos os erros são iguais. Separe erros de interface (o CLI rejeita input válido) de erros de lógica (o agente pede algo absurdo). Só os primeiros se corrigem com tolerância.

  3. Meça depois. A taxa de erro antes das mudanças foi 15,9%. Da próxima vez que analisar, saberei se baixou. Sem a primeira medição, não há baseline.

O lql se instala com brew install frr149/tools/lql. O repo está em github.com/frr149/lql.

Este artigo foi escrito originalmente em espanhol e traduzido com a ajuda de IA.