Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

hawser

Épinglez une stack de dépôts Git à un lockfile — pour qu’un coéquipier, un runner CI ou un auditeur extraie l’arborescence identique, partout. Un seul binaire. En Rust.

haw compose de nombreux dépôts Git indépendants en une seule stack reproductible. Un manifeste déclaratif (haw.toml) décrit les stacks et les dépôts qu’ils composent ; un lockfile committé (haw.lock) épingle chaque dépôt à une révision exacte — pour que n’importe quel coéquipier ou machine CI reconstruise l’arborescence identique au bit près. Pas de sous-modules, pas de HEAD détachées, pas de runtime Python — un seul binaire statique.

haw init haw.toml   # déclarer les dépôts
haw sync            # cloner chaque dépôt, écrire haw.lock (SHA exacts)
haw verify          # barrière CI : code de sortie 3 si l'arborescence dérive du lockfile

Ceci est la documentation de référence pour hawser 0.1.7. Vous découvrez le projet ? Commencez par le cours Learn pour un parcours guidé et pratique ; utilisez les pages ci-dessous comme référence.

Au-delà du cœur

La composition reproductible (ci-dessus) est le fondement. Construites dessus, quatre capacités supplémentaires — un seul binaire :

  • Composer, à grande échelle. Les stacks et les overlays composent les dépôts en variantes nommées. Clone superficiel (--depth) et partiel (--filter=blob:none), ainsi qu’un magasin d’objets partagé via les alternates de git (--shared) — pas de liens symboliques, donc cela fonctionne sous Windows.
  • Orchestrer. Exécutez build, test, ou n’importe quelle commande sur toute la flotte en parallèle (-j N) ; haw grep se diffuse sur chaque dépôt ; haw verify est une barrière de dérive qui renvoie le code de sortie 3 pour la CI.
  • Collaborer. Une fonctionnalité = une branche répartie sur N dépôts, avec des PR/MR liées entre elles sur GitHub, GitLab et Bitbucket, un statut agrégé de relecture + CI, et land pour fusionner dans l’ordre des dépendances. Plus une fusion collaborative parallèle (planresolvecleanup).
  • Exploiter. Un cockpit TUI de style k9s (haw sans argument) — une grille de flotte en direct que vous explorez pour le diff d’un dépôt, les vérifications d’une PR, ou la progression en direct d’une exécution CI, puis vous agissez au clavier. Filtre flou, marques + actions en lot, une vue problèmes uniquement, mises en évidence de la dérive, une barre de commande et six thèmes.
  • Gouverner. SBOM (CycloneDX + SPDX), provenance SLSA/in-toto, signatures cosign/minisign, hooks de cycle de vie, une barrière de secrets/hygiène, et des bundles evidence pour la qualification.

L’étendre

haw <name> exécute haw-<name> depuis votre PATH — étendez la CLI sans forker. Les plugins sont des sous-processus qui parlent un contrat JSON (haw.plugin/1 en entrée, haw.plugin.report/1 en sortie), ils peuvent donc être écrits en Rust, Python, Go ou shell. Générez-les, découvrez-les et installez-les :

haw plugins new my-check --lang python   # squelette exécutable
haw plugins list --remote                # découvrir les plugins communautaires depuis l'index
haw plugins install aspice               # installer un plugin officiel ou communautaire

Les schémas JSON publiés et de fins bindings (Python, Go) rendent cela trivial ; voir la liste soigneusement sélectionnée AWESOME-HAW-PLUGINS.

Distribuer et installer

haw est distribué sous forme d’un unique binaire statique. Installez-le via crates.io, Homebrew, Scoop, AUR, Nix, .deb/.rpm, Docker, ou une archive précompilée signée — chaque release comporte une somme de contrôle .sha256 et une signature cosign sans clé. Les artefacts de la flotte sont publiés vers des registres privés (Nexus, Artifactory, GitLab, Bitbucket) via haw publish.

cargo install hawser         # depuis crates.io (canonique)
brew install nastwinns/tap/hawser   # macOS / Linux (Homebrew)
curl -L … | tar xz           # binaire musl statique précompilé

Sécurité

haw exécute du code déclaré dans le manifeste et des binaires de votre PATH — considérez les deux comme des entrées de confiance. Le crate est #![forbid(unsafe_code)], HTTPS passe uniquement par rustls, les actions sont épinglées par SHA, les releases sont signées par cosign, et cargo audit/cargo deny contrôlent chaque push. Lisez le modèle de confiance complet.

Essayez-le dans votre navigateur

La page d’accueil affiche le cockpit TUI en direct en WebAssembly — aucune installation requise : https://nastwinns.github.io/hawser/

Où aller ensuite

  • Learn — le cours guidé et pratique, de zéro à une flotte gouvernée.
  • Install — la matrice complète des canaux + vérification de signature + air-gap.
  • Distribution — mettre en miroir les releases vers Nexus/Artifactory/GitLab/Bitbucket.
  • Domains — comment la boucle s’applique à l’embarqué, aux microservices, au ML, à l’infra, au mobile.
  • Architecture — organisation des crates, modèle de concurrence, abstraction de la forge.
  • CLI design & TUI keymap — le lexique complet des verbes et les raccourcis clavier du cockpit.
  • Extending — plugins, hooks, authentification et intégration CI/CD.
  • Plugins — écrire et soumettre des plugins haw-<name>.
  • Compliance — qualification de l’outil, SBOM/CRA, signature, RGPD.
  • Security — le modèle de confiance : ce que haw exécute, la confiance des plugins, les tokens.

Liens

Installer hawser

hawser est distribué sous forme d’un unique binaire nommé haw. Cette page constitue la matrice d’installation complète : tous les canaux, comment vérifier la release signée, le workflow air-gap et la compilation depuis les sources. Pour la version courte, voir la section Install du README.

La release actuelle est la v0.1.7, publiée avec des archives signées et reproductibles pour chaque plateforme prise en charge.

Matrice des canaux

CanalPlateformeCommande / sourcePrérequis
crates.iotout (Rust)cargo install hawserchaîne d’outils Rust 1.90+
HomebrewmacOS + Linuxbrew install nastwinns/tap/hawserHomebrew
ScoopWindowsscoop bucket add nastwinns https://github.com/Nastwinns/scoop-bucket puis scoop install hawserScoop
Binaire musl statiqueLinux x86_64télécharger haw-0.1.7-x86_64-unknown-linux-musl.tar.gz (voir ci-dessous)aucun (sans dépendance)
Archive précompiléeLinux gnu (x86_64/aarch64), Linux musl (x86_64), macOS (x86_64/aarch64), Windows (x86_64)GitHub Releaseaucun (optionnel : cosign, sha256sum pour vérifier)
Registres privéstoutmiroir Nexus / Artifactory / GitLab / Bitbucket — voir DISTRIBUTION.mdidentifiants du registre
Dockertout (avec Docker)docker build -t haw .Docker + le dépôt
Depuis les sourcestout (Rust)cargo install --git … ou cargo build --releasechaîne d’outils Rust 1.90+

Tous les canaux installent le même binaire haw. cargo install hawser est l’installation Rust canonique.

Gestionnaires de paquets

crates.io (Rust)

cargo install hawser

Compile depuis les sources avec votre chaîne d’outils locale et place haw dans ~/.cargo/bin. Nécessite une chaîne d’outils Rust 1.90+.

Homebrew (macOS + Linux)

brew install nastwinns/tap/hawser

Le tap se trouve à Nastwinns/homebrew-tap. Homebrew récupère l’archive précompilée pour votre plateforme, aucun compilateur n’est donc nécessaire.

Scoop (Windows)

scoop bucket add nastwinns https://github.com/Nastwinns/scoop-bucket
scoop install hawser

AUR (Arch Linux)

Un paquet hawser-bin (précompilé à partir de la GitHub Release) — voir packaging/aur/PKGBUILD :

yay -S hawser-bin            # ou : paru -S hawser-bin

Nix (flake)

Exécutez sans installer, ou ajoutez à un profil — le flake compile haw depuis les sources et l’enveloppe avec git :

nix run github:Nastwinns/hawser              # exécuter une fois
nix profile install github:Nastwinns/hawser  # installer

Debian / RPM

Chaque GitHub Release fournit un .deb et un .rpm pour Linux x86_64 (gnu) :

# Debian/Ubuntu
curl -sSLO https://github.com/Nastwinns/hawser/releases/latest/download/hawser_0.1.7-1_amd64.deb
sudo dpkg -i hawser_0.1.7-1_amd64.deb
# Fedora/RHEL
sudo rpm -i https://github.com/Nastwinns/hawser/releases/latest/download/hawser-0.1.7-1.x86_64.rpm

Binaire musl statique (Linux, sans dépendance, adapté à l’air-gap)

L’installation Linux universelle recommandée. Le build musl est entièrement statique — pas de glibc, pas de runtime — il s’exécute donc de manière identique sur n’importe quel hôte Linux, s’intègre dans des conteneurs minimaux et s’installe proprement sur des machines air-gap sous forme d’un seul fichier.

curl -sSL https://github.com/Nastwinns/hawser/releases/download/v0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz \
  | tar xz && sudo install haw /usr/local/bin/

Pour les hôtes air-gap, téléchargez l’archive (ainsi que ses .sha256, .sig et .pem) sur une machine connectée, vérifiez-la (ci-dessous), copiez les quatre fichiers, puis installez.

Archives précompilées (signées)

Chaque plateforme est fournie sous forme d’archive sur la GitHub Release :

  • haw-0.1.7-x86_64-unknown-linux-gnu.tar.gz
  • haw-0.1.7-aarch64-unknown-linux-gnu.tar.gz
  • haw-0.1.7-x86_64-unknown-linux-musl.tar.gz (statique)
  • haw-0.1.7-x86_64-apple-darwin.tar.gz
  • haw-0.1.7-aarch64-apple-darwin.tar.gz
  • haw-0.1.7-x86_64-pc-windows-msvc.zip

Chaque archive est accompagnée de :

  • <archive>.sha256 — une somme de contrôle SHA-256
  • <archive>.sig et <archive>.pem — une signature sans clé cosign et son certificat

La release est reproductible et signée. La vérification est optionnelle mais recommandée, et c’est tout l’intérêt sur des hôtes verrouillés ou air-gap.

Vérifier la somme de contrôle

sha256sum -c haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.sha256

Attendez-vous à … OK. (Sous macOS, shasum -a 256 -c est l’équivalent.)

Vérifier la signature cosign

La vérification sans clé contrôle la signature par rapport au journal de transparence Sigstore. Vous devez avoir cosign installé :

cosign verify-blob \
  --certificate haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.pem \
  --signature   haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.sig \
  --certificate-identity-regexp 'https://github.com/Nastwinns/hawser' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  haw-0.1.7-x86_64-unknown-linux-musl.tar.gz

Attendez-vous à Verified OK. Une fois vérifié, décompressez et installez :

tar xzf haw-0.1.7-x86_64-unknown-linux-musl.tar.gz
sudo install haw /usr/local/bin/

Workflow air-gap

  1. Sur une machine connectée, téléchargez l’archive et ses compagnons .sha256, .sig et .pem.
  2. Vérifiez la somme de contrôle et la signature cosign (ci-dessus) — cela établit la confiance tant que vous avez encore accès réseau au journal de transparence.
  3. Copiez les quatre fichiers vers l’hôte air-gap.
  4. Vérifiez à nouveau la somme de contrôle hors ligne (sha256sum -c …), décompressez et installez.

Le binaire musl statique n’a aucune dépendance d’exécution, donc rien d’autre n’a besoin de franchir l’air gap.

Registres privés (Nexus / Artifactory / GitLab / Bitbucket)

Les organisations qui mettent en miroir les releases sur un registre interne peuvent récupérer exactement les mêmes archives signées (ainsi que .sha256, .sig, .pem et les .deb/.rpm) depuis Nexus, Artifactory, GitLab ou Bitbucket. Chaque release taguée publie d’abord la GitHub Release, puis met en miroir les artefacts vers ceux de ces registres qui sont configurés.

Voir DISTRIBUTION.md pour les chemins de téléversement exacts, la matrice de secrets pour activer chaque registre, et les commandes de téléchargement/installation propres à chaque registre. Exemple (Nexus) :

curl -u "$NEXUS_USER:$NEXUS_PASS" -O \
  "$NEXUS_URL/repository/raw-hosted/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Vérifiez la somme de contrôle et la signature cosign exactement comme pour la GitHub Release (ci-dessus).

Docker

Une image se construit directement à partir du Dockerfile du dépôt :

docker build -t haw .
docker run --rm haw --version

Nécessite Docker et un checkout du dépôt.

Depuis les sources

Nécessite une chaîne d’outils Rust 1.90+.

Installez le dernier main directement depuis Git :

cargo install --git https://github.com/Nastwinns/hawser hawser

Ou clonez et compilez un binaire de release :

git clone https://github.com/Nastwinns/hawser
cd hawser
cargo build --release
# binaire dans target/release/haw

Vérifier l’installation

Quel que soit le canal utilisé :

haw --version

Retour au README.

0. Qu’est-ce que hawser ?

hawser — the beam that binds the repos

Bienvenue ! Si tu as déjà travaillé sur un produit qui vit dans plus d’un dépôt Git, ce cours est fait pour toi. À la fin, tu sauras composer des dépôts, lancer du travail sur toute une flotte, ouvrir des pull requests multi-dépôts, et piloter tout ça depuis un cockpit au clavier — confortablement.

Commençons par le pourquoi. Car une fois que le problème devient clair, chaque commande de haw prend soudain tout son sens.

Version control across many repositories

Plusieurs dépôts, un tout coordonné — voilà le casse-tête que hawser résout.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Reconnaître les trois taxes d’un produit multi-dépôts : la coordination des versions, les PR inter-dépôts et la reproductibilité.
  • Expliquer hawser en une ligne — un package.json + lockfile, mais pour une flotte de dépôts Git.
  • Garder tout le modèle mental en tête : manifeste → lockfile → stacks.
  • Savoir quand hawser est le bon outil pour ton monde.

🧩 Le problème : un produit, plusieurs dépôts

Imagine un vrai produit. Ce n’est presque plus jamais un seul dépôt. C’est une bibliothèque partagée, trois ou quatre services, un SDK, un peu d’infrastructure — chacun dans son propre dépôt Git, avec ses propres branches, sa propre CI, sa propre histoire.

A product split across many repositories

Un produit, éparpillé sur une étagère de dépôts séparés.

Ce découpage, c’est du bon génie logiciel. Mais il a un prix :

  • « Quelles versions vont ensemble ? » Le service A fonctionne avec la version 2.1 de la bibliothèque partagée — mais quel commit correspond exactement à la 2.1 ? Et ton collègue a-t-il bien le même ?
  • « Une fonctionnalité, cinq pull requests. » Un seul changement touche quatre dépôts. Tu crées une branche dans chacun à la main, tu ouvres quatre PR, et tu pries pour les fusionner dans le bon ordre.
  • « Ça marche sur ma machine. » Personne ne peut reproduire l’ensemble exact des commits qui étaient en production en mars dernier, parce que cet ensemble n’a jamais été noté nulle part.
This is fine — a cartoon dog sipping coffee as the room burns

Gérer dix dépôts et leurs versions à la main. Tout va bien. 🔥

Voici l’idée : un seul dépôt avait déjà résolu ces problèmes il y a des années. Ton package.json (ou Cargo.toml) déclare tes dépendances, et un lockfile (package-lock.json, Cargo.lock) fige les versions résolues exactes, pour que tout le monde — toi, ton collègue, le runner de CI — reconstruise un arbre identique.

Le modèle mental en une phrase : hawser, c’est un package.json + lockfile, mais pour une flotte de dépôts Git au lieu d’une flotte de paquets npm.

⚓ Ce que fait hawser

hawser compose un stack logiciel à partir de plusieurs dépôts Git, le fige dans un lockfile, et te laisse piloter chaque build, PR, revue et exécution de CI multi-dépôts depuis un seul endroit.

Ce n’est pas un wrapper autour de Git et ça ne réimplémente pas Git. C’est la couche au-dessus de Git — ce que ton package.json, ton tableau de projet et ton tableau de bord de PR faisaient discrètement pour un seul dépôt, désormais fait pour plusieurs.

L’outil en ligne de commande s’appelle haw. C’est un binaire unique, écrit en Rust, sans aucun runtime à installer.

🧠 Le modèle mental : manifeste → lockfile → stacks

Trois concepts portent tout le système. Apprends-les maintenant et tout le reste en découle.

  1. Le manifeste — haw.toml. C’est ton intention. Tu déclares quels dépôts existent, où ils vivent, et quelle révision tu veux. Vois-le comme le bloc dependencies.

  2. Le lockfile — haw.lock. C’est la réalité résolue. Quand tu synchronises, haw fige chaque dépôt sur un SHA de commit exact et l’écrit ici. Tu committes ce fichier. Désormais un collègue — ou la CI, ou un auditeur — reconstruit l’arbre identique, octet pour octet.

  3. Les stacks. Un stack est une composition nommée de dépôts. Un même manifeste peut définir plusieurs stacks qui partagent les mêmes dépôts sans les copier. (Un stack dans haw, c’est simplement « ces dépôts, ensemble, sous ce nom ».)

haw.toml   (intent)  ─────►  haw sync  ─────►  haw.lock   (pinned SHAs, committed)
   │                                               │
   └── declares repos + stacks                     └── the reproducible baseline

Sur le disque, il n’y a ni sous-modules, ni liens symboliques, ni HEAD détachés — chaque dépôt est un clone Git ordinaire et complet. haw se contente de les garder synchronisés et coordonnés.

🎯 Quand se tourner vers hawser

hawser est agnostique du domaine — un dépôt est un dépôt, un build est la commande shell que tu déclares. Il brille dès qu’un produit est réparti sur plusieurs dépôts :

Si ton monde ressemble à……hawser t’apporte
Microservices backend — une fonctionnalité couvrant N services + un proto/lib partagéune branche + des PR liées sur exactement les dépôts qu’elle touche
Plateformes ML / data — modèle + pipeline + infra de serviceune base de référence figée et reproductible des trois
Plateforme / infra — modules Terraform + charts Helmune base de référence déployée, versionnée et contrôlée contre la dérive
Mobile — une app + son SDK maisonl’app et le SDK modifiés et publiés de concert
Embarqué / automobile — un HAL/BSP partagé réutilisé sur plusieurs ECUdes bases de référence reproductibles, prêtes pour l’audit + des preuves de conformité

La boucle — composer → figer → modifier → builder/tester → gouverner — est la même dans chaque cas. Seuls les dépôts et les commandes de build déclarées changent. Voir Domains pour savoir comment chacun s’y rattache.

🚀 Ce que tu sauras faire à la fin de ce cours

  • Écrire un haw.toml, le synchroniser, et lire le lockfile en toute confiance.
  • Lancer des builds, des tests, des commandes et des recherches sur toute une flotte en parallèle.
  • Livrer une fonctionnalité sur N dépôts en un seul changeset — brancher, ouvrir la PR, atterrir dans l’ordre.
  • Vivre dans le cockpit TUI et fusionner, approuver et inspecter sans quitter le terminal.
  • Étendre haw avec un plugin que tu as écrit toi-même.
  • Le brancher dans la CI avec reproductibilité, signature et preuves d’audit.

C’est un ensemble de compétences réel et productif — et nous le construirons une petite étape à la fois.

Et voici où nous allons — le cockpit au clavier dans lequel tu vivras dès le chapitre 4 :

Le cockpit haw : lire la flotte, plonger dans n’importe quel dépôt ou PR, et agir — sans quitter le terminal.

🙌 À toi de jouer

Avant de toucher la moindre commande, fais l’expérience de pensée. Imagine le dernier produit sur lequel tu as travaillé qui vivait dans plus d’un dépôt. Note :

  • Combien de dépôts était-ce, vraiment ?
  • La dernière fois qu’une fonctionnalité t’a forcé à ouvrir des PR dans plusieurs d’entre eux à la fois — comment as-tu gardé le fil ?
  • Pourrais-tu reproduire, aujourd’hui, l’ensemble exact des commits qui étaient en production il y a trois mois ? Garde ces réponses. Dès le chapitre 3, tu auras une réponse en une commande à chacune.

✅ Récapitulatif

  • Répartir un produit sur plusieurs dépôts Git est normal, mais cela te coûte en coordination des versions, en PR multi-dépôts et en reproductibilité.
  • hawser est un manifeste + lockfile pour une flotte de dépôts — comme package.json + package-lock.json, mais pour des dépôts entiers.
  • Trois concepts : manifeste (haw.toml, l’intention) → lockfile (haw.lock, la réalité figée) → stacks (compositions nommées de dépôts).
  • La CLI est haw : un seul binaire Rust, aucun runtime.
  • Il convient à tout domaine où un produit s’étend sur plusieurs dépôts.

👉 La suite

Installons l’outil sur ta machine et ouvrons le cockpit pour la première fois → 1. Installation et première exécution.

1. Installation et première exécution

Dans ce chapitre, tu vas mettre le binaire haw sur ta machine, confirmer qu’il fonctionne, activer la complétion par tabulation — puis lancer ta toute première exécution de haw, en regardant le cockpit au clavier s’ouvrir. C’est court : on te veut vite devant un prompt, et on veut que tu voies où le cours se dirige.

Setting up the haw tool on your machine

Une configuration unique, puis tu es au prompt pour le reste du cours.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Installer haw via le canal qui convient à ta machine — Cargo, Homebrew, Scoop ou le binaire statique musl.
  • Confirmer que le binaire est sur ton PATH et affiche haw 0.1.7.
  • Activer la complétion par tabulation du shell pour qu’il complète les sous-commandes et les options au fur et à mesure que tu apprends.
  • Lancer haw une première fois et regarder le cockpit s’ouvrir — un avant-goût de là où tu vas.

L’outil est livré sous forme d’un binaire unique nommé haw. Aucun runtime, aucun interpréteur, rien à maintenir à jour à côté. La version actuelle est la v0.1.7.

📦 1. Installe-le

Choisis la ligne qui correspond à ta configuration — toutes installent le même binaire haw.

cargo install hawser                              # Rust / crates.io (canonique)
brew install nastwinns/tap/hawser                 # macOS + Linux (Homebrew)

Sous Windows, utilise Scoop :

scoop bucket add nastwinns https://github.com/Nastwinns/scoop-bucket
scoop install hawser

Sur un serveur Linux, un conteneur ou un hôte isolé du réseau, le binaire musl statique est le choix le plus simple — il est entièrement statique (pas de glibc, pas de runtime), donc un seul fichier s’exécute :

curl -sSL https://github.com/Nastwinns/hawser/releases/download/v0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz \
  | tar xz && sudo install haw /usr/local/bin/

Astuce : cargo install hawser est l’installation Rust canonique. Elle compile depuis les sources et dépose haw dans ~/.cargo/bin — assure-toi que ce répertoire est bien dans ton PATH.

Pour tous les autres canaux (.deb/.rpm, AUR, Nix, Docker), ainsi que la vérification de signature et le workflow air-gap complet, voir Installing hawser.

✅ 2. Vérifie que ça marche

Quel que soit le canal utilisé, confirme que le binaire est dans ton PATH :

haw --version

Tu devrais voir la version s’afficher :

haw 0.1.7

Si tu obtiens « command not found », c’est que le répertoire d’installation n’est pas encore dans ton PATH — pour cargo, c’est ~/.cargo/bin. Corrige ça et relance.

Maintenant, jette un œil à la surface complète des commandes — pas besoin de tout lire, prends juste le pouls :

haw --help

Tu verras les sous-commandes que nous couvrirons : sync, status, tree, run, build, test, change, plugins, et d’autres encore. Chacune est un seul mot facile à deviner.

Un rapide aperçu de haw en ligne de commande — chaque sous-commande est un seul mot facile à deviner.

⌨️ 3. Active la complétion du shell

C’est un petit gain de confort qui paie tout au long du cours : tu appuies sur Tab et le shell complète les sous-commandes et les flags à ta place.

haw completions <shell> imprime un script de complétion sur stdout. Redirige-le vers le bon endroit pour ton shell :

haw completions zsh  > ~/.zfunc/_haw                 # zsh
haw completions bash > /etc/bash_completion.d/haw    # bash
haw completions fish > ~/.config/fish/completions/haw.fish   # fish

Astuce : Pour zsh, assure-toi que ~/.zfunc est dans ton $fpath (ajoute fpath=(~/.zfunc $fpath) avant compinit dans ton ~/.zshrc), puis redémarre ton shell. Désormais haw sy<Tab> se complète en haw sync.

🚁 4. Ta première exécution — ouvre le cockpit

Voici la récompense. haw n’est pas qu’un lot de sous-commandes : lance-le sans aucune sous-commande et il ouvre un cockpit plein écran, piloté au clavier, pour ta flotte.

haw

Tu n’as pas encore d’espace de travail, donc il n’y a rien de réel à afficher. Bonne nouvelle : il y a un contrôleur de démo intégré, rempli de dépôts, de PR et d’exécutions de CI factices, pour que chaque vue contienne quelque chose — sans réseau, sans configuration :

haw dash --demo

Une grille de flotte en direct remplit le terminal. Déplace-toi avec / (ou k/j, façon Vim), appuie sur Enter pour plonger dans un dépôt, Esc pour revenir, ? pour l’aide en surimpression, et q pour quitter.

Le cockpit que tu viens d’ouvrir : la grille de flotte, les explorations détaillées et les actions au clavier — le tout dans le terminal.

Juste un avant-goût pour l’instant. Fouille un peu, puis quitte avec q. Nous ferons une vraie visite guidée du cockpit au chapitre 4 — mais d’abord il te faut une vraie flotte à lui pointer, et c’est ce que construisent les trois chapitres suivants.

✅ Récapitulatif

  • haw est un binaire unique — installe-le avec cargo, brew, scoop, ou l’archive musl statique.
  • haw --version doit afficher haw 0.1.7 ; haw --help liste toutes les commandes.
  • haw completions <shell> te donne la complétion par tabulation — configure-la maintenant, tu te remercieras plus tard.
  • haw seul (ou haw dash --demo) ouvre le cockpit — ton premier aperçu du centre de contrôle, exploré en profondeur au chapitre 4.
  • La matrice d’installation complète couvre les releases signées et les hôtes isolés du réseau.

🙌 À toi de jouer

Point de contrôle de deux minutes — prouve ton installation avant de continuer :

  • Lance haw –version et confirme que tu vois haw 0.1.7. S’il affiche « command not found », le répertoire d’installation n’est pas encore sur ton PATH — corrige ça d’abord.
  • Configure les complétions pour ton shell, redémarre-le, puis tape haw sy et appuie sur Tab. Ça devrait se compléter en haw sync.
  • Lance haw dash –demo, déplace-toi avec /, appuie sur Enter pour entrer dans un dépôt et Esc pour en sortir, puis q pour quitter. Tu viens de rencontrer le cockpit.

👉 La suite

Maintenant, donnons à haw quelque chose de réel à gérer. Premier arrêt : le manifeste — là où tu déclares ta flotte → 2. Le manifeste.

2. Le manifeste

Tout ce que fait haw part d’un seul fichier : haw.toml, le manifeste. C’est ton intention — quels dépôts existent, où ils vivent, quelle révision tu veux, et comment ils se composent en stacks. Dans ce chapitre, tu vas en écrire un vrai, de haut en bas, et construire le modèle mental. On ne synchronisera pas encore — c’est pour le prochain chapitre. Ici, on soigne d’abord la déclaration.

Composing a stack from building blocks

Un stack, ce ne sont que des blocs de construction — des dépôts emboîtés sous un seul nom, déclarés dans le manifeste.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Déclarer un remote une fois et le réutiliser sur tous les dépôts.
  • Ajouter des dépôts, chacun avec un rev (branche, tag ou SHA) et des groups libres.
  • Composer les dépôts en stacks nommées — partagées, jamais copiées.
  • Recourir aux overlays quand un dépôt a besoin de surcharges par variante.
  • Lire un haw.toml entier et savoir exactement ce qu’il va faire.

🛠️ 1. Crée un espace de travail

Crée un répertoire vide et dépose-y un manifeste. Nous utiliserons de petits dépôts publics réels, issus du compte octocat de GitHub — les mêmes que ceux de l’exemple livré examples/quickstart — pour que dès le prochain chapitre tu puisses réellement faire haw sync dessus via HTTPS, sans authentification.

mkdir my-first-stack && cd my-first-stack

Crée maintenant haw.toml avec ce contenu :

[remote.gh]
url = "https://github.com/octocat"
forge = "github"

[repo.hello-world]
remote = "gh"
repo = "Hello-World.git"
rev = "master"
groups = ["core"]

[repo.spoon-knife]
remote = "gh"
repo = "Spoon-Knife.git"
rev = "main"
groups = ["web"]

# `site` composes the two repos into one named stack.
[stack.site]
repos = ["hello-world", "spoon-knife"]
description = "A shared core repo plus the fork-demo front end."

Lis-le de haut en bas, car il est le modèle mental. Prenons-le un bloc à la fois.

🌐 2. Le remote — nomme une URL de base une seule fois

[remote.gh]
url = "https://github.com/octocat"
forge = "github"

Un [remote.NAME] nomme une URL de base une seule fois, pour ne pas la répéter sur chaque dépôt. forge indique à haw quelle API parler plus tard (pour les PR, la CI, etc.) — github, gitlab, ou bitbucket. Tu peux déclarer plusieurs remotes ; une flotte peut s’étendre sur plusieurs forges, et plus tard un changeset peut ouvrir une PR sur l’une et une MR sur l’autre depuis une seule commande.

📦 3. Les dépôts — une déclaration chacun

[repo.hello-world]
remote = "gh"
repo = "Hello-World.git"
rev = "master"
groups = ["core"]

Chaque [repo.NAME] déclare un dépôt :

  • remote + repo — sur quel remote il vit et son chemin là-bas. Combinés, ils se résolvent en https://github.com/octocat/Hello-World.git.
  • revla révision que tu veux : une branche (master), un tag (v6.1.2), ou un SHA exact. haw détecte automatiquement de quel type il s’agit. C’est ton intention — une branche mouvante jusqu’à ce que le lockfile la fige (prochain chapitre).
  • groups — des étiquettes libres par lesquelles tu filtreras des commandes plus tard (ex. haw status --group core). Un dépôt peut appartenir à plusieurs groupes.

rev est une intention, pas un ancrage. Écrire rev = "master" signifie « suivre master ». Rien n’est figé tant que tu ne synchronises pas et qu’un lockfile n’enregistre pas le commit exact — c’est toute l’histoire de la reproductibilité, et c’est l’objet entier du chapitre 3.

🧱 4. Les stacks — compose des dépôts sous un nom

[stack.site]
repos = ["hello-world", "spoon-knife"]
description = "A shared core repo plus the fork-demo front end."

Un stack est une composition nommée de dépôts. Il ne copie rien — il dit simplement « ces dépôts, ensemble, sous ce nom ». Un même manifeste peut définir plusieurs stacks qui partagent les mêmes dépôts :

[stack.site]
repos = ["hello-world", "spoon-knife"]

[stack.core-only]
repos = ["hello-world"]

hello-world apparaît dans les deux stacks, mais il n’y a jamais qu’un seul clone de lui sur le disque. Les stacks sont la façon dont tu découpes une grande flotte en jeux de travail que tu builds et testes réellement ensemble — tu feras haw switch <stack> pour passer de l’un à l’autre, et tu limiteras la portée d’une commande à l’un avec --stack.

🧬 5. Les overlays — des surcharges par variante (quand tu en as besoin)

La plupart des dépôts n’ont besoin de rien de plus que les champs ci-dessus. Mais parfois un dépôt doit être légèrement différent dans une composition particulière — une branche différente pour une variante de release, par exemple. C’est un overlay : un ensemble nommé de surcharges par dépôt, appliquées au moment du lock.

[repo.app-mqtt]
remote = "gh"
repo = "app-mqtt.git"
rev = "main"

# Override app-mqtt's rev only when the `release` overlay is active.
[overlay.release.app-mqtt]
rev = "release/2.x"

Avec l’overlay release actif, app-mqtt se verrouille sur release/2.x au lieu de main ; tout le reste ne bouge pas. Ne recours aux overlays que lorsqu’un dépôt doit vraiment varier selon la variante — pour le reste de ce cours, nous n’en aurons pas besoin. Voir CLI design pour la sémantique complète des overlays.

Astuce : Tu n’es pas obligé d’écrire chaque dépôt à la main. haw repo add et haw stack add éditent le manifeste à ta place, et haw import convertit un west.yml existant ou un default.xml de Google-repo directement en un haw.toml.

🗺️ 6. La vue d’ensemble

Voici le manifeste sous forme de schéma — l’intention qui s’écoule vers les stacks :

haw.toml
├─ [remote.gh]        base URL + forge, named once
├─ [repo.hello-world] remote + repo + rev + groups   ┐
├─ [repo.spoon-knife] remote + repo + rev + groups   ├─ the repos (your intent)
│                                                    ┘
└─ [stack.site]       repos = [hello-world, spoon-knife]   ← a named composition

Voilà le modèle mental complet du manifeste : les remotes nomment où, les repos déclarent quoi et quelle rev, les groups les découpent, les stacks les composent, et les overlays les ajustent par variante. Rien ici n’a encore touché le réseau ni le disque — c’est de la pure déclaration.

Tu viens de déclarer une flotte. Un remote, deux dépôts et un stack — c’est un haw.toml réel et synchronisable. Il passe à l’échelle de la même façon jusqu’à une centaine de dépôts et une douzaine de stacks.

🙌 À toi de jouer

Approprie-toi le manifeste — aucune synchro nécessaire, ce n’est que de la déclaration :

  • Ajoute un troisième dépôt à haw.toml — essaie octocat/git-consortium.git sur le remote gh — donne-lui un rev et mets-le dans un nouveau groupe.
  • Ajoute-le à la liste repos de la stack site, puis ajoute une deuxième stack, core-only, qui ne liste que hello-world. Remarque que le dépôt est partagé, pas copié.
  • Esquisse sur papier ce que tu attends sur le disque après une synchro : combien de clones ? (Indice : un par dépôt, peu importe le nombre de stacks qui le référencent.)

✅ Récapitulatif

  • Le manifeste (haw.toml) est ton intention — rien n’est cloné ni figé tant que tu ne synchronises pas.
  • [remote.NAME] nomme une URL de base + une forge une seule fois ; les dépôts y font référence.
  • [repo.NAME] déclare un dépôt avec une rev (branche/tag/SHA, détectée automatiquement) et des groups libres.
  • [stack.NAME] compose des dépôts sous un nom — partagés, jamais copiés ; un dépôt peut appartenir à plusieurs stacks et groupes.
  • [overlay.…] applique des surcharges par variante au moment du lock, pour le rare dépôt qui doit varier.

👉 La suite

Tu as déclaré la flotte. Maintenant, rendons-la réelle — clonons-la, figeons-la dans un lockfile, et regardons haw détecter la dérive → 3. La synchro et le lockfile.

3. La synchro et le lockfile

C’est ici que tout s’éclaire. Tu as un haw.toml du chapitre précédent. Maintenant tu vas lancer haw sync pour cloner la flotte pour de vrai, lire le lockfile qui la fige sur des commits exacts, prouver qu’elle est reproductible et — délibérément — casser quelque chose pour voir haw détecter la dérive.

Tout se clone ici en HTTPS sans authentification, donc tu peux vraiment lancer chaque commande au fil de ta lecture. Garde ouvert l’espace de travail my-first-stack du chapitre 2.

Syncing and pinning a fleet to a lockfile

La synchro clone la flotte ; le lockfile la fige sur des commits exacts — toute l’astuce de la reproductibilité.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Lancer haw sync pour cloner la flotte et générer haw.lock.
  • Lire la flotte avec haw tree et haw status, et comprendre chaque colonne.
  • Lire les vrais champs du lockfile — rev (le SHA résolu), source-rev et branch.
  • Resynchroniser et confirmer que c’est idempotent — le lock, pas la branche, fait désormais foi.
  • Provoquer une dérive exprès et la détecter avec haw verify (exit 3), puis restaurer.

La boucle de composition complète que tu vas lancer : sync clone la flotte, puis tree / status la lisent et le lock l’épingle.

🔄 1. Sync — tout cloner et écrire le lock

Une seule commande matérialise l’arbre :

haw sync

haw résout le rev de chaque dépôt en un commit exact, le clone et — parce qu’il n’y a pas encore de lockfile — en écrit un. Tu verras la progression par dépôt et un récapitulatif. Ensuite, jette un œil autour de toi :

$ ls
haw.lock   haw.toml   hello-world/   spoon-knife/

Les voilà : deux clones Git réels et complets, plus un haw.lock tout neuf. Pas de sous-modules, pas de liens symboliques — tu pourrais faire cd hello-world && git log et c’est juste du Git.

Astuce : haw sync est idempotent. Relance-le et, comme le lock épingle déjà des SHA exacts, haw s’assure simplement que ton arbre correspond — aucune surprise, sans risque à répéter dans les scripts et la CI.

🔍 2. Explorer la flotte

Maintenant les commandes de lecture. D’abord, la forme des choses :

haw tree
haw.toml
└─ site
   ├─ hello-world  master  (https://github.com/octocat/Hello-World.git)
   └─ spoon-knife  main  (https://github.com/octocat/Spoon-Knife.git)

Voilà ton arbre stack → dépôt : la stack site, les deux dépôts en dessous, chacun avec son rev déclaré et son origine.

Maintenant le bilan de santé :

haw status
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  master                    7fd1a60b   -      -
spoon-knife  main                      d0dd1f61   -      -

Lis les colonnes de gauche à droite : le dépôt, la branche sur laquelle il se trouve, son SHA HEAD court, si l’arbre de travail a des changements non committés (DIRTY) et si HEAD diffère du SHA verrouillé (DRIFT). Pour l’instant tout est propre et synchronisé — les tirets signifient « tout va bien ».

Astuce : Dans un terminal, tout est codé par couleur — noms de dépôts en cyan, revs en jaune, vert pour propre, rouge pour la dérive. Redirige la sortie n’importe où et elle bascule automatiquement en texte brut (elle respecte NO_COLOR), donc c’est compatible avec les scripts par défaut.

🔒 3. Lire le lockfile

Ouvre haw.lock dans ton éditeur. Chaque dépôt est épinglé à un SHA complet de 40 caractères dans son champ rev — le commit exact, pas le nom de branche — tandis que source-rev consigne ce que tu as demandé et branch consigne la branche d’où vient ce SHA :

# extrait — tes SHA seront différents
[[repo]]
name = "hello-world"
url = "https://github.com/octocat/Hello-World.git"
path = "hello-world"
rev = "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d"   # le commit résolu — voici l'épingle
source-rev = "master"                               # ce que tu as déclaré dans haw.toml
branch = "master"                                   # la branche d'où ce SHA a été résolu
groups = ["core"]

Voilà toute l’astuce de la reproductibilité. Ton manifeste disait « master » (une cible mouvante), mais le lock a figé le commit exact que master pointait au moment où tu as synchronisé — c’est le SHA qui est maintenant dans rev. source-rev se souvient de ton intention (master) et branch se souvient d’où ça venait, donc haw peut plus tard re-résoudre si tu le demandes. Committe haw.lock aux côtés de haw.toml, et quiconque clone et lance haw sync obtient précisément ces commits — pas ce que master se trouve être aujourd’hui.

Ne l’édite pas à la main. haw.lock est généré. Tu le committes, mais tu changes l’intention dans haw.toml et tu laisses haw lock / haw sync régénérer le lock.

♻️ 4. Prouve que c’est reproductible — relance sync

haw sync

Parce que le lock existe, haw synchronise vers les SHA épinglés, pas vers là où les branches ont bougé. Relance haw status et tu verras la même flotte propre. C’est tout l’intérêt de l’idempotence : le lock, pas la branche, est désormais la source de vérité.

🧭 5. Vois la dérive de tes propres yeux

La reproductibilité n’est utile que si tu peux détecter quand l’arbre s’écarte de la référence. Provoquons cela exprès. Déplace un dépôt vers un commit différent à la main :

cd hello-world
git checkout HEAD~1     # recule d'un commit — maintenant HEAD ≠ le SHA verrouillé
cd ..

Demande à haw ce qu’il en pense :

haw status
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  (detached)                553c2077   -      YES
spoon-knife  main                      d0dd1f61   -      -

La voilà — DRIFT: YES sur hello-world. Faire un checkout d’un commit précis détache aussi HEAD (d’où (detached) dans la colonne BRANCH), et son HEAD ne correspond plus au SHA verrouillé. haw status l’a signalé, mais pour la CI tu veux une commande qui échoue en cas de dérive. C’est verify :

haw verify

verify vérifie que l’arbre sur disque correspond à haw.lock et sort avec le code 3 quand ce n’est pas le cas — un garde-fou de dérive propre et scriptable. Vérifie le code de sortie :

haw verify; echo "code de sortie : $?"
  ✗ hello-world  drift (head != lock)
verify failed: 1 repo(s) diverge from haw.lock
exit code: 3

Ce code de sortie 3 est ce sur quoi un pipeline CI se cale : « l’arbre a dérivé du lock — arrête le build. » (Quand tout correspond, verify affiche verified: tree matches haw.lock (2 repos) et sort avec 0.)

Wide-eyed, shocked reaction

haw verify détectant la dérive dans la CI juste avant une release.

Maintenant remets-le en place. haw sync restaure chaque dépôt à son SHA verrouillé :

haw sync
haw verify; echo "code de sortie : $?"
verified: tree matches haw.lock (2 repos)
exit code: 0

Propre à nouveau. Tu viens de voir la boucle complète : déclarer → synchroniser → épingler → détecter la dérive → restaurer.

Tu viens de faire la boucle fondamentale. Déclarer l’intention, l’épingler, détecter la dérive et restaurer la référence — les quatre mêmes mouvements passent à l’échelle de deux dépôts octocat à une flotte de cent dépôts.

🙌 À toi de jouer

Prouve que le lockfile est vraiment la source de vérité :

  • Ouvre haw.lock et trouve le SHA épinglé de chaque dépôt dans son champ rev. Confirme que c’est un commit de 40 caractères, pas un nom de branche, et que source-rev montre toujours ce que tu as déclaré.
  • Fais dériver un dépôt exprès (cd hello-world && git checkout HEAD~1 && cd ..), lance haw verify; echo $?, et confirme que tu obtiens le code de sortie 3.
  • Lance haw sync pour restaurer, puis haw verify; echo $? à nouveau — retour à exit 0. Cet aller-retour, c’est la garantie de reproductibilité.

✅ Récapitulatif

  • haw sync clone chaque dépôt et écrit haw.lock — de vrais clones Git, sans sous-modules/liens symboliques. C’est idempotent.
  • haw tree montre la forme stack→dépôt ; haw status montre branche/HEAD/dirty/drift par dépôt.
  • haw.lock épingle chaque dépôt à un SHA exact de 40 caractères dans rev, avec source-rev (ton intention déclarée) et branch à côté — committe-le pour des rebuilds identiques octet pour octet.
  • Drift = HEAD diffère du lock. haw status le signale ; haw verify sort avec le code 3 en cas de dérive (ton garde-fou CI). haw sync restaure la référence.

👉 La suite

Tu sais composer et épingler une flotte — maintenant vivons dedans. Découvre le cockpit qui pilote tout ça au clavier → 4. Le cockpit TUI.

4. Le cockpit TUI

Tout ce que tu as fait en ligne de commande a un foyer : un cockpit vivant, piloté au clavier — ta salle de contrôle pour toute la flotte. C’est dans l’esprit de k9s ou htop : une interface terminal plein écran où tu vois la flotte, tu plonges dans n’importe quel dépôt, PR ou run CI, et tu agis — fusionner, approuver, checkout — sans jamais quitter le terminal.

Tu l’as croisé un instant au chapitre 1. Maintenant que tu as une vraie flotte, faisons la visite guidée complète. Ouvre-le et suis le mouvement.

The hawser TUI cockpit dashboard

La salle de contrôle pour toute la flotte — lire, plonger et agir sans quitter le terminal.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Ouvrir le cockpit avec un simple haw (ou haw dash –demo pour explorer hors ligne).
  • Lire la grille de flotte en direct — les mêmes colonnes que haw status, qui se rafraîchissent d’elles-mêmes.
  • Maîtriser la boucle centrale : lire → entrer (Enter) → agir → revenir (Esc).
  • Agir depuis la rangée de repos : synchroniser, marquer, exécuter, filtrer, parcourir les fichiers, et sauter vers les vues PR / CI / gouvernance avec les chiffres 17.
  • Ouvrir le menu d’actions contextuel d’une vue avec a — fusionner, approuver, checkout, request-PR, land — chacune protégée par une confirmation.
  • Parcourir les fichiers de n’importe quel dépôt à n’importe quelle branche, tag ou SHA — en local ou directement depuis la forge, sans checkout.
  • Utiliser la barre de commande (:) qui reflète la CLI que tu connais déjà.

Ta salle de contrôle : la grille de la flotte, les plongées, les vues PR/CI et les actions au clavier — le tout dans le terminal.

🚁 1. Ouvre-le

Lance haw sans sous-commande :

haw

Tu veux juste explorer sans espace de travail réel ni réseau ? Utilise le contrôleur de démo intégré — il est peuplé de dépôts, PR et runs CI préfabriqués, donc chaque vue a quelque chose à montrer :

haw dash --demo

Dans les deux cas, tu atterris sur la grille de la flotte — l’écran d’accueil du cockpit.

📋 2. Lis la grille de la flotte

 haw ▸ ~/work/gateway ───────────────────────── stack: gateway   lock: ✓   repos: 3/3
──────────────────────────────────────────────────────────────────────────────────────
   REPO        BRANCH ▲      HEAD       DIRTY   DRIFT   ↑ / ↓    MERGE
   kernel      v6.1.2        a1b2c3d4     ·       ·      0 / 0     —
 ◉ hal         main          9f8e7d6c    yes      ·      2 / 0     —
▸⚠ app-mqtt    release/2.x   4d5e6f7a     ·      DRIFT   0 / 5     —
──────────────────────────────────────────────────────────────────────────────────────
 hal  ›  path hal/   branch main (ahead 2)   dirty   locked 9f8e7d6c   grp firmware
──────────────────────────────────────────────────────────────────────────────────────
 [s]ync [f]iles [x]shell [!]exec [/]filter [p]roblems [a]ctions [1-7]views [:]cmd [?]help

C’est haw status, vivant. Chaque ligne est un dépôt ; les colonnes sont les mêmes que tu connais déjà — branche, HEAD, dirty, drift, en avance/en retard. Le est ton curseur, marque un dépôt sélectionné et signale un problème (comme la dérive sur app-mqtt). La grille s’actualise automatiquement environ toutes les 5 secondes en veille — jamais pendant que tu tapes — et F5 / Ctrl-R actualisent à la demande.

Déplace-toi avec / (ou k/j), exactement comme Vim.

🔬 3. Plonge — la boucle fondamentale est lire → plonger → agir

Place le curseur sur un dépôt et appuie sur Enter. Tu plonges dans le détail Git de ce dépôt : branche, SHA, statut de l’arbre de travail, log récent, diffstat, remotes. Appuie sur Esc (ou b) pour remonter d’un niveau.

C’est le rythme de tout le cockpit : lire la grille → plonger dans une chose → agir dessus → ressortir. Tu n’es jamais à plus d’une touche du détail ou de l’action.

⌨️ 4. Agis sur la flotte depuis la rangée de repos

Des touches uniques sur la ligne du curseur font des choses. Les essentielles :

ToucheFait
ssync — les dépôts marqués s’il y en a, sinon le dépôt sous le curseur, sinon la stack
fparcourir les fichiers du dépôt — disque local ou directement depuis la forge
xbasculer dans un shell dans ce dépôt (quitte le cockpit)
!lancer une commande (exec) dans le dépôt, dans sa vue de détail
/filtrer la grille en direct de façon floue — /knl restreint à kernel
pvue problèmes uniquement — seulement les dépôts qui demandent de l’attention
Spacemarquer / démarquer le dépôt sous le curseur ()
rrun une commande — sur les dépôts marqués s’il y en a, sinon toute la flotte
ggoto — quitter et afficher le chemin du dépôt sous le curseur (cd "$(haw dash)")

Le git-fetch, le changement de stack et le lock ont migré vers la barre de commande — :fetch, :stack (sélecteur) ou :stack NAME, et :lock. Une touche que tu utiliseras partout est a — le menu d’actions. Il ouvre une petite popup listant exactement les actions que la vue courante prend en charge, chacune avec sa propre sous-touche ; choisis-en une et toute action d’écriture (fusionner, atterrir, …) te demande quand même y/n d’abord. Les vues sans action te le disent tout simplement.

Astuce : Les marques sont le super-pouvoir du cockpit. Appuie sur Space sur quelques dépôts, puis s (sync) ou r (run) agit sur ce seul ensemble. C’est comme ça que tu réalises une opération chirurgicale sur la flotte sans toucher à un manifeste.

🌐 5. Les vues réseau — PR, CI et agir dessus

Changer de vue tient en une touche : les chiffres 17 sautent directement vers une vue depuis n’importe quelle liste de premier niveau. Les vues réseau se chargent à la demande (rien ne touche le réseau tant que tu ne le demandes pas) :

ToucheVuealias :
1flotte:fleet
2changesets:changesets
3PR/MR — toutes les PR/MR ouvertes à travers la flotte:prs
4runs CI — runs récents, progression en direct:ci
5arbre:tree
6gouvernance — plugins, SBOM, conclusions:governance
7plugins:plugins

Dans les vues PR/MR (3) ou CI (4), Enter plonge dans le détail — les relecteurs et les checks d’une PR, ou les jobs et la progression en direct d’un run CI. Appuie sur d pour lire le diff d’une PR, l pour les logs d’un run CI, et f pour parcourir les fichiers modifiés d’une PR. (Un log CI que la forge a périmé — GitHub renvoie 410 — s’affiche honnêtement comme « logs indisponibles — expirés ou vides » plutôt que comme une erreur.)

Pour agir, appuie sur a pour le menu d’actions. Dans la vue PR/MR, il propose :

Sous-toucheFait
mfusionner la PR/MR sur sa forge
aapprouver la PR/MR
ccheckout la branche de la PR en local

Chaque écriture est protégée par une confirmation y/n pour que tu ne fusionnes jamais par erreur de doigt. o ouvre la ligne sous le curseur dans ton navigateur. Ainsi tout le flux inter-forges du chapitre 5 — voir les PR, approuver, fusionner — est ici même, au clavier uniquement, le tout sous a.

📂 6. Parcourir fichiers & branches — n’importe quelle ref, locale ou forge

Appuie sur f sur n’importe quel dépôt pour ouvrir son navigateur de fichiers — une vue en lecture seule (il ne stage ni ne committe jamais, il lit seulement). Trois touches le transforment en véritable explorateur de code :

ToucheFait
Tbasculer entre la liste plate à un seul répertoire ⇄ un arbre navigable (/ pour déplier/replier, /Enter déplier, replier)
rsélecteur de ref — choisis une branche ou un tag dans la liste, ou tape n’importe quel SHA ; la vue se recharge à hauteur de cette ref et l’en-tête affiche @ <ref>
eéditer le fichier sous le curseur dans ton $EDITOR (fichiers locaux uniquement)
Rbasculer entre le disque local ⇄ la forge

La récompense : avec r tu peux lire un fichier tel qu’il existe sur n’importe quelle branche du remote — à travers GitHub, GitLab ou Bitbucket — sans rien checkout. Pointe vers la branche feature/x d’un collègue, lis le fichier directement depuis l’API de la forge, et ne touche jamais à ton arbre de travail. e est la seule exception à la lecture seule : il remet le fichier à ton éditeur pour une correction locale rapide, puis recharge la liste.

💬 7. La barre de commande — un seul langage pour la CLI et la TUI

Appuie sur : pour ouvrir la barre de commande (la palette de commandes). Ses verbes reflètent la CLI que tu as déjà apprise, et la ligne de statut renvoie la commande exacte que chacun lance — ainsi le cockpit sert aussi de moyen de découvrir la CLI :

:sync              sync the current stack
:grep TODO         fleet-wide grep
:switch platform   switch to another stack
:change land FEAT-42
:theme nord        change the skin live

Apprends-en une, connais les deux. :name amène aussi le curseur sur un dépôt par son nom.

🎨 8. Deux vues de plus, et les thèmes

  • 7 (:plugins) — la vue Plugins : chaque plugin disponible (les clés [plugins] du manifeste réunies avec les exécutables haw-* sur ton PATH) ; Enter en lance un et affiche sa sortie dans un panneau. Tu en construiras un qui atterrira ici au chapitre 8.
  • :errors (:err) — la vue Errors : un journal déroulant des échecs de cette session, pour qu’une erreur passagère ne disparaisse jamais du défilement avant que tu aies pu la lire.

Thèmes. Sept habillages intégrés — classic, catppuccin (par défaut), dracula, nord, gruvbox, solarized, monochrome. classic est un habillage neutre en ANSI standard qui se lit aussi bien sur les terminaux clairs que sombres. Tape :theme sans argument pour ouvrir un sélecteur interactif (j/k pour se déplacer, Enter pour appliquer) ; l’habillage choisi est appliqué en direct et enregistré dans ta configuration. :theme nord change toujours directement. Définis-en un au démarrage avec HAW_THEME=nord haw, et NO_COLOR force monochrome.

Personnalise-le — ~/.config/haw/config.toml. Optionnel et tolérant (un fichier absent ou partiel utilise simplement les valeurs par défaut). Définis un thème de démarrage, l’éditeur de la touche e, un en-tête replié et la cadence de rafraîchissement au repos — en plus de remapper un sous-ensemble sûr de touches d’action :

[ui]
theme = "classic"        # startup skin
editor = "nvim"          # the `e` key's editor ($VISUAL/$EDITOR still win)
compact_header = false   # start with the one-line header
refresh_secs = 5         # idle auto-refresh cadence (2–60)

[keys]                   # remap: sync, goto, run, shell, files, problems, watch
sync = "s"

Les sélecteurs écrivent ce fichier pour toi : :editor (sans argument) liste les éditeurs de ton PATH, et :compact bascule l’en-tête replié — chacun persiste ton choix. Les globales gelées (j, k, :, /, ?, q, b, space, g, w, chiffres) ne peuvent pas être réassignées ; un remap invalide est ignoré avec une note au démarrage, et les indices de l’en-tête montrent toujours la touche réellement active.

Appuie sur ? à tout moment pour l’aide en surimpression, et q (ou Ctrl-C) pour quitter.

Astuce : Chaque action lourde tourne sur un worker en arrière-plan, donc l’interface ne se fige jamais pendant qu’une synchro, un fetch ou un appel à la forge est en cours. Continue de naviguer.

🙌 À toi de jouer

Aucun espace de travail ni réseau nécessaire — le contrôleur de démo a tout ce qu’il faut pour bricoler :

  • Lance haw dash –demo. Déplace-toi avec /, puis appuie sur Enter pour entrer dans un dépôt et Esc pour revenir — ressens le rythme lire → entrer → agir → revenir.
  • Appuie sur Space pour marquer deux ou trois dépôts (regarde le ), puis / pour filtrer la grille en fuzzy. Appuie sur 3 pour les PR et 4 pour la CI, puis a pour voir le menu d’actions. Appuie sur ? pour l’aide en surimpression.
  • Appuie sur f sur un dépôt pour parcourir ses fichiers, puis T pour l’arborescence et r pour lire un fichier à une autre branche ou tag. Appuie sur b pour revenir.
  • Appuie sur : et tape :theme nord. Le thème change en direct — et la ligne de statut affiche la commande exacte, si bien que le cockpit t’apprend la CLI au fur et à mesure.

✅ Récapitulatif

  • haw seul (ou haw dash --demo) ouvre le cockpit — un haw status vivant sur lequel tu peux agir.
  • La boucle est lire → plonger (Enter) → agir → revenir (Esc).
  • Touches de la flotte : s sync, f fichiers, x shell, ! exec, / filtrer, p problèmes, Space marquer, r run, g goto, a actions.
  • Les sauts de vue sont les chiffres 17 : 1 flotte, 2 changesets, 3 PR, 4 CI, 5 arbre, 6 gouvernance, 7 plugins. Dans les PR/CI, a (actions) fait fusionner/approuver/checkout (protégé par confirmation), d diff, l logs, f fichiers de la PR.
  • Navigateur de fichiers (f) : T bascule l’arbre, r choisit une branche/tag/SHA (lire n’importe quelle ref, locale ou forge, sans checkout), e édite en local, R bascule entre local ⇄ forge.
  • : est une barre de commande (palette) qui reflète la CLI ; :plugins et :errors atteignent ces vues ; sept thèmes (dont classic) via :theme / HAW_THEME, un sélecteur interactif :theme/:editor, et un ~/.config/haw/config.toml pour les thèmes, l’éditeur, les options d’affichage et les raccourcis personnalisés.

👉 La suite

Tu as vu tout le cockpit lire la flotte. Passons maintenant au mouvement signature qu’il pilote — livrer une fonctionnalité à travers plusieurs dépôts comme un seul changeset coordonné → 5. Les changesets à travers les dépôts.

5. Les changesets à travers les dépôts

C’est la fonctionnalité emblématique de hawser — celle qui justifie à elle seule tout l’outil. Dans ce chapitre, tu vas prendre une seule fonctionnalité qui touche plusieurs dépôts et la piloter comme un tout : une branche commune à tous, des pull requests reliées entre elles, et une fusion qui atterrit dans le bon ordre.

Reviewing cross-repo pull requests as one changeset

Une fonctionnalité, un seul flux de revue — même quand le code est réparti sur plusieurs dépôts et plusieurs forges.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Comprendre ce qu’est un changeset : une fonctionnalité = une branche sur N dépôts.
  • Démarrer un changeset avec haw change start — la même branche dans chaque dépôt, en un seul geste.
  • Suivre toute la fonctionnalité sur un seul écran avec haw change status.
  • Ouvrir des PR/MR liées entre elles avec haw change request — sur GitHub, GitLab et Bitbucket.
  • Fusionner dans l’ordre des dépendances avec haw change land, pour que main ne casse jamais.

Une fonctionnalité, plusieurs dépôts : change start crée la branche partout, status agrège, request ouvre les PR reliées.

😤 1. La douleur que ça supprime

Repense à la dernière fois qu’une fonctionnalité s’étalait sur plusieurs dépôts. Tu as fait cette danse :

  1. git checkout -b feature dans le dépôt A. Puis le dépôt B. Puis le dépôt C.
  2. Pousser chacun. Ouvrir une PR dans A. Puis B. Puis C.
  3. Courir après les revues sur trois pages de PR distinctes.
  4. Les fusionner — et ne pas oublier que la bibliothèque partagée doit fusionner avant les services qui en dépendent, sinon main casse.

Rien ne reliait ces trois PR entre elles. Il n’existait aucun artefact unique qui disait “ce sont une seule fonctionnalité.” Un changeset est exactement cet artefact : une fonctionnalité = une branche à travers N dépôts, avec des PR/MR reliées et un atterrissage ordonné.

Les changesets ont besoin d’une vraie forge (GitHub, GitLab ou Bitbucket) et d’un token pour ouvrir les PR. Les étapes start et status sont locales et sans danger à essayer partout ; request et land dialoguent avec la forge. On te signalera laquelle est laquelle.

🌱 2. Démarre le changeset

Tu donnes un nom à la fonctionnalité et (optionnellement) les dépôts qu’elle touche :

haw change start FEAT-42 --repos api,billing,proto
changeset `FEAT-42` started across 3 repo(s):
  proto    -> change/FEAT-42
  billing  -> change/FEAT-42
  api      -> change/FEAT-42

En un seul geste, haw a créé la même branchechange/FEAT-42 par défaut — dans chacun des trois dépôts. Maintenant tu fais tes modifications et tu committes dans chaque dépôt comme du Git normal. Le nom de la branche les relie entre eux.

Quelques options utiles au démarrage :

  • --repos a,b,c — se limiter aux dépôts que la fonctionnalité touche (par défaut : tous les dépôts).
  • --branch <name> — utiliser un nom de branche personnalisé au lieu de change/<id>.
  • --skip-branch — adopter la branche sur laquelle chaque dépôt se trouve déjà, au lieu d’en créer une.
  • --label <l> — attacher un label (répétable) qui sera transmis aux PR/MR plus tard.

📊 3. Regarde-la prendre forme — change status

À tout moment, retrouve toute la fonctionnalité sur un seul écran :

haw change status FEAT-42

Ceci agrège, par dépôt : la branche, si chaque dépôt est dessus, s’il est sale, son HEAD — et une fois les PR existantes, l’état de revue et le statut CI de chaque PR/MR. Avant l’ouverture de toute PR, ça ressemble à ceci (lancé sur le changeset DEMO-1 de l’« À toi de jouer » ci-dessous, donc les dépôts sont les deux clones octocat) :

changeset `DEMO-1`
REPO         BRANCH                   ON IT     DIRTY  HEAD       PR
hello-world  change/DEMO-1             yes       -      7fd1a60b   —
spoon-knife  change/DEMO-1             yes       -      d0dd1f61   —
(no PR/MRs yet — open them with `haw change request DEMO-1`)

Au lieu de trois onglets de navigateur, un seul tableau de bord. C’est l’équivalent, pour un changeset, de haw status.

Astuce : Ajoute --format json à change status (il émet un document stable haw.change-status/1) quand tu veux rediriger l’état vers un autre outil ou un script :

$ haw change status DEMO-1 --format json
{
  "id": "DEMO-1",
  "repos": [
    {
      "branch": "change/DEMO-1",
      "dirty": false,
      "head": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
      "missing": false,
      "name": "hello-world",
      "on_branch": true,
      "pr": null
    }
  ],
  "schema": "haw.change-status/1"
}

🔀 4. Ouvre les pull requests — change request

Quand tes branches sont poussées et prêtes, une seule commande ouvre des PR/MR reliées entre elles — une par dépôt — sur la forge où réside chaque dépôt :

haw change request FEAT-42

Voici la partie discrètement puissante : tes dépôts n’ont pas tous à être sur la même forge. haw parle GitHub, GitLab et Bitbucket, donc une fonctionnalité qui couvre un service GitHub et une bibliothèque GitLab obtient une PR sur GitHub et une MR sur GitLab, reliées entre elles, depuis cette unique commande. Tous les labels que tu as passés à start sont transmis ici. Cible une branche de base précise avec --base <branch>.

Cette étape a besoin d’un token de forge dans ton environnement — par ex. export GITHUB_TOKEN=$(gh auth token). haw lit les tokens uniquement depuis les variables d’environnement, ne les stocke jamais. Les étapes en lecture seule (start, status) n’ont besoin d’aucun token.

🛬 5. Atterris dans l’ordre des dépendances — change land

Les revues sont faites, les checks sont au vert. Maintenant fusionne — mais dans le bon ordre. Rappelle-toi que proto est une bibliothèque partagée dont api et billing dépendent. Fusionner un service avant que sa bibliothèque n’atterrisse, c’est comme ça qu’on casse main.

haw connaît l’ordre parce que ton manifeste le déclare. Souviens-toi de la clé deps de l’exemple microservices :

[repo.gateway]
deps = ["proto"]     # proto doit atterrir avant gateway

Donc land fusionne les PR/MR dans un ordre topologique stable — les dépendances d’abord — et s’arrête à la première défaillance plutôt que de laisser un fouillis à moitié fusionné :

haw change land FEAT-42
landing changeset `FEAT-42` in dependency order:
  proto    ✓ merged
  billing  ✓ merged
  api      ✓ merged
changeset `FEAT-42` landed.

Étape de forge — nécessite un token. Comme request, land dialogue avec la forge et requiert un token (par ex. export GITHUB_TOKEN=$(gh auth token)) ainsi que des droits de fusion sur chaque dépôt. La sortie ci-dessus est illustrative : contre les dépôts publics en lecture seule octocat qu’utilise ce cours, request/land ne peuvent ni pousser ni fusionner, donc ne t’attends pas à ces lignes sans tes propres dépôts accessibles en écriture et un token.

proto a fusionné en premier parce que tout en dépend ; les services ont suivi. Une seule commande, le bon ordre, pas de main cassé.

Jubilant celebration reaction

Une commande. Cinq dépôts fusionnés dans le bon ordre. On l’a fait.

change land fusionne les PR/MR reliées dans l’ordre topologique — les dépendances d’abord — et s’arrête à la première défaillance.

🖼️ 6. La valeur, en une seule image

        one feature (FEAT-42)
                │
   ┌────────────┼────────────┐
 proto        billing        api          ← change start:  one branch across N repos
   │            │             │
 MR/PR        PR             PR            ← change request: cross-linked, any forge
   │            │             │
   └──── land in deps order ──┘            ← change land:  proto → billing → api

Tu n’as jamais perdu de vue quelles PR étaient « la fonctionnalité », tu n’as jamais fusionné dans le mauvais ordre, et tu as tout piloté depuis quatre commandes. C’est tout l’intérêt d’un changeset : une fonctionnalité multi-dépôts qui se comporte comme un changement unique et cohérent.

Astuce : Tu travailles sur plusieurs dépôts et tu veux sauter dans l’un d’eux ? haw change goto FEAT-42 <repo> affiche son chemin pour que tu puisses faire cd "$(haw change goto FEAT-42 api)". Et haw change snapshot save <name> enregistre la branche + le HEAD de chaque dépôt pour que tu puisses restaurer plus tard l’état multi-dépôts exact.

🙌 À toi de jouer

La moitié locale du flux ne nécessite aucun jeton de forge, alors essaie-la dans un espace de travail dès maintenant :

  • Lance haw change start DEMO-1 –repos hello-world,spoon-knife et confirme que la même branche change/DEMO-1 apparaît dans les deux dépôts (jette un œil avec haw run ‘git branch –show-current’).
  • Lance haw change status DEMO-1 et lis l’agrégat branche/dirty/HEAD — un seul tableau de bord au lieu de deux onglets.
  • Maintenant esquisse-le sur papier : pour une vraie fonctionnalité couvrant une lib partagée et deux services, dans quel ordre doivent-ils land ? (La lib d’abord — c’est la clé deps qui fait son travail.)

✅ Récapitulatif

  • Un changeset est une fonctionnalité à travers N dépôts : une branche partagée, des PR/MR reliées, une fusion ordonnée.
  • haw change start <id> --repos a,b,c crée la même branche dans chaque dépôt.
  • haw change status <id> agrège branche + revue + CI sur toute la fonctionnalité.
  • haw change request <id> ouvre des PR/MR reliées entre elles sur GitHub, GitLab et Bitbucket.
  • haw change land <id> fusionne dans l’ordre des dépendances (d’après le deps de chaque dépôt), en s’arrêtant à la première défaillance.
  • start/status sont locales ; request/land ont besoin d’un token de forge dans l’environnement.

👉 La suite

Tu sais livrer une fonctionnalité à travers la flotte. Maintenant, compilons, testons et vérifions le tout — le workflow quotidien qui le garde au vert → 6. Compiler, tester et vérifier.

6. Compiler, tester et vérifier

Tu sais composer une flotte, l’épingler et livrer des changesets à travers elle. Maintenant, faisons en sorte qu’elle gagne son pain au quotidien. La promesse d’un outil multi-dépôts est simple : faire quelque chose sur chaque dépôt d’un coup, en parallèle, sans boucle for bricolée à la main — puis verrouiller toute la flotte pour que la CI reste au vert.

Dans ce chapitre, tu vas lancer des commandes arbitraires à travers la flotte, câbler build et test, rechercher dans chaque dépôt d’un seul coup, et utiliser verify comme le garde-fou anti-dérive qui relie le tout. Garde l’espace de travail my-first-stack ouvert.

Running commands across the whole fleet in parallel

Une commande en entrée, toute la flotte traitée en parallèle — sans boucle for bricolée à la main.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Diffuser n’importe quelle commande sur tous les dépôts avec haw run.
  • Déclarer build = / test = par dépôt et piloter toute la flotte avec haw build / haw test — diffusé en direct, avec une couleur par dépôt.
  • Chercher dans tous les dépôts d’un coup avec haw grep.
  • Verrouiller la flotte contre le lock avec haw verify — la barrière anti-dérive de la CI.
  • Maîtriser le rayon d’action avec -j (parallélisme) et --group (portée).

haw run déploie une commande sur chaque dépôt en parallèle ; verify verrouille la flotte contre le lock.

⚙️ 1. Lance n’importe quelle commande sur chaque dépôt — haw run

Le cheval de trait. haw run prend une commande en positionnel et la lance dans chaque dépôt, en parallèle :

haw run 'git log -1 --oneline'
── hello-world ──
7fd1a60 Merge pull request #6 from Spaceghost/patch-1
── spoon-knife ──
d0dd1f6 Pointing to the guide for forking
ran in 2/2 repos

haw regroupe la sortie par dépôt pour que tu saches toujours quel dépôt a dit quoi. Tout ce que tu taperais dans un dépôt fonctionne : git fetch, git status -s, un linter, une commande shell en une ligne.

Astuce : Mets la commande entre guillemets. haw run 'git status -s' passe toute la chaîne comme une seule commande ; sans guillemets, ton shell essaierait d’interpréter les flags lui-même.

🔨 2. Déclare build et test — puis lance-les sur toute la flotte

haw reste agnostique vis-à-vis du système de build : chaque dépôt nomme la commande shell qui le compile ou le teste, et haw les déploie. Tu les déclares dans le manifeste avec build = et test =.

Dans le monde réel, chaque dépôt apporte sa propre chaîne d’outils. Voici la forme, tirée de l’exemple microservices livré :

[repo.proto]
build = "buf generate"
test  = "buf lint"

[repo.gateway]
build = "go build ./..."
test  = "go test ./..."

[repo.billing]
build = "cargo build --release"
test  = "cargo test"

[repo.accounts]
build = "npm ci && npm run build"
test  = "npm test"

Nos dépôts octocat n’ont rien de réel à compiler — et si tu lances haw build maintenant, il te le dit et sort avec un code non nul :

$ haw build
error: no cloned repo declares a `build` command in the manifest

Alors déclarons des commandes triviales pour voir le mécanisme. Ajoute une ligne build = et test = aux deux dépôts dans ton my-first-stack/haw.toml :

[repo.hello-world]
remote = "gh"
repo = "Hello-World.git"
rev = "master"
groups = ["core"]
build = "echo built"
test  = "echo tested"

[repo.spoon-knife]
remote = "gh"
repo = "Spoon-Knife.git"
rev = "main"
groups = ["web"]
build = "echo built"
test  = "echo tested"

Maintenant deux commandes pilotent toute la flotte :

haw build     # lance le `build =` de chaque dépôt, en parallèle
haw test      # lance le `test =` de chaque dépôt, en parallèle
$ haw build
── hello-world ──
built
── spoon-knife ──
built
build ran in 2/2 repos

La sortie est diffusée en direct au fur et à mesure que chaque dépôt s’exécute et regroupée sous un en-tête par dépôt, donc sur un terminal en couleur le flux de chaque dépôt porte sa propre couleur — tu peux lire un build sur toute la flotte sans perdre de vue quel dépôt a produit quelle ligne. Les dépôts qui ne déclarent pas la commande (ou qui ne sont pas clonés) sont simplement ignorés. Et voici le détail qui en fait une brique de CI :

Brique de CI : haw build et haw test sortent avec un code non nul si un dépôt échoue. C’est pourquoi la même commande que tu lances en local s’intègre directement dans un pipeline — le pipeline s’arrête dès que le build d’un dépôt casse.

🔎 3. Cherche dans toute la flotte — haw grep

Besoin de trouver chaque usage d’un symbole, d’un TODO, d’une API obsolète à travers tous les dépôts ? haw grep déploie git grep sur chaque dépôt cloné d’un seul coup :

haw grep guide
spoon-knife (1 hit(s))
  README.md:9:For some more information on how to fork a repository, [check out our guide, "Forking Projects""](http://guides.github.com/overviews/forking/). Thanks! :sparkling_heart:
1 hit(s) in 2 repo(s) for `guide`

Chaque dépôt avec une correspondance obtient un en-tête et ses résultats en dessous, et une ligne finale totalise les résultats et les dépôts fouillés — un dépôt sans correspondance n’apparaît tout simplement pas. Il recherche dans les fichiers suivis via Git, donc c’est rapide et ça ignore gratuitement tes artefacts de build. Limite-le à une seule stack avec --stack <name> quand la flotte est grande.

🛡️ 4. Verify — le garde-fou de la CI

Compiler et tester au vert n’est que la moitié de l’histoire. Avant tout ça, la CI doit prouver que l’arbre est celui que tu as épinglé — aucune dérive par rapport à haw.lock. C’est haw verify, que tu as rencontré au chapitre 3 :

haw verify; echo "code de sortie : $?"
verified: tree matches haw.lock (2 repos)
exit code: 0

verify affirme que l’arbre sur disque correspond au lock et sort avec le code 3 en cas de dérive — un garde-fou propre et scriptable. C’est la première vraie étape de chaque pipeline : synchroniser sur le lock, verify que tu es bien dessus, puis compiler et tester. Le chapitre 7 câble le job complet sync → verify → build → test.

Astuce : verify lit uniquement l’arbre et le lock — aucun réseau, aucun build. C’est assez peu coûteux pour tourner comme un hook git pre-commit (haw hooks install) afin que la dérive n’atteigne même jamais la CI.

🎚️ 5. Maîtrise le rayon d’action : parallélisme et groupes

Deux leviers gardent les commandes sur toute la flotte sous contrôle.

Parallélisme — -j. Par défaut, haw lance jusqu’à min(cores, 8) dépôts à la fois. Plafonne-le quand une tâche est lourde (ou qu’un runner de CI est petit) :

haw test -j 4     # au plus 4 dépôts qui compilent/testent à la fois
haw run -j 1 'git fetch'   # entièrement en série

Groupes — --group. Tu te souviens des labels groups = [...] de ton manifeste ? Ils existent précisément pour que tu puisses agir sur une tranche de la flotte. Dans my-first-stack, hello-world est dans le groupe core et spoon-knife est dans web :

haw status --group core
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  master                    7fd1a60b   -      -

Seulement le dépôt core. Le même filtre --group fonctionne sur sync, status et run (ainsi que build/test), et il est répétable — passe-le deux fois pour sélectionner deux groupes. Un filtre vide signifie tout ; un filtre exclut les dépôts sans groupe.

haw run --group core 'git log -1 --oneline'   # seulement les dépôts core

🗺️ 6. Quand utiliser chacune

Une carte rapide pour choisir le bon verbe sans réfléchir :

Tu veux…UtilisePourquoi
lancer une commande ad hoc partouthaw run '<cmd>'ponctuel, ne fait pas partie du manifeste
compiler tout le produithaw buildlance le build = déclaré de chaque dépôt, code de sortie prêt pour la CI
tester tout le produithaw testlance le test = déclaré de chaque dépôt, échoue à la moindre défaillance
prouver que l’arbre correspond au lockhaw verifygarde-fou anti-dérive, sort avec le code 3 en cas de dérive
trouver du texte à travers tous les dépôtshaw grep <pat>git grep sur toute la flotte, sortie regroupée
se limiter à une partie de la flotteajouter --group <g>agir sur une tranche étiquetée
dompter une exécution lourde ou de CIajouter -j <n>plafonner les dépôts concurrents

La règle générale : run pour les commandes ponctuelles, build/test pour les commandes que tes dépôts déclarent, verify pour verrouiller. Les commandes déclarées sont celles que tu voudras identiques en local et en CI.

🙌 À toi de jouer

Mets la flotte à l’épreuve dans my-first-stack :

  • Ajoute les lignes triviales build = / test = ci-dessus, puis lance haw build et haw test — regarde la sortie diffusée par dépôt et le résumé 2/2 repos.
  • Lance haw grep guide sur toute la flotte (un vrai résultat dans spoon-knife). Puis restreins : haw run –group core ‘git log -1 –oneline’ — seul le dépôt core devrait répondre.
  • Lance haw verify; echo $? et confirme exit 0 sur un arbre propre. Force-le entièrement en série avec haw run -j 1 ‘git status -s’ et regarde les dépôts se traiter un par un.

✅ Récapitulatif

  • haw run '<cmd>' lance n’importe quelle commande dans chaque dépôt en parallèle, sortie regroupée par dépôt.
  • Déclare build = / test = par dépôt ; haw build / haw test les déploient, diffusent en direct la sortie par dépôt, et sortent avec un code non nul à la moindre défaillance — ils font ainsi aussi office d’étapes de CI.
  • haw grep <pattern> est un git grep sur toute la flotte.
  • haw verify verrouille l’arbre contre haw.lock (sort avec le code 3 en cas de dérive) — le premier geste de chaque pipeline.
  • -j N plafonne le parallélisme ; --group G (répétable) restreint les commandes aux dépôts étiquetés.

👉 La suite

Tu sais compiler, tester et verrouiller la flotte en local. Maintenant, faisons monter tout ça en qualité production — confiance, CI, signature et audit → 7. Passer en production.

7. Passer en production

Tu sais composer une flotte, travailler à travers elle, livrer des changesets, la compiler et la tester. Ce dernier chapitre porte sur le fait de faire tout cela pour de vrai — en toute sécurité, en CI, avec une piste d’audit — et sur la seule échappatoire d’extensibilité qui relie les fonctionnalités de gouvernance : les plugins. C’est ce qui distingue un petit outil local sympa d’un outil auquel tu confies le verrouillage de tes releases.

Taking haw to production

D’un petit outil local sympa à un outil auquel tu confies le verrouillage de tes releases — c’est parti.

🎯 Dans ce chapitre, tu vas apprendre à…

  • Intérioriser le modèle de confiance — le manifeste est du code de confiance, les jetons ne vivent que dans l’environnement.
  • Câbler le pipeline CI en quatre temps : sync → verify → build → test.
  • Imposer la reproductibilité avec haw sync –locked et haw verify.
  • Étendre haw avec des plugins — tout haw <name> inconnu lance haw-<name>, sans fork.
  • Distribuer les artefacts avec haw publish, et produire SBOM, provenance et signatures.
  • Regrouper une piste d’audit avec haw evidence et la faire correspondre aux normes de conformité.

🛡️ 1. D’abord, le modèle de confiance — parce que ça compte

Avant de lancer haw sur quoi que ce soit que tu n’as pas écrit, intériorise une règle :

The hawser trust model

Traite un haw.toml comme un Makefile : puissant, et ne lance que ceux en qui tu as confiance.

Le manifeste est du code de confiance. Les commandes build, test, run et exec d’un haw.toml sont exécutées à travers ton shell.

Lancer haw build, haw run ou haw sync sur un checkout non fiable revient à lancer son Makefile. Traite haw.toml exactement comme un Makefile ou un bloc scripts de package.json : ne le lance que sur des manifestes en qui tu as confiance.

Deux corollaires que tu connais déjà à moitié :

  • Les plugins sont des binaires de confiance. haw <name> lance haw-<name> depuis ton PATH avec l’intégralité de ton environnement (y compris les éventuels tokens). N’installe que des plugins en qui tu as confiance ; garde ton PATH propre.
  • Les tokens vivent uniquement dans l’environnement. haw lit les tokens de forge depuis les variables d’environnement au moment de l’appel, et ne les stocke ni ne les journalise jamais. L’authentification du transport Git reste gérée par tes clés SSH / ton credential helper existants — haw n’y touche pas.

Lis le modèle de confiance en entier avant de brancher haw sur quoi que ce soit de partagé.

🧩 2. Étends-le avec des plugins — sans fork

Avant de brancher le pipeline, découvre l’échappatoire qui alimente les fonctionnalités de gouvernance ci-dessous. haw suit le même modèle que git, cargo et kubectl : toute sous-commande que haw ne reconnaît pas est déléguée à un exécutable haw-<name> présent sur ton PATH.

Extending haw with plugins

Un plugin, c’est juste un programme qui lit du JSON et affiche du JSON — écris-en un dans n’importe quel langage.

haw jira sync      # pas intégré → lance `haw-jira sync`

Le plugin s’exécute dans un processus séparé (un plugin défaillant ne peut jamais faire planter haw), haw lui transmet la flotte courante en JSON via HAW_JSON + stdin (haw.plugin/1), et le code de sortie du plugin devient celui de haw — un plugin est donc un gate de CI à part entière. Découvre-les, installe-les et génère-en le squelette :

haw plugins list                     # plugins natifs + installés
haw plugins list --remote            # l'index communautaire
haw plugins install aspice           # délègue à cargo install
haw plugins new mycheck --lang python   # squelette exécutable (rust|python|go|shell)

Le squelette est un plugin complet et exécutable : il lit le contexte, gère --help et --format json, émet un document haw.plugin.report/1, et échoue en mode ouvert en dehors d’un espace de travail. Mets-le sur ton PATH et il devient instantanément une sous-commande haw — sans recompiler haw.

Astuce : les fonctionnalités de gouvernance propres à haw — SBOM, signature, secret-gate — sont livrées sous forme de plugins sur exactement ce modèle, donc rien ici n’est de seconde zone. Le contrat complet, les JSON Schemas et les bindings de langage se trouvent dans Plugins.

La vraie puissance, ce sont les hooks de cycle de vie : abonne un plugin à une phase dans [plugins] et il se déclenche automatiquement autour des opérations de flotte — c’est exactement ainsi que sont branchées les fonctionnalités de chaîne d’approvisionnement ci-dessous.

🔧 3. Le pipeline de CI — toujours les mêmes quatre mouvements

Voici le fruit de tout ce que tu as appris aux chapitres 3 et 6. Un pipeline haw a partout la même forme :

sync  →  verify  →  build  →  test
  • sync l’arbre vers les SHAs épinglés dans haw.lock,
  • verify que l’arbre correspond au lock — le gate de dérive, sortie 3 en cas de dérive,
  • build et test toute la flotte (chacun fait échouer le job avec un code non nul).

Un job GitHub Actions, tout droit sorti du README :

jobs:
  flotte:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4          # le dépôt du manifeste (haw.toml + haw.lock)
      - run: cargo install hawser          # ou télécharge le binaire musl signé
      - run: haw sync --filter=blob:none   # clone partiel → rapide sur les grandes flottes
      - run: haw verify                    # sortie 3 si l'arbre dérive de haw.lock
      - run: haw build
      - run: haw test
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}   # uniquement si une étape appelle l'API de la forge

La CI GitLab, ce sont les quatre mêmes mouvements identiques avec variables: { GITLAB_TOKEN: $CI_JOB_TOKEN }.

Astuce : sur une grande flotte, associe haw sync --filter=blob:none (clone partiel — tout l’historique, blobs paresseux) à un cache du magasin d’objets partagé. Les clones restent rapides sans casser les SHAs épinglés, car chaque commit reste accessible.

♻️ 4. La reproductibilité, imposée

Tout le pipeline repose sur haw.lock. Deux flags le rendent hermétique en CI :

haw sync --locked        # échoue sauf si haw.lock existe (pas de résolution de rev)
haw verify               # vérifie arbre == lock, sortie 3 en cas de dérive

--locked refuse d’inventer un lock à la volée — la CI doit compiler à partir de la baseline committée, jamais résoudre à neuf. verify prouve ensuite que le checkout y correspond. Ensemble, ils garantissent que l’arbre en CI est octet pour octet celui que tu as committé.

📦 5. Distribution — publie ce que la flotte a produit

Une fois la flotte compilée, haw publish téléverse ses artefacts vers un registre d’artefacts generic/raw — Nexus, Artifactory, GitLab ou Bitbucket :

haw publish dist/*.tar.gz --to nexus
haw publish dist/*.tar.gz --to nexus --dry-run   # affiche le plan, pas d'identifiants, pas de réseau

--dry-run affiche exactement ce qui serait téléversé (cible, nom, version, chaque fichier) sans toucher au réseau ni nécessiter d’identifiants — parfait pour brancher l’étape en toute sécurité d’abord. Les identifiants proviennent des variables d’environnement de la cible (par ex. NEXUS_URL, NEXUS_USER, NEXUS_PASS).

🔏 6. Signature, SBOM et provenance — la chaîne d’approvisionnement

C’est là que haw prend toute sa valeur pour les releases sérieuses. Chaque release signée est livrée avec une somme de contrôle .sha256 et une signature cosign sans clé (.sig/.pem) que tu peux vérifier hors ligne — même sur un hôte isolé du réseau (voir Installer hawser).

Pour ta flotte, les fonctionnalités de chaîne d’approvisionnement sont livrées sous forme des plugins de gouvernance que tu as découverts en haut de ce chapitre, abonnés à des phases du cycle de vie :

[plugins]
compliance = ["post-build"]    # SBOM (CycloneDX + SPDX) après un build
artifact   = ["post-land"]     # provenance SLSA/in-toto + signature cosign/minisign
gate       = ["pre-request"]   # le gate secret/hygiène bloque une mauvaise PR avant son ouverture
  • SBOM — une nomenclature (CycloneDX + SPDX) de ce qui est entré dans le build.
  • Provenance — des enregistrements SLSA/in-toto de comment ça a été compilé et par qui.
  • Signature — des signatures cosign/minisign pour que les consommateurs puissent vérifier l’authenticité.

Et quand quelqu’un demande « qu’est-ce qui a été livré exactement ? », haw evidence regroupe le manifeste, le lock, le journal d’audit et le statut dans une seule archive :

haw evidence --out haw-evidence.tar.gz
wrote evidence bundle haw-evidence.tar.gz

(Lancé seul, haw evidence écrit ./haw-evidence.tar.gz par défaut ; --out ne fait que choisir le chemin.)

🏛️ 7. L’angle conformité / automobile

Si tu travailles sous une norme — ISO 26262, DO-178C, Automotive SPICE, CRA — les éléments ci-dessus sont tes preuves, parce qu’ils s’ancrent dans le lock épinglé :

Norme / artefactComment haw la couvre
Automotive SPICEhaw-aspice émet une traçabilité dépôt → SHA épinglé → domaine de processus
MISRA Chaw-misra lance cppcheck --addon=misra sur toute la flotte comme gate pre-request
ISO 26262 / DO-178Carchive haw evidence + SBOM + provenance issus des plugins de gouvernance
AUTOSAR ARXMLdépôts de config épinglés à des SHAs exacts dans haw.lock, versionnés avec le code

Le lock reproductible est le fondement : il transforme « la baseline qui était en production en mars » en un fait exact, re-compilable et auditable, plutôt qu’en une supposition. Voir Domaines et Conformité pour le mapping complet.

🪝 8. Hooks d’intégrité — attrape la dérive avant qu’elle soit committée

Un dernier garde-fou. haw hooks install écrit un hook pre-commit dans chaque dépôt qui lance haw verify — ainsi un commit qui ferait dériver l’arbre du lock est attrapé localement, avant même d’atteindre la CI :

haw hooks install
  ✓ hello-world  pre-commit -> haw verify
  ✓ spoon-knife  pre-commit -> haw verify
installed the integrity pre-commit in 2 repo(s)

haw hooks list affiche les hooks de cycle de vie que l’espace de travail définit (exécutables sous .haw/hooks) — distincts du pre-commit d’intégrité par dépôt ci-dessus. Sur un espace de travail neuf où aucun n’est encore défini, il t’indique où les ajouter :

haw hooks list
no lifecycle hooks — add executables under /path/to/my-first-stack/.haw/hooks

Voilà toute la boucle de production. Lock épinglé, gate de dérive, build/test de flotte, artefacts signés et une archive de preuves — les mêmes primitives servent un projet perso comme un audit ISO 26262.

✅ Récapitulatif

  • Le manifeste est du code de confiance — ne lance haw que sur des haw.toml / plugins en qui tu as confiance ; les tokens restent dans l’environnement, jamais stockés.
  • Le pipeline de CI, c’est toujours sync → verify → build → test ; verify sort avec 3 en cas de dérive.
  • --locked + verify imposent que la CI compile la baseline committée, de manière reproductible.
  • haw publish --to <nexus|artifactory|gitlab|bitbucket> distribue les artefacts (--dry-run pour prévisualiser).
  • Tout haw <name> inconnu lance un plugin haw-<name> depuis le PATH — sans fork ; les plugins de gouvernance ajoutent SBOM, provenance et signature sur les phases du cycle de vie ; haw evidence regroupe la piste d’audit ; les releases sont signées avec cosign.
  • Les mêmes primitives se transposent directement sur la conformité ASPICE / ISO 26262 / DO-178C / CRA.

🎉 Tu l’as fait

Course complete — you've learned hawser

Toute la boucle, cochée — composer, orchestrer, livrer, étendre et gouverner.

Tu es passé de « c’est quoi ce truc, au juste ? » à composer une flotte, l’épingler à un lockfile, vivre dans le cockpit, livrer des changesets cross-dépôts, compiler et tester le tout, l’étendre avec un plugin, et faire tourner l’ensemble en production avec une piste d’audit. Voilà tout l’outil.

Et maintenant :

Maintenant, va composer quelque chose. Bienvenue à bord.

8. Construis un plugin — et laisse Claude écrire tes commits

Pair-programming with Claude to build a haw plugin

Tu apportes l’espace de travail, Claude apporte la prose — du pair-programming pour tes commits et tes PR.

Tu as composé une flotte, tu l’as épinglée, tu as vécu dans le cockpit, tu as livré des changesets et tu les as verrouillés en CI. Maintenant, la partie amusante : tu vas étendre haw toi-même. Et à la fin de ce chapitre, tu auras appris à un assistant IA — Claude — à lire toute ta flotte et à écrire tes commits et le texte de tes pull requests à ta place.

Ce chapitre ne présuppose rien. Si tu n’as jamais écrit de plugin haw, si tu ne sais pas ce qu’est MCP et si tu n’as jamais touché au protocole de plugin, tu es exactement au bon endroit. On définira chaque terme dès sa première apparition, on parcourra chaque commande et on te montrera la sortie que tu dois attendre à chaque étape.

On va construire un vrai plugin — haw-commit-ai — et le faire grandir en deux niveaux :

  • Niveau 1 — les fondations (un seul dépôt). Apprends la mécanique des plugins et la poignée de main MCP un dépôt à la fois. Honnête et simple. À ce niveau, Claude voit déjà tout seul le diff d’un unique dépôt, donc on lui apprend surtout à parler proprement le protocole de haw.
  • Niveau 2 — la puissance inter-dépôts (à l’échelle du changeset). La récompense. Claude, tout seul, ne peut pas voir un changeset couvrant toute la flotte et s’étendant sur plusieurs dépôts. haw le peut. On tend à Claude le diff inter-dépôts combiné et on le laisse écrire une pull request cohérente qui raconte tous les dépôts ensemble. C’est ce que ni Claude ni un outil mono-dépôt comme lazygit ne peuvent faire seuls.

Dans ce chapitre, tu vas apprendre à…

  • Comprendre ce qu’est vraiment un plugin haw, et comment haw le trouve et l’exécute.
  • Lire le contexte haw.plugin/1 que haw remet à chaque plugin, champ par champ.
  • Générer un plugin fonctionnel avec haw plugins new.
  • Émettre les deux formes machine que haw comprend : un haw.plugin.report/1 pour --format json et un panneau haw.plugin.view/1 pour la vue Plugins du cockpit (7).
  • Comprendre ce qu’est MCP, et transformer le même script en serveur MCP pour que Claude puisse appeler de petits outils sûrs.
  • Niveau 2 : remets à Claude le diff inter-dépôts combiné et laisse-le rédiger une seule PR à l’échelle de la flotte — l’histoire inter-dépôts qu’aucun outil mono-dépôt ne peut raconter.

Niveau 1 — les fondations (un seul dépôt)

C’est la base sur laquelle tu construis tout le reste : un dépôt à la fois, et chaque concept dont tu as besoin pour comprendre le protocole de plugin et la poignée de main MCP. C’est volontairement simple. Prends tes marques ici, puis le Niveau 2 débloque la puissance inter-dépôts.

Vois le Niveau 1 comme les petites roues : utile, honnête et volontairement pas encore magique. Reste jusqu’au Niveau 2 — c’est là que ça décolle.

1. Ce qu’est vraiment un plugin haw

Partons de zéro. Un plugin haw n’est rien de plus qu’un programme exécutable nommé haw-<name> qui se trouve quelque part sur ton PATH. C’est toute l’idée. Il n’y a pas de registre de plugins où s’inscrire, pas de bibliothèque dynamique à charger, pas d’API spéciale à lier.

haw suit la même convention que git, cargo et kubectl : quand tu tapes une sous-commande qu’il ne reconnaît pas, il cherche un exécutable correspondant et le lance. Voici le déroulement en mots :

  1. Tu tapes haw commit-ai.
  2. haw vérifie ses sous-commandes intégrées. commit-ai n’en fait pas partie.
  3. haw parcourt chaque répertoire de ton PATH à la recherche d’un exécutable nommé haw-commit-ai.
  4. Il en trouve un, le lance comme un processus séparé et lui transmet tes arguments.
  5. Tout ce que ce programme imprime devient la sortie ; le code de sortie qu’il renvoie devient le code de sortie de haw.

Donc haw commit-ai veut vraiment juste dire « lance le programme haw-commit-ai et donne-lui du contexte sur mon espace de travail ». On appelle ça le dispatch par PATH, et c’est pourquoi tu peux publier haw-jira, haw-sbom ou haw-whatever sans jamais toucher au code source de haw. Un plugin cassé ne peut pas faire planter haw, parce qu’il tourne dans son propre processus.

Tu peux voir exactement quels répertoires haw analyse :

haw plugins path      # affiche les répertoires du PATH où haw cherche les binaires haw-*

On va construire un plugin appelé haw-commit-ai. Il a deux visages, tous deux issus d’un seul script :

  • En tant que plugin ordinaire (haw commit-ai), il rédige des messages de commit et du texte de PR à partir de tes diffs.
  • En tant que serveur MCP, il laisse Claude lire ton espace de travail et tes diffs et écrire lui-même le vrai texte de commit et de PR — en toute sécurité.

On fera d’abord le plugin simple, puis on ajoutera le visage MCP.

2. Le contexte que haw tend à chaque plugin : haw.plugin/1

Quand haw lance ton plugin, il ne se contente pas de démarrer un programme aveugle. Il tend au plugin un contexte : un document JSON qui décrit ton espace de travail. Ce document porte un nom de schéma, haw.plugin/1, et c’est le contrat entre haw et chaque plugin.

D’où vient-il ? haw fournit le même JSON à deux endroits, pour que tu lises celui qui t’arrange :

  • la variable d’environnement HAW_JSON, et
  • l’entrée standard (stdin) du plugin.

Le contenu est identique dans les deux. La variable d’environnement est en général plus pratique parce que sa lecture ne bloque jamais.

Qu’y a-t-il dedans ? Voici un exemple réel du contexte à l’intérieur d’un espace de travail :

{
  "schema": "haw.plugin/1",
  "root": "/path/to/workspace",
  "stack": "gateway",
  "repos": [
    { "name": "kernel", "path": "/path/to/workspace/kernel", "rev": "v6.1.2", "groups": ["firmware"] },
    { "name": "hal",    "path": "/path/to/workspace/hal",    "rev": "main",   "groups": ["firmware"] }
  ]
}

Lisons-le champ par champ :

  • schema — toujours "haw.plugin/1". Il t’indique quelle version du contrat tu as sous les yeux.
  • root — le chemin absolu vers la racine de l’espace de travail (le répertoire qui contient ton manifeste). Tout ce que le plugin écrit doit rester à l’intérieur.
  • stack — le nom de la stack active (la sélection nommée de dépôts avec laquelle tu travailles). Ici, c’est "gateway".
  • repos — la liste des dépôts en jeu. Chaque entrée a :
    • name — le nom court du dépôt (kernel, hal).
    • path — son emplacement absolu sur le disque. C’est le champ important : pour lancer git diff ou git commit sur un dépôt, tu appelles un shell dans ce path.
    • rev — la révision épinglée (un tag comme v6.1.2 ou une branche comme main).
    • groups — les groupes auxquels le dépôt appartient (["firmware"]).

Lancé en dehors d’un espace de travail, le contexte se réduit à un simple {"schema": "haw.plugin/1"} — pas de root, pas de repos. Un plugin bien élevé vérifie si root et repos sont présents et fait quelque chose de sensé quand ils sont absents.

Voici comment lire ce contexte en Python, étape par étape. Lis d’abord la variable d’environnement ; si elle est vide, replie-toi sur stdin ; s’il n’y a rien du tout, renvoie le contexte minimal pour que le plugin ne plante jamais :

import json, os, sys

def read_context() -> dict:
    raw = os.environ.get("HAW_JSON", "")       # 1. préférer la variable d'env (ne bloque jamais)
    if not raw and not sys.stdin.isatty():     # 2. se replier sur stdin s'il est passé en pipe
        raw = sys.stdin.read()
    if not raw:                                # 3. rien du tout → contexte minimal
        return {"schema": "haw.plugin/1"}
    try:
        ctx = json.loads(raw)                  # 4. parser le JSON
    except ValueError:
        return {"schema": "haw.plugin/1"}      # 5. mal formé → dégradation gracieuse
    return ctx if isinstance(ctx, dict) else {"schema": "haw.plugin/1"}

Une fois que tu as ctx, tout le reste n’est que du travail shell ordinaire : ctx["repos"] te donne le path sur disque de chaque dépôt, et git diff / git commit ne sont que des appels de sous-processus dans ce chemin.

3. Les trois formes de sortie qu’un plugin peut imprimer

Un plugin peut imprimer trois types de sortie différents, selon la façon dont il est appelé. Tu n’es pas obligé de tous les prendre en charge, mais un bon plugin le fait :

  1. Texte brut — le comportement par défaut. Quand quelqu’un lance haw commit-ai dans un terminal, imprime un texte convivial et lisible par un humain.
  2. Un rapport machine — appelé avec --format json, imprime un document haw.plugin.report/1 : {schema, plugin, ok, summary, findings}. Les outils et la CI parsent ça au lieu de gratter du texte humain.
  3. Un panneau de cockpit — quand haw veut afficher ton plugin dans le cockpit TUI, il définit la variable d’environnement HAW_RENDER=1 et place "intent": "render" dans le contexte. Ton plugin imprime alors un document haw.plugin.view/1 : {schema, title, lines[]}. haw dessine ces lignes dans la vue Plugins du cockpit (appuie sur 7).

Les trois schémas JSON vivent dans schemas/ — ce sont la source de vérité pour chaque champ.

4. Générer le squelette du plugin

Tu n’as pas à écrire tout ça depuis un fichier vide. haw génère un squelette exécutable qui implémente déjà le contrat pour toi.

Prérequis : tu as besoin de Python 3 installé. Vérifie-le :

python3 --version      # n'importe quel Python 3.x récent convient

Génère maintenant le squelette :

haw plugins new commit-ai --lang python
created ./haw-commit-ai/haw-commit-ai   (executable, python3)
created ./haw-commit-ai/README.md
next:
  chmod is already set — drop it on PATH:
    PATH="$PWD/haw-commit-ai:$PATH" haw commit-ai

Deux fichiers apparaissent dans un nouveau répertoire ./haw-commit-ai/ :

  • haw-commit-ai — l’exécutable du plugin lui-même (un script Python avec un shebang #!/usr/bin/env python3, déjà marqué exécutable).
  • README.md — des notes pour le plugin.

Le squelette lit déjà $HAW_JSON, gère --help et --format json, et émet un haw.plugin.report/1. C’est un plugin correct et fonctionnel tel quel. Dans les sections suivantes, on remplacera son corps par notre version capable de MCP.

Pourquoi Python ? Parce que le SDK MCP qu’on utilisera côté Claude est pensé d’abord pour Python. Le visage plugin reste sans dépendance (bibliothèque standard uniquement) ; seul le visage --mcp a besoin d’un paquet supplémentaire, qu’on installera plus tard avec pip install mcp.

5. Les visages du plugin : texte humain, rapport JSON et panneau de cockpit

Construisons le plugin. On va l’introduire morceau par morceau pour que rien ne reste mystérieux, puis on te montrera le script complet.

D’abord, quelques petits utilitaires. context_repos extrait la liste des dépôts du contexte en toute sécurité, et _run est un mince wrapper autour de l’exécution d’une commande shell et de la capture de sa sortie :

import json, os, subprocess, sys

def context_repos(ctx):
    r = ctx.get("repos")
    return [x for x in r if isinstance(x, dict)] if isinstance(r, list) else []

def _run(cmd, cwd=None):
    p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=False)
    return p.returncode, p.stdout, p.stderr

def repo_diff_text(path):                        # changements indexés + non indexés par rapport à HEAD
    rc, out, _ = _run(["git", "-C", path, "diff", "HEAD"], cwd=path)
    return out

Ensuite, changeset_repos détermine sur quels dépôts agir. Il demande à haw quels dépôts le changeset courant a touchés ; si ça revient vide, il se replie sur tout dépôt ayant des changements sales (non committés) :

def changeset_repos(ctx):                        # dépôts touchés, sinon dépôts sales
    root, repos = ctx.get("root"), context_repos(ctx)
    if root:
        rc, out, _ = _run(["haw", "change", "status", "--format", "json"], cwd=root)
        if rc == 0 and out.strip():
            try: data = json.loads(out)
            except ValueError: data = {}
            names = {r.get("name") for r in data.get("repos", []) if isinstance(r, dict)}
            touched = [r for r in repos if r.get("name") in names]
            if touched: return touched
    dirty = []
    for r in repos:
        rc, out, _ = _run(["git", "-C", r["path"], "status", "--porcelain"], cwd=r["path"])
        if out.strip(): dirty.append(r)
    return dirty

Maintenant, les deux sorties machine. emit_report imprime le document haw.plugin.report/1 pour --format json, et emit_view imprime le panneau haw.plugin.view/1 pour le cockpit :

def emit_report(ctx):
    repos = changeset_repos(ctx) or context_repos(ctx)
    findings = [{"level": "info", "message": f"{r['name']} : rédiger un commit"} for r in repos]
    print(json.dumps({"schema": "haw.plugin.report/1", "plugin": "commit-ai",
                       "ok": True, "summary": f"{len(repos)} dépôt(s)", "findings": findings}, indent=2))

def emit_view(ctx):
    repos = changeset_repos(ctx) or context_repos(ctx)
    lines = [f"{r['name']:<16} rédiger un commit" for r in repos] or ["rien à committer"]
    print(json.dumps({"schema": "haw.plugin.view/1",
                      "title": "commit-ai — commits proposés", "lines": lines}))

Voilà tout le visage plugin — la partie qui n’a besoin d’aucun paquet externe. Le visage MCP arrive ensuite.

6. Ce qu’est MCP, et pourquoi c’est important ici

Avant d’écrire le visage MCP, définissons MCP, parce que tu ne peux pas brancher ce que tu ne comprends pas.

MCP (Model Context Protocol) est une façon standard pour un assistant IA d’appeler les outils que tu exposes. En trois phrases :

  1. C’est un protocole simple parlé via stdio (entrée/sortie standard) à l’aide de messages JSON-RPC — ton programme lit les requêtes sur stdin et écrit les réponses sur stdout.
  2. Ton programme annonce un ensemble d’outils (des fonctions nommées avec des arguments typés), et un assistant IA comme Claude peut les appeler et lire les résultats.
  3. C’est tout : MCP est le pont qui laisse Claude faire des choses dans ton monde — lire un diff, committer un dépôt — au lieu de seulement en discuter.

Pourquoi c’est important ici : haw connaît toute ta flotte — chaque dépôt, chaque chemin, chaque changeset. Si on expose ce savoir sous forme d’outils MCP, Claude peut les appeler pour lire tes diffs et écrire des messages de commit et du texte de PR précis. Claude arrête de deviner et se met à travailler à partir du vrai diff.

La bonne nouvelle : tu n’implémentes pas toi-même le protocole filaire JSON-RPC. Le SDK MCP officiel embarque un utilitaire appelé FastMCP qui transforme une simple fonction Python en outil avec un seul décorateur. Tu écris des fonctions normales ; FastMCP gère le protocole.

7. Le visage MCP : les outils que Claude appellera

Voici la fonction run_mcp(). Elle importe FastMCP (en échouant gracieusement si le paquet n’est pas installé), crée un serveur et enregistre chaque outil avec le décorateur @mcp.tool(). La docstring de chaque fonction est ce que Claude voit comme description de l’outil, donc on les écrit clairement.

Deux petits garde-fous sont définis d’abord : _repo_path recherche par nom le chemin sur disque d’un dépôt, et _within_root garantit que toute écriture reste à l’intérieur du root de l’espace de travail :

def run_mcp():
    try:
        from mcp.server.fastmcp import FastMCP
    except ImportError:
        sys.stderr.write("haw-commit-ai --mcp needs the MCP SDK: pip install mcp\n")
        return 1
    mcp = FastMCP("haw-commit-ai")

    def _repo_path(ctx, repo):
        return next((r.get("path") for r in context_repos(ctx) if r.get("name") == repo), None)

    def _within_root(root, path):                 # garde-chemin : les écritures restent à l'intérieur de root
        if not root or not path: return False
        root_abs, path_abs = os.path.realpath(root), os.path.realpath(path)
        return path_abs == root_abs or path_abs.startswith(root_abs + os.sep)

Maintenant les cinq outils du Niveau 1. Lis les docstrings — c’est ce que Claude lit aussi :

    @mcp.tool()
    def haw_context() -> dict:
        """Racine de l'espace de travail, stack courante et dépôts (name, path, rev, groups)."""
        ctx = read_context()
        return {"root": ctx.get("root"), "stack": ctx.get("stack"), "repos": context_repos(ctx)}

    @mcp.tool()
    def repo_diff(repo: str) -> str:
        """Le diff git indexé+non indexé d'un dépôt — vois ce qui a changé."""
        path = _repo_path(read_context(), repo)
        return repo_diff_text(path) if path else f"aucun dépôt nommé {repo!r}"

    @mcp.tool()
    def changeset_repos_tool() -> list:
        """Dépôts touchés par le changeset courant, sinon les dépôts sales."""
        return changeset_repos(read_context())

    @mcp.tool()
    def write_commit(repo: str, message: str) -> str:
        """git commit -m dans un dépôt. Garde-chemin sur la racine de l'espace de travail."""
        ctx = read_context(); path = _repo_path(ctx, repo)
        if not _within_root(ctx.get("root"), path):
            return f"refusé : {repo!r} est en dehors de la racine de l'espace de travail."
        rc, out, err = _run(["git", "-C", path, "commit", "-m", message], cwd=path)
        return f"committé {repo} :\n{out}" if rc == 0 else f"échec du commit :\n{err or out}"

    @mcp.tool()
    def draft_pr(repo: str, title: str, body: str, submit: bool = False) -> str:
        """Renvoie le texte de la PR. À blanc par défaut — ne pousse jamais sauf si submit=True."""
        text = f"# {title}\n\n{body}"
        if not submit:
            return text + "\n\n(exécution à blanc — passe submit=True pour lancer `haw change request`)"
        ctx = read_context()
        rc, out, err = _run(["haw", "change", "request", "--title", title, "--body", body],
                            cwd=ctx.get("root"))
        return f"{text}\n\n[request: {'ok' if rc == 0 else 'failed'}]\n{out or err}"

    mcp.run()
    return 0

Voici ce que fait chaque outil et pourquoi il existe :

  • haw_context() — tend à Claude la forme de l’espace de travail : root, stack et dépôts. C’est ainsi que Claude apprend que ta flotte existe.
  • repo_diff(repo) — renvoie le diff d’un dépôt pour que Claude lise exactement ce qui a changé avant d’écrire à ce sujet.
  • changeset_repos_tool() — indique à Claude quels dépôts sont en jeu en ce moment.
  • write_commit(repo, message) — committe réellement, mais uniquement à l’intérieur de la racine de l’espace de travail (le garde-chemin refuse tout ce qui est en dehors).
  • draft_pr(repo, title, body) — renvoie le texte de la PR ; à blanc par défaut, donc il ne pousse jamais rien sauf si tu passes explicitement submit=True.

Enfin, main() relie tous les visages ensemble — aide, MCP, rapport JSON, rendu cockpit et le texte humain par défaut :

def main():
    args = sys.argv[1:]
    if "-h" in args or "--help" in args:
        print("haw-commit-ai — rédige des commits/PR ; --mcp pour servir Claude"); return 0
    if "--mcp" in args: return run_mcp()
    ctx = read_context()
    if "--format" in args and "json" in args: emit_report(ctx); return 0
    if os.environ.get("HAW_RENDER") == "1" or ctx.get("intent") == "render":
        emit_view(ctx); return 0
    repos = changeset_repos(ctx) or context_repos(ctx)
    print(f"haw-commit-ai — {len(repos)} dépôt(s). Lance avec --mcp pour laisser Claude écrire.")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Le listing ci-dessus est la version Niveau 1. Le fichier livré examples/plugins/haw-commit-ai/haw-commit-ai est la version entièrement commentée (avec un vrai squelette de commit conventionnel et un modèle de corps de PR) et porte aussi les outils inter-dépôts du Niveau 2 qu’on ajoute plus bas. Les deux passent python3 -m py_compile et tournent avec zéro dépendance en mode plugin.

8. Lance le plugin — pas encore de MCP, pas encore de Claude

Prouvons d’abord qu’il fonctionne comme un plugin simple. Rends le fichier exécutable et place son répertoire sur ton PATH pour la commande :

chmod +x haw-commit-ai
PATH="$PWD:$PATH" haw commit-ai               # brouillon humain
PATH="$PWD:$PATH" haw commit-ai --format json # un haw.plugin.report/1

La première commande imprime une ligne conviviale. La seconde imprime un rapport JSON que tu peux parser — quelque chose comme :

{
  "schema": "haw.plugin.report/1",
  "plugin": "commit-ai",
  "ok": true,
  "summary": "2 dépôt(s)",
  "findings": [
    { "level": "info", "message": "kernel : rédiger un commit" },
    { "level": "info", "message": "hal : rédiger un commit" }
  ]
}

Pas de recompilation, aucun changement du cœur : dépose haw-commit-ai sur ton PATH et haw dispatche vers lui comme vers n’importe quelle commande intégrée.

Et parce qu’il émet aussi un haw.plugin.view/1, ton plugin trouve sa place dans le cockpit — ouvre haw dash, appuie sur 7, et le voilà dans le panneau Plugins :

9. Brancher le serveur MCP dans Claude Code

Maintenant, le côté Claude. Installe d’abord le SDK MCP — c’est la seule dépendance, et seul le visage --mcp en a besoin :

pip install mcp

Enregistre le serveur auprès de Claude Code — une seule commande. Utilise un chemin absolu vers ton fichier de plugin :

claude mcp add haw-commit-ai -- python3 /abs/path/to/haw-commit-ai --mcp

Ou, par projet, dépose-le dans un fichier .mcp.json à la racine de ton projet :

{
  "mcpServers": {
    "haw-commit-ai": {
      "command": "python3",
      "args": ["/abs/path/to/haw-commit-ai", "--mcp"]
    }
  }
}

Vérifie que Claude voit le serveur :

claude mcp list            # haw-commit-ai devrait apparaître dans la liste

Dans une session Claude, /mcp affiche le serveur connecté et ses outils. Au Niveau 1, ce sont haw_context, repo_diff, changeset_repos_tool, write_commit et draft_pr. Une fois que tu ajoutes le Niveau 2 ci-dessous, changeset_diff et draft_changeset_pr les rejoignent.

10. Exemple concret — Claude écrit ton texte de commit et de PR

Fais un changement sur deux dépôts de ton espace de travail (disons kernel et hal), indexe-les, puis demande à Claude — depuis le répertoire de l’espace de travail :

« Lis les diffs des dépôts touchés par mon changeset courant et écris un message de commit conventionnel pour chacun. Puis rédige un unique corps de PR inter-dépôts. Committe chaque dépôt avec son message ; laisse la PR en brouillon à blanc. »

Claude va :

  1. appeler changeset_repos_tool() → voit kernel, hal,
  2. appeler repo_diff("kernel") et repo_diff("hal") → lit exactement ce qui a changé,
  3. écrire des messages de commit conventionnels (ex. fix(kernel): guard against null irq handler),
  4. appeler write_commit("kernel", …) et write_commit("hal", …) — chacun protégé par garde-chemin vers ton espace de travail,
  5. appeler draft_pr("kernel", "…", "…") → renvoie un corps de PR à blanc (rien n’est poussé).

Tu relis les brouillons, et quand tu es satisfait, tu lances haw change request toi-même (ou tu laisses Claude appeler draft_pr(..., submit=True)).

Soyons honnêtes à propos du Niveau 1. À ce stade, Claude Code voit déjà nativement le diff d’un unique dépôt — tu ne lui as pas encore donné de super-pouvoir, tu lui as juste appris à parler proprement le protocole de haw. La vraie puissance arrive au Niveau 2 : lui montrer un changeset entier, réparti sur plusieurs dépôts, dans une seule vue.

Jusqu’ici, on a surtout réinventé ce que Claude fait gratuitement pour un seul dépôt. Garde la foi : le niveau suivant est la partie qu’il ne peut pas faire tout seul.

This is fine — a cartoon dog sipping coffee as the room burns

« On a construit tout un plugin pour faire ce que Claude faisait déjà. » C’est très bien — le Niveau 2 corrige ça.

À toi de jouer (Niveau 1)

  • Génère le tien : haw plugins new commit-ai –lang python, puis lance la face sans dépendance avec haw commit-ai –format json et confirme que tu obtiens un document haw.plugin.report/1.
  • Dépose le plugin sur le PATH, ouvre haw dash, appuie sur 7, et sélectionne commit-ai — ton panneau haw.plugin.view/1 s’affiche directement dans le cockpit.
  • pip install mcp, enregistre-le avec claude mcp add …, et demande à Claude de lire le diff d’un dépôt et de proposer un commit — sans committer. Puis laisse-le appeler write_commit, et observe le garde-fou de chemin en action en lui demandant de committer un chemin hors de l’espace de travail (il devrait refuser).

Niveau 2 — la puissance inter-dépôts (à l’échelle du changeset)

Voici l’argument, tranchant : Claude seul ne peut pas voir un changeset couvrant toute la flotte. Il peut lire le diff d’un dépôt — mais un changeset haw s’étend sur plusieurs dépôts à la fois (kernel, hal, app…), et cette histoire combinée vit entre les dépôts. Un outil mono-dépôt comme lazygit ne peut pas la montrer non plus. haw connaît tout le changeset, donc haw peut tendre à Claude l’image complète.

On ajoute deux outils au même plugin — pas de nouveau script, pas de nouveau serveur. Ils transforment haw-commit-ai de « un sympathique assistant de commit » en « la chose qui donne à un LLM une vision de toute la flotte ».

11. Pourquoi l’inter-dépôts est la valeur unique

Imagine que tu ajoutes une fonctionnalité — disons un flag irq_mask — et qu’elle doive atterrir dans trois dépôts à la fois : le pilote kernel qui possède le registre, la couche hal qui le fait circuler, et l’app qui l’expose sur la CLI. Le diff de chaque dépôt, lu seul, n’est qu’un fragment. Le sens — « ces trois mouvements ne sont qu’une seule fonctionnalité et doivent atterrir ensemble » — n’existe que lorsque tu vois les trois diffs côte à côte.

  • Claude, tout seul, lit un dépôt à la fois. Il ne peut pas voir les fragments comme une seule histoire.
  • Un outil mono-dépôt voit un seul dépôt, point final.
  • haw connaît le changeset, donc il peut concaténer le diff de chaque dépôt en un seul document et le tendre à Claude. Désormais, Claude écrit une PR cohérente qui raconte toute la fonctionnalité.

Cette vue combinée est la valeur unique. Tout ce qui compose le Niveau 2 existe pour la livrer.

12. Deux outils inter-dépôts

Dépose-les à côté des outils du Niveau 1 (le fichier livré examples/plugins/haw-commit-ai/haw-commit-ai les possède déjà). D’abord, l’utilitaire qui construit le diff combiné — il concatène le diff de chaque dépôt sous un en-tête clair === <repo> === :

def changeset_diff_text(ctx):
    """Le diff git COMBINÉ à travers chaque dépôt du changeset courant."""
    repos = changeset_repos(ctx) or context_repos(ctx)
    if not repos:
        return "pas de changeset et pas de dépôts sales — rien à comparer."
    chunks = []
    for r in repos:                                  # en-têtes clairs par dépôt
        diff = repo_diff_text(r["path"]) if r.get("path") else ""
        body = diff.rstrip() if diff.strip() else "(aucun changement)"
        chunks.append(f"=== {r.get('name','?')} ===\n{body}")
    return "\n\n".join(chunks)                        # toute l'histoire, de haut en bas

C’est la fonction phare : un seul appel, et Claude voit les diffs de kernel, hal et app concaténés sous des en-têtes === <repo> === — le changeset couvrant toute la flotte comme un unique document lisible.

Ensuite, l’utilitaire qui construit un unique squelette de PR inter-dépôts — un emplacement de résumé combiné plus une section par dépôt, que Claude remplit ensuite avec de la prose :

def draft_changeset_pr_body(ctx, title):
    """UN squelette de PR inter-dépôts cohérent racontant tous les dépôts ensemble."""
    repos = changeset_repos(ctx) or context_repos(ctx)
    lines = [f"# {title}", "", "## Résumé combiné", "",
             "<!-- un récit couvrant tous les dépôts et pourquoi ils évoluent ensemble -->", "",
             "## Changements par dépôt", ""]
    for r in repos:
        files, changed = diff_stat(r.get("path", "")) if r.get("path") else (0, 0)
        stat = f" ({files} fichier(s), {changed} ligne(s))" if files or changed else ""
        lines += [f"### {r.get('name','?')}{stat}", "", "<!-- ce qui a changé ici et pourquoi -->", ""]
    lines += ["## Tests", "", "- [ ] `haw build`", "- [ ] `haw test`", ""]
    return "\n".join(lines)

Les deux sont enveloppés en outils MCP avec le décorateur FastMCP, exactement comme ceux du Niveau 1 — ajoute-les à l’intérieur de run_mcp() :

    @mcp.tool()
    def changeset_diff() -> str:
        """Le diff git COMBINÉ à travers TOUS les dépôts du changeset, avec des en-têtes = par dépôt.
        La vue couvrant toute la flotte qu'un outil mono-dépôt ne peut pas te donner."""
        return changeset_diff_text(read_context())

    @mcp.tool()
    def draft_changeset_pr(title: str) -> str:
        """UN squelette de PR inter-dépôts cohérent racontant tous les dépôts ensemble.
        À blanc — assemble l'ossature ; tu remplis la prose, puis `haw change request`."""
        return draft_changeset_pr_body(read_context(), title)

draft_changeset_pr(title) renvoie un corps de PR : le plugin assemble le squelette (sections par dépôt plus un emplacement de résumé combiné), et Claude remplit la prose à partir des diffs. À blanc par défaut ; passe le résultat à haw change request pour ouvrir les PR liées à travers la flotte.

write_commit reste protégé par garde-chemin. Il n’y a pas d’outil inter-dépôts « tout écrire » par conception — pour un changeset, tu committes par dépôt (Claude appelle write_commit pour chacun, chacun gardé sur root), puis tu lances haw change request pour ouvrir les PR liées à travers la flotte. Les écritures restent petites, relisables et à l’intérieur de ton espace de travail.

13. Exemple concret — une seule PR pour un changeset de trois dépôts

Touche deux ou trois dépôts dans un changeset — disons kernel, hal et app — indexe-les, puis demande à Claude, depuis l’espace de travail :

« Appelle changeset_diff() pour lire tout mon changeset, puis draft_changeset_pr() et écris une seule PR qui raconte l’histoire combinée — une section par dépôt plus un résumé combiné. »

Claude va :

  1. appeler changeset_diff() → un document avec === kernel ===, === hal ===, === app ===, le diff de chaque dépôt en dessous,
  2. appeler draft_changeset_pr("…") → obtient le squelette avec une section par dépôt,
  3. remplir la prose pour en faire un unique récit cohérent.

Voici un résultat illustratif (étiqueté comme illustratif — ta prose correspondra à tes diffs réels) :

# feat: propagate the new irq-mask flag end to end

## Combined summary
A new `irq_mask` flag flows from the kernel driver up through the HAL and into
the app's config surface. The three repos move together so the feature lands atomically.

## Per-repo changes
### kernel
Add `irq_mask` to the driver's register write and guard the null-handler path.
### hal
Thread `irq_mask` through the HAL's `configure()` and expose it in the C header.
### app
Surface `--irq-mask` on the CLI and wire it to the HAL call.

C’est ce que ni Claude ni un outil mono-dépôt ne peuvent faire seuls. Un outil mono-dépôt voit trois diffs déconnectés ; haw plus ce plugin tendent à Claude le changeset, alors il écrit l’unique histoire qui les traverse. Quand tu es satisfait, committe chaque dépôt (write_commit, par dépôt) et lance haw change request pour ouvrir les PR liées à travers la flotte.

Jubilant celebration reaction

Un seul prompt. Trois dépôts. Un récit de PR cohérent. C’est la récompense — va fêter ça.

14. Notes de sécurité — voici la partie importante

Des outils d’écriture plus un LLM, ça veut dire que les garde-fous comptent. C’est la section que tu ne survoles pas. Le plugin intègre les garde-fous d’office :

  • Écritures protégées par garde-chemin. write_commit et draft_pr(submit=True) refusent tout chemin de dépôt qui n’est pas à l’intérieur du root de l’espace de travail — Claude ne peut pas committer en dehors de ta flotte.
  • À blanc par défaut. draft_pr ne renvoie que du texte ; il ne pousse jamais ni ne force-pousse sauf si tu passes explicitement submit=True.
  • Aucun secret dans le plugin. L’authentification à la forge vient de ton environnement — la résolution de token normale de haw (GITHUB_TOKEN, et ainsi de suite). Le plugin ne stocke rien.
  • Processus séparé, codes de sortie honnêtes. Le plugin tourne hors processus ; un bug ne peut pas faire planter haw, et un code de sortie non nul se propage pour que la CI verrouille toujours.

À toi de jouer (Niveau 2)

  • Touche à trois dépôts dans un changeset, puis demande à Claude d’appeler changeset_diff() et draft_changeset_pr() et de rédiger un seul récit de PR couvrant les trois (kernel / hal / app plus un résumé combiné). Compare-le à ce que tu obtiendrais en interrogeant Claude dépôt par dépôt — l’histoire inter-dépôts n’apparaît que lorsqu’il voit tout le changeset d’un coup.
  • Étends le plugin : ajoute un outil repo_log(repo, n) pour que Claude puisse voir l’historique récent et écrire de meilleurs messages. Garde-le en lecture seule.
  • Ajoute un décompte de lignes par dépôt à la sortie du diff combiné, pour que Claude sache quel dépôt porte l’essentiel du changement avant de commencer à écrire.

Glossaire

  • plugin — un exécutable nommé haw-<name> sur ton PATH ; haw le lance comme haw <name>.
  • dispatch par PATH — la convention (partagée avec git/cargo/kubectl) où haw lance une sous-commande inconnue en trouvant un exécutable correspondant sur PATH.
  • haw.plugin/1 — le contexte JSON que haw tend à chaque plugin (via HAW_JSON ou stdin), décrivant root, stack et repos.
  • haw.plugin.report/1 — le rapport machine qu’un plugin imprime pour --format json.
  • haw.plugin.view/1 — le panneau de cockpit qu’un plugin imprime quand haw lui demande de s’afficher (HAW_RENDER=1, "intent": "render").
  • MCP (Model Context Protocol) — un protocole JSON-RPC standard sur stdio qui laisse un assistant IA appeler les outils que tu exposes.
  • stdio — les flux d’entrée et de sortie standard d’un programme ; les messages MCP y transitent.
  • outil — une fonction nommée (avec des arguments typés) que ton serveur MCP annonce pour que Claude l’appelle.
  • FastMCP — l’utilitaire du SDK MCP officiel qui transforme une fonction Python en outil MCP avec un décorateur.

Ce que tu as appris

  • Un plugin est n’importe quel exécutable nommé haw-<name> sur PATH ; haw lui tend le contexte haw.plugin/1 via $HAW_JSON / stdin et propage son code de sortie.
  • Il peut imprimer un haw.plugin.report/1 (--format json) et un panneau haw.plugin.view/1 (intention de rendu, HAW_RENDER=1) pour la vue Plugins du cockpit (7).
  • MCP est un protocole standard sur stdio qui laisse Claude appeler les outils que tu exposes ; le même script devient un serveur MCP avec --mcp, en utilisant FastMCP.
  • Le Niveau 1 (un seul dépôt) enseigne le protocole : haw_context, repo_diff, changeset_repos_tool, write_commit, draft_pr — mais Claude voit déjà un dépôt nativement.
  • Le Niveau 2 (inter-dépôts) est la vraie puissance : changeset_diff tend à Claude le diff combiné à travers tout le changeset, et draft_changeset_pr le pousse à écrire un récit de PR couvrant toute la flotte — l’histoire inter-dépôts que ni Claude ni un outil mono-dépôt ne peuvent raconter seuls.
  • Garde-fous : garde-chemin sur les écritures à l’intérieur de root (committe par dépôt, puis haw change request), garde la rédaction de PR à blanc par défaut, et ne stocke jamais de secrets — l’authentification reste dans ton environnement.

Et maintenant, où aller ?

Tu peux maintenant étendre haw dans n’importe quel langage et donner à un LLM des outils sûrs et riches en contexte. À partir d’ici :

Voilà tout l’outil — maintenant, va construire ta propre poutre. Bienvenue à bord.

Exemples

Chaque exemple est un haw.toml réel et exécutable dans le dépôt sous examples/. Clonez-le (ou copiez le manifeste), puis haw sync et explorez. Les exemples de domaine composent de vrais dépôts amont publics, haw sync a donc besoin du réseau ; quelques étapes de build utilisent Docker (indiqué par exemple).

Commencez par quickstart, puis passez au domaine qui correspond à votre flotte.

Apprendre en pratiquant

ExempleCe qu’il montre
quickstartDeux dépôts octocat + un stack — toute la boucle sync → status → lock → verify (le cours la parcourt).
haw-helloUn plugin minuscule — le contrat haw <name>haw-<name> en ~20 lignes.
governanceHooks de cycle de vie [plugins] — SBOM / provenance / porte câblés aux phases.

Par domaine

ExempleDomaineCe qu’il montre
microservicesBackend4 services + un proto/lib partagé ; une fonctionnalité branchée, ouverte en PR, et fusionnée (land) ensemble dans l’ordre des dépendances.
ml-platformML / donnéesModèle + pipeline de données + infra de service épinglés en une seule référence reproductible, avec des stacks + un overlay.
automotiveEmbarqué / AUTOSARConfig ARXML + HAL partagé + deux applications ECU, builds multi-chaînes d’outils, [plugins] misra + aspice.
automotive-pinnedEmbarquéUne flotte automobile entièrement épinglée par SHA — la référence de reproductibilité/audit.
embedded-bspEmbarquéUn BSP/HAL partagé réutilisé à travers les stacks ECU via des overlays.
embedded-realEmbarquéCinq vrais amonts (CoreMark, cJSON, Monocypher, libcanard, Mbed-TLS) — tous se compilent avec un seul haw build -j4 (validé).
rpi-picoEmbarqué / firmwareDeux vrais firmwares Raspberry Pi Pico (RP2040) (embassy blinky + rp-hal) compilés de manière croisée vers Cortex-M0+ avec le thumbv6m-none-eabi intégré de Rust — sans chaîne d’outils ARM — plus cJSON (ctest 19/19). Build+test validés ; les trois disposent d’une CI GitHub Actions active.
devops-infraDevOps / InfraTrois vrais amonts — terraform-aws-vpc (init+validate), les helm-charts Prometheus (helm lint), une application Dockerfile (docker build+hadolint) — build+test 3/3 (validé).
ml-aiML / IARuntime LLM réel depuis les sources — llama.cpp compilé en llama-cli + vérification d’analyse de nanoGPT, build+test 2/2 (validé).
mobileMobileApplication+SDK épinglés de concert — le SDK OkHttp se compile+teste pour de vrai via une image Docker JDK-21 ; la moitié application Now-in-Android est un motif (nécessite le SDK Android).

Recettes réelles de build & d’émulation

Pour un câblage build/test prêt à copier-coller vers de vraies chaînes d’outils — cross-compilation Docker (Cortex-M4), FreeRTOS démarré sous QEMU, et des motifs pour EB tresos / Vector / Green Hills / IAR / Tasking / Zephyr / Renode — voir Recettes d’intégration.

Recettes d’intégration — relier haw à votre toolchain

haw ne regroupe ni ne réimplémente jamais un compilateur, un générateur de config ou un émulateur. Il ne fait que déléguer au shell la commande build = / test = par dépôt que vous déclarez (avec le répertoire du dépôt comme répertoire de travail, de sorte que $PWD dans la commande est le chemin du dépôt). C’est là toute la surface d’intégration : mettez la commande de votre toolchain dans build =, et haw pilote toute la flotte — en parallèle, épinglée à haw.lock, avec un code de sortie de qualité CI.

Ainsi « intégrer haw » est le même geste unique que votre compilateur soit gcc, une image Docker ou une suite automobile sous licence à 50 k€. Cette page montre les deux :

  • Recettes réellement exécutées (toolchains ouvertes : cross-compilation Docker, émulation QEMU, FreeRTOS) — avec une vraie sortie capturée.
  • Schémas de câblage pour les outils propriétaires/sous licence (Vector, EB tresos, Green Hills, IAR, Tasking, Renode) — la forme exacte, honnêtement marquée non exécutée ici (nous n’avons pas les licences), afin que rien ne soit inventé.

L’idée unique : haw build / haw test exécutent build = / test = par dépôt et échouent (non nul) si la commande d’un dépôt échoue. Rien dans haw n’est spécifique à ARM, Docker ou QEMU. Enveloppez n’importe quelle toolchain de la même manière.


Toolchain dans un conteneur (aucune cross-toolchain hôte nécessaire)

Vous installez rarement un cross-compilateur sur la machine de chaque développeur. Placez-le dans une image Docker une fois pour toutes et référencez-le depuis build =. Les deux images utilisées ci-dessous :

# haw-arm-gcc — bare-metal ARM cross-compiler
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc-arm-none-eabi libnewlib-arm-none-eabi make ca-certificates \
 && rm -rf /var/lib/apt/lists/*
# haw-arm-emu — same, plus the QEMU emulator for on-CI firmware runs
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc-arm-none-eabi libnewlib-arm-none-eabi make git ca-certificates qemu-system-arm \
 && rm -rf /var/lib/apt/lists/*
$ docker run --rm haw-arm-emu sh -c 'arm-none-eabi-gcc --version | head -1; qemu-system-arm --version | head -1'
arm-none-eabi-gcc (15:10.3-2021.07-4) 10.3.1 20210621 (release)
QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.31)

Recette 1 — Cross-compilation Docker (ARM Cortex-M bare-metal) ✅ exécutée

littlefs (un vrai système de fichiers tolérant aux pannes pour MCU) compilé en archive statique Cortex-M4 à l’intérieur de l’image de toolchain ; l’étape test = vérifie que l’objet produit est authentiquement ARM.

[repo.littlefs]
url    = "https://github.com/littlefs-project/littlefs.git"
rev    = "master"
groups = ["firmware"]
build  = "docker run --rm -v \"$PWD\":/w -w /w haw-arm-gcc sh -c 'arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os -Wall -c lfs.c lfs_util.c && arm-none-eabi-ar rcs lfs-cortexm4.a lfs.o lfs_util.o && arm-none-eabi-size lfs-cortexm4.a'"
test   = "docker run --rm -v \"$PWD\":/w -w /w haw-arm-gcc sh -c 'arm-none-eabi-objdump -f lfs.o | grep -i \"architecture: arm\" && echo CONFIRMED_ARM_CORTEX_M_OBJECT'"

[stack.fw]
repos = ["littlefs"]

Vraie sortie capturée (haw build puis haw test, tous deux en sortie 0) :

$ haw build
── littlefs ──
   text    data     bss     dec     hex  filename
  21880       0       0   21880    5578  lfs.o (ex lfs-cortexm4.a)
    120       0       0     120      78  lfs_util.o (ex lfs-cortexm4.a)
build ran in 1/1 repos

$ haw test
── littlefs ──
architecture: armv7e-m, flags 0x00000011:
CONFIRMED_ARM_CORTEX_M_OBJECT
test ran in 1/1 repos

armv7e-m est l’ISA du Cortex-M4 — un authentique objet ARM bare-metal, produit par arm-none-eabi-gcc à l’intérieur de Docker, orchestré par haw, avec un haw en machine native.


Recette 2 — Exécution émulée QEMU (FreeRTOS sur Cortex-M3) ✅ exécutée

La démo QEMU officielle de FreeRTOS construite avec le FreeRTOS-Kernel, puis démarrée sur qemu-system-arm -M mps2-an385 -cpu cortex-m3. L’ordonnanceur exécute la démo blinky (une tâche + un timer logiciel alimentant une file) et imprime sur l’UART semi-hébergé ; test = cherche un marqueur vivant via grep, de sorte qu’un RTOS réellement en cours d’exécution sort en 0 et une image morte sort en 1.

[repo.freertos]
url    = "https://github.com/FreeRTOS/FreeRTOS.git"
rev    = "main"
groups = ["rtos"]
build  = "docker run --rm -v \"$PWD\":/w -w /w haw-arm-emu make -C FreeRTOS/Demo/CORTEX_MPS2_QEMU_IAR_GCC/build/gcc"
test   = "docker run --rm -v \"$PWD\":/w -w /w haw-arm-emu sh -c 'timeout 8 qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -kernel FreeRTOS/Demo/CORTEX_MPS2_QEMU_IAR_GCC/build/gcc/output/RTOSDemo.out -monitor none -nographic -serial stdio -semihosting-config enable=on,target=native 2>&1 | head -40 | grep -q \"Message received from task\" && echo QEMU_FREERTOS_RUN_CONFIRMED'"

[stack.rtos]
repos = ["freertos"]

Vraie sortie capturée :

$ haw build        # relie output/RTOSDemo.out
   text    data     bss     dec     hex  filename
  23902     232  121589  145723   2393b  ./output/RTOSDemo.out
build ran in 1/1 repos

$ haw test         # l'ordonnanceur FreeRTOS s'exécute réellement sous QEMU
── freertos ──
Message received from task
Message received from task
... (37 au total, plus 3 "Message received from software timer") ...
QEMU_FREERTOS_RUN_CONFIRMED
test ran in 1/1 repos

Contrôle négatif vérifié : pointez le grep vers un marqueur qui n’apparaît jamais et l’étape QEMU sort en 1 — de sorte que haw test échoue réellement si le RTOS ne démarre pas, plutôt que de toujours passer.

Les submodules sont tolérants aux pannes. haw sync --recurse-submodules initialise chaque submodule indépendamment et saute ceux qui sont cassés/injoignables avec un avertissement au lieu d’interrompre — important pour les gros dépôts amont comme FreeRTOS/FreeRTOS qui déclarent une vaste forêt de submodules lourds (wolfSSL, plusieurs AWS IoT SDK, …). Le noyau dont votre cible a besoin (FreeRTOS/Source) est initialisé ; un submodule qui renvoie 404 imprime simplement haw: skipped submodule '<path>': … et la synchronisation réussit tout de même. (les transports ext/fd/file restent strictement désactivés dans les récupérations de submodules — la garde contre la RCE est préservée.)

Remplacez QEMU par Renode en une ligne — test = "renode --console -e 'include @sim.resc; start; sleep 5; quit'" puis cherchez le log UART via grep de la même manière.


Davantage de flottes embarquées validées ✅ exécutées

Au-delà des recettes mono-dépôt de cross-compilation/QEMU ci-dessus, le manifeste examples/embedded-real/ compose cinq vrais dépôts amont embarqués/critiques pour la sûreté publics en une seule flotte et les construit tous avec un unique haw build -j4. Chaque commande de ce manifeste a été réellement exécutée et vue réussir sur un hôte clang/cmake/make :

DépôtDomaineRésultat
CoreMarkbenchmarkconstruit et exécute le benchmark
cJSONdonnéesconstruit + ctest 19/19
Monocyphercryptoconstruit libmonocypher.a
libcanardprotocolela vérification de compilation C11 passe
Mbed-TLSsécuritéconstruit libmbedcrypto/tls/x509.a (nécessite les submodules)

Vraie sortie capturée (haw test sur la flotte, sortie 0) :

── coremark ──
CoreMark 1.0 : 26021.337497 / Apple LLVM 17.0.0 (clang-1700.0.13.5) -O2 -DPERFORMANCE_RUN=1   / Heap
COREMARK_RAN
── cjson ──
100% tests passed out of 19
── monocypher ──
MONOCYPHER_LIB_OK
test ran in 3/3 repos

Mbed-TLS déclare ses framework/ et tf-psa-crypto/ comme submodules git ; haw sync --recurse-submodules les initialise (tolérant aux pannes — un submodule cassé est sauté avec un avertissement, pas fatal). Le même checkout cJSON cross-compile également en objet Cortex-M4 bare-metal via haw-arm-emu (architecture: armv7e-m), de sorte qu’une seule flotte couvre un build hôte et un build croisé. La table complète par dépôt, les prérequis et la recette croisée figurent dans le README de l’exemple.


Schémas pour toolchains sous licence / propriétaires ⚠️ non exécutées ici

Voici les formes de câblage exactes pour les outils automobiles/de sûreté commerciaux. Nous ne pouvons pas les exécuter (sous licence) — les flags suivent l’interface batch/CLI de chaque fournisseur ; adaptez-les à votre projet selon la documentation du fournisseur. L’essentiel : c’est la même délégation au shell build =.

Le modèle mental AUTOSAR par dépôt ECU : config (ARXML) → générateur fournisseur → C BSW/RTE généré → compilateur fournisseur → ELF, le tout épinglé dans haw.lock.

# EB tresos (Elektrobit) — génère le BSW depuis la config, puis compile
[repo.ecu-comfort]
url    = "git@gitlab.company.com:ecu/comfort-bsw.git"
rev    = "release/2.4"
groups = ["ecu", "autosar"]
build  = "$TRESOS_BASE/bin/tresos_cmd.sh -p ComfortEcu generate && make -C output"
test   = "make -C output test"     # VectorCAST / Tessy / votre harnais MCAL

[plugins]
misra  = ["pre-request"]   # contrôle le code généré + manuel selon MISRA C
aspice = ["post-land"]     # émet la traçabilité ASPICE lors de la finalisation du changement
# Vector MICROSAR / DaVinci Configurator Pro
build = "DVConfiguratorCmd -d PowertrainEcu.dpa --generateAll && make"

# Compilateurs (prêts à l'emploi — une chaîne chacun ; une flotte peut les mélanger)
build = "gbuild -top default.gpj"                 # Green Hills MULTI (ccarm)
build = "iarbuild MyProject.ewp -build Release"   # IAR Embedded Workbench (iccarm)
build = "amk -f project.mk"                        # TASKING (cctc / carm)
build = "make CC=dcc"                              # Wind River Diab

# Zephyr RTOS — west pilote lui-même le build de carte + l'exécution QEMU/renode
build = "west build -b qemu_cortex_m3 samples/hello_world"
test  = "west build -t run"        # ou : twister

Parce que chacun n’est qu’une chaîne, un seul haw.toml peut composer un ECU GHS à côté d’un ECU IAR à côté d’une passerelle gcc, et haw build -j8 les construit tous en parallèle avec un succès/échec correct par dépôt et une sortie non nulle si l’un casse — votre porte CI. (haw peut aussi importer un west.yml Zephyr / un manifeste repo de Google : haw import --from west.yml.)


Pourquoi c’est important pour le travail réglementé

  • Reproductible. Chaque dépôt de config/BSW/RTE est épinglé à un SHA exact dans haw.lock — le code généré est auditable et identique au bit près d’une reconstruction à l’autre (l’argument pour ASPICE / ISO 26262 / DO-178C).
  • Orchestré. haw exécute générer → compiler → émuler sur toute la flotte d’ECU en parallèle, avec un code de sortie CI, en utilisant vos outils sous licence sans modification.
  • Gouverné. Les plugins sur les phases du cycle de vie produisent les produits de travail de qualification — misra (porte pre-request), aspice / SBOM / provenance / signature (post-build/post-land), les bundles haw evidence. Voir Plugins, Domains, Compliance.

Vous n’adaptez pas votre toolchain à haw. Vous mettez sa commande dans build =, et haw pilote la flotte.

Domaines

haw compose, orchestre et livre le changement à travers de nombreux dépôts Git — dans n’importe quel domaine. Rien dans le manifeste, le lockfile, le flux de changeset, le build/test à l’échelle de la flotte, ou les hooks de gouvernance n’est spécifique à un secteur. Un dépôt est un dépôt ; un build est n’importe quelle commande shell que vous déclarez ; une PR est une PR.

Cette page montre comment la même boucle —

composer (manifeste) → épingler (lock) → modifier (changeset) → build/testgouverner (hooks + evidence)

— se projette sur cinq mondes différents. L’embarqué/automobile est un cas de preuve parmi plusieurs, pas l’identité de haw.

Chaque section nomme la difficulté, puis la correspondance. Des manifestes illustratifs se trouvent sous examples/.


Microservices backend

La difficulté. Une seule fonctionnalité visible par l’utilisateur s’étend sur quatre services et un dépôt protobuf/lib partagé. Vous créez une branche pour chacun à la main, ouvrez quatre PR, et essayez de vous souvenir de les fusionner dans le bon ordre sans casser main. Il n’existe aucun artefact unique qui dise « ces cinq SHA constituent la fonctionnalité ».

Comment haw se projette :

  • Manifeste / lock. Déclarez les services plus le dépôt proto partagé ; committez haw.lock afin que la CI et chaque coéquipier résolvent l’ensemble identique de SHA.
  • Changeset. haw change start FEAT-42 --repos api,billing,proto crée une branche unique à travers exactement les dépôts que la fonctionnalité touche ; haw change request ouvre les PR liées, haw change status agrège la revue + la CI, et haw change land les fusionne dans l’ordre deps (proto avant ses consommateurs).
  • Build / test. Chaque service déclare ses propres build/test (cargo, go test, npm test, ./gradlew) ; haw build -j N les déploie en parallèle et se termine avec un code non nul si l’un échoue — intégrez-le directement dans un pipeline.
  • Gouverner. Une porte pre-request peut appliquer une politique (scan de secrets, vérification de licence) avant l’ouverture de toute PR.

Voir examples/microservices/.


Plateformes ML / données

La difficulté. Un dépôt de modèle, le dépôt de pipeline de données qui l’alimente, et le dépôt d’infra de service qui le déploie dérivent l’un par rapport à l’autre. Reproduire « le modèle qui était en production en mars » revient à deviner quel commit du pipeline l’a produit.

Comment haw se projette :

  • Manifeste / lock. Épinglez ensemble les dépôts de modèle, de pipeline et de service à des SHA exacts. Le lockfile committé est la référence reproductible — un clone des mois plus tard résout les trois mêmes arborescences.
  • Stacks / overlays. Un stack training et un stack serving peuvent partager le dépôt de pipeline sans duplication ; un overlay suit main pour le modèle tandis que tout le reste demeure épinglé.
  • Build / test. Déclarez test = "pytest" sur le pipeline, build = "dvc repro" ou une commande d’entraînement sur le modèle, et un plan/apply d’infra sur le service — haw fait appel au shell, il n’embarque jamais de chaîne d’outils.
  • Gouverner. Les hooks SBOM + provenance enregistrent exactement quels SHA de modèle, de données et d’infra ont été livrés ensemble — la piste d’audit d’une release ML.

Voir examples/ml-platform/.


Plateforme / infra

La difficulté. Les modules racines Terraform, les sous-modules réutilisables et les charts Helm vivent dans des dépôts séparés. Un changement d’un module partagé nécessite des montées de version coordonnées sur chaque consommateur, et « ce qui a été déployé » est réparti sur N dépôts à N révisions.

Comment haw se projette :

  • Manifeste / lock. Composez les dépôts de modules et de charts en une seule flotte épinglée ; le lock est l’enregistrement de la référence déployée.
  • Changeset. Montez la version d’un module partagé et de ses consommateurs sur une branche unique à travers les dépôts, ouvrez leurs PR ensemble, et fusionnez dans l’ordre des dépendances.
  • Build / test. Déclarez test = "terraform validate" / "helm lint" et une commande build/plan par dépôt ; haw test exécute les vérifications de toute la flotte en parallèle.
  • Gouverner. haw verify (sortie 3 en cas de dérive) est une porte de CI qui échoue si l’arborescence extraite ne correspond plus à la référence d’infra épinglée.

Voir examples/microservices/ pour le motif de changeset — la même forme s’applique aux dépôts Terraform/Helm.


Mobile

La difficulté. Un dépôt d’application dépend d’un dépôt de SDK interne. Une fonctionnalité nécessite un changement dans les deux, publiés de concert, mais les deux dépôts ont des branches, des PR et une CI indépendantes.

Comment haw se projette :

  • Manifeste / lock. Épinglez ensemble les dépôts de l’application et du SDK ; le lock garantit que l’application se compile contre le commit exact du SDK testé.
  • Changeset. Une branche à travers application + SDK, des PR liées entre elles, fusionnées SDK d’abord via deps.
  • Build / test. Déclarez les commandes Gradle/Xcode/fastlane par dépôt ; haw build pilote les deux.
  • Gouverner. Les hooks de signature + SBOM capturent ce qui a été livré dans une release.

Embarqué & automobile

La difficulté. Un HAL/BSP/MCAL partagé est réutilisé sur de nombreux ECU ; la configuration AUTOSAR vit dans des dépôts ARXML qui doivent rester épinglés à côté du code ; et l’ensemble doit être reproductible et auditable pour la qualification de sécurité fonctionnelle.

Comment haw se projette :

  • Manifeste / lock. Épinglez BSW/MCAL à des tags/SHA exacts et épinglez les dépôts de config ARXML AUTOSAR dans le lock à leurs côtés — la référence est la preuve d’audit, reproductible octet par octet des années plus tard.

  • Stacks / overlays. Plusieurs ECU (gateway, body, …) partagent une seule fondation BSW+MCAL sous forme de stacks séparés, sans duplication ; les overlays échangent les révisions d’une variante sans réécrire la liste des dépôts.

  • Changeset. Un correctif inter-ECU se branche à travers les dépôts affectés et fusionne dans l’ordre deps (le logiciel de base avant les applications ECU qui en dépendent).

  • Build / test — indépendant de la chaîne d’outils. haw fait appel au shell pour la commande build/test déclarée par dépôt et n’embarque aucun compilateur. Cela signifie qu’il pilote ce que le dépôt déclare :

    • les étapes de configuration + génération Vector MICROSAR / DaVinci,
    • les générateurs Elektrobit (EB) tresos,
    • les compilateurs Green Hills (MULTI / ccrh, …), IAR (iccarm), Tasking, Wind River Diab, et arm-none-eabi-gcc. Chacun est nommé dans le build = / test = de ce dépôt ; haw n’en embarque et n’en exige jamais aucun.
  • Gouverner — correspondance avec les normes. Les hooks de gouvernance se projettent directement sur le travail normatif :

    Norme / artefactComment haw la couvre
    Automotive SPICE (ASPICE)haw-aspice émet une traçabilité dépôt → SHA épinglé → domaine de processus
    MISRA Chaw-misra exécute cppcheck --addon=misra à travers la flotte comme une porte pre-request
    ISO 26262 / DO-178Chaw evidence regroupe (manifeste + lock + audit + status) plus SBOM + provenance provenant des plugins de gouvernance
    AUTOSAR ARXMLles dépôts de config épinglés à des SHA exacts dans haw.lock, versionnés avec le code qu’ils configurent

Voir examples/automotive/ et examples/automotive-pinned/.


Le fil conducteur

Sur les cinq, les pièces mobiles sont identiques — seuls les dépôts et les commandes build/test déclarées changent :

Étape de la boucleBackendML / donnéesInfraMobileEmbarqué / automobile
Composerservices + proto partagémodèle + pipeline + servicemodules + chartsapplication + SDKBSW/MCAL + ARXML + applications ECU
Épinglerensemble de fonctionnalités reproductibleréférence de modèle reproductibleréférence déployéesynchronisation application↔SDKréférence d’audit
Modifierfonctionnalité à travers les servicesmodèle + pipeline ensemblemodule + consommateursapplication + SDKcorrectif inter-ECU
Build/testcargo/go/npmpytest/dvcterraform/helmGradle/XcodeVector/EB/GHS/IAR/gcc
Gouvernerporte de politiqueSBOM/provenanceverify de dérivesignatureASPICE/MISRA/26262/DO-178C

Un seul binaire, une seule boucle, tous les domaines.

Distribuer hawser vers des registres privés

Chaque version taguée publie d’abord une GitHub Release signée. Une fois celle-ci réussie, le job distribute dans .github/workflows/release.yml réplique les mêmes artefacts vers l’un des quatre registres privés — Nexus, Artifactory, GitLab et Bitbucket — pour les organisations qui installent depuis un miroir interne plutôt que depuis GitHub.

Chaque registre est opt-in : une cible n’est tentée que lorsque ses secrets sont configurés. Un dépôt sans aucun secret défini publie tout de même sa version avec succès — le job distribute journalise un clair skipping <target>: secret not set et l’ensemble du job est continue-on-error, de sorte qu’une panne de registre ne peut jamais faire échouer la version.

Ce qui est téléversé

Pour chaque registre configuré, le job téléverse l’ensemble complet des artefacts de la version :

  • haw-<version>-<target>.tar.gz / .zip — les archives de plateforme
  • haw-<version>-<target>.<ext>.sha256 — sommes de contrôle SHA-256
  • haw-<version>-<target>.<ext>.sig et .pem — signature cosign sans clé + certificat
  • hawser_<version>-1_amd64.deb et hawser-<version>-1.x86_64.rpm — paquets Linux

Ce sont les fichiers identiques à ceux attachés à la GitHub Release, de sorte que les sommes de contrôle et les signatures cosign se vérifient de la même manière quel que soit le miroir depuis lequel vous avez récupéré (voir INSTALL.md → Verify).

Matrice des secrets

Configurez-les comme secrets de dépôt (ou d’organisation) GitHub Actions. Seuls les registres dont les secrets requis sont présents recevront des publications.

RegistreSecretRequis ?DéfautObjet
NexusNEXUS_URLrequisURL de base, par ex. https://nexus.example.com
NEXUS_USERrequisNom d’utilisateur
NEXUS_PASSrequisMot de passe / jeton
NEXUS_REPOfacultatifraw-hostedNom du dépôt raw hosted
ArtifactoryARTIFACTORY_URLrequisURL de base, par ex. https://artifactory.example.com/artifactory
ARTIFACTORY_TOKENrequisJeton Bearer / d’identité
ARTIFACTORY_REPOfacultatifgeneric-localClé de dépôt générique
GitLabGITLAB_TOKENrequisJeton d’accès personnel/de projet (scope api)
GITLAB_PROJECT_IDrequisID de projet numérique
GITLAB_URLfacultatifhttps://gitlab.comURL de base de l’instance auto-hébergée
BitbucketBITBUCKET_USERrequisNom d’utilisateur
BITBUCKET_TOKENrequisMot de passe d’application / jeton d’accès
BITBUCKET_WORKSPACErequisSlug de l’espace de travail
BITBUCKET_REPOrequisSlug du dépôt

Un registre est ignoré (journalisé, pas en échec) sauf si tous ses secrets requis sont définis.

Disposition et installation par registre

Partout, <version> est le tag sans le v initial (par ex. 0.1.7).

Nexus (dépôt raw hosted)

Chaque fichier est envoyé par PUT vers un dépôt raw hosted sous un chemin versionné :

<NEXUS_URL>/repository/<NEXUS_REPO>/haw/<version>/<file>

Téléversement (ce que la CI exécute, par fichier) :

curl -u "$NEXUS_USER:$NEXUS_PASS" \
  --upload-file haw-0.1.7-x86_64-unknown-linux-musl.tar.gz \
  "$NEXUS_URL/repository/raw-hosted/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Consommer :

curl -u "$NEXUS_USER:$NEXUS_PASS" -O \
  "$NEXUS_URL/repository/raw-hosted/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"
tar xzf haw-0.1.7-x86_64-unknown-linux-musl.tar.gz && sudo install haw /usr/local/bin/

Artifactory (dépôt générique)

Chaque fichier est envoyé par PUT (auth Bearer) vers un dépôt générique sous un chemin versionné :

<ARTIFACTORY_URL>/<ARTIFACTORY_REPO>/haw/<version>/<file>

Téléversement (par fichier) :

curl -H "Authorization: Bearer $ARTIFACTORY_TOKEN" \
  --upload-file haw-0.1.7-x86_64-unknown-linux-musl.tar.gz \
  "$ARTIFACTORY_URL/generic-local/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Consommer :

curl -H "Authorization: Bearer $ARTIFACTORY_TOKEN" -O \
  "$ARTIFACTORY_URL/generic-local/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

GitLab (registre de paquets générique + Release)

Deux choses se produisent. D’abord, chaque fichier est envoyé par PUT vers le registre de paquets générique du projet :

<GITLAB_URL>/api/v4/projects/<GITLAB_PROJECT_ID>/packages/generic/haw/<version>/<file>

Ensuite, une GitLab Release est créée pour le tag, avec assets.links[] pointant vers chaque fichier de paquet téléversé (une release existante pour le tag est tolérée, pas une erreur).

Téléversement (par fichier) :

curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  --upload-file haw-0.1.7-x86_64-unknown-linux-musl.tar.gz \
  "https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/packages/generic/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Consommer :

curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" -O \
  "https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/packages/generic/haw/0.1.7/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Ou ouvrez la page Deploy → Releases du projet et téléchargez depuis les assets de la release.

Bitbucket (Downloads du dépôt)

Chaque fichier est envoyé par POST (multipart) vers la zone Downloads du dépôt :

https://api.bitbucket.org/2.0/repositories/<BITBUCKET_WORKSPACE>/<BITBUCKET_REPO>/downloads

Téléversement (par fichier) :

curl -u "$BITBUCKET_USER:$BITBUCKET_TOKEN" \
  -X POST \
  "https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO/downloads" \
  -F files=@haw-0.1.7-x86_64-unknown-linux-musl.tar.gz

Consommer (les fichiers arrivent sous l’onglet Downloads du dépôt ; les noms de fichiers sont à plat, non versionnés) :

curl -u "$BITBUCKET_USER:$BITBUCKET_TOKEN" -O -L \
  "https://bitbucket.org/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO/downloads/haw-0.1.7-x86_64-unknown-linux-musl.tar.gz"

Bitbucket Downloads est un espace de noms à plat (pas de dossiers par version), la <version> est donc portée dans le nom de fichier lui-même.

haw publish — téléverser des artefacts depuis le CLI

Le job CI distribute réplique les archives de release, mais vous pouvez pousser vous-même n’importe quels artefacts (sorties de build, un bundle evidence, un SBOM) vers les quatre mêmes registres avec haw publish. Il utilise les chemins de téléversement et l’authentification identiques à ceux de la CI, en lisant les identifiants depuis les mêmes variables d’environnement.

haw publish <files…> --to <nexus|artifactory|gitlab|bitbucket>
            [--name <NAME>] [--version <VER>] [--url <URL>]
            [--dry-run] [--insecure] [--format json]
OptionSignification
<files…>Fichiers ou globs à téléverser. Par défaut haw-evidence.tar.gz s’il est présent et qu’aucun fichier n’est fourni.
--toRegistre cible : nexus, artifactory, gitlab, ou bitbucket (requis).
--nameNom du paquet. Par défaut : le stack courant, sinon le nom du répertoire de l’espace de travail.
--versionVersion du paquet. Par défaut : le SHA HEAD court, sinon unversioned.
--urlRemplace l’URL de base de la cible (sinon prise dans la variable d’environnement de la cible).
--dry-runAffiche exactement ce qui serait téléversé (méthode, URL, emplacement d’authentification) et se termine — sans réseau, sans identifiants requis.
--insecureAutorise un registre non-HTTPS (http://). Par défaut, les registres http:// sont rejetés ; sans cette option, haw publish refuse d’envoyer des identifiants en clair.
--format jsonÉmet un résumé JSON {target, name, version, uploads:[…]}.

Les identifiants proviennent de l’environnement, par cible (mêmes variables que la matrice des secrets CI ci-dessus) :

CibleVariables d’env.
NexusNEXUS_URL, NEXUS_USER, NEXUS_PASS, optional NEXUS_REPO (default raw-hosted)
ArtifactoryARTIFACTORY_URL, ARTIFACTORY_TOKEN, optional ARTIFACTORY_REPO (default generic-local)
GitLabGITLAB_TOKEN, GITLAB_PROJECT_ID, optional GITLAB_URL (default https://gitlab.com)
BitbucketBITBUCKET_USER, BITBUCKET_TOKEN, BITBUCKET_WORKSPACE, BITBUCKET_REPO
haw publish ./out/*.bin --to nexus                 # téléverser les sorties de build vers Nexus raw-hosted
haw publish --to gitlab                             # téléverser haw-evidence.tar.gz vers les packages GitLab
haw publish sbom.json haw-evidence.tar.gz --to artifactory   # plusieurs fichiers à la fois
haw publish app.bin --to bitbucket                  # POST vers les Downloads du dépôt Bitbucket
haw publish app.bin --to nexus --dry-run            # afficher le plan (méthode/URL/auth), sans réseau
haw publish app.bin --to nexus --format json        # résumé de téléversement lisible par machine

Vérifier après téléchargement

Quel que soit le miroir, vérifiez exactement comme avec la GitHub Release — téléchargez les .sha256, .sig et .pem correspondants aux côtés de l’archive :

sha256sum -c haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.sha256
cosign verify-blob \
  --certificate haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.pem \
  --signature   haw-0.1.7-x86_64-unknown-linux-musl.tar.gz.sig \
  --certificate-identity-regexp 'https://github.com/Nastwinns/hawser' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  haw-0.1.7-x86_64-unknown-linux-musl.tar.gz

Voir INSTALL.md pour le flux complet de vérification et d’air-gap.


Retour à INSTALL.md.

Architecture de hawser

hawser est un gestionnaire d’espace de travail multi-dépôts. Il épingle une flotte de dépôts git à des commits exacts, pilote leur cycle de vie PR/MR sur GitHub et GitLab, et présente toute la flotte à travers un cockpit ratatui orienté clavier (le binaire haw). Ce document décrit comment le code est réellement organisé — les frontières entre crates, le modèle de concurrence et les invariants qui le maintiennent testable.

L’objectif de conception qui façonne tout ce qui suit : la logique métier ne fait jamais d’E/S qu’elle ne peut simuler. Les effets de bord git traversent le trait GitBackend, les appels de forge traversent le trait Forge et les effets de bord du TUI traversent le trait Controller. Chaque couture a une implémentation de production et un faux de test, de sorte que l’essentiel du code est testé unitairement sans réseau ni vrai git.

1. Vue d’ensemble

L’espace de travail (Cargo.toml, resolver = "3", édition 2024, unsafe_code = "forbid" sur tout l’espace de travail) est un ensemble de petites crates. haw-core détient le modèle de domaine et ne dépend de rien d’autre que des crates de sérialisation. Tout le reste en rayonne. Le binaire hawser (livré sous le nom haw) est la seule crate qui relie les pièces entre elles et le seul endroit où anyhow est autorisé.

                    +----------------------+
                    | haw-tui              |   depends ONLY on
                    | (ratatui cockpit)    |   ratatui + nucleo + haw-core types
                    +----------+-----------+   — no git, no network.
                               | Controller trait (side-effect seam)
                               v
+---------+   +----------+   +----------+   +-----------+   +-----------+
| hawser  |-->| haw-core |   | haw-git  |   | haw-forge |   | haw-merge |
| (bin)   |   | (domain) |<--| ShellGit |   | Forge     |   | slice/seal|
+----+----+   +----------+   +----------+   +-----------+   +-----------+
     |            ^   ^            (impl of        (octocrab /
     |            |   |         GitBackend)         reqwest)
     +---> haw-git, haw-forge, haw-merge, haw-tui  (binary wires them all)

haw-plugin ....... SDK for out-of-process `haw-<name>` plugin binaries
haw-artifact ..... plugin: SLSA/in-toto provenance + signing
haw-compliance ... plugin: CycloneDX 1.5 + SPDX 2.3 SBOM
haw-git-gate ..... plugin: secret/hygiene gate (gitleaks or heuristic)
xtask ............ release/packaging automation (`cargo xtask dist`)

haw-core n’a aucune dépendance envers haw-git, haw-forge ou haw-tui : il déclare le trait GitBackend (haw-core/src/git/mod.rs) et laisse les appelants injecter une implémentation. haw-git dépend de haw-core et implémente ce trait ; la flèche pointe de l’implémentation vers le trait.

2. Responsabilités des crates

CrateResponsabilitéTypes publics clés
haw-coreModèle de domaine, sans parti pris d’E/S. Manifeste → lock → état de l’espace de travail, résolution, snapshots, journal d’audit, dispatch de plugins.manifest::Manifest, lock::Lockfile / LockedRepo, workspace::Workspace / RepoStatus / SyncPlan / RepoTask, resolver::ResolvedRepo, git::GitBackend (trait)
haw-gitGitBackend de production : délègue au git de l’utilisateur. Assistant de fan-out borné.ShellGit, parallel::fan_out
haw-forgeOrchestration PR/MR + CI derrière le trait Forge ; cycle de vie des changesets agnostique de la forge.Forge (trait), github::GitHub, gitlab::GitLab, ForgeFactory / Tokens, ForgeError, OpenPr / CiRun / PrStatus
haw-mergeFusion collaborative optionnelle de style mergetopus : découpe une fusion riche en conflits par chemin de premier niveau, résout pièce par pièce, scelle en un seul commit sur une branche d’intégration.MergeBackend (trait), git::GitMerge
haw-tuiLe cockpit haw. Rend et dispatche ; ne connaît rien de git ni du réseau.Controller (trait), run, Snapshot, FleetPr / FleetCiRun, Exit
haw-pluginSDK pour créer des binaires de plugin haw-<name> hors processus (contexte haw.plugin/1 en entrée / haw.plugin.report/1 en sortie).run, Report
hawserLe binaire haw. CLI Clap, relie core+git+forge+merge+tui, détient anyhow et la surface d’erreur actionnable.CliController, DemoController, main
xtaskRelease/packaging : construit un binaire de release, archive sous dist/, affiche le SHA-256 pour la formule Homebrew / le manifeste Scoop.main

3. La frontière Controller

Les dépendances de haw-tui sont ratatui, nucleo-matcher et haw-core (pour les types de données simples qu’il rend). Il n’a aucune dépendance envers haw-git, haw-forge ou tokio. Chaque effet de bord dont le cockpit a besoin — rafraîchir le statut, récupérer les PR, fusionner, basculer sur une branche, lire un arbre de fichiers — est une méthode de Controller (haw-tui/src/lib.rs:351) :

#![allow(unused)]
fn main() {
pub trait Controller: Send {
    fn snapshot(&mut self) -> io::Result<Snapshot>;
    fn fleet_prs(&mut self) -> io::Result<Vec<FleetPr>>;
    fn pr_merge(&mut self, repo: &str, number: u64) -> io::Result<String>;
    fn pr_checkout(&mut self, repo: &str, number: u64) -> io::Result<String>;
    fn repo_tree(&mut self, repo: &str, subpath: &str, remote: bool) -> io::Result<Vec<FileEntry>>;
    fn file_content(&mut self, repo: &str, path: &str, remote: bool) -> io::Result<String>;
    // ~25 verbes au total ; tous renvoient io::Result et tous sont Send.
}
}

C’est une inversion de dépendance délibérée. Le TUI est au sommet du graphe et ne dépend que d’une abstraction ; le binaire fournit l’implémentation concrète :

  • CliController (hawser/src/main.rs:2295) est l’implémentation de production. Chaque méthode ouvre un Workspace, construit un backend ShellGit et/ou un client Forge, et exécute les mêmes chemins de code que les sous-commandes CLI (p. ex. sync_filtered réutilise Workspace::plan_sync + fan_out).
  • DemoController (hawser/src/main.rs:3052) renvoie des données préfabriquées en mémoire et n’atteint aucun espace de travail, git ni réseau. haw dash --demo rend chaque vue de manière déterministe pour les enregistrements GIF et, surtout, pour les tests.

Parce que la couture est un objet trait (Box<dyn Controller> passé à haw_tui::run), les 59 tests unitaires de haw-tui construisent des fixtures App et vérifient les spans rendus ainsi que les Job dispatchés vers le canal du worker — pas de terminal, pas de git, pas de sockets. La logique du cockpit est exercée sans interface graphique.

4. Modèle de concurrence

Le cockpit est mono-thread pour le rendu et pilote tout le travail bloquant sur un thread worker dédié unique. Il n’y a aucun async dans l’UI.

Pourquoi pas du tout async. Les clients de forge et les délégations au shell git sont intrinsèquement bloquants ; rendre toute l’UI async n’apporterait qu’un runtime et des fonctions colorées. À la place, la boucle de rendu reste synchrone et décharge le travail bloquant sur un thread, en communiquant via deux canaux std::sync::mpsc.

Le worker. haw_tui::run (haw-tui/src/lib.rs:886) crée un canal Job et un canal Outcome, puis spawn_worker (:914) déplace le Box<dyn Controller> sur un std::thread::spawn. Le worker est une boucle séquentielle : while let Ok(job) = jobs.recv(), faisant correspondre chaque Job à un appel Controller et renvoyant un Outcome. La sérialisation est une fonctionnalité : entrer dans une vue pendant qu’un job est en cours enfile tout de même la lecture ; le worker l’exécute après le job courant (commentaires à :1005, :1144), de sorte que la navigation n’est jamais refusée.

Les enums de canal (:426, :463) :

#![allow(unused)]
fn main() {
enum Job {
    Refresh, ChangesetPrs(String), FleetPrs, FleetCi, Governance,
    RepoDetail(String), PrDetail(String, u64), CiDetail(String, u64),
    PrDiff(String, u64), CiLogs(String, u64),
    RepoTree(String, String, bool), FileContent(String, String, bool, String),
    Action(&'static str, ActionKind),   // verbes à effet de bord (sync, land, merge, ...)
}

enum Outcome {
    Snapshot(Box<io::Result<Snapshot>>), FleetPrs(Box<io::Result<Vec<FleetPr>>>),
    Detail(String, Box<io::Result<String>>),   // exploration détaillée partagée (git de dépôt / PR / CI)
    Tree(Box<io::Result<Vec<FileEntry>>>),
    Action(&'static str, io::Result<String>),
    // ...
}
}

Les résultats sont boxés pour garder l’enum petite malgré des charges utiles volumineuses (snapshots, diffs). L’étiquette &'static str sur Action traverse sans modification pour que le gestionnaire d’outcome sache ce qui s’est achevé.

Rester réactif. La boucle d’événements (event_loop, :1345) ne bloque jamais sur le worker. À chaque itération : vider outcomes.try_recv() (non bloquant) et appliquer les résultats, rafraîchir automatiquement et de manière opportuniste en cas d’inactivité (cadence de 5 s, supprimée pendant la saisie/les overlays/le travail en cours, :1525), dessiner une image, puis event::poll(Duration::from_millis(120)) pour la saisie. app.busy: Option<&'static str> conditionne un spinner et empêche le double-dispatch ; il est effacé à l’arrivée de l’Outcome correspondant. Les vues réseau (Prs/Ci/Governance) sont strictement à la demande — le rafraîchissement automatique en cas d’inactivité ne touche que le snapshot de statut local.

Séparément, le travail CLI inter-dépôts (sync, run) utilise haw_git::parallel::fan_out (haw-git/src/parallel.rs) : fan-out borné entre dépôts avec un simple std::thread::scope et un index atomique partagé, jobs.clamp(1, items.len()) workers. Pas de tokio là non plus.

Le piège runtime.enter() d’octocrab

Forge est un trait synchrone, mais octocrab est async. github::GitHub détient un runtime privé tokio::runtime::Builder::new_current_thread().enable_all().build() (haw-forge/src/github.rs:16) et appelle runtime.block_on(...) pour chaque requête — une façade synchrone au-dessus d’un client async, un seul thread worker, aucun runtime global partagé.

Le bug subtil contre lequel cela protège : construire le client octocrab n’est pas lui-même un await, mais en interne il lance une tâche worker tower::buffer, et ce spawn panique avec « no reactor running » s’il n’y a pas de réacteur Tokio actif dans le contexte du thread courant. block_on n’établit ce contexte que pour la future qu’il pilote — pas pour l’appel synchrone builder.build(). Le correctif est la garde dans client (github.rs:32) :

#![allow(unused)]
fn main() {
// le client d'octocrab lance un worker tower::buffer au build, qui a besoin d'un
// réacteur Tokio actif ; entrer dans le runtime pour que le spawn ne panique pas.
let _guard = self.runtime.enter();
builder.build()...
}

runtime.enter() renvoie un EnterGuard qui installe le réacteur pour la portée courante, de sorte que le spawn tower::buffer trouve un réacteur. Un test de régression dédié construit le client avec un jeton vide et sans réseau pour éviter que cela ne se casse à nouveau silencieusement (github.rs:626, client_builds_inside_runtime_without_panic).

5. L’abstraction Forge

Forge (haw-forge/src/lib.rs:130) est un seul trait avec deux implémentations de production :

  • github::GitHuboctocrab (REST v3) sur le runtime privé current-thread décrit ci-dessus. Prend en charge github.com et Enterprise (base /api/v3).
  • gitlab::GitLabreqwest::blocking::Client contre REST v4. Aucun runtime nécessaire ; les MR se mappent sur le vocabulaire PR neutre vis-à-vis de la forge.

ForgeFactory::client_for (impl Tokens, :273) choisit l’implémentation à partir de la clé forge = explicite du manifeste si elle est présente, sinon par sous-chaîne d’hôte de l’URL (detect, :347), lit les jetons depuis les variables d’environnement conventionnelles (en se rabattant sur un gh auth token connecté, :261), et renvoie un Box<dyn Forge>.

Liste bon marché vs exploration détaillée. Le trait se scinde délibérément en scans de flotte bon marché et explorations détaillées coûteuses, de sorte que les vues de flotte se chargent vite et que le détail n’est récupéré qu’à Enter :

  • list_open_prs / list_ci_runs — un appel borné par dépôt, plafonné à OPEN_PRS_LIMIT = 25 / CI_RUNS_LIMIT = 15 pour garder le nombre de requêtes borné sur les dépôts actifs. Renvoie des lignes OpenPr / CiRun neutres vis-à-vis de la forge.
  • pr_detail / ci_run_detail / pr_diff / ci_logs / file_blob — les récupérations d’exploration détaillée, chacune renvoyant du texte brut plafonné par DIFF_LINE_CAP = 600, LOG_LINE_CAP = 800, FILE_LINE_CAP = 600 via cap_lines (qui ajoute une note « truncated, N more line(s) »).

Gestion des types de média. octocrab décode le JSON, mais les diffs, les blobs bruts et les logs sont du texte brut. GitHub::get_text (github.rs:77) contourne octocrab avec un petit GET reqwest bloquant portant un en-tête Accept personnalisé et suivant les redirections, renvoyant Ok(None) sur 404 :

  • diffs unifiés : Accept: application/vnd.github.v3.diff (l’endpoint pulls renvoie le diff tel quel) ;
  • contenu de fichier brut : application/vnd.github.raw ;
  • logs de job Actions : servis via une redirection 302 vers une URL signée (les logs expirés apparaissent comme un message clair, pas comme une erreur).

Le contrat de rapport en texte brut (pas d’ANSI ; l’appelant applique le style) est ce qui permet aux mêmes chaînes de détail de s’afficher à l’identique dans le CLI et dans la vue détaillée défilable du TUI.

6. Modèle de reproductibilité

Le contrat central est un pipeline à trois étapes : manifeste → lock → état.

  1. haw.toml (manifeste)manifest::Manifest (model.rs:16) : remotes, dépôts, stacks, overlays. Intention rédigée par l’humain. Le rev d’un dépôt est une référence branche/tag/sha.
  2. haw.lock (lockfile)lock::Lockfile (lock/mod.rs:34), LOCK_VERSION = 1, #[serde(deny_unknown_fields)]. Généré par machine. Chaque LockedRepo épingle rev (le SHA résolu exact), source_rev (la référence de manifeste dont il a été résolu) et branch (les dépôts ne sont jamais laissés détachés). Le lock couvre tous les dépôts du manifeste, pas seulement un stack — de sorte que changer de stack ne réécrit jamais le lock ; les overlays ne prennent effet qu’à la régénération.
  3. État de l’espace de travail — le répertoire .haw/ : stack courant, snapshots, journal d’audit. Workspace (workspace/mod.rs:54) lit le manifeste + lock et planifie la synchronisation (plan_syncSyncPlan de RepoTask ciblant chaque SHA verrouillé).

Le lock est l’artefact de preuve/d’audit. Il est déterministe et uniquement LF — un test golden (hawser/tests/golden.rs::lockfile_is_deterministic_and_lf_only) vérifie des octets identiques sur deux exécutions avec les mêmes entrées, pas de CRLF, saut de ligne final. Ce déterminisme est ce qui rend le lock committable et comparable comme enregistrement de provenance de build ; haw-compliance et haw-artifact consomment directement les rev épinglés pour émettre les SBOM et la provenance SLSA.

Dérive. RepoStatus (workspace/mod.rs:99) porte head (le HEAD réel du dépôt), locked_rev (ce que dit haw.lock) et drift: bool — vrai lorsque HEAD diffère du rev verrouillé. Workspace::status le calcule par dépôt ; haw status/haw verify et la grille de flotte du cockpit l’exposent. pin (:257) fait l’inverse : réécrit le lock à partir des HEAD courants (sans réseau), transformant l’état de travail en nouvelle vérité épinglée.

7. Gestion des erreurs

Stratégie à deux couches, scindée proprement à la frontière du binaire :

  • Les bibliothèques utilisent des erreurs typées via thiserror : ForgeError (haw-forge/src/lib.rs:116MissingToken, UnknownForge, Api, …), LockError, WorkspaceError / SyncError, GitError, ManifestError, MergeError. Les appelants peuvent faire un match sur la variante. Le trait Controller réduit celles-ci à io::Result à la couture du TUI (io::Error::other), car le cockpit ne fait jamais que rendre le message.
  • Le binaire utilise anyhow. hawser est la seule crate qui dépend d’anyhow ; run() renvoie anyhow::Result et ajoute .context(...) aux sites d’appel. main (main.rs:609) affiche le error: de premier niveau, parcourt err.chain().skip(1) pour les causes et — la partie actionnable — exécute hint_for (:632) sur le texte d’erreur en minuscules pour attacher un correctif d’une ligne : pas de manifeste → haw init ; jeton manquant → définir HAW_GITHUB_TOKEN / gh auth login ; « drift »/« lock » → haw sync ; « not a git repo » → haw sync pour cloner.

Les lints de l’espace de travail (Cargo.toml) règlent clippy::unwrap_used et clippy::expect_used sur warn dans tout l’espace de travail ; les modules de test réactivent l’usage avec #![cfg_attr(test, allow(...))].

8. Tests

~173 tests, stratifiés pour correspondre aux coutures :

  • Purs/unitaires dans chaque crate : haw-core (édition de manifeste, allers-retours de lock, cycle de vie des changements, resolver, snapshots), haw-forge (repo_coords pour chaque forme d’URL, detect, cap_lines, progress_bar).
  • haw-tui (59 tests) pilotent des fixtures d’état App (p. ex. fleet_app()) et vérifient deux choses : le contenu/les couleurs des Span rendus par les helpers purs draw_*, et le Job exact dispatché sur le canal du worker après un appui touche (rx.try_recv()Ok(Job::FleetPrs), etc.). Pas de terminal, pas de réseau — la couture Controller et le canal rendent tout le cockpit testable sans interface graphique.
  • FakeForge / FakeGit (haw-forge/tests/orchestrate.rs) : l’orchestration des changesets (request/status/land) s’exécute contre des faux en mémoire injectés via FakeFactory, de sorte que la logique de cycle de vie inter-dépôts est vérifiée sans HTTP.
  • Golden de bout en bout (hawser/tests/golden.rs) : construit de vrais dépôts git dans des répertoires temporaires, exécute le binaire haw réel et vérifie une sortie standard normalisée par rapport à des chaînes golden — tree, status + le contrat CI du code de sortie 3 pour dépôt sale, sync, le schéma JSON stable haw.status/1 et le déterminisme du lockfile. Ces tests s’exécutent sur la matrice CI, donc leur réussite signifie que le binaire livré se comporte comme attendu.

La triade Forge/GitBackend/Controller est la raison pour laquelle cette couverture est peu coûteuse : chaque dépendance coûteuse a un faux, et le seul endroit où elles sont reliées à de vraies E/S — hawser — est couvert par les tests golden du binaire.

Conception de la CLI — lexique et options

Objectif : un lexique qu’un nouvel utilisateur comprend sans glossaire, et des options qui corrigent ce qui manquait toujours aux utilisateurs de repo/west.

Lexique (canonique depuis la v0.1)

TermeSignificationRemplace / rejeté
repoun dépôt Git dans l’espace de travail ([repo.NAME])brick (alias accepté), project (jargon de l’outil repo)
stackune composition nommée de dépôts ([stack.NAME], repos = [...])product (alias accepté)
overlaysurcharges nommées par dépôt appliquées au moment du verrouillageprofile, variant
changesetune fonctionnalité répartie sur N dépôts (branche + PR/MR)topic, issue
groupétiquette libre sur un dépôt, utilisée pour filtrer les commandesconservé de l’outil repo, désormais réellement câblé
revce que vous demandez : branche, tag ou SHA — le type est détecté automatiquementrevision, refspec
lock / pinSHA résolu dans haw.lockfreeze (renommage prévu : haw pin / haw unpin)
driftHEAD diffère du SHA verrouillé

Les anciennes orthographes (brick, product, bricks, --product, --bricks) sont interprétées indéfiniment comme des alias ; la sérialisation et la documentation n’utilisent que les nouveaux termes.

Verbes (commandes)

Règle : un verbe devinable par action, proche de git, sans jargon. Les anciens noms sont conservés comme alias masqués pour que rien ne casse.

VerbeFaitAlias (accepté)
haw init <path>amorcer un espace de travail à partir d’un manifeste
haw syncmatérialiser l’arborescence selon haw.lock (écrit le lockfile s’il est absent)
haw treeafficher l’arborescence stack → dépôtgraph
haw statusétat de la flotte : branche, head, modifications, dérive par dépôtst
haw run '<cmd>'exécuter une commande dans chaque dépôt, en parallèle (positionnel)forall (with -c)
haw lockrésoudre la rev de chaque dépôt → SHA dans haw.lock
haw pinépingler haw.lock sur les checkouts actuels (sans réseau)freeze
haw unpinrestaurer haw.lock sur les revs du manifesteunfreeze
haw switch <stack>enregistrer une stack comme courante et la synchroniser
haw repo add|remove|listmodifier les dépôts du manifestebrick
haw stack add|remove|listmodifier les stacks du manifesteproduct
haw change start|status|listworkflow de fonctionnalité inter-dépôts (changeset)
haw grep <pat>diffuser un grep sur chaque dépôt
haw verifybarrière de dérive — code de sortie 3 si l’arborescence diverge de haw.lock
haw buildexécuter la commande build du manifeste de chaque dépôt sur toute la flotte
haw testexécuter la commande test du manifeste de chaque dépôt sur toute la flotte
haw hooks installinstaller les hooks de cycle de vie depuis .haw/hooks/
haw evidenceregrouper SBOM / provenance / signatures dans haw-evidence.tar.gz
haw publish <files> --to <registry>téléverser des artefacts vers un registre privé (voir DISTRIBUTION.md)
haw import --from <west.yml|default.xml>convertir un manifeste west / Google-repo en haw.toml
haw merge plan|resolve|status|cleanup|abortfusion collaborative parallèle (par tranche)
haw completions <shell>afficher un script de complétion shell sur stdout
haw plugins new|list|installgénérer, découvrir et installer des plugins haw-<name>
haw (no args) / haw dashouvrir le cockpit TUItui

haw run prend la commande en argument positionnel (haw run 'git fetch') ; -c/--command fonctionne toujours via l’alias forall. Lancer haw sans sous-commande ouvre le tableau de bord (comme htop, k9s).

Gestion des revs (conviviale par défaut)

  • Un seul champ : rev = "main" | "v6.1.2" | "<40-hex sha>". Pas de clé type = ; le type est détecté (refs/heads > tag pelé > tag > SHA complet).
  • Affichage : les SHA sont affichés sur 8 caractères partout ; haw.lock stocke les 40 complets.
  • Jamais détaché : les revs de branche sont extraites sur une branche du même nom, les tags/SHA sur haw/<rev>.

Groupes (implémentés)

  • groups = ["firmware", "ci"] sur un dépôt.
  • haw sync --group firmware, haw status --group ci, haw forall --group firmware -c ... (répétable ; filtre vide = tout ; un filtre exclut les dépôts sans groupe).
  • Les groupes sont enregistrés dans haw.lock afin que le filtrage fonctionne hors ligne.

Grille des options

OptionCommandesRemarque
--stack <S>sync, treealias --product ; défaut : dernier switch, sinon la seule stack
--overlay <O>lock, sync*, treerépétable, le dernier l’emporte ; *sync uniquement lors de la génération du lockfile
--group <G>sync, status, runrépétable
--repos a,bchange startalias --bricks
--slug <S>repo addchemin du dépôt sous --remote (alias --repo) ; avec --remote, pas --url
-j, --jobs <N>sync, switch, rundéfaut min(cœurs, 8)
--recurse-submodulessynccloner/mettre à jour les sous-modules git de chaque dépôt, épinglés au superprojet
--skip-branchchange startadopter les branches actuelles (RepoFleet)
--branch <B>change startdéfaut change/<id>

Raccourcis clavier de la TUI

style k9s/lazygit, priorité au clavier. Trois mécanismes portent tout :

  • les chiffres 17 changent de vue (depuis n’importe quelle vue liste de premier niveau),
  • a ouvre les actions contextuelles de la vue courante (un menu de style lazygit),
  • : est la barre de commande pour le reste — ses verbes reflètent la CLI (apprenez-en un, connaissez les deux).

Les données se chargent sur un worker en arrière-plan — l’interface ne se bloque jamais. La grille de la flotte se rafraîchit automatiquement toutes les ~5 s au repos (jamais pendant que vous saisissez, dans un overlay, une confirmation, ou lorsqu’une tâche est en cours) ; F5 / ctrl-r rafraîchissent à la demande.

Global (figé — ces touches signifient la même chose dans chaque vue)

ToucheAction
/ or k/jdéplacer le curseur (dans un détail : faire défiler d’une ligne)
enterexplorer en détail (stack → flotte → détail dépôt/PR/CI) · confirmer une invite y/n
esc / b / effacer un filtre actif, sinon revenir d’un niveau
qquitter · ctrl-c forcer la sortie
/filtrer la grille en flou (en direct, insensible à la casse : /knlkernel)
:barre de commande (reflète les verbes de la CLI, voir ci-dessous)
?overlay d’aide
F5 / ctrl-rrafraîchir maintenant
ctrl-d / ctrl-udemi-page bas / haut · PageUp / PageDown page entière
ggoto — quitter et afficher le chemin du dépôt sous le curseur (cd "$(haw dash)")
wbasculer le mode veille — rafraîchir automatiquement la flotte et la vue PR/CI ouverte
spacemarquer / démarquer le dépôt sous le curseur (Fleet et Changeset uniquement ; affiché sous forme de )

Sauts de vue (17) — depuis n’importe quelle vue liste

ToucheVuealias :
1flotte:fleet
2changesets:changesets
3PR/MR:prs
4exécutions CI:ci
5arbre:tree
6gouvernance:governance
7plugins:plugins

Les chiffres sont inertes dans les vues de défilement/détail (détail dépôt/PR/CI, fichiers, grep) — sautez depuis une liste. Le tri (</>/.) s’applique aux tables Fleet, PR/MR et CI.

Vue Fleet

ToucheAction
ssync — les dépôts marqués s’il y en a, sinon le dépôt sous le curseur, sinon la stack
spacemarquer / démarquer le dépôt sous le curseur (affiché sous forme de )
rexécuter une commande — sur les dépôts marqués s’il y en a, sinon sur toute la flotte
pfiltre problèmes uniquement (⚠ modifié / dérive / en retard / manquant)
xouvrir un shell dans le dépôt sous le curseur (quitte le cockpit)
fparcourir les fichiers du dépôt sous le curseur (disque local ou forge)
!exécuter une commande shell dans le dépôt sous le curseur (dans sa vue détail)
enterexplorer le détail git du dépôt sous le curseur (branche, SHA, statut, log, diffstat, remotes)

Le changement de stack, le lock et le git-fetch sont passés dans la barre de commande : :stack (sélecteur) / :stack NAME, :lock, :fetch. L’épinglage du lockfile se fait avec p dans la vue Stacks (ou :pin).

Les marques persistent entre les vues Fleet et Changeset ; lorsque des marques sont posées, s (sync) et r/:run n’agissent que sur l’ensemble marqué.

Vue PR/MR de la flotte (3) et vue CI (4)

ToucheAction
enterexplorer en détail — PR/MR : relecteurs, vérifications, corps, url · exécution CI : jobs, étapes, conclusion
amenu d’actions — PR/MR : m fusionner · a approuver · c checkout (chacune demande y/n)
dlire le diff de la PR/MR (défilable)
llire les logs de l’exécution/pipeline CI (défilables)
fparcourir les fichiers modifiés de la PR/MR (vue fichiers-PR)
oouvrir la ligne sous le curseur dans votre navigateur
< > .trier la table
b / escretour

a (actions) et d sont également disponibles depuis le détail d’une PR/MR. Le rechargement se fait désormais simplement avec F5 / ctrl-r.

Vue Fichiers (f depuis un dépôt)

Un navigateur en lecture seule : visualisez ou sélectionnez un fichier à n’importe quelle ref, sur le disque local ou la forge (GitHub / GitLab / Bitbucket). Il n’indexe/ne committe jamais — il ne fait que lire. Deux modes partagent le même contexte dépôt / ref / portée et basculent avec T : une liste plate d’un seul répertoire (par défaut) et un arbre navigable et extensible.

ToucheAction
enterouvrir un répertoire, ou afficher le contenu d’un fichier (défilable)
Tbasculer vers la vue arbre (et revenir)
rsélecteur de ref — lire les fichiers À LA DATE d’une branche / tag / SHA choisi
eéditer le fichier sous le curseur dans $EDITOR (fichiers locaux uniquement)
Rbasculer entre l’arbre du disque local et la vue forge
b / escremonter d’un répertoire, puis revenir à la flotte
xouvrir un shell dans le dépôt

Vue Arbre (T depuis Fichiers)

ToucheAction
enter / déplier le répertoire (ou ouvrir le fichier) sous le curseur
replier le répertoire (ou sauter à son parent et le replier)
rsélecteur de ref (identique à Fichiers)
Trevenir à la liste plate
Rbasculer local ⇄ forge
b / escrevenir à la flotte

L’arbre récupère une fois tous les chemins de fichiers du dépôt à la ref active, puis se déplie/replie côté client. Les répertoires repliés affichent , dépliés ; les fichiers sont indentés sous leurs parents.

Sélecteur de ref (r dans l’un ou l’autre mode)

r ouvre une fenêtre listant les branches puis les tags du dépôt (j/k + enter pour choisir), plus une ligne de saisie pour taper une ref arbitraire ou un SHA de commit. Sélectionner une ref recharge la vue courante À LA DATE de cette ref (la liste plate se réenracine à la racine du dépôt ; l’arbre récupère à nouveau ses chemins). Le titre du panneau affiche honnêtement la ref active : @ main, @ v1.0.0, @ a1b2c3d, ou @ HEAD (local) / @ default (distant) lorsqu’aucune n’est épinglée. Les refs locales proviennent de git for-each-ref / git ls-tree / git show <ref>:<path> ; les refs et arbres de la forge proviennent de l’API REST de chaque forge.

e suspend le cockpit, confie le TTY courant à $VISUAL/$EDITOR (avec repli sur nvim/vim/vi) sur le chemin absolu du fichier, puis reprend et recharge la liste. Il est refusé sur la vue forge (R) et sur les répertoires ; si le dépôt n’est pas sur le disque, il propose de synchroniser.

Vue Erreurs, vue Plugins, vue Gouvernance

Accédez-y depuis une vue liste (Erreurs via :errors/:err, Plugins via 7/:plugins, Gouvernance via 6/:governance). Dans Gouvernance, o ouvre l’artefact du plugin sous le curseur (SBOM / provenance / …). Le rechargement se fait avec F5 / ctrl-r ; b / esc reviennent en arrière.

Vue Changeset

ToucheAction
nnouveau changeset
spacesélectionner / désélectionner un dépôt
amenu d’actions — r demander des PR/MR liées entre elles (sélectionnées, ou toutes si aucune) · l intégrer dans l’ordre des dépendances (chacune demande y/n)
galler au dépôt sous le curseur

Menu d’actions (a)

a ouvre une fenêtre actions encadrée listant les actions contextuelles de la vue courante, chacune avec sa sous-touche. Appuyer sur une sous-touche listée déclenche cette action — les actions d’écriture conservent leur barrière de confirmation y/n. esc (ou toute touche non listée) annule. Les vues sans action le signalent.

Barre de commande (:)

Les verbes reflètent la CLI, et la ligne d’état affiche la commande exacte que chacun exécute, de sorte que la TUI sert aussi à découvrir la CLI.

CommandeAction
:stackouvrir le sélecteur de changement de stack (alias :stacks)
:stack NAME / :switch NAMEbasculer vers une stack
:lockcommitter le lockfile (résoudre les revs → SHA)
:fetchgit fetch sur le dépôt sous le curseur
:errors / :errvue erreurs — échecs collectés sur toute la flotte
:fleet / :changesets / :treesauts de vue (identique à 1 / 2 / 5)
:prs / :civues PR/MR / CI à l’échelle de la flotte (identique à 3 / 4)
:governance / :pluginsvue gouvernance / plugins (identique à 6 / 7)
:syncsynchroniser la stack courante
:run CMDexécuter une commande (sur les dépôts marqués dans Fleet, sinon sur la flotte)
:build / :test / :verifybuild / test / vérification de dérive sur la flotte
:pin / :locképingler les HEAD / committer le lockfile
:change [ID | start ID | land ID | request ID]workflow de changeset
:merge [cleanup <repo> | abort <repo>]lister / sceller / abandonner les fusions en cours
:grep <pat>diffuser un grep sur chaque dépôt
:sh CMDexécuter une commande shell dans le dépôt sous le curseur
:problemsbasculer le filtre problèmes uniquement (⚠ modifié/dérive/en retard/manquant)
:watchbasculer le rafraîchissement automatique en veille (identique à w)
:<repo>déplacer le curseur de la flotte vers un dépôt dont le nom correspond
:theme [NAME]changer de thème en direct (sans argument, ouvre le sélecteur de thèmes ; le thème choisi est persisté)
:editor [CMD]définir l’éditeur de la touche e (sans argument, ouvre un sélecteur des éditeurs du PATH ; persiste)
:compactbasculer l’en-tête replié sur une ligne (persiste)
:helpoverlay d’aide

Thèmes / skins

Sept habillages intégrés : classic, catppuccin (par défaut), dracula, nord, gruvbox, solarized, monochrome. classic est un habillage neutre en ANSI 16 couleurs qui se lit aussi bien sur les terminaux clairs que sombres. :theme sans argument ouvre un sélecteur interactif (mettant en évidence l’habillage actif) ; appuyer sur enter applique l’habillage en direct et le persiste dans [ui].theme de la configuration. :theme <name> change (et persiste) toujours directement.

Configuration utilisateur — ~/.config/haw/config.toml

Optionnel. Un fichier absent, une table partielle ou une clé inconnue retombent tous sur des valeurs par défaut raisonnables (le cockpit n’échoue jamais en cas d’absence). HAW_CONFIG remplace le chemin.

[ui]
theme = "classic"        # startup theme (a built-in name)
editor = "nvim"          # editor for the `e` key
compact_header = false   # start with the header collapsed
refresh_secs = 5         # idle auto-refresh cadence (clamped 2–60)

[keys]                   # remap a SAFE subset of action keys (single chars)
sync = "s"               # remappable: sync, goto, run, shell, files, problems, watch
goto = "g"

Priorité :

  • Thème : NO_COLOR (non vide) → monochrome ; sinon HAW_THEME (env) s’il nomme un thème intégré ; sinon [ui].theme ; sinon le catppuccin par défaut.
  • Éditeur : $VISUAL$EDITOR (l’env l’emporte toujours) → [ui].editor → le premier de nvim/vim/vi sur le PATHvi.

Raccourcis personnalisés ([keys])

Chaque entrée associe un nom d’action à un unique caractère de touche. Seul un sous-ensemble sûr est remappable — sync, goto, run, shell, files, problems, watch. Les globales gelées (j, k, :, /, ?, q, b, space, g, w, et les sauts de vue par chiffres 17) ne peuvent jamais être la cible d’un remap ; un remap qui vise une globale, duplique un autre remap, entre en collision avec une touche d’action par défaut ou n’est pas un unique caractère est ignoré avec un avertissement au démarrage. Quand un remap est actif, les indices de l’en-tête montrent la touche active pour rester honnêtes. La touche par défaut d’origine continue de fonctionner aussi (un remap ajoute, ne retire jamais).

Livré depuis la rédaction de cette conception

  • haw pin / haw unpin (alias freeze/unfreeze).
  • --label <L> sur change start, transmis aux PR/MR lors de change request.
  • clé forge = "github" | "gitlab" sur [remote.X] pour les hôtes que l’heuristique d’URL ne détecte pas.
  • deps = [...] sur un dépôt — change land fusionne dans un ordre topologique stable.
  • haw verify, haw sync --locked, --format json sur status/tree, code de sortie 3 en cas de dérive.
  • haw build / haw test (commandes par dépôt dans le manifeste), hooks de cycle de vie dans .haw/hooks/, haw hooks install, haw evidence, plugins haw-<name>.
  • Nuance de lexique : --slug sur repo add accepte --repo comme alias ; haw run prend la commande en argument positionnel (forall -c fonctionne toujours).
  • Dans la TUI, g (goto) quitte et affiche le chemin du dépôt — cd "$(haw dash)" — au lieu de lancer un shell imbriqué.
  • TUI : rafraîchissement automatique en direct au repos (~5 s), filtre flou / (nucleo), tri par colonne (</>/.), marques + s/r en lot, détails pour les dépôts/PR/exécutions CI, le menu d’actions a (fusionner / approuver / checkout dans PR/MR ; demander-PR / intégrer dans Changeset), la vue gouvernance à l’échelle de la flotte (6), le navigateur de fichiers (f) avec un arbre navigable (T), un sélecteur de ref (r) et l’édition locale (e), et sept thèmes dont classic (HAW_THEME, NO_COLOR, :theme en direct avec un sélecteur interactif).
  • Fichier de configuration de la TUI ~/.config/haw/config.toml : [ui] thème de démarrage / éditeur / compact_header / refresh_secs, plus [keys] raccourcis personnalisés pour un sous-ensemble sûr de touches d’action. Les sélecteurs interactifs :theme / :editor persistent un choix ; :compact bascule l’en-tête replié.

Prévu (pas encore implémenté)

  • Commodités liées aux tags : haw lock --as-of <tag> ; haw status indiquant le type de rev (branch/tag/sha).
  • haw auth login — flux OAuth par appareil + trousseau du système d’exploitation (voir ARCHITECTURE DR-14).
  • TUI : prise en charge de la souris.

hawser — Extensions, plugins, authentification & CI/CD

Comment hawser reste ouvert à ses frontières : il orchestre git, les forges et les outils de build — il ne les réimplémente jamais. Tout ce qui est spécifique à une tâche (systèmes de build, forges, étapes personnalisées) s’enfiche depuis l’extérieur via des interfaces stables et sans surprise. Se combine avec ARCHITECTURE.md (internes) et COMPLIANCE.md (preuves).

Règles de conception, dans l’ordre :

  1. Natif git. Si c’est déjà un fichier texte que git comprend (lock, alternates, gitdir), utilisez-le. Jamais de base de données cachée.
  2. Orchestrer, ne pas réimplémenter. haw décide quoi et quand ; les outils de l’utilisateur font le comment. haw ne connaît rien de CMake, Bazel, Yocto, Jenkins — il délègue au shell.
  3. Tuyaux Unix. Vue humaine dans le TUI ; vue machine via --format json sur stdout. Tout ce que haw affiche, un script peut le consommer.
  4. Échec ouvert. Une forge inconnue, un jeton manquant, un plugin absent dégrade une seule fonctionnalité — cela ne bloque jamais sync/tree/status.

1. Mécanismes d’extension

Quatre couches, de la moins coûteuse d’abord. Choisissez la plus basse qui résout le problème.

1.1 run — exécuter n’importe quelle commande sur tous les dépôts

La trappe de sortie universelle. Exécution parallèle de commandes sur chaque dépôt (filtrable par groupe).

haw run 'git fetch --tags'
haw run --group firmware 'cmake --build build'

Aucun plugin nécessaire pour les 80 % des cas : « faire X dans chaque dépôt ».

1.2 Hooks — exécuter des scripts à des points du cycle de vie

Hooks de style git déclenchés autour des opérations haw. Les scripts se trouvent dans .haw/hooks/ (ou sont déclarés dans le manifeste) et reçoivent le contexte via l’environnement + JSON sur stdin.

HookSe déclenche
pre-sync / post-syncavant/après un haw sync
pre-lock / post-lockautour de la (re)génération du lockfile
post-switchaprès haw switch <stack>
post-change-startaprès la création d’une branche de changeset

Exemple — installer un hook git qui rejette un commit lorsque haw.lock est périmé (la garantie d’intégrité « à la git ») :

haw hooks install    # écrit un pre-commit qui exécute `haw verify --lock`

1.3 Commandes par dépôt dans le manifeste

Déclarez comment un dépôt est construit/testé afin que haw puisse le piloter sans coder en dur le moindre outil de build.

[repo.app-mqtt]
remote = "github"
repo   = "app-mqtt.git"
rev    = "release/2.x"
build  = "cmake --build build --preset release"   # haw délègue simplement au shell
test   = "ctest --test-dir build"

haw reste agnostique quant au système de build : CMake, Bazel, Meson, Cargo, Make — tout n’est que des chaînes de caractères.

1.4 Plugins de sous-commande — haw-<name> sur le PATH

Le modèle git / cargo / kubectl. Un haw foo … qui n’est pas intégré exécute haw-foo depuis le PATH, en transmettant les arguments et le contexte de l’espace de travail (via l’environnement + HAW_JSON sur stdin). La communauté livre haw-jira, haw-bazel, haw-sbom-scan sans toucher au cœur.

  • Découverte : tout exécutable nommé haw-* sur le PATH.
  • Contrat : haw transmet la racine de l’espace de travail, le stack courant et les dépôts résolus au format JSON sur stdin ; le plugin affiche les résultats au format JSON sur stdout ; haw les rend ou les redirige.
  • Isolation : les plugins sont des processus distincts — un plugin défectueux ne peut pas faire planter haw.

Le cœur reste petit. haw maintient le moteur de composition + orchestration ; les intégrations (chaque particularité de forge, chaque outil de build, chaque tracker) vivent dans les plugins et les hooks.

1.5 Interface machine — --format json

Chaque commande de lecture (status, tree, change status, verify, evidence) propose --format json avec un schéma stable et versionné, plus des codes de sortie stables. C’est ce que consomment la CI, les tableaux de bord et les plugins. Le TUI est pour les humains ; le JSON est pour les machines.


2. Authentification (fonctionne sur n’importe quel dépôt)

Le déclic d’adoption : l’authentification de transport est gratuite ; l’authentification de forge est sur adhésion. haw n’invente jamais son propre magasin d’identifiants.

2.1 Transport (clone / fetch / push) — zéro configuration

haw délègue au git de l’utilisateur, il hérite donc automatiquement de l’authentification git existante :

  • Clés SSH via ssh-agent (le défaut en entreprise/embarqué).
  • HTTPS via les assistants git credential (Git Credential Manager, osxkeychain, cache).

Fonctionne avec n’importe quel hôte — GitHub, GitLab, Gitea, Bitbucket, auto-hébergé, SSH nu — sans intégration. Si git clone fonctionne, haw sync fonctionne. C’est exactement ainsi que repo et west s’authentifient : ils ne le font pas.

2.2 API de forge (ouvrir/lire PR-MR, statut CI) — jeton, uniquement à l’usage

Seules les fonctionnalités PR/MR nécessitent des identifiants d’API, résolus dans cet ordre :

  1. Environnement : GITHUB_TOKEN / GH_TOKEN, GITLAB_TOKEN, HAW_FORGE_TOKEN.
  2. Réutiliser le jeton stocké d’un CLI existant : gh auth token, glab auth.
  3. git credential pour les jetons HTTPS.
  4. Flux d’appareil OAuth (haw auth login) — affiche un code à saisir dans un navigateur, sans redirection localhost, fonctionne sans interface/via SSH ; le jeton est stocké dans le trousseau de l’OS. C’est le modèle gh / docker login / aws sso.
  • Auto-hébergé : URL de base d’API configurable par forge (GitHub Enterprise, GitLab self-managed). Obligatoire pour le marché cible.
  • Isolé (air-gapped) : jeton via env/fichier uniquement ; pas de flux navigateur, pas de sortie (voir COMPLIANCE §6).
  • Ne jamais persister un jeton dans haw.toml, haw.lock, les journaux ou l’état de l’espace de travail (COMPLIANCE §5.6). Masquez les chaînes ayant l’apparence d’un identifiant.

2.3 Détection de forge

haw met en correspondance l’URL distante de chaque dépôt → forge (GitHub/GitLab/…) via le trait Forge. Hôte inconnu → le transport fonctionne toujours ; seules les fonctionnalités PR/MR se désactivent pour ce dépôt. Échec ouvert.


3. Intégration CI/CD

haw est conçu pour être piloté par des pipelines, pas seulement par des humains.

3.1 Checkout reproductible

haw sync --locked      # matérialise l'arbre exact de haw.lock ; échoue si le lock est manquant/périmé
haw verify             # vérifie que arbre sur disque == lock (porte de dérive) ; non nul en cas de dérive

--locked est le contrat CI : pas de résolution de rev, pas de non-déterminisme réseau — le lock committé fait loi. Déterministe sur Linux/macOS/Windows (COMPLIANCE §8).

3.2 Portes via JSON + codes de sortie

haw status --format json | jq -e '.repos[] | select(.dirty or .drift)' && exit 1 || true

Codes de sortie stables : 0 ok, valeurs non nulles distinctes pour dérive / échec de vérification / échec de signature.

3.3 Jetons en CI

Injectez les jetons de forge comme secrets CI (GITHUB_TOKEN dans Actions, variables masquées dans GitLab). Pas de connexion interactive dans les pipelines. Le transport utilise la clé SSH du runner ou un jeton de déploiement.

3.4 Cache de partage d’objets (CI rapide)

haw sync --locked --shared   # git alternates contre un cache miroir préchauffé ; fichier texte, pas de liens symboliques

Mettez le miroir en cache entre les exécutions pour éviter de re-cloner de grands arbres de dépôts.

3.5 Preuves dans le pipeline de version

haw evidence --out haw-evidence.tar.zst   # baseline + SBOM + provenance + enregistrement de config de l'outil

Attachez à la version pour le paquet de données de certification (COMPLIANCE §3, §4).

3.6 GitHub Actions (ébauche)

- uses: actions/checkout@v4
- run: cargo install hawser
- run: haw sync --locked --shared
  env: { GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} }
- run: haw verify
- run: haw run --group firmware 'cmake --build build --preset release'

3.7 GitLab CI (ébauche)

build:
  script:
    - haw sync --locked
    - haw verify
    - haw run 'make'
  variables: { GITLAB_TOKEN: $CI_JOB_TOKEN }

4. Où cela s’inscrit dans le plan

Ajouts au plan par phases dans ARCHITECTURE.md §6 ; chaque élément est cadré pour garder le cœur petit et pousser les intégrations vers les frontières. État au 2026-07-15 : tout ce qui suit est livré, à l’exception de la connexion par flux d’appareil OAuth (reportée — ARCHITECTURE DR-14) et de la charge utile SBOM/provenance complète de haw evidence (bundle actuel : manifeste, lock, journal d’audit, JSON de statut, enregistrement de l’outil).

CapacitéCouchePhase
run en parallèle sur les dépôts (alias forall)cœur3
--format json + schémas/codes de sortie stablescœur1→3
Transport de forge (natif git, zéro configuration)haw-git1
Jetons d’API de forge (env / réutilisation gh-glab)haw-forge1 (GH), 3 (GL)
Connexion par flux d’appareil OAuth + trousseauhaw-forgereporté (DR-14)
URL de base de forge auto-hébergéehaw-forge3
Hooks de cycle de vie (pre/post-sync, …)cœur + haw hooks4
haw hooks install (pre-commit lock périmé)cœur4
Commandes build/test par dépôt dans le manifestemodèle de manifeste4
Plugins de sous-commande (haw-<name> sur le PATH)dispatch hawser5
Contrat JSON stdin/stdout des pluginscœur5
Porte de dérive haw verify (CI)cœur1→2
Bundle haw evidencecœur3
Cache de partage d’objets --sharedhaw-git2

Contrainte directrice : le cœur ne développe jamais de dépendance forte envers un outil de build, un tracker ou un système de CI spécifique. Ceux-ci arrivent sous forme de hooks, de commandes par dépôt ou de plugins haw-*.

Plugins

haw suit le modèle git / cargo / kubectl : toute sous-commande que haw ne reconnaît pas est déléguée à un exécutable présent sur votre PATH. Livrez haw-jira, haw-bazel, haw-sbom-scan sans toucher au cœur.

Un plugin est :

  • un exécutable nommé haw-<name> quelque part sur le PATH,
  • exécuté comme un processus séparé — un plugin défaillant ou bloqué ne peut pas faire planter haw,
  • il reçoit le contexte de l’espace de travail au format JSON afin de pouvoir agir sur la flotte courante.

Contrat de répartition

Lorsque vous exécutez haw <name> <args...> et que <name> n’est pas une commande intégrée, haw :

  1. Résout le nom de binaire haw-<name> et le lance via une recherche dans le PATH.
  2. Transmet <args...> tels quels comme argv du plugin (haw ne les analyse pas).
  3. Transmet le contexte de l’espace de travail sous forme de document JSON haw.plugin/1 de deux façons :
    • dans la variable d’environnement HAW_JSON, et
    • écrit sur le stdin du plugin. (Les deux transportent le document identique — lisez celui qui vous convient.)
  4. Laisse les stdout et stderr du plugin connectés au terminal (ou au pipe avec lequel haw a été invoqué) — le plugin affiche directement.
  5. Attend le plugin et propage son code de sortie comme code de sortie propre de haw (borné à 0–255 ; un plugin tué par un signal remonte comme 1).

Si aucune commande intégrée ne correspond et qu’aucun haw-<name> n’est trouvé sur le PATH, haw échoue avec :

error: no built-in `<name>` and no `haw-<name>` on PATH: No such file or directory (os error 2)

et se termine avec un code non nul. La répartition échoue proprement : un mot inconnu est une erreur, jamais un plantage.

Le contexte haw.plugin/1

Le document JSON que haw fournit (via HAW_JSON et stdin). À l’intérieur d’un espace de travail, il est entièrement renseigné :

{
  "schema": "haw.plugin/1",
  "root": "/path/to/workspace",
  "stack": "gateway",
  "repos": [
    { "name": "kernel", "path": "/path/to/workspace/kernel", "rev": "v6.1.2", "groups": ["firmware"] },
    { "name": "hal",    "path": "/path/to/workspace/hal",    "rev": "main",   "groups": ["firmware"] }
  ]
}
ChampSignification
schemaToujours "haw.plugin/1". Vérifiez-le avant de faire confiance au reste.
rootRacine absolue de l’espace de travail (l’arborescence contenant haw.toml).
stackNom du stack courant, ou null si aucun n’est sélectionné.
repos[]Dépôts résolus : name, path absolu, rev et groups.

Exécuté en dehors d’un espace de travail, haw répartit toujours le plugin mais le contexte se réduit au seul marqueur de schéma :

{ "schema": "haw.plugin/1" }

Un plugin bien conçu vérifie la présence de root/repos et adopte un comportement sensé lorsqu’ils sont absents (afficher l’aide, opérer sur le répertoire courant, ou se terminer proprement).

Panneaux TUI — afficher votre propre surface de cockpit

Le cockpit (haw dash) dispose d’une vue Plugins à part entière (appuyez sur 7, ou :plugins) qui liste chaque plugin disponible — les clés du [plugins] du manifeste unies avec les exécutables haw-* découverts sur le PATH. En sélectionner un avec Enter exécute le plugin dans une render intent et affiche sa sortie dans le panneau de détail défilable intitulé plugin: <name>.

Le contrat de rendu ajoute deux signaux au contexte haw.plugin/1 normal afin qu’un plugin puisse savoir qu’on lui demande un panneau lisible par un humain (plutôt que d’être déclenché pour une phase du cycle de vie) :

  • la variable d’environnement HAW_RENDER=1 est définie, et
  • le JSON de contexte (sur HAW_JSON et stdin) porte "intent": "render".
{
  "schema": "haw.plugin/1",
  "intent": "render",
  "root": "/path/to/workspace",
  "stack": "gateway",
  "repos": [ /* ... comme ci-dessus ... */ ]
}

Lorsqu’il les détecte, le plugin doit afficher un panneau sur stdout et se terminer. Deux formes de sortie sont acceptées :

  1. Structurée — un document haw.plugin.view/1. haw affiche son title suivi de chaque chaîne dans lines :

    {
      "schema": "haw.plugin.view/1",
      "title": "SBOM status",
      "lines": [
        "kernel   ✓ SBOM emitted",
        "hal      ✓ SBOM emitted",
        "app-mqtt ⚠ stale"
      ]
    }
    
  2. Texte brut — tout ce qui n’est pas un document haw.plugin.view/1 est affiché tel quel comme corps du panneau. Cela permet à un plugin de printf un rapport simple sans aucun JSON.

La sortie est limitée en nombre de lignes pour borner le panneau. Un plugin qui ne produit aucune sortie affiche un court texte de remplacement. Un plugin qui n’est pas sur le PATH signale une erreur claire dans le cockpit plutôt que de le faire planter.

Gérer les plugins

haw plugins (au pluriel) est la surface de gestion — découvrir ce qui existe, voir ce qui est installé, et installer les plugins de première partie. C’est une sous-commande définie, elle n’entre donc jamais en collision avec la répartition haw <name> : haw plugins list exécute toujours la commande intégrée, même si un binaire haw-plugins se trouve sur le PATH.

haw plugins list

Un tableau fusionnant trois sources, dédupliquées par nom :

  • Catalogue officiel — les plugins de première partie livrés dans ce dépôt.
  • Installés — chaque exécutable haw-<name> trouvé sur le PATH.
  • Abonnés — les entrées [plugins] du manifeste de l’espace de travail et leurs phases (lorsqu’exécuté à l’intérieur d’un espace de travail ; se réduit proprement lorsqu’il n’y en a aucun).
$ haw plugins list
NAME        STATUS     SUBSCRIBED    DESCRIPTION
artifact    available  -             SLSA/in-toto provenance + cosign/minisign signing
aspice      installed  pre-request   ASPICE/qualification traceability from the pinned fleet
compliance  available  post-build    SBOM (CycloneDX + SPDX) generation
...

STATUS vaut installed lorsque le binaire haw-<name> est sur le PATH, sinon available. SUBSCRIBED liste les phases du manifeste, ou -. Un plugin découvert sur le PATH qui ne figure pas dans le catalogue apparaît tout de même (avec la source path).

--format json émet un document haw.plugins/1 pour l’outillage :

haw plugins list --format json | jq '.plugins[] | select(.installed | not) | .name'
{
  "schema": "haw.plugins/1",
  "plugins": [
    {
      "name": "aspice",
      "crate": "haw-aspice",
      "installed": true,
      "subscribed_phases": ["pre-request"],
      "description": "Traçabilité ASPICE/qualification depuis la flotte épinglée",
      "source": "catalog"
    }
  ]
}

haw plugins install <name>

Installe un binaire de plugin via cargo install. Un nom de catalogue (aspice) est résolu vers son crate (haw-aspice) ; toute autre valeur est utilisée telle quelle, un nom de crate complet fonctionne donc aussi. Les plugins de première partie sont membres de l’espace de travail (pas encore sur crates.io), la source par défaut est donc --git https://github.com/Nastwinns/hawser :

haw plugins install aspice                 # cargo install --git <repo> haw-aspice
haw plugins install aspice --dry-run        # afficher la commande, ne rien exécuter
haw plugins install haw-foo --git https://example.com/me/plugins   # source personnalisée
haw plugins install haw-foo --git https://example.com/me/plugins --tag v1.2.0   # épingler à un tag
haw plugins install haw-foo --git https://example.com/me/plugins --rev 9f3c1a2   # épingler à un commit
haw plugins install some-crate --locked     # respecter le Cargo.lock du crate

Épinglez une source --git personnalisée avec --tag <TAG> ou --rev <SHA> (mutuellement exclusifs). Ils ne s’appliquent qu’à une source --git personnalisée — la source de première partie par défaut est déjà épinglée automatiquement à cette version de hawser. Les installations sont toujours --locked pour la reproductibilité (--locked est désormais sans effet, conservé pour compatibilité).

haw affiche exactement ce qu’il va exécuter ($ cargo install …) avant de le lancer, diffuse la sortie de cargo et propage le code de sortie de cargo. --dry-run affiche la commande et se termine sans toucher à cargo. Si cargo n’est pas sur le PATH, haw échoue avec une erreur exploitable pointant vers https://rustup.rs.

haw plugins path

Affiche les répertoires que haw parcourt à la recherche de plugins haw-* (les entrées du PATH) — déposez un exécutable haw-<name> dans l’un d’entre eux pour le rendre détectable :

haw plugins path

Générer le squelette d’un plugin

haw plugins new <name> --lang <rust|python|go|shell> [--dir <path>] écrit un squelette de plugin exécutable qui implémente déjà le contrat : il lit le contexte haw.plugin/1 depuis HAW_JSON (en se rabattant sur stdin), gère --help et --format json, émet un document haw.plugin.report/1, et se réduit proprement lorsqu’il est exécuté en dehors d’un espace de travail. La cible par défaut est ./haw-<name> ; --dir la remplace. haw refuse d’écraser un répertoire non vide.

haw plugins new sbom --lang shell     # ./haw-sbom/haw-sbom (POSIX sh) + README.md
haw plugins new sbom --lang python    # ./haw-sbom/haw-sbom (python3) + README.md
haw plugins new sbom --lang go        # ./haw-sbom/{main.go, go.mod, README.md}
haw plugins new sbom --lang rust      # crate cargo : Cargo.toml + src/main.rs + README.md
haw plugins new sbom --lang shell --dir /tmp/sbom   # choisir le répertoire cible

Selon le langage, le point d’entrée et l’étape de build diffèrent :

--langFichier(s) d’entréeLe rendre exécutable
shellhaw-<name> (POSIX sh exécutable)déjà exécutable — déposer sur le PATH
pythonhaw-<name> (exécutable, python3)déjà exécutable — déposer sur le PATH
gomain.go + go.mod (module haw-<name>)go build -o haw-<name>
rustCargo.toml ([[bin]] haw-<name>) + src/main.rscargo build --release

Chaque squelette est livré avec un README.md contenant la recette « déposer sur le PATHhaw <name> » et un extrait d’abonnement [plugins]. Les squelettes rust et go sont autonomes — celui en rust ne dépend que de serde/serde_json, d’aucun crate haw. Après le build, placez le binaire sur le PATH et exécutez-le :

haw plugins new demo --lang shell --dir /tmp/haw-demo
PATH="/tmp/haw-demo:$PATH" haw demo
HAW_JSON='{"schema":"haw.plugin/1"}' /tmp/haw-demo/haw-demo --format json

La sortie de haw plugins new liste chaque fichier créé et affiche les étapes suivantes exactes (build + invocation PATH=…) pour le langage choisi.

Découvrir les plugins communautaires

haw plugins list --remote fusionne un index communautaire dans le tableau local. Chaque plugin fusionné affiche le STATUS available et la source remote avec sa description ; tout ce qui est déjà installé, dans le catalogue ou abonné conserve son propre statut (la déduplication se fait par nom).

haw plugins list --remote
haw plugins list --remote --index https://example.com/plugins-index.json
haw plugins list --remote --format json   # les entrées distantes portent "source":"remote"

L’URL d’index par défaut est https://raw.githubusercontent.com/Nastwinns/hawser/main/plugins-index.json ; passez --index <url> pour pointer vers le vôtre. Un échec réseau ou d’analyse n’est pas fatal — haw affiche un avertissement et se rabat sur la liste locale uniquement.

Le format haw.plugins.index/1

L’index est un unique document JSON :

{
  "schema": "haw.plugins.index/1",
  "plugins": [
    {
      "name": "sbom",
      "crate": "haw-sbom",
      "git": "https://github.com/you/haw-sbom",
      "description": "Génération de SBOM CycloneDX pour la flotte épinglée"
    }
  ]
}
ChampSignification
schemaToujours "haw.plugins.index/1".
plugins[]Une entrée par plugin.
nameLe verbe que les utilisateurs saisissent (haw <name>).
crateNom du crate pour cargo install (facultatif).
gitURL du dépôt source (facultatif).
descriptionRésumé d’une phrase affiché dans haw plugins list.

Ajouter votre plugin à l’index communautaire

Ouvrez une PR sur le plugins-index.json à la racine du dépôt qui ajoute une entrée — name, crate, git, et une description d’une phrase. Une fois fusionnée, elle apparaît pour tous ceux qui exécutent haw plugins list --remote.

Interface machine — consommer la propre sortie de haw

Les plugins ont rarement besoin de re-dériver l’état de la flotte : les commandes de lecture de haw parlent déjà JSON. Chaque commande de lecture (status, tree, change status, verify, evidence) offre --format json avec un schéma stable et versionné et des codes de sortie stables. Faites appel à haw et analysez sa sortie :

haw status --format json | jq '.repos[] | select(.dirty)'

Le contexte haw.plugin/1 vous indique se trouve l’espace de travail ; --format json vous indique dans quel état il se trouve. Voir EXTENDING.md §1.5 pour le contrat d’interface machine.

Hello, plugin — en deux langages

Les deux versions ci-dessous implémentent la même commande : haw hello affiche un message de salutation, --help se décrit lui-même, et --format json émet du JSON haw.plugin/1.

Shell POSIX

Une version complète et fonctionnelle se trouve dans examples/haw-hello. L’essentiel :

#!/usr/bin/env sh
set -eu

case "${1:-}" in
-h | --help)
	echo "haw-hello — say hello. Options: --help, --format json"
	exit 0
	;;
esac

# haw nous transmet le contexte de l'espace de travail dans $HAW_JSON (et sur stdin).
root=$(printf '%s' "${HAW_JSON:-}" | sed -n 's/.*"root":"\([^"]*\)".*/\1/p')

if [ "${1:-}" = "--format" ] && [ "${2:-}" = "json" ]; then
	printf '{"schema":"haw.plugin/1","plugin":"hello","root":"%s"}\n' "$root"
	exit 0
fi

if [ -n "$root" ]; then
	printf 'hello from haw-hello — workspace at %s\n' "$root"
else
	printf 'hello from haw-hello (no workspace here)\n'
fi

Rendez-le exécutable et déposez-le sur le PATH :

chmod +x haw-hello
PATH="$PWD:$PATH" haw hello

Rust

Un binaire autonome — aucune dépendance envers un crate haw.

cargo new --bin haw-hello
cd haw-hello

src/main.rs :

use std::env;
use std::process::ExitCode;

fn main() -> ExitCode {
    let args: Vec<String> = env::args().skip(1).collect();

    if args.iter().any(|a| a == "-h" || a == "--help") {
        println!("haw-hello — say hello. Options: --help, --format json");
        return ExitCode::SUCCESS;
    }

    // haw transmet le contexte haw.plugin/1 dans HAW_JSON (également sur stdin).
    let ctx = env::var("HAW_JSON").unwrap_or_default();
    let root = ctx
        .split("\"root\":\"")
        .nth(1)
        .and_then(|s| s.split('"').next())
        .unwrap_or("");

    if args == ["--format", "json"] {
        println!(r#"{{"schema":"haw.plugin/1","plugin":"hello","root":"{root}"}}"#);
        return ExitCode::SUCCESS;
    }

    if root.is_empty() {
        println!("hello from haw-hello (no workspace here)");
    } else {
        println!("hello from haw-hello — workspace at {root}");
    }
    ExitCode::SUCCESS
}

Compilez-le et exécutez-le comme un plugin :

cargo build --release
PATH="$PWD/target/release:$PATH" haw hello

(Pour de vrais plugins, analysez HAW_JSON avec serde_json plutôt que par découpage de chaînes.)

Écrire dans n’importe quel langage

Le contrat de plugin est indépendant du langage — ce n’est que du JSON sur HAW_JSON / stdin (haw.plugin/1) et du JSON sur stdout (haw.plugin.report/1 pour les phases du cycle de vie, haw.plugin.view/1 pour la render intent du TUI). Tout langage capable de lire une variable d’environnement et d’afficher du JSON peut être un plugin haw.

Le répertoire schemas/ contient les JSON Schemas officiels (draft 2020-12) — la source de vérité pour chaque nom et forme de champ. Validez les E/S de votre plugin par rapport à eux.

De fines liaisons de référence reflètent ces schémas afin que vous n’ayez pas à écrire le JSON à la main :

  • Pythonbindings/python (haw_plugin) : Context.from_env(), Report.emit(), view(title, lines). Aucune dépendance au-delà de la stdlib.
  • Gobindings/go (hawplugin) : ReadContext(), Report.Emit(), View(title, lines). Stdlib uniquement.
  • Shell POSIX et Rust — le examples/haw-hello et la section « Hello, plugin » ci-dessus montrent des implémentations sans aucune dépendance.

Pour une liste organisée de plugins existants à installer ou dont vous inspirer, voir AWESOME-HAW-PLUGINS.md.

Plugins d’exemple de démarrage

Le répertoire examples/plugins/ fournit de petits starters exécutables — de zéro à quelques dépendances légères, POSIX sh ou bibliothèque standard Python 3, tous en lecture seule / dry-run par défaut. Chacun est un vrai exécutable haw-<name> avec --help, --format json (un haw.plugin.report/1) et l’intent de rendu du cockpit (un haw.plugin.view/1). Copies-en un sur ton PATH et lis-le comme modèle.

PluginLangageCe qu’il fait / comment l’essayer
haw-fleet-statusPOSIX shPanneau de santé compact par dépôt — branche, modifié ?, en avance/en retard. Pur git, zéro dépendance. haw fleet-status
haw-dockerPOSIX shSignale les ressources Dockerfile/compose par dépôt ; lint avec hadolint et vérifie les images locales avec docker lorsqu’il est présent (dégrade en douceur). haw docker
haw-webPython 3Compte/valide les *.html (doctype, titre, équilibre des balises), signale les *.css, rapporte les tailles. Bibliothèque standard uniquement. haw web
haw-k8sPOSIX shTrouve les *.yaml sous k8s//deploy//manifests/ et valide chacun avec kubectl apply --dry-run=client (hors ligne ; ne touche jamais un cluster). haw k8s
haw-commit-aiPython 3Rédige le texte de commit/PR à partir de ton changeset — et fait aussi office de serveur MCP pour que Claude lise les diffs. haw commit-ai

Intégration éditeur — Neovim

examples/nvim (haw.nvim) est un petit plugin Lua sans dépendance qui exécute haw depuis Neovim en appelant le binaire haw via le shell — pas de serveur :

  • :HawSync — exécute haw sync, affiche le résultat.
  • :HawStatushaw status dans un buffer temporaire.
  • :HawDash — ouvre haw dash (le cockpit TUI) dans une division de terminal.
  • :HawFleet — liste la flotte (dépôt / branche / état) dans un buffer temporaire, analysé depuis haw status --format json.

Installe avec lazy.nvim / packer / le :packadd natif — voir le README.

Conventions

  • Nommez-le haw-<verb>. Le verbe est ce que les utilisateurs saisissent : haw-jirahaw jira. Gardez-le court et non revendiqué par les commandes intégrées (haw --help les liste).
  • --help auto-descriptif. Les utilisateurs y découvrent les options de votre plugin ; haw ne documente pas les plugins à votre place.
  • Humain sur stdout, JSON sur --format json. Affichez une ligne lisible par défaut ; émettez un document haw.plugin/1 (ou votre propre schéma versionné) sous --format json afin que d’autres outils puissent vous chaîner via un pipe.
  • Les codes de sortie ont un sens. 0 = succès. Non nul = échec, et haw le propage — les portes de CI et les chaînes && en dépendent. Ne sortez pas avec 0 en cas d’erreur.
  • Échouez proprement. Gérez élégamment le contexte sans espace de travail (JSON limité au schéma). Ne présumez pas que root/repos existent. Ne vous bloquez jamais : votre processus bloque haw jusqu’à ce qu’il se termine.
  • Restez un processus séparé. Vous obtenez l’isolation gratuitement — n’essayez pas d’accéder aux entrailles de haw ; consommez plutôt --format json et le contexte haw.plugin/1.

Distribuer votre plugin

Tout exécutable nommé haw-<name> sur le PATH fonctionne. Deux voies courantes :

  • Publiez un crate. Nommez le binaire haw-<name> ; les utilisateurs l’obtiennent avec cargo install haw-<name>, qui le dépose dans ~/.cargo/bin (généralement sur le PATH).
  • Livrez un binaire ou un script. Déposez haw-<name> dans n’importe quel répertoire du PATH (/usr/local/bin, ~/.local/bin, ~/bin). Les scripts shell comptent — marquez-les comme exécutables.

Vérifiez avec :

which haw-<name>   # haw finds exactly what your shell finds
haw <name> --help

Soumettre votre plugin

Vous avez construit quelque chose d’utile ? Partagez-le. Voir CONTRIBUTING.md pour la liste de vérification build/test et les règles de bienséance des PR, puis ouvrez une PR qui ajoute votre plugin à la liste communautaire — une ligne : nom, description d’une phrase, et un lien. Nous gardons le cœur petit à dessein ; l’écosystème vit dans les plugins.

Phases du cycle de vie

Les plugins peuvent s’abonner aux phases du cycle de vie dans la table [plugins] du manifeste et sont invoqués hors processus avec --haw-phase <name> (par ex. un plugin SBOM sur post-build). Le crate SDK facultatif haw-plugin offre aux auteurs Rust l’ergonomie Context/Report tout en compilant vers un binaire haw-<name> autonome.

hawser — Spécification de conformité, certification et sécurité

Marché cible : programmes critiques pour la sûreté et la sécurité (avionique, spatial, défense, ferroviaire, automobile, industriel, médical). Ces clients ne peuvent adopter un outil de SCM que s’il produit des preuves de certification, s’il est lui-même qualifiable, s’il présente une posture de sécurité auditable et s’il est irréprochable en matière de protection des données. Ce document précise ce que hawser doit fournir, par domaine, et met en correspondance chaque exigence avec une fonctionnalité technique et une phase de livraison.

Terminologie : « l’outil » = le binaire haw + haw-core. « Le demandeur » = l’équipe sûreté/sécurité du client qui détient l’argumentaire de certification. hawser fournit des preuves et un kit de qualification ; le demandeur détient la décision finale dans son plan (PSAC, plan de sûreté, hypothèses SEooC).


1. La place de hawser dans le cycle de vie

hawser est un outil de gestion de configuration logicielle (SCM) + composition. Il décide quelle révision de quel dépôt entre dans un build et consigne cette décision de manière immuable. Il ne compile, ne génère ni ne vérifie pas de code embarqué/aéroporté. Cette délimitation de périmètre est la phrase la plus importante pour la qualification : elle borne les modes de défaillance de l’outil à « sélectionne/consigne le mauvais jeu de sources » — et non « émet du code objet erroné ».

Conséquences de ce périmètre :

  • La sortie de l’outil (haw.lock + arbre matérialisé) est vérifiable de manière indépendante par le build et la revue en aval, ce qui réduit la rigueur de qualification requise.
  • Les preuves qu’il produit (baseline, SBOM, provenance) sont consommées par les processus de SCM, de sûreté et de sécurité du client — hawser est un producteur de preuves, pas une autorité.

2. Qualification de l’outil

hawser est pertinent pour la qualification dans toutes les normes de sûreté, car un outil de configuration/composition peut sélectionner la mauvaise source dans un build de sûreté. La classification et la rigueur diffèrent selon la norme ; les artefacts que hawser doit fournir sont en grande partie communs (§2.6).

2.1 DO-178C / DO-330 (aéroporté) & DO-278A (sol/ATM)

  • Qualification de l’outil régie par la DO-330. Le critère est déterminé par la capacité de l’outil à insérer une erreur dans le produit et/ou à ne pas en détecter une :
    • Si le processus du client re-vérifie l’arbre matérialisé/le build de manière indépendante → Critère 3 → généralement TQL-5 (rigueur la plus faible).
    • Si le lockfile/l’arbre est réputé fiable sans re-vérification indépendante → Critère 1TQL-4 (ou supérieur en DAL A/B).
  • Recommandation de hawser aux demandeurs : conservez le build et la revue en aval comme activité de vérification afin que l’outil reste en Critère 3 / TQL-5. Documentez ce choix dans le PSAC.
  • Livrables fournis par hawser : Tool Operational Requirements (TOR), Tool Qualification Plan (TQP), exigences de l’outil + cas de test + résultats avec traçabilité, gabarit de Tool Accomplishment Summary (TAS) et une liste des limitations connues / errata.

2.2 ISO 26262-8:2018 §11 (automobile) + ASPICE

  • Déterminez le Tool Confidence Level TCL1–3 à partir de Tool Impact (TI) × Tool error Detection (TD). hawser est TI2 (un dysfonctionnement peut violer une exigence de sûreté) avec un TD1/TD2 atteignable si le client vérifie le build → généralement TCL1–TCL2.
  • Méthodes de qualification prises en charge par hawser : 1a confiance accrue par l’usage (métriques d’utilisation, gabarit d’historique de terrain), 1b évaluation du processus de développement de l’outil (nos preuves de SDLC, §7), 1c validation de l’outil (notre suite de tests + résultats).
  • Correspondance ASPICE : SUP.8 Configuration Management et SUP.10 Change Request Management (voir §3). Fournissez la table de correspondance comme artefact commercial/d’audit.

2.3 IEC 61508-3 (sûreté fonctionnelle industrielle)

  • Classification comme outil de support hors ligne T2/T3. hawser contribue à ce qui est construit → à traiter comme T3 sauf si le build du client re-dérive indépendamment le jeu de sources.
  • Fournir : preuves de validation de l’outil, enregistrement de la version + configuration, liste des défauts connus.

2.4 EN 50128 / EN 50716 (ferroviaire)

  • Classe d’outil T2/T3 (§6.7). Fournissez un Tool Qualification Report et la preuve que l’outil est utilisé dans son enveloppe opérationnelle validée.

2.5 Spatial (ECSS-E-ST-40 / ECSS-Q-ST-80) & médical (IEC 62304)

  • Spatial : outil utilisé dans le cadre d’un plan de SCM ; fournir les enregistrements de configuration + validation.
  • Médical IEC 62304 §8 gestion de la configuration/SOUP : fournir SBOM + preuves de baseline.

2.6 Le jeu d’artefacts de qualification commun (produit une seule fois, mis en correspondance par norme)

  1. Tool Operational Requirements — ce que haw doit faire, sous forme d’énoncés vérifiables.
  2. Matrice de traçabilité exigences → tests (chaque exigence a un test).
  3. Suite de tests + résultats lisibles par machine par LTS publiée, par OS pris en charge.
  4. Manuel de sûreté/d’exploitation — usage prévu, enveloppe opérationnelle, contraintes et les mesures d’évitement d’erreur que l’utilisateur doit appliquer (p. ex. « committer le lockfile », « vérifier que le build re-dérive l’arbre »).
  5. Limitations connues & errata, versionnés.
  6. Enregistrement de configuration — version exacte de l’outil, versions des dépendances, provenance du build.
  7. Tables de correspondance par norme (DO-330 / ISO 26262-8 / IEC 61508 / EN 50128 / ASPICE).

Le déterminisme est une exigence stricte pour tout ce qui précède : mêmes entrées ⇒ haw.lock et sélection d’arbre identiques au bit près, sur chaque OS. Pas d’horloge murale, pas d’ordre d’itération de map, pas de non-déterminisme réseau dans la résolution. Voir §8.


3. Reproductibilité & preuves de SCM

L’atout commercial et de certification. hawser transforme « faites confiance à notre processus » en « voici la baseline signée et reproductible ».

  • haw.lock = la baseline de configuration. Chaque dépôt épinglé à un id d’objet exact. Committé, relisible par diff, vérifiable par machine.
  • Résolution déterministe. Algorithme de résolution documenté et versionné (voir resolver/mod.rs) ; la priorité des overlays est totale et stable.
  • Détection de dérive. haw status / haw verify prouve que l’arbre sur disque correspond à la baseline — c’est bel et bien une activité de vérification de configuration pour le plan de SCM du client.
  • Bundle de preuves. haw evidence émet : baseline (lock) + SBOM + attestation de provenance + enregistrement de configuration de l’outil, sous forme d’une archive signée unique pour le paquet de données de certification.

Correspondance avec les normes :

ExigenceDO-178CISO 26262ASPICE
Baselines / ids de configurationSCM (Table A-8)Partie 8 §7SUP.8
Contrôle des changements entre dépôtsSCMPartie 8 §8SUP.10
Entrées de build reproductiblesSCM / SCPartie 8 §7SUP.8
Traçabilité de ce qui a été livréSCM / verifyPartie 8 §7–8SUP.8/10

4. Sécurité de la chaîne d’approvisionnement & SBOM

Vent réglementaire favorable — de plus en plus obligatoire, pas optionnel.

  • Export SBOM dans les deux formats dominants : CycloneDX et SPDX 2.3 (ISO/IEC 5962), SPDX 3.0 quand il sera stable. Inclut les éléments minimaux NTIA (fournisseur, composant, version, id unique, relation de dépendance, auteur, horodatage).
  • hawser émet un SBOM du produit composé (dépôts + leurs ids épinglés) et livre un SBOM de haw lui-même (son arbre de dépendances Rust) à chaque version.
  • EU Cyber Resilience Act (Règlement (UE) 2024/2847). En vigueur depuis déc. 2024 ; les obligations principales s’appliquent ~déc. 2027, le signalement des vulnérabilités/incidents ~sept. 2026. Exige des fabricants de produits comportant des éléments numériques qu’ils maintiennent un SBOM et gèrent les vulnérabilités. Le chemin lock→SBOM de hawser est un facilitateur de conformité direct pour les clients, et hawser lui-même doit être conforme au CRA en tant que produit que nous vendons.
  • US EO 14028 / NIST SSDF (SP 800-218) et provenance SLSA v1.0. hawser émet des attestations de provenance de build/composition (style in-toto) pour que l’arbre composé porte un « ce qui est entré, d’où, à quel id » vérifiable.

5. Cryptographie & intégrité

Les clients sensibles exigent une intégrité cryptographique de bout en bout et un inventaire cryptographique documenté. Principes de conception : vérifier par défaut là où le client l’active, ne jamais inventer de cryptographie, être remplaçable par du FIPS, être rigoureux quant aux secrets.

5.1 Vérification de signature (intégrité des sources)

  • Vérifier les signatures de commit/tag avant checkout lorsque le client l’active : OpenPGP (GPG), signature SSH et Sigstore / gitsign (sans clé). Modes de politique : off | warn | require. require fait échouer sync sur tout dépôt non signé/non vérifié.
  • Ancres de confiance configurées par espace de travail (ensembles de signataires autorisés, trousseaux, racines Fulcio).

5.2 Intégrité de la baseline

  • Lockfile signé / attestation de résolution signée. haw lock --sign produit une signature détachée ou une attestation in-toto sur les octets canoniques du lock, afin qu’un relecteur puisse prouver que la baseline n’a pas été altérée après approbation.
  • Une sérialisation du lock canonique et stable au bit près est un prérequis (§8).

5.3 Identité de l’outil (un outil qualifié doit être vérifiable en identité)

  • Versions de haw signées et reproductibles. Signatures Cosign/Sigstore + sommes de contrôle SHA-256 + provenance de build SLSA pour chaque artefact et chaque LTS. Les clients vérifient que le binaire qu’ils exécutent correspond à celui qui est qualifié.

5.4 Agilité de hachage

  • Prendre en charge le format d’objet SHA-256 de git aux côtés de SHA-1 et consigner lequel est utilisé dans le lock. Les programmes de sûreté à longue durée de vie survivent à la marge de sécurité anti-collision de SHA-1 ; stockez l’id le plus robuste disponible. Ne jamais s’appuyer sur SHA-1 comme seule garantie d’intégrité d’une baseline.

5.5 Module cryptographique & transport

  • TLS vers les API de forge via rustls ; aucune rétrogradation de protocole, TLS minimum épinglé à 1.2 (préférer 1.3). Pour les environnements FIPS 140-3, livrer un build reposant sur un module validé FIPS (p. ex. rustls + aws-lc-rs FIPS, ou OpenSSL FIPS système) et documenter la frontière.
  • Publier un inventaire cryptographique (algorithmes, bibliothèques, versions) — requis pour les audits d’agilité cryptographique et les échanges FIPS/Common Criteria.

5.6 Gestion des secrets

  • Les jetons de forge et identifiants ne sont jamais écrits dans haw.toml, haw.lock, les journaux ou l’état de l’espace de travail. Récupérez-les depuis le trousseau de l’OS, les assistants git credential ou un gestionnaire de secrets (Vault) via env/assistant. Masquez toute chaîne ayant l’apparence d’un identifiant dans les journaux et les erreurs.

5.7 Contrôle des exportations (hawser en tant que produit intégrant de la cryptographie)

  • hawser utilise/lie de la cryptographie → probablement ECCN 5D002 au titre de l’EAR américain. Démarche : auto-classification comme grand public (740.17 / note grand public) et, pour le cœur open source, dépôt de la notification de code source de chiffrement publié auprès du BIS/NSA (§742.15(b)). Publiez l’ECCN et la classification afin que les clients puissent valider l’importation/l’usage dans leur juridiction.
  • Wassenaar/double usage : logiciel commercial à double usage ; le maintenir hors du champ ITAR en ne regroupant pas de données techniques contrôlées. Les clients de la défense détiennent les contrôles de leur programme ITAR/EAR.

6. Protection des données (RGPD & équivalents)

hawser traite des données personnelles de manière incidente : nom + e-mail de l’auteur/committer git, noms d’utilisateur de forge, identités des relecteurs de PR/MR, acteur CI, horodatages de revue. Toutes sont des données personnelles au titre du RGPD (et du CCPA, LGPD, UK-GDPR).

Position de conception — c’est un argument de vente, pas seulement une contrainte :

  • Local d’abord, zéro sortie par défaut. Pour un fonctionnement local, les données ne quittent jamais la machine/le réseau de l’opérateur → le client est le responsable de traitement et l’éditeur de hawser n’est pas un sous-traitant (rien ne nous est transmis). C’est la posture la plus irréprochable possible pour les réseaux classifiés/réglementés.
  • Aucune télémétrie par défaut. Toute télémétrie future est strictement sur adhésion, anonymisée, hébergeable dans l’UE et entièrement documentée ; les builds isolés (air-gapped) n’ont aucun chemin réseau vers nous.
  • Activation de licence hors ligne. La validation de licence ne doit pas appeler nos serveurs avec des données personnelles ; fournissez une activation par fichier/hors ligne pour les environnements isolés et souverains.
  • Résidence / souveraineté des données. Les déploiements auto-hébergés et isolés conservent toutes les données à l’intérieur du périmètre du client. Pour tout composant hébergé optionnel (serveur de licences, miroir), proposez une région UE et un Data Processing Agreement (DPA) signé.
  • Registres de traitement (Art. 30) & support AIPD. Fournissez une description des flux de données et un gabarit d’entrée d’AIPD documentant exactement quels champs de données personnelles hawser lit et où ils vont (réponse : nulle part, pour un usage local).
  • Droit à l’effacement vs historique immuable. hawser ne crée aucun nouveau magasin de données personnelles ; il lit des métadonnées git/forge qui existent déjà. Les demandes d’effacement sont traitées au niveau de l’historique git / de la couche forge par le client — hawser documente cette frontière plutôt que de prétendre satisfaire l’effacement sur des objets de commit immuables.
  • Livrez un avis de confidentialité et un contact security.txt / .well-known/security.txt.

7. Sécurité de l’outil lui-même (SDLC sécurisé)

Un outil vendu à des programmes critiques pour la sécurité fait lui-même partie de la surface d’attaque. La preuve d’un SDLC sécurisé est une condition préalable à l’achat et alimente la méthode 1b d’ISO 26262 / l’assurance de développement d’outil de la DO-330.

  • Sûreté mémoire. unsafe_code = "forbid" sur tout l’espace de travail (déjà en place). Un outil de SCM en Rust, à sûreté mémoire, est un différenciateur concret face aux acteurs en place en C/Python — affichez-le.
  • Assurance des dépendances. cargo-audit (avis RUSTSEC), cargo-deny (politique de licence
    • avis + verrous de crates bannies), et revue de la chaîne d’approvisionnement (cargo-vet/cargo-crev) en CI, faisant échouer le build en cas de violation.
  • Builds vendorisés + reproductibles. Vendorisez les dépendances pour une reconstruction en environnement isolé ; épinglez la toolchain (rust-toolchain.toml) ; builds de version reproductibles au bit près.
  • SBOM propre publié à chaque version (§4) avec signatures (§5.3).
  • Divulgation des vulnérabilités & gestion des CVE. Politique publiée, contact sécurité, fenêtre de divulgation coordonnée, émission de CVE et engagement de rétroportage par LTS (§ commercial).
  • Surface réseau bornée et documentée. L’outil effectue des E/S réseau uniquement pour les opérations git/forge explicites invoquées par l’utilisateur ; documentez chaque sortie. Aucun appel caché.
  • Moindre privilège. Aucune élévation ; aucune écriture en dehors de l’espace de travail + du répertoire de cache configuré.

8. Auditabilité & déterminisme (transversal)

  • Contrat de déterminisme. Mêmes manifeste + lock + overlays ⇒ résolution et sélection d’arbre identiques au bit près sur Linux/macOS/Windows. Pas de Date.now, pas d’itération non ordonnée dans la sérialisation (utiliser des maps ordonnées — déjà IndexMap), émission TOML canonique pour le lock.
  • Journal d’audit structuré. Chaque opération mutante consigne l’acteur, l’opération, le dépôt affecté, l’id d’objet avant/après, l’horodatage — lisible par machine (JSON) pour la capture de preuves en CI.
  • Sortie lisible par machine. --format json sur status/verify/graph/evidence pour que les pipelines puissent capturer et comparer automatiquement les preuves de certification. Schémas stables et versionnés.
  • Codes de sortie stables pour que les portes CI soient fiables (0 ok, valeurs non nulles distinctes pour dérive / échec de vérification / échec de signature).

9. Backlog de fonctionnalités mis en correspondance avec les exigences de conformité

Ce qui doit être construit pour rendre ce qui précède réel. Les phases se réfèrent à ARCHITECTURE.md §6.

FonctionnalitéPermetPhase
haw.lock + résolution déterministePreuve de baseline, qualification1
Détection de dérive (status/verify)Activité de vérification de configuration1
--format json + schémas stables + codes de sortieAuditabilité, capture de preuves en CI1
Sérialisation du lock canonique/stable au bit prèsBaseline signée, déterminisme1
Journal d’audit structuréAuditabilité2
Export SBOM (CycloneDX + SPDX)CRA, EO 14028, SOUP IEC 623042
Vérification de signature de commit/tag (gpg/ssh/sigstore)Intégrité des sources2/3
Lockfile signé / attestation in-totoIntégrité de la baseline3
Attestation de provenance (SLSA/in-toto)Assurance de la chaîne d’approvisionnement3
Bundle haw evidencePaquet de données de certification en une passe3
Prise en charge du format d’objet SHA-256 + enregistrementAgilité de hachage, programmes à longue durée de vie3
Versions de haw signées + reproductiblesIdentité de l’outil, qualification1→continu
Dépendances vendorisées + build d’outil reproductibleIsolement réseau, SDLC sécurisé1→continu
Variante de build cryptographique validée FIPSEnvironnements FIPS 140-3ultérieur
Activation de licence hors ligneIsolement réseau, RGPD zéro sortiecommercial
Document d’inventaire cryptographiqueAgilité cryptographique / audits FIPS / CCdoc
Kit de qualification par LTSDO-330 / ISO 26262 / IEC 61508 / EN 50128commercial

Contactez les mainteneurs pour savoir comment ces éléments sont packagés, licenciés et pris en charge.

Sécurité & modèle de confiance

Cette page décrit ce à quoi haw fait confiance et ce qu’il exécute, afin que vous puissiez évaluer le rayon d’impact de son exécution sur un dépôt donné. Pour signaler une vulnérabilité et connaître les versions prises en charge, voir le SECURITY.md à la racine du dépôt.

En une ligne : haw exécute du code écrit dans le manifeste et du code présent dans votre PATH. Considérez les deux comme des entrées de confiance.

Le manifeste est du code de confiance

Un espace de travail est défini par un manifeste haw.toml. Ce manifeste peut déclarer des commandes build, test, run et exec, ainsi que des hooks de cycle de vie. Lorsque vous exécutez une opération sur la flotte, haw exécute ces commandes via votre shell, avec votre environnement et votre arbre de travail :

  • haw build / haw test exécutent les commandes build / test du manifeste.
  • haw run <cmd> / haw exec <cmd> exécutent des commandes arbitraires sur toute la flotte.
  • haw sync peut déclencher des hooks et des étapes post-checkout déclarées dans le manifeste.

Par conséquent, exécuter haw build, haw run, haw exec ou haw sync sur un checkout non fiable équivaut à exécuter le Makefile de ce dépôt. Un haw.toml malveillant peut faire tout ce que votre shell peut faire.

Règle générale : traitez haw.toml exactement comme un Makefile ou le bloc scripts d’un package.json. N’exécutez de commandes de cycle de vie que sur des manifestes que vous avez relus ou en lesquels vous avez confiance. Les commandes d’inspection en lecture seule (haw status, haw tree, haw verify) n’exécutent pas les commandes du manifeste et peuvent être exécutées en toute sécurité sur un manifeste non fiable.

Les plugins sont des binaires de confiance

haw suit le schéma d’extension de git / cargo / kubectl : toute sous-commande que haw ne reconnaît pas est déléguée à un exécutable haw-<name> trouvé dans votre PATH. Par exemple, haw jira sync exécute haw-jira.

Deux propriétés comptent pour la sécurité :

  1. Les plugins sont des exécutables ordinaires résolus depuis le PATH. C’est le haw-<name> vers lequel votre PATH pointe qui s’exécute. Un PATH falsifié (ou un binaire haw-* déposé dans l’un de ses répertoires) peut détourner une sous-commande.
  2. Les plugins héritent de la totalité de votre environnement, y compris tout token de forge (GITHUB_TOKEN, GITLAB_TOKEN, HAW_*, …) exporté dans le shell qui a lancé haw.

Par conséquent : n’installez que des plugins de confiance et gardez votre PATH propre — préférez des emplacements d’installation absolus et connus, et évitez de placer des répertoires non fiables ou modifiables par tous en tête des chemins système.

Les plugins s’exécutent bien comme des processus séparés, de sorte qu’un plugin défectueux ou bloqué ne peut pas faire planter haw — mais l’isolation des processus n’est pas ici une frontière de sécurité : un plugin s’exécute avec vos privilèges et vos secrets.

Tokens et identifiants

  • Les tokens de forge sont lus uniquement depuis les variables d’environnement et utilisés exclusivement pour les requêtes API (ouvrir des PR, lire l’état de la CI). Ils ne sont jamais écrits sur le disque ni jamais journalisés par haw.
  • L’authentification du transport Git n’est pas gérée par haw — elle reste du ressort de vos clés SSH existantes ou de votre credential helper git.
  • La composition en lecture seule (haw sync, status, tree, verify) ne nécessite aucun token ; seules les fonctionnalités de forge en ont besoin.

Voir la section Secrets & tokens du README pour l’ordre de priorité exact par forge.

Renforcement de la chaîne d’approvisionnement (ce dépôt)

Le projet haw lui-même applique des contrôles standard de chaîne d’approvisionnement :

  • Les GitHub Actions sont épinglées à des SHA de commit complets (avec le tag lisible par un humain dans un commentaire de fin) dans chaque workflow, de sorte qu’un tag d’action compromis ou repointé ne puisse pas altérer les artefacts de release avant leur signature.
  • Les artefacts de release sont signés avec cosign (sans clé / OIDC).
  • cargo audit et cargo deny s’exécutent à chaque push/PR et selon une planification hebdomadaire (voir .github/workflows/audit.yml et deny.toml) pour bloquer les avis de sécurité connus, les licences interdites et les sources de dépendances inattendues.

Signalement

Pour signaler une vulnérabilité, suivez la procédure du SECURITY.md à la racine du dépôt : utilisez le signalement privé de vulnérabilité de GitHub ou envoyez un e-mail au mainteneur. N’ouvrez pas de ticket public pour les problèmes de sécurité.