El usuario más frecuente de mi CLI no soy yo
lql es una CLI en Rust para gestionar issues de Linear. La escribí porque ninguna de las alternativas existentes funcionaba para mi caso de uso real: un agente de IA que gestiona issues de forma autónoma.
Por qué tuve que escribir mi propia CLI
El MCP de Linear fue el primer intento. La idea es elegante: un servidor MCP que expone la API de Linear directamente al agente. En la práctica, era lento, inestable, y el agente tenía que construir queries GraphQL desde cero en cada llamada. Cada llamada era una oportunidad de inventarse un campo que no existe. Lo desinstalé a las dos semanas.
La CLI de la comunidad (linear, de schpet) fue el segundo intento. Diseñada para humanos: menús interactivos, selección con flechas, prompts de confirmación. Un agente no puede navegar menús interactivos. Next.
El «agente» de Linear. En marzo de 2026, Linear anunció su propio agente con IA. Suena perfecto hasta que lees la letra pequeña: solo funciona dentro de la interfaz web de Linear. No se puede llamar desde terminal, no tiene API, no se integra con nada externo. Es un chatbot pegado a su propia UI. Si tu flujo de trabajo es «el agente que programa también gestiona las issues», el agente de Linear no resuelve nada.
Diseño basado en datos: el primer análisis
Antes de escribir lql, parseé 165 sesiones de Claude Code buscando cada error al interactuar con Linear. El resultado: más de 500 errores, 370 reintentos, y una estimación conservadora de 700.000 tokens al mes desperdiciados. --sort olvidado 40 veces. --state "Todo" en vez de --state unstarted, 12 veces. --no-interactive ausente y la CLI colgada esperando input de teclado, 64 veces. (Los detalles completos están en el post original.)
Con esos datos diseñé la interfaz de lql. No adiviné qué necesitaba un agente — lo medí. De ahí salieron las decisiones fundacionales: output compacto en TOON (~25 tokens por issue en vez de JSON verbose), aliases para flags que los LLMs confunden (--status → --state), normalización de valores (Todo → unstarted, urgent → 1), y mensajes de error que sugieren el comando correcto en vez de limitarse a decir «flag desconocido».
Sin saberlo, estaba aplicando la Ley de Postel. Pero eso lo descubrí después.
El segundo análisis: un mes con lql en producción
lql lleva un mes en producción. Claude Code la llama entre 30 y 50 veces al día — creando issues, actualizando estados, enlazando dependencias, consultando detalles. Yo no la llamo nunca. No me interesa. No quiero gestionar issues a mano; para eso tengo un agente. Si algún día tengo que ejecutar lql yo mismo, algo ha ido muy mal.
El único usuario de lql es un LLM. Eso cambia todo el diseño.
Así que repetí el ejercicio: parseé los logs de sesión de Claude Code buscando errores al llamar a lql.
| Métrica | Valor |
|---|---|
| Sesiones analizadas | 200 |
llamadas de lql |
1.324 |
Errores (is_error: true) |
210 |
| Tasa de error | 15,9% |
El 15,9% no incluye llamadas canceladas por paralelismo (cuando un tool call falla y Claude cancela los demás de ese batch). Solo errores reales de la CLI.
Clasificación de errores
No todos los errores son iguales. Algunos revelan convenciones que faltan; otros revelan operaciones que deberían existir.
| Error | Frecuencia | Ejemplo real |
|---|---|---|
| Label no encontrada | 20 | --label tokamak (no existe en ese team) |
--title como flag en create |
8 | lql create --title "Epic: ..." --team PROD |
show/get en vez de view |
6 | lql show PROD-911 |
Args de relate en orden incorrecto |
12 | lql relate PROD-834 PROD-833 blocked-by |
update --team (mover issue) |
15+ intentos | lql update PRIV-32 --team PROD |
relates en vez de related |
2 | lql relate PROD-912 relates PROD-910 |
--body en comment |
1 | lql comment PROD-926 --body "texto" |
--comments en view |
1 | lql view PROD-824 --comments |
El resto eran errores de la API de Linear, problemas de autenticación con 1Password, o errores de shell (quoting roto en heredocs largos).
Qué revelan los errores
Los LLMs no leen --help. Adivinan por intuición semántica.
Cuando un desarrollador no sabe cómo funciona un comando, ejecuta lql view --help. Cuando Claude no sabe, adivina. Y adivina bien el 84% del tiempo — pero el 16% restante revela sus sesgos.
lql show es más intuitivo que lql view. La mayoría de herramientas usan show: kubectl get, docker inspect, git show. Claude no consulta la documentación de lql para elegir el verbo. Usa el que le parece más natural dados los miles de CLIs que ha visto en su training data.
La solución no es documentar mejor. Es aceptar el sinónimo:
#[command(alias = "show", alias = "get")]
View(ViewOpts),
Una línea. Seis errores eliminados.
Los agentes prefieren flags nombrados a argumentos posicionales
En lql create, el título es un argumento posicional:
lql create "Mi título" --team PROD
Claude, en 8 ocasiones, lo escribió así:
lql create --title "Mi título" --team PROD
--title no existía como flag. Para un humano esto es obvio — lees el --help y ves que <TITLE> es posicional. Para un LLM, los flags nombrados son más seguros porque no dependen de la posición.
La corrección: aceptar ambos.
pub struct CreateOpts {
pub title: Option<String>,
#[arg(long = "title", hide = true)]
pub title_flag: Option<String>,
// ...
}
El flag --title está oculto en --help (los humanos no lo necesitan), pero funciona.
Si puedes detectar el error, corrígelo en vez de rechazarlo
El caso más revelador. lql relate espera tres argumentos posicionales en orden estricto:
lql relate <FROM> <RELATION_TYPE> <TO>
Claude escribió esto 12 veces:
lql relate PROD-834 PROD-833 blocked-by
El orden natural para un LLM es FROM TO TYPE — «relaciona esto con esto, de esta forma». El orden de la CLI es FROM TYPE TO — «desde aquí, tipo de relación, hacia allí».
La filosofía POSIX dice: rechaza la entrada incorrecta con un error descriptivo. La filosofía AX dice: si puedes detectar que el segundo argumento es un issue ID y el tercero es un tipo de relación, reordénalos automáticamente.
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!(
"ℹ Reordered: relate {} {} {} → relate {} {} {}",
args[2], args[3], args[4], fixed[2], fixed[3], fixed[4]
);
return Some(fixed);
}
None
}
La detección es determinista: un issue ID tiene el formato TEAM-123 (mayúsculas, guión, número). Un tipo de relación no. No hay ambigüedad posible.
Se emite una nota a stderr (ℹ Reordered: ...) para que quede registro de la corrección. Si alguna vez la heurística falla, el usuario puede rastrear qué pasó.
Si una operación se intenta repetidamente, debería existir
El agente intentó lql update PRIV-32 --team PROD más de 15 veces a lo largo de múltiples sesiones. Mover una issue de un team a otro es una operación legítima de Linear que lql simplemente no implementaba.
No era un error de interfaz. Era una feature que faltaba. Los datos lo hicieron visible.
Añadir --team a update requirió 3 líneas en el parser de clap y una llamada adicional a meta.find_team() en la lógica de actualización. La API de Linear ya soportaba teamId en la mutación issueUpdate.
Lo que NO se arregla con tolerancia
Hay que ser honesto sobre los límites. De los 210 errores:
-
20 eran labels que no existen. Claude inventaba labels como
tokamakoimprovementque no están en ese team de Linear. Esto no se arregla con alias — requiere que el agente consulte los labels disponibles antes de crear.lqlya devuelve sugerencias fuzzy («Closest: …»), pero Claude no siempre reintenta. -
18 eran errores de la API (labels de team incorrecto, queries GraphQL inválidas en
raw). Estos son errores del agente, no de la CLI. -
7 eran de autenticación (1Password caído o sesión expirada). Infraestructura, no interfaz.
La tolerancia de interfaz cubre quizá un 60% de los errores. El resto requiere que el agente sea más disciplinado o que la herramienta valide más cosas antes de enviar a la API.
Postel’s Law aplicada a CLIs
Jon Postel escribió en 1980: «Be conservative in what you send, be liberal in what you accept» (RFC 761). Es el principio de robustez de TCP. Todo protocolo de internet que funciona lo aplica.
Nadie lo aplica a CLIs. La ortodoxia POSIX es lo contrario: rechaza cualquier input que no coincida exactamente con la especificación, devuelve un error descriptivo, y deja que el usuario corrija. Cuando tu usuario es un humano que lee el error y ajusta, funciona. Cuando tu usuario es un LLM que reintenta con una variación aleatoria, es una pérdida de tiempo y tokens.
Agentic Experience es Postel’s Law aplicada a argumentos de CLI. No es una idea nueva. Es un principio de 1980 que nunca se aplicó a este contexto.
De los datos emergen cinco reglas concretas:
-
Acepta sinónimos naturales. Si el verbo existe en CLIs populares (
show,get,display), acéptalo como alias. No cuesta nada y elimina errores de vocabulario. -
Acepta flags nombrados además de posicionales. Los LLMs prefieren
--title "X"a poner"X"en la posición correcta. Ocúltalos en--helpsi no quieres confundir a los humanos. -
Reordena antes que rechaza. Si los argumentos son de tipos distinguibles (issue ID vs. enum string), el orden incorrecto se puede detectar y corregir automáticamente.
-
Normaliza variantes cercanas.
relates→related,blockedby→blocked-by. La distancia edit es 1. El coste de aceptarlo es cero. El coste de rechazarlo es un error y un reintento. -
Si se intenta >3 veces, probablemente debería existir. Los logs de sesión son una mina de datos sobre features que faltan. Un agente no insiste en una operación absurda 15 veces. Si insiste, la operación tiene sentido y la herramienta no la soporta.
El meta-ángulo
Usé Claude para parsear los logs de Claude, clasificar los errores de Claude al usar mi herramienta, y luego implementar los fixes. La herramienta se adapta a su usuario más frecuente con datos de ese mismo usuario.
Todo el código es público. El commit con las correcciones es 34f1f08. Los datos se pueden reproducir parseando los JSONL en ~/.claude/projects/.
Cómo hacerlo tú
-
Parsea los logs. Los ficheros JSONL de Claude Code están en
~/.claude/projects/<project>/. Cadatool_resultconis_error: truees un dato. El formato es el mismo para cualquier herramienta, no solo para CLIs. -
Clasifica antes de arreglar. No todos los errores son iguales. Separa errores de interfaz (la CLI rechaza input válido) de errores de lógica (el agente pide algo absurdo). Solo los primeros se arreglan con tolerancia.
-
Mide después. El error rate antes de los cambios fue 15,9%. La próxima vez que analice, sabré si bajó. Sin la primera medición, no hay baseline.
lql se instala con brew install frr149/tools/lql. El repo está en github.com/frr149/lql.
Read this article in English.



