Travailler avec Azure DevOps via pharos
pharos est une CLI qui vous donne accès à Azure DevOps depuis un terminal, authentifiée par une variable d'environnement plutôt que par une connexion navigateur.
Tout ce qu'elle peut faire est listé ci-dessous. N'exécutez pas pharos --help pour le découvrir — cela coûte plusieurs appels et cette section contient les mêmes informations.
whoami à qui appartient ADO_PAT. @Me résout cette personne
types ce que --state et --type ACCEPTENT, par type
query QUELS éléments de travail — qui vous sont assignés, dans un sprint,
d'un type, encore ouverts. Éléments hydratés, pas des identifiants.
task <id> un élément de travail complet : champs, commentaires,
pièces jointes, relations AVEC titres, et le
contenu + discussion de chaque page wiki liée
--children un EPIC et tout ce qu'il contient, en
UN SEUL appel. --depth <n> pour plus profond (max 5)
--compact aplatir les identités, supprimer les clés de tableau
create <type> --title "…" un élément de travail. --parent va dans le MÊME correctif
update <id> modifier un champ : --state --priority --assignee
--title, ou --field Name=valeur pour autre chose
link <id> --parent <id> relier deux éléments. Aussi --child --related
unlink <id> --parent <id> --predecessor --successor --duplicate
link <id> --wiki-page <path> lier une PAGE WIKI à un élément de travail — le côté élément
wiki links <path> quels éléments de travail lient une page (ADO n'a pas d'API)
wiki link <path> --item <id> le côté WIKI ; --item répétable
wiki unlink <path> --item <id>
history <id> ce qui a CHANGÉ, champ par champ, qui et quand
iterations | areas les sprints avec leurs dates, et les zones
links chaque type de relation que cette ORGA possède, et quel
--flag y accède (six sur ~dix-huit)
fields [--type T] ce qu'un champ ACCEPTE — valeurs autorisées
attach <id> <file> mettre un FICHIER sur un élément de travail (liste Pièces jointes)
detach <id> <url-ou-guid> en retirer un. --yes. Ou --name <fichier>
download <id-élément|guid|url> lire un fichier. Un ID D'ÉLÉMENT DE TRAVAIL avec --name <fichier>,
ou --all --out <rép>. --out ou rien, c'est écrit
image <file> télécharger une IMAGE pour utilisation dans le texte. Imprime le
markdown à coller. PAS la même chose que attach
people [query] qui peut être @mentionné, avec le format @<guid>
delete <id> --yes → Corbeille (pas de suppression définitive, intentionnellement)
restore <id> ramener un élément
deleted ce qu'il y a dans la Corbeille, avec les noms.
--top <n> — elle renvoie 50 par défaut et signale
le total réel comme "count"
wiki list | tree | read <path> | write <path> | delete <path>
wiki move <path> <new path> déplacer une page ; les sous-pages suivent
wiki rename <path> <new name> le même appel, feuille seulement. LES DEUX ont besoin de --yes
wiki duplicate <path> [to] un verbe que Azure DevOps n'a pas. "<path> - Copie N"
wiki image <file> une image pour une PAGE ou un commentaire de page. Point de terminaison
différent de `image`, et le nom est rendu unique
wiki import <file...> .md .txt .docx .pdf .rtf .html -> pages. Un
fichier .pptx est refusé, intentionnellement — voir ci-dessous
--under <path> --as <name>
comment list | add | edit | delete <cible> est un id d'élément de travail OU un chemin wiki
comment react | unreact | reactors like dislike heart hooray smile confused
hooks list | check | create | repoint | delete webhooks en temps réel.
`check` nécessite --hub <url> ; `list` montre l'URL
déjà utilisée
plan <file> un plan d'implémentation → une arborescence d'éléments de travail
setup org, projet, token → trousseau + profil shell
Saisie de texte : --text / --file / --stdin. Global : --pretty pour un humain,
--yes pour les verbes destructifs, --dry-run pour prévisualiser.
ADO_ORG, ADO_PROJECT et ADO_PAT sont déjà dans l'environnement après
la configuration. Ne les vérifiez pas avant de travailler ; une absence se signale
comme "kind": "config" avec code de sortie 2, c'est le seul moment où cela compte.
Trouver le travail : pharos query
N'utilisez pas curl et le point de terminaison WIQL. Cette compétence vous donnait auparavant une
recette pour exactement cela, car il n'y avait pas de verbe de requête. Il y en a un maintenant,
et il fait ce que la recette ne pouvait pas faire : WIQL renvoie uniquement des identifiants, donc la recette
vous donnait seize numéros bruts et un appel par élément pour leur donner un sens.
pharos query --mine # assigné à vous, toujours ouvert
pharos query --sprint "Sprint 1" # --sprint current pour @currentIteration
pharos query --type Epic --state Doing # tous deux répétables
pharos query --assignee "[email protected]"
pharos query --tag api --all # --all inclut le travail terminé
pharos query --wiql "SELECT [System.Id] FROM WorkItems WHERE …" # échappatoire
Les drapeaux se combinent (ET). La sortie est des éléments hydratés — id, type, titre, état, assigné,
itération, tags, priorité, modifié — donc --mine est une seule commande, pas une requête
suivie d'une récupération par résultat. Utilisez pharos task <id> uniquement pour ceux
sur lesquels vous allez réellement travailler.
Lisez le champ openness avant de signaler un décompte. "12 ouverts" n'a pas de sens
tant que vous ne savez pas ce qui a été compté comme terminé, et Azure DevOps permet à un
modèle de processus de renommer chaque état. query lit les catégories d'état du projet
et vous indique quels états il a traités comme terminaux ; s'il n'a pas pu les lire, il le dit
et retombe sur une estimation, et c'est le signal pour passer --state explicitement. assignedTo nomme qui
@Me a réellement résolu — un jeton partagé ou de service signifie que "assigné à moi"
peut silencieusement désigner quelqu'un d'autre.
--all se combine avec tous les autres filtres, donc --mine --all est la façon de distinguer
rien ne vous est assigné de rien n'est ouvert. Un simple pharos query --all est
valide et renvoie l'ensemble du projet.
--sprint accepte soit le nom simple soit le fieldPath et normalise entre eux —
la règle fieldPath concerne System.IterationPath en tant que valeur de champ. Mais
un sprint nommé correspond avec UNDER (sous-itérations incluses) tandis que --sprint current correspond avec = (exact), donc ce ne sont pas les mêmes requêtes.
--wiql accepte également une requête FROM WorkItemLinks … MODE (MustContain) et
hydrate les identifiants de la même manière, ce qui vous permet de trouver chaque élément portant un
type de relation donné en un seul appel.
N'écrivez PAS [System.State] NOT IN GROUP 'Completed' si vous utilisez --wiql.
Elle analyse, renvoie 200 et correspond à tout — IN GROUP ne couvre que les catégories
de TYPE d'élément de travail, et un groupe inconnu se résout en ensemble vide sans
erreur. Mesuré, le 05/08/2026.
Modifier un élément de travail
pharos update 225 --state Doing # changer l'état quand le travail avance
pharos update 225 --priority 1 --assignee "[email protected]"
pharos update 225 --field Microsoft.VSTS.Scheduling.RemainingWork=3
pharos attach 225 ./bestsellers.xlsx --comment "Les chiffres"
update lit l'élément et applique la modification sous une opération test sur /rev,
donc si quelqu'un a écrit entre votre lecture et votre écriture, vous obtenez un conflict
plutôt que de perdre sa modification. Définir une valeur qu'il a déjà n'écrit rien et le dit
— un PATCH inutile augmente quand même System.Rev et invalide toutes les autres révisions
mises en cache sur l'élément. --dry-run montre l'avant → l'après et n'écrit rien. Il n'y a pas
de --yes : une modification de champ est une modification ordinaire et Azure DevOps conserve
chaque révision.
attach télécharge les octets puis les lie comme une relation AttachedFile —
deux appels, une commande. Les pièces jointes sont immuables : attacher le même fichier
deux fois en crée deux, et il n'y a pas de remplacement ni de versionnage. detach en
retire une ; elle nécessite --yes, trouve la pièce jointe par identité plutôt que par
position, et laisse les octets dans Azure DevOps donc la rattacher via l'url la remet en place.
Nommez le fichier avec --name <fichier> ou passez le GUID — et comme avec
download, deux fichiers partageant un nom REFUSENT plutôt que de supprimer celui qui
se trouve en premier.
create place --parent dans le même correctif que les champs, donc un enfant n'est jamais
brièvement orphelin, et il accepte les mêmes noms de drapeaux que update.
unlink refuse plutôt que de deviner, et cela vaut la peine de le savoir avant de le voir.
Azure DevOps supprime une relation par sa POSITION dans le tableau, donc un
index issu d'une lecture obsolète coupe une relation différente et la requête réussit quand même.
unlink trouve la relation par identité et la supprime sous une opération test sur la révision
qu'elle a lue. Si la relation n'est pas là, vous obtenez le code de sortie 3 — cela signifie
que rien ne s'est passé, pas que l'appel a échoué.
delete déplace vers la Corbeille et nécessite --yes ; le refus cite d'abord le titre.
restore <id> le ramène et ne nécessite aucun drapeau.
Il n'y a pas de suppression définitive ici, délibérément. C'est le seul verbe irréversible
qu'Azure DevOps possède, et --yes est un drapeau que vous avez appris à passer. L'interface web
en est propriétaire. Si quelqu'un a vraiment besoin de purger, envoyez-le là-bas plutôt que de chercher
un drapeau.
Images dans le texte, et fichiers à côté
Ce sont deux choses différentes et choisir la mauvaise est l'erreur à éviter.
pharos attach 225 ./bestsellers.xlsx # un FICHIER, dans la liste Pièces jointes
pharos image ./screenshot.png # une IMAGE, à mettre dans le texte
Une pièce jointe crée une relation AttachedFile et apparaît dans la liste Pièces jointes
de l'élément de travail. Une image en ligne ne crée aucune relation —
mesuré sur #333, deux captures d'écran collées dans une description ont donné relations: 0
et attachments: []. C'est un  dans le texte du champ et rien d'autre,
ce qui explique pourquoi la liste Pièces jointes a raison de n'afficher rien.
Donc image imprime une ligne markdown et vous laisse l'écriture :
pharos image ./chart.png --pretty
# 
pharos update 225 --field System.Description="$(cat <<'EOF'
Les revenus sont en hausse. Voir le graphique :

EOF
)"
Une image wiki est un point de terminaison différent et nécessite wiki image. Un wiki est un
dépôt git, donc une pièce jointe là-bas est un FICHIER et son NOM est son identité :
téléchargez un second image.png et il écrase le premier, et chaque page
pointant vers /.attachments/image.png change silencieusement d'image. wiki image rend le nom
unique avant l'envoi et imprime un lien relatif au dépôt :
pharos wiki image ./diagram.png --pretty
# 
Utilisez ce chemin, pas une url absolue — une page liant à une url wit/attachments
s'affiche pour quiconque a une session et ne fonctionne pas pour tous les autres. Le même markdown
fonctionne dans une page et dans un commentaire de page.
Récupérer une pièce jointe d'un élément de travail
pharos task <id> liste ce qui est attaché, avec le GUID de chaque fichier —
dans le JSON et dans --pretty. Ensuite, récupérez-le en un seul appel, par nom :
pharos download 41 --name Skills.pptx --out ./Skills.pptx
pharos download 41 --all --out ./attachments # toutes les pièces jointes de l'élément
pharos download <guid-or-url> --out ./file.bin # quand vous en avez déjà un
--out est ce qui écrit. Sans lui, vous obtenez la taille et aucun fichier —
les octets bruts sur la sortie standard corrompraient le JSON que chaque autre verbe imprime.
Un simple pharos download 41 est une erreur d'utilisation portant la liste — nom, GUID
et taille — donc choisir le bon fichier ne coûte jamais un second appel. Deux pièces jointes
avec le même nom refusent plutôt que de deviner : les pièces jointes sont immuables, donc
attacher un fichier deux fois en crée deux et le nom n'est pas une identité.
detach accepte le même --name <fichier>, donc le verbe que vous utilisez après avoir lu
task --pretty accepte ce qu'il vous a montré :
pharos detach 41 --name Skills.pptx --yes # --dry-run le prévisualise d'abord
Lire ce qu'il contient est VOTRE travail, avec VOS outils
download vous donne des octets sur le disque et s'arrête là. Transformer un .docx, .pptx,
.xlsx ou .pdf en quelque chose de lisible est le travail de votre environnement.
N'utilisez pas pharos-convert pour cela. C'est le convertisseur de wiki import,
partagé avec l'application macOS pour que les deux produisent la même page à partir du même fichier —
sa sortie est conçue pour devenir une page wiki, et l'utiliser ici couple ce que
vous lisez au chemin d'import de l'application. Il ne peut pas non plus lire les fichiers .pptx.
Deux façons dont cela échoue silencieusement, toutes deux mesurées sur un vrai élément de travail :
- Vous avez cherché l'outil au mauvais endroit.
which markitdownsur le PATH système etimport docxsur lepython3système retournent tous deux vides sur une machine où ces outils sont installés — dans un venv par compétence. Un agent qui exécute ces deux vérifications conclut "rien ici" et contourne des outils qui étaient là depuis le début. Regardez où votre agent conserve ses outils avant de conclure qu'ils sont absents. - Un texte non vide n'est pas une preuve que vous avez lu le document. Un vrai
fichier
.pptxs'est extrait en sept fragments, environ 90 caractères ; l'intégralité de la spécification se trouvait dans deux PNG intégrés. Tout lecteur de texte seul renvoie quelque chose pour ce fichier et a l'air d'avoir fonctionné. Pour un diaporama, ou un PDF numérisé, le contenu est généralement constitué des images — extrayez-les et regardez-les réellement.
Demandez-vous d'abord si vous lisez les PDF nativement. Si c'est le cas, c'est une commande.
De nombreux agents — Claude Code parmi eux — lisent un PDF visuellement, page par page, comme une personne le regarde. Cela couvre un scan sans aucun texte, avec aucune étape d'OCR. Si c'est votre cas, tout le problème se réduit à une seule conversion :
soffice --headless --convert-to pdf f.docx --outdir ./out # .pptx, .xlsx aussi
#
…
[truncé pour la traduction]

