Trabalhando com Azure DevOps usando pharos
pharos é um CLI que oferece o Azure DevOps a partir de um shell, autenticado por uma
variável de ambiente em vez de um login no navegador.
Tudo o que ele pode fazer está listado abaixo. Não execute pharos --help para descobrir
— isso custa várias chamadas e esta seção tem a mesma informação.
whoami a quem o ADO_PAT pertence. @Me resolve para isso
types quais --state e --type serão ACEITOS, por tipo
query QUAIS itens de trabalho — atribuídos a você, em uma sprint,
de um tipo, ainda abertos. Itens hidratados, não ids.
task <id> um item de trabalho, completo: campos, comentários,
anexos, relações COM títulos, e o conteúdo + discussão
de cada página wiki vinculada
--children uma EPIC e tudo abaixo dela, em
UMA chamada. --depth <n> para mais profundo (máx 5)
--compact achata identidades, remove chaves de quadro
create <type> --title "…" um item de trabalho. --parent vai no MESMO patch
update <id> alterar um campo: --state --priority --assignee
--title, ou --field Nome=valor para qualquer outra coisa
link <id> --parent <id> relacionar dois itens. Também --child --related
unlink <id> --parent <id> --predecessor --successor --duplicate
link <id> --wiki-page <caminho> vincular uma PÁGINA WIKI a um item de trabalho — o lado do item
wiki links <caminho> quais itens de trabalho vinculam a uma página (ADO não tem API)
wiki link <caminho> --item <id> o lado WIKI; --item repetível
wiki unlink <caminho> --item <id>
history <id> o que MUDOU, campo por campo, quem e quando
iterations | areas as sprints com suas datas, e as áreas
links todo tipo de relação que esta ORG tem, e qual
--flag o alcança (seis de ~dezoito)
fields [--type T] o que um campo vai ACEITAR — valores permitidos
attach <id> <arquivo> colocar um ARQUIVO em um item de trabalho (lista de Anexos)
detach <id> <url-ou-guid> remover um. --yes. Ou --name <arquivo>
download <id-do-item|guid|url> ler um de volta. Um ID DE ITEM DE TRABALHO com --name <arquivo>,
ou --all --out <dir>. --out ou nada é escrito
image <arquivo> enviar uma IMAGEM para uso dentro de texto. Imprime o
markdown para colar. NÃO é o mesmo que attach
people [consulta] quem pode ser @mencionado, com a forma @<guid>
delete <id> --yes → Lixeira (sem exclusão permanente, de propósito)
restore <id> trazer de volta
deleted o que está na Lixeira, com nomes.
--top <n> — retorna 50 por padrão e relata
o total verdadeiro como "count"
wiki list | tree | read <caminho> | write <caminho> | delete <caminho>
wiki move <caminho> <novo caminho> mover uma página; subpáginas vêm junto
wiki rename <caminho> <novo nome> a mesma chamada, apenas folha. AMBAS precisam de --yes
wiki duplicate <caminho> [para] um verbo que o Azure DevOps não tem. "<caminho> - Cópia N"
wiki image <arquivo> uma imagem para uma PÁGINA ou comentário de página. Endpoint
diferente de `image`, e o nome é único
wiki import <arquivo...> .md .txt .docx .pdf .rtf .html -> páginas. Um
.pptx é recusado, de propósito — veja abaixo
--under <caminho> --as <nome>
comment list | add | edit | delete <alvo> é um id de item de trabalho OU um caminho wiki
comment react | unreact | reactors like dislike heart hooray smile confused
hooks list | check | create | repoint | delete ganchos de serviço em tempo real.
`check` precisa de --hub <url>; `list` mostra a URL
já em uso
plan <arquivo> um plano de implementação → uma árvore de itens de trabalho
setup org, projeto, token → chaveiro + perfil do shell
Entrada de texto: --text / --file / --stdin. Global: --pretty para humanos,
--yes para verbos destrutivos, --dry-run para pré-visualizar.
ADO_ORG, ADO_PROJECT e ADO_PAT já estão no ambiente após a
configuração. Não os verifique antes de trabalhar; uma falta se anuncia como
"kind": "config" no código de saída 2, que é a única vez que importa.
Encontrando o trabalho: pharos query
Não recorra a curl e ao endpoint WIQL. Este skill costumava fornecer uma
receita exata para isso, porque não havia um verbo de consulta. Agora existe um, e
ele faz a parte que a receita não conseguia: WIQL retorna apenas ids, então a receita
te dava dezesseis números crus e uma chamada por item para fazê-los significar algo.
pharos query --mine # atribuído a você, ainda aberto
pharos query --sprint "Sprint 1" # --sprint current para @currentIteration
pharos query --type Epic --state Doing # ambos repetíveis
pharos query --assignee "[email protected]"
pharos query --tag api --all # --all inclui trabalho concluído
pharos query --wiql "SELECT [System.Id] FROM WorkItems WHERE …" # escape hatch
Flags são combinadas com AND. A saída são itens hidratados — id, tipo, título, estado,
responsável, iteração, tags, prioridade, alterado — então --mine é um comando, não uma consulta
seguida de uma busca por resultado. Siga com pharos task <id> apenas para os
poucos que você realmente vai trabalhar.
Leia o campo openness antes de reportar uma contagem. "12 abertos" é
sem sentido até que você saiba o que foi contado como concluído, e o Azure DevOps permite que um
template de processo renomeie cada estado. query lê as categorias de estado do próprio projeto
e informa quais estados tratou como terminais; se não puder lê-los, avisa
isso e recorre a um palpite, e esse é o seu sinal para passar
--state explicitamente. assignedTo nomeia para quem @Me realmente resolveu — um
token compartilhado ou de serviço faz com que "atribuído a mim" signifique silenciosamente outra pessoa.
--all compõe com todos os outros filtros, então --mine --all é como você sabe
se nada está atribuído a você de nada aberto. Um simples pharos query --all é
válido e retorna todo o projeto.
--sprint aceita tanto o nome simples quanto o fieldPath e normaliza entre
eles — a regra do fieldPath é sobre System.IterationPath como um valor de campo. Mas
uma sprint nomeada corresponde com UNDER (sub-iterações incluídas) enquanto --sprint current corresponde com = (exato), então não são a mesma consulta.
--wiql também aceita uma consulta FROM WorkItemLinks … MODE (MustContain) e
hidrata os ids da mesma forma, que é como você encontra cada item carregando um determinado
tipo de relação em uma única chamada.
NÃO escreva [System.State] NOT IN GROUP 'Completed' se você recorrer a --wiql.
Ele analisa, retorna 200 e corresponde a tudo — IN GROUP cobre apenas categorias de tipo de item de
trabalho, e um grupo desconhecido resolve para o conjunto vazio sem erro.
Medido, 2026-08-05.
Alterando um item de trabalho
pharos update 225 --state Doing # mover o estado quando o trabalho se move
pharos update 225 --priority 1 --assignee "[email protected]"
pharos update 225 --field Microsoft.VSTS.Scheduling.RemainingWork=3
pharos attach 225 ./bestsellers.xlsx --comment "Os números"
update lê o item e aplica a alteração sob uma operação test em /rev,
então alguém que escreveu entre sua leitura e sua escrita recebe um conflict em vez de perder sua edição.
Definir um valor que já existe não escreve nada e informa isso — um PATCH inútil
ainda incrementa System.Rev e invalida todas as outras revisões em cache
no item. --dry-run mostra o antes → depois e não escreve nada. Não há
--yes: uma edição de campo é uma edição comum e o Azure DevOps mantém todas as revisões.
attach envia os bytes e então os vincula como uma relação AttachedFile —
duas chamadas, um comando. Anexos são imutáveis: anexar o mesmo arquivo
duas vezes cria dois deles, e não há substituição nem versionamento. detach
remove um; precisa de --yes, encontra o anexo por identidade em vez de por posição,
e deixa os bytes no Azure DevOps, então reanexar a url o coloca de volta. Nomeie o
arquivo com --name <arquivo> ou passe o GUID — e como com
download, dois arquivos compartilhando um nome são RECUSADOS em vez de remover o que
vier primeiro.
create coloca --parent no mesmo patch que os campos, então um filho nunca é
brevemente um órfão, e aceita os mesmos nomes de flag que update.
unlink recusa em vez de adivinhar, e isso é bom saber antes de ver.
O Azure DevOps remove uma relação pela sua POSIÇÃO no array, então um
índice de uma leitura desatualizada corta um link diferente e a solicitação ainda é bem-sucedida.
unlink encontra a relação por identidade e a remove sob uma operação test na
revisão que leu. Se a relação não estiver lá, você recebe o código de saída 3 — isso significa
que nada aconteceu, não que a chamada falhou.
delete move para a Lixeira e precisa de --yes; a recusa cita o título
primeiro. restore <id> traz de volta e não precisa de flag.
Não há exclusão permanente aqui, deliberadamente. É o único verbo irreversível
que o Azure DevOps tem, e --yes é uma flag que você aprendeu a passar. A interface web
é a dona dela. Se alguém realmente precisar purgar, envie-o para lá em vez de procurar
uma flag.
Imagens em texto e arquivos ao lado
Estas são duas coisas diferentes e escolher a errada é o erro que vale a pena evitar.
pharos attach 225 ./bestsellers.xlsx # um ARQUIVO, na lista de Anexos
pharos image ./screenshot.png # uma IMAGEM, para colocar dentro do texto
Um anexo cria uma relação AttachedFile e aparece na lista de Anexos do
item de trabalho. Uma imagem inline não cria relação alguma —
medido no #333, duas capturas de tela coladas em uma descrição deram relations: 0
e attachments: []. É um  no texto do campo e nada mais,
que é por que a lista de Anexos está certa em não mostrar nada para isso.
Então image imprime uma linha markdown e deixa a escrita para você:
pharos image ./chart.png --pretty
# 
pharos update 225 --field System.Description="$(cat <<'EOF'
A receita aumentou. Veja o gráfico:

EOF
)"
Uma imagem wiki é um endpoint diferente e precisa de wiki image. Uma wiki é um
repositório git, então um anexo lá é um ARQUIVO e seu NOME é sua identidade:
envie uma segunda image.png e ela cai sobre a primeira, e toda página apontando
para /.attachments/image.png muda silenciosamente de imagem. wiki image torna o nome
único antes de enviar e imprime um link relativo ao repositório:
pharos wiki image ./diagram.png --pretty
# 
Use esse caminho, não uma url absoluta — uma página vinculando a uma url wit/attachments
renderiza para qualquer um com uma sessão e quebra para todos os outros. O mesmo
markdown funciona em uma página e em um comentário de página.
Obtendo um anexo de um item de trabalho
pharos task <id> lista o que está anexado, com o GUID de cada arquivo —
no JSON e em --pretty. Então pegue-o em uma chamada, pelo nome:
pharos download 41 --name Skills.pptx --out ./Skills.pptx
pharos download 41 --all --out ./attachments # todos os anexos do item
pharos download <guid-or-url> --out ./file.bin # quando você já tem um
--out é o que escreve. Sem ele você recebe o tamanho e nenhum arquivo —
bytes brutos no stdout corromperiam o JSON que todo outro verbo imprime.
Um simples pharos download 41 é um erro de uso carregando a lista — nome, GUID
e tamanho — então escolher o arquivo certo nunca custa uma segunda chamada. Dois anexos
com o mesmo nome recusam em vez de adivinhar: anexos são imutáveis, então
anexar um arquivo duas vezes cria dois deles e o nome não é uma identidade.
detach aceita o mesmo --name <arquivo>, então o verbo que você usa depois de ler
task --pretty aceita o que ele mostrou:
pharos detach 41 --name Skills.pptx --yes # --dry-run pré-visualiza primeiro
Ler o que está dentro é SEU trabalho, com SUAS ferramentas
download coloca bytes no disco e para por aí. Transformar um .docx, .pptx,
.xlsx ou .pdf em algo que você pode ler é trabalho do seu ambiente.
Não use pharos-convert para isso. É o conversor de wiki import, compartilhado
com o aplicativo macOS para que ambos produzam a mesma página a partir do mesmo arquivo —
sua saída é moldada para se tornar uma página wiki, e recorrer a ela aqui acopla o que
você lê ao caminho de importação do aplicativo. Ele também não consegue ler .pptx.
Duas maneiras pelas quais isso falha silenciosamente, ambas medidas em um item de trabalho real:
- Você verificou a ferramenta no lugar errado.
which markitdowncontra o PATH do sistema eimport docxcontra opython3do sistema ambos retornam vazios em uma máquina onde essa ferramenta está instalada — em um venv por skill. Um agente que executa essas duas verificações conclui "nada aqui" e contorna ferramentas que estavam lá o tempo todo. Olhe onde seu agente mantém suas ferramentas antes de concluir que estão ausentes. - Texto não vazio não é prova de que você leu o documento. Um
.pptxreal extraiu para sete fragmentos, cerca de 90 caracteres; a especificação inteira estava em duas PNGs embutidas. Todo leitor somente texto retorna algo para essa apresentação e parece que funcionou. Para uma apresentação de slides, ou um PDF escaneado, o payload geralmente são as imagens — extraia-as e realmente olhe para elas.
Primeiro pergunte se você lê PDFs nativamente. Se sim, isso é um comando.
Muitos agentes — Claude Code entre eles — leem um PDF visualmente, página por página, da mesma forma que uma pessoa olha para ele. Isso cobre um escaneamento sem texto nenhum, sem etapa de OCR. Se esse é o seu caso, todo o problema se reduz a uma conversão:
soffice --headless --convert-to pdf f.docx --outdir ./out # .pptx, .xlsx também
# …

