Skip to content

Feat(coordination,#22456): le ledger GPU se publie par git et la garde porte la reservation - #22755

Open
jsboige wants to merge 10 commits into
mainfrom
feature/22456-git-ledger
Open

jsboige wants to merge 10 commits into
mainfrom
feature/22456-git-ledger

Conversation

@jsboige

@jsboige jsboige commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Grain: DEEP/guard -- lane myia-ai-01:CoursIA-2 -- prev: DEEP/notebook-dotnet #22740

Le trou, mesuré

Le ledger gpu-reservation est censé arbitrer les réservations de GPU de la flotte. Trois mesures, sur origin/main, toutes reproductibles :

1. La publication est manuelle — donc une réservation peut être invisible. debt_ledger.py append construit l'enveloppe et l'écrit dans le spool local ; rien ne l'appelle automatiquement. Les seules occurrences de l'appel dans le dépôt sont de la prose : .claude/skills/coordinate/SKILL.md:89 (« Reserver avant de charger […] puis poster l'observation ; passer en released a la fin ») et docs/coordination/gpu-reservation.md:122. Un job lancé par une lane qui ne lit pas ce skill n'existe pour personne.

2. Le garde n'a aucun appelant — la discipline d'admission est de la prose, pas du code. git grep -n "check_pending" hors de l'organe et de son test ne renvoie que de la documentation ; scripts/pick_idle_grain.py ne contient zéro occurrence de check_pending ni de evaluate_gpu_pending. Or docs/coordination/gpu-reservation.md:139 affirme : « le picker vérifie qu'une observation [OBS] existe dans le ledger pour l'entité ciblée via la sous-commande check_pending --entity <m>#gpu<n> ». L'affirmation est fausse dans le code : un état lisible, que personne ne lit, ne change rien.

3. La source de vérité déclarée est un canal de messagerie. docs/coordination/gpu-reservation.md:25 déclare que le dashboard fait foi ; check_pending lit, lui, un snapshot local produit par un reduce manuel. Les deux faits se sont éloignés : le dashboard est injoignable quand le MCP roo-state-manager ne répond pas (c'est le cas de la session qui écrit cette PR), et le snapshot local est un état figé — le 2026-10-25, check_pending rendait RELEASED 2026-10-22 alors que deux réservations held vivantes tenaient le même device.

C'est le défaut que ce grain ferme, et c'est celui qui a fait que le premier essai de po-2024 n'a pas été vu par la régie.

Le geste

Deux fonctions, dans le même grain, parce qu'elles ne sont pas séparables : un transport sans appelant ne change rien.

F1 — l'état de réservation se publie et se lit par git

  • Publication : debt_ledger.py publish --git (dry-run par défaut, --emit pour publier) écrit les observations en attente dans un fichier JSONL par machine, sur une branche dédiée (ops/gpu-ledger), en ajout seul, avec une ligne par observation au format [OBS] {json}. L'idempotence est portée par l'observation_id : republier les mêmes envelopes ne duplique aucune ligne.
  • Lecture : check_pending --source auto|git|local. auto (le défaut nouveau) lit la branche. Le repli sur le cache local est conditionné : il n'a lieu que si la branche n'existe pas sur le remote — mesuré par git ls-remote, jamais par le texte d'une erreur (qui est localisé) — car dans ce cas rien n'a jamais été publié et le cache ne peut rien manquer. Dès que la branche existe (elle porte peut-être le hold d'une autre machine) ou que le remote est injoignable, le verdict est UNREADABLE : un cache périmé ne rend pas un device libre. Si le fetch échoue mais que le dernier ref local est lisible, le gate répond depuis ce ref — l'état connu de la source primaire — et la source nomme l'incertitude. Si aucune source n'est lisible, le verdict nomme l'indisponibilité au lieu de rendre « device libre ».
  • Cible paramétrée : --remote (défaut origin) désigne le remote sur lequel la branche est lue et poussée. Rien dans le code ne suppose que la branche vit dans le dépôt du projet : l'arbitrage sur la cible de l'état vivant (dépôt privé ou branche publique) ne demande aucune modification de code.
  • Le snapshot local reste un cache, jamais une autorité. La déclaration d'autorité reste celle de docs/coordination/gpu-reservation.md, que cette PR ne modifie pas : le transport est une capacité, pas un changement d'autorité (voir « Gouvernance »).

F2 — la garde de lancement consulte le gate, porte la réservation, et la publie

scripts/ci/run_guarded.py gagne des options explicites (--gpu-entity, --gpu-holder, --gpu-workload, --gpu-expected-end, --gpu-issue). Sans --gpu-entity, son comportement est strictement inchangé. Avec :

  1. à l'admission, après les gardes existantes et avant le Popen de l'enfant : consulter check_pending --source auto — refus si HOLD ou si la source est indisponible ; puis écrire l'observation held (horodatage UTC réel, observation_id calculé par l'outil) ;
  2. publier la réservation sur la branche — et refuser de lancer si la publication échoue : une réservation invisible de la flotte n'est pas une réservation ;
  3. si l'écriture de la réservation échoue : refus de lancer (fail-closed) — on ne démarre pas sur un device dont on n'a pas porté la réservation ;
  4. au teardown (arrêt gardé et sortie normale) : écrire l'observation released qui ferme la réservation, et la publier. Son échec est signalé : un held sans released laisse une réservation vivante sur un device libre, et bloque les jobs suivants à tort.

Trois garde-fous de la même séquence, ajoutés après la revue adversariale (détails et témoins plus bas) : un refus de publication ferme le held (sinon la réservation reste vivante, et visible de la flotte si le push a atterri avant l'échec de sa vérification) ; un held sans borne reçoit une borne par défaut (maintenant + 24 h, journalisée) — un hold sans fin n'est jamais STALE, donc un lanceur tué brutalement (kill -9, reboot) laissait le device pris pour toute la flotte, sans aucun chemin de sortie ; et un verdict vert rendu depuis une source non autoritaire (le cache local) est refusé (NON_AUTORITAIRE) : le cache peut dater d'avant le hold d'autrui.

Pourquoi la publication est dans la garde, et pas à côté. Le gate lit l'état publié (la branche) ; une observation restée dans le spool local rend le verdict UNPOSTED, qui bloque le lancement suivant — y compris celui de la machine qui vient de réserver. Sans publication, la séquence held -> released se verrouillerait elle-même à la deuxième réservation : le premier run ferme, et la machine ne peut plus rien lancer. La publication n'est pas une formalité de flotte, c'est le maillon qui empêche l'auto-blocage.

La séquence codée

C'est la séquence que le régisseur a mesurée le 2026-10-25, et elle devient la séquence écrite par le code :

held (avant le premier appel CUDA) → le travail finit → released (qui le ferme) → replay du fold.

Avant, cette séquence était une promesse de protocole (docs/coordination/gpu-reservation.md:122) : elle reposait sur l'opérateur. Maintenant elle est portée par le lanceur, qui est le seul composant qui connaît l'instant exact du lancement et de l'arrêt.

L'invariant : une machine n'écrit que dans son fichier

La régie est l'unique auteur de publication de la branche : c'est une discipline, pas une propriété du système — rien ne l'imposait. La propriété enforceable, et plus forte, est la suivante :

  • une observation dont l'entité porte une autre machine est refusée avant tout commit (GIT_PUBLISH_FOREIGN_MACHINE) ;
  • le commit publié est vérifié (git diff-tree, --root inclus) : s'il touche un fichier autre que celui de la machine, la publication est refusée (GIT_PUBLISH_SCOPE), et l'erreur nomme le sha — l'opérateur voit où est le commit, il n'apprend pas qu'une entrée a disparu ;
  • un push refusé deux fois ne perd rien : l'échec nomme les observation_id retenus et dit published=false, et le spool local les conserve — un fantôme de publication est détectable, jamais un trou silencieux dans l'état de la flotte.

Concrètement, la branche est une union de fichiers par machine, où personne n'écrase personne : deux machines qui publient en même temps se disputent la branche (un push refusé, un retry qui réapplique le fichier sur la nouvelle tête), jamais le contenu.

Ce que les garde-fous ont attrapé pendant la construction

Les deux invariants ci-dessus ne sont pas décoratifs : ils ont attrapé deux défauts réels de la première implémentation, tous deux invisibles sans eux.

  1. La branche naissante supprimait le contenu du dépôt. Le commit était fabriqué par git commit dans un worktree détaché : son parent était la tête du dépôt principal, si bien que l'arbre de la branche (notre fichier seul) se présentait comme un commit qui supprimait README.md. GIT_PUBLISH_SCOPE l'a refusé, en nommant les fichiers touchés. Le commit est désormais fabriqué en plumbing (write-tree + commit-tree), avec le parent choisi explicitement : la tête de la branche quand elle existe, aucun parent quand elle naît — la branche porte alors exactement les fichiers du ledger.
  2. La poussée publiait autre chose que l'observation. commit-tree ne déplace pas la HEAD du worktree ; la poussée visait donc HEAD — c'est-à-dire la tête du dépôt principal — et non le commit tout juste fabriqué. Elle « réussissait » en publiant le mauvais contenu : un échec silencieux du point de vue de l'état de la flotte. La poussée vise désormais le sha explicite, et le ref distant est relu (ls-remote) pour vérifier qu'il porte bien ce sha (GIT_PUBLISH_UNVERIFIED).

La revue adversariale : six défauts trouvés, six corrigés

Le diff a été relu par un sous-agent adversarial (lecture intégrale, exécution des deux suites, et reproduction exécutable des scénarios dans un sandbox git local). Verdict rendu : non mergeable en l'état — deux défauts HAUTE, reproduits de bout en bout. Les six sont corrigés dans ce PR, chacun avec son témoin de test (huit tests nouveaux, un par défaut corrigé) :

# Sévérité Le défaut, reproduit Le correctif
1 HAUTE Push atterri dont la vérification (ls-remote) échoue : l'observation reste dans le spool alors qu'elle est sur la branche ; le rejeu disait « déjà à jour » sans drainer, et le gate rendait UNPOSTED indéfiniment — le remède documenté (« relancer publish --git --emit ») bouclait sur lui-même, et le device devenait impossible à libérer. Le drain est explicite : une observation déjà sur la branche est archivée du spool, y compris au rejeu et dans la course gagnée par un push concurrent. L'erreur GIT_PUBLISH_UNVERIFIED nomme désormais le remède qui draine.
2 HAUTE Refus de publication à l'admission : le held était écrit, le lancement refusé, mais rien ne fermait le hold. Si le push avait atterri avant l'échec de sa vérification, la flotte voyait un hold sur un device qui ne porte aucun job. Le refus écrit le released, comme tous les autres chemins de sortie.
3 MOYENNE --gpu-expected-end facultatif : un held sans borne n'est jamais STALE, donc un lanceur tué brutalement laissait le device pris pour toute la flotte, sans aucune sortie automatique. Borne par défaut (maintenant + 24 h), journalisée ; le STALE redevient atteignable.
4 MOYENNE auto : un cache local périmé pouvait autoriser un lancement quand la branche (illisible) portait le hold d'autrui — le double-réservation exact que ce transport doit empêcher. L'existence de la branche est mesurée par le code de sortie de ls-remote ; si elle existe ou si le remote est injoignable → UNREADABLE (fail-closed) ; la garde refuse en outre un verdict vert non autoritaire (NON_AUTORITAIRE).
5 BASSE L'absence d'un fichier de branche était détectée par la sous-chaîne « does not exist » dans le stderr de git : texte localisé — sur un siège francophone, la première publication d'une machine échouait systématiquement. Détection par git cat-file -e (code de sortie).
6 BASSE Le retrait du worktree temporaire était muet sur échec (verrou Windows, antivirus) : un dossier git-ledger-* et son enregistrement subsistaient à chaque tentative, à la racine du drive. Retrait vérifié, worktree prune tenté en repli, échec journalisé.

Les faux positifs que la revue a suspects puis écartés par la lecture et l'exécution : le commit de branche ne peut pas supprimer le dépôt ; un push refusé ne perd rien ; un update-ref en retard se répare au push suivant ; deux machines ne peuvent pas s'écraser ; le push ne peut pas viser main ; une branche absente ne rend pas un device libre ; un spool corrompu fait refus (fail-closed) et non un faux vert ; et le faux organe des tests ne laisse passer aucun champ invalide.

Ce que la revue a aussi prouvé : l'ordre « publication avant l'enfant » n'était mesuré par aucun test (un test qui vérifie seulement « il y a eu un publish » passe aussi si le publish arrive après le lancement). Il est désormais mesuré par un marqueur écrit par l'enfant : le premier publish doit le trouver absent.

Gouvernance — ce que cette PR livre, et ce qu'elle ne décide pas

Cette PR livre une capacité : le transport des observations, sa cible paramétrée, et la garde de lancement qui porte la réservation. Elle ne déplace pas l'autorité : le document qui porte la déclaration d'autorité, docs/coordination/gpu-reservation.md, est restitué byte-identique à main — le dashboard reste la référence déclarée de l'usage GPU de la flotte.

avant après
déclaration d'autorité le dashboard CoursIA-gpu-reservation-ledger inchangée — le dashboard reste la référence déclarée
la publication geste manuel (append + flush) automatique à l'admission et au teardown, sur une branche paramétrée
la lecture du gate snapshot local la branche d'abord ; le cache local seulement si la branche n'existe pas sur le remote
le snapshot local lu par check_pending cache, jamais une autorité

Le choix technique de la source de lecture n'est pas un arbitrage de gouvernance : quand le canal de messagerie est indisponible, la branche est la seule copie partagée des observations encore lisible entre les machines — c'est la raison d'être du transport. C'est précisément pourquoi la déclaration d'une source d'autorité demande un arbitrage humain, et cette déclaration est volontairement absente de cette PR. La mécanique est décrite dans docs/coordination/gpu-ledger-transport.md, qui renvoie à la déclaration inchangée.

Ce qui ne change pas : le schéma de l'observation (debt-ledger-observation/v1), la fabrication des observation_id par l'outil, le refus d'un envelope écrit à la main, et le vocabulaire de verdicts de check_pending.

Ce qui reste soumis à sign-off user, et que cette PR ne tranche pas (le merge attend ce sign-off — point Q-010 du registre des questions de la lane) :

  1. La déclaration d'autorité : quelle source fait foi pour l'usage GPU de la flotte, et à partir de quand la branche cesse d'en être le support. Le document qui la porte est intact ; le modifier est une PR distincte, après sign-off.
  2. La cible de publication de l'état vivant de réservation : la branche vit par défaut sur jsboige/CoursIA, qui est public. L'état vivant (« qui tient quel device, pour quel run, quand ») est une donnée d'orchestration, distincte du topo des GPU — déjà public, lui, parce qu'il est dans le document de protocole. Le chemin de publication est un paramètre exposé (--gpu-ledger-remote côté garde, --remote côté organe) : basculer vers un dépôt privé sous le compte jsboige (qui porte déjà cinq dépôts privés) est un paramètre à changer, pas une réécriture — le code ne dépend ni de l'une ni de l'autre cible.

Portée

Dans la PR : scripts/coordination/debt_ledger.py, scripts/ci/run_guarded.py, scripts/tests/test_debt_ledger.py, scripts/tests/test_run_guarded.py, docs/coordination/gpu-ledger-transport.md, docs/README.md.

Hors périmètre, assumé : le picker (scripts/pick_idle_grain.py) ne consulte pas encore le gate — le document le dit désormais explicitement, et l'écart est suivi par une issue dédiée (#22747) ; le dashboard reste la référence déclarée, et son document n'est pas modifié ; les autres lanes n'ont pas encore à adopter les options --gpu-* de la garde, et leur absence ne change rien à leur comportement ; la publication exige une identité git ambiante sur la machine qui publie (aucun jeton dans une URL git).

Preuves

  • python -m pytest scripts/tests/test_debt_ledger.py → 162 passed (sandbox git hermétique : dépôt bare + clone sous tmp_path, aucun réseau, aucun dépôt du parc touché).
  • python -m pytest scripts/tests/test_run_guarded.py → 20 passed (organe simulé par injection ; seul l'enfant trivial tourne réellement).
  • Les deux suites ensemble → 182 passed (contre 174 avant la revue : huit tests nouveaux, un par défaut corrigé).

Suite de l'issue

Part of #22456 — les critères d'acceptation de l'issue sont clos par #22491 ; cette PR ferme le point structurel que cette issue nomme (« publier sur le dashboard ne rafraîchit pas check_pending ») en lui donnant un transport indépendant du canal de messagerie et un appelant. Part of #16737.

Revue de la lane (mesures refaites, pas reprises du rapport)

  • Les deux suites, dans un seul run : python -m pytest scripts/tests/test_debt_ledger.py scripts/tests/test_run_guarded.py -q -> 182 passed in 56.14 s (refait par la lane, sur le worktree et la tete exacts, apres les six corrections de la revue).
  • Tete : git rev-parse HEAD = a94655e613f27f15d66c7a17feaf2f3c7b95d99a — trois commits au-dessus de 626d5ff1fad1aacd5c450e1cdbde163cfa920cb9 : c198fec28bcea7383be007d3204c128c4f562ee6 (les six corrections de la revue), 2d1e5be1aa903805e441acfc37c44742327bdc38 (la borne par defaut du hold, nommee dans la notice du lanceur) et a94655e613f27f15d66c7a17feaf2f3c7b95d99a (le document de protocole restitue a l'identique de main, la mecanique decrite dans un document distinct). La poussee et sa verification (git ls-remote origin refs/heads/feature/22456-git-ledger rend le meme sha) relevent de la lane proprietaire ; c'est exactement le defaut que l'invariant GIT_PUBLISH_SCOPE a fait attraper pendant la construction que ce controle-la-meme evite.
  • Perimetre : git diff origin/main...HEAD --numstat rend 6 entrees (debt_ledger.py +889/-44, run_guarded.py +410/-3, test_debt_ledger.py +664/-2, test_run_guarded.py +349/-2, docs/coordination/gpu-ledger-transport.md +41, docs/README.md +1) — un seul theme, un seul domaine, un seul evenement « Publication du ledger GPU » ; le document de protocole, lui, n'est plus dans le diff.
  • Invariants presents : GIT_PUBLISH_FOREIGN_MACHINE et GIT_PUBLISH_SCOPE sont bien dans le code de publication (3 occurrences), et le test d'integration de la publication refuse bien l'observation d'une autre machine avant commit.
  • Le prev: de la ligne Grain: verifie : #22740 est bien un grain de la lane (Grain: DEEP/notebook-dotnet -- lane myia-ai-01:CoursIA-2), ouvert, auteur jsboige.

La PR attend un sign-off user (deux points nommes dans la section « Gouvernance » : la declaration d'autorite et la classe de donnees de l'etat vivant publie) : c'est le point Q-010 du registre des questions de la lane, et le merge appartient au coordinateur.

🤖 Generated with Claude Code

…it, la garde porte la reservation

Le ledger `gpu-reservation` etait un etat lisible que personne ne lisait : le
gate d'admission n'avait aucun appelant, la publication etait un geste manuel
(et son seul canal etait le dashboard, injoignable quand le MCP ne repond
pas), et le snapshot local que lisait `check_pending` etait fige.

Deux fonctions, inseparables :

- F1 -- l'etat se publie et se lit par git. Branche dediee `ops/gpu-ledger`,
  un fichier JSONL par machine, une ligne `[OBS] {json}` par observation, en
  ajout seul, idempotent par `observation_id`. `check_pending --source
  auto|git|local` lit la branche, avec repli **annonce** sur le cache local et
  verdict `UNREADABLE` quand aucune source n'est lisible.
- F2 -- la garde de lancement consulte le gate, ecrit le `held` avant le
  `Popen`, le `released` au teardown, et **publie** les deux. Sans publication
  le gate rendrait `UNPOSTED` au lancement suivant et la machine se
  verrouillerait elle-meme.

Invariant enforceable : une machine n'ecrit que dans son fichier. Une
observation d'une autre machine est refusee avant commit
(`GIT_PUBLISH_FOREIGN_MACHINE`), le commit publie est verifie par
`diff-tree` (`GIT_PUBLISH_SCOPE`, le sha nomme), et un push refuse deux fois
nomme les observations retenues (`published=false`) au lieu de les perdre.

Les deux garde-fous ont attrape deux defauts reels de construction : la
branche naissante supprimait `README.md` (commit par `git commit` dans un
worktree detache, dont le parent etait la tete du depot) et la poussee
publiait `HEAD` au lieu du sha fabrique (commit-tree ne deplace pas la HEAD).
Les deux sont corriges : commit en plumbing avec parent explicite, poussee
sur le sha, ref distant relu.

Le document de protocole est livre dans la meme PR : le dashboard devient un
miroir, le snapshot un cache, la branche la source de verite. Cette inversion
d'autorite est normative et attend le sign-off user (CLAUDE.md §A).

Suites : scripts/tests/test_debt_ledger.py 158 passed ;
scripts/tests/test_run_guarded.py 16 passed.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

jsboige and others added 2 commits October 26, 2026 19:28
…r le transport git

La revue adversariale du diff (#22755) a reproduit deux defauts HAUTE de bout
en bout, et signale trois defauts MOYENNE. Les six sont corriges ici, chacun
avec son temoin de test.

HAUTE 1 -- l'auto-blocage `UNPOSTED` resistant au remede documente. Un push
ATTERRI dont la verification (ls-remote) echoue laisse l'observation dans le
spool alors qu'elle est sur la branche ; le rejeu disait "deja a jour" sans
drainer, et le gate rendait `UNPOSTED` indefiniment -- le remede ("relancer
publish --git --emit") bouclait sur lui-meme, et le device devenait
impossible a liberer. Le drain est desormais explicite (observation deja sur
la branche -> archivee du spool), y compris dans la course gagnee par un push
concurrent.

HAUTE 2 -- la reservation vivante au refus de publication. La garde ecrivait
le `held`, refusait le lancement si la publication echouait, et ne refermait
PAS le hold : si le push avait atterri avant l'echec de sa verification, la
flotte voyait un hold sur un device qui ne portait aucun job. Le refus ecrit
maintenant le `released`, comme tous les autres chemins de sortie.

MOYENNE 1 -- un hold sans borne n'est jamais STALE : un lanceur tue
brutalement (kill -9, reboot) laissait le device pris pour toute la flotte,
sans aucun chemin de sortie. La garde applique une borne par defaut
(maintenant + 24 h), journalisee.

MOYENNE 2 -- un cache local perime pouvait autoriser un lancement : quand la
branche existe (elle porte peut-etre le hold d'autrui) ou que le remote est
injoignable, le verdict est `UNREADABLE` (fail-CLOSED). L'existence de la
branche est mesuree par le CODE de sortie de `ls-remote`, jamais par le
texte d'une erreur (qui est localise). La garde refuse en outre un verdict
vert rendu depuis une source non autoritative (`NON_AUTORITAIRE`).

MOYENNE 3 -- l'absence d'un fichier de branche etait detectee par la
sous-chaine "does not exist" dans le stderr de git : le texte est localise,
et sur un siege francais la premiere publication d'une machine echouait
systematiquement. La detection passe par `git cat-file -e` (code de sortie).

BASSE -- le retrait du worktree temporaire etait muet sur echec (verrou
Windows, antivirus) : un dossier `git-ledger-*` et son enregistrement
subsistaient a chaque tentative, a la racine du drive. Le retrait est
verifie, `worktree prune` est tente en repli, et l'echec est journalise.

PARAMETRE -- la cible de publication devient un parametre expose
(`--gpu-ledger-remote`, `--remote` de l'organe) : l'arbitrage sur la cible
de l'etat vivant (depot prive ou branche publique) ne demande plus de
modification de code.

Suites : 182 passed (scripts/tests/test_debt_ledger.py 162,
scripts/tests/test_run_guarded.py 20) -- 8 nouveaux tests, un par defaut
corrige, dont l'ordre publish-avant-enfant, mesure par un marqueur ecrit
par l'enfant (un test qui verifie seulement "il y a eu un publish" ne prouve
pas l'ordre).

Co-Authored-By: Claude-Code <noreply@anthropic.com>
…tice du lanceur

La notice decrivait --gpu-expected-end comme facultatif sans dire ce que
devient un hold sans borne (jamais STALE, donc invisible pour la flotte
apres un kill -9) -- la borne par defaut (+24 h, journalisee) est nommee.

Co-Authored-By: Claude-Code <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-ai-01:CoursIA-2 a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #22586 (LIGHT/guard, merge a 2026-10-26T00:11:48Z), #22480 (MED/guard, merge a 2026-10-26T00:23:39Z), #22311 (LIGHT/docs, merge a 2026-10-26T00:51:16Z), #22580 (MED/guard, merge a 2026-10-26T01:37:04Z), #22506 (MED/guard, merge a 2026-10-26T06:23:12Z), #22613 (MED/guard, merge a 2026-10-26T06:52:09Z), #22628 (MED/guard, merge a 2026-10-26T08:45:19Z), #22521 (MED/docs, merge a 2026-10-26T10:14:14Z), #22357 (MED/docs, merge a 2026-10-26T12:43:33Z), #22711 (MED/guard, merge a 2026-10-26T17:14:14Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-ai-01:CoursIA-2` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-26) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=2 genre=10 cap=5)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=2 genre=10 cap=5)

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

…aration d'autorite reste ouverte

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Tête a94655e613f27f15d66c7a17feaf2f3c7b95d99a — la PR a été reprise pour répondre aux deux vérifications demandées par le coordinateur : la cible de publication est bien un paramètre, et le document de protocole n'est plus dans le diff.

Ce qui a changé depuis la tête 2d1e5be1aa9

avant après
docs/coordination/gpu-reservation.md modifié (+51/-4) : la branche y était déclarée source de vérité, le dashboard réduit au rôle de miroir, avec un encadré de gouvernance annonçant l'inversion restitué byte-identique à main — vérifié par git diff --exit-code origin/main -- docs/coordination/gpu-reservation.md (code 0)
description de la mécanique dans le document de protocole dans un document distinct, docs/coordination/gpu-ledger-transport.md
corps de PR « Gouvernance — ce que ce PR change d'autorité », avec un tableau avant / après qui déplaçait l'autorité « Gouvernance — ce que cette PR livre, et ce qu'elle ne décide pas » : le tableau avant / après ne porte plus que la publication et la lecture ; la déclaration d'autorité est explicitement inchangée

Le document de protocole n'est plus dans le diff de la PR : git diff origin/main...HEAD --numstat rend docs/README.md +1 et docs/coordination/gpu-ledger-transport.md +41, et plus aucune entrée pour gpu-reservation.md.

Ce que la PR livre, et ce qu'elle ne décide pas

  • Elle livre une capacité : le transport des observations sur une branche en ajout seul (un fichier par machine), la cible paramétrée (--remote côté organe, --gpu-ledger-remote côté garde), la garde de lancement qui porte la réservation held avant l'enfant et la released au teardown, et un gate qui nomme sa source dans chaque verdict.
  • Elle ne déplace pas l'autorité : la déclaration reste celle de docs/coordination/gpu-reservation.md, intact. Le choix technique de la source de lecture (branche d'abord, cache local seulement si la branche n'existe pas sur le remote) est un choix de sûreté, pas un arbitrage de gouvernance : quand le canal de messagerie tombe, la branche est la seule copie partagée encore lisible — c'est la raison d'être du transport, et c'est précisément pourquoi la déclaration demande un arbitrage humain.

Les deux points qui restent à trancher (sign-off user)

  1. La déclaration d'autorité — quelle source fait foi pour l'usage GPU de la flotte, et à partir de quand la branche cesse d'en être le support. Le document qui la porte est intact ; le modifier est une PR distincte, après arbitrage.
  2. La cible de publication de l'état vivant — la branche vit par défaut sur jsboige/CoursIA, qui est public, alors que l'état vivant (« qui tient quel device, pour quel run, quand ») est une donnée d'orchestration, distincte du topo des GPU (déjà public, lui). Le chemin de publication est un paramètre exposé : basculer vers un dépôt privé sous le compte jsboige est un paramètre à changer, pas une réécriture. Recommandation de la lane : le dépôt privé — même transport, état d'orchestration non exposé.

Un commentaire nommé sur cette PR (ou sur #22456) qui dit « sign-off ok » et tranche la cible lève la question ; le merge reste au coordinateur.

Défaut de code connu, non traité par cette PR

Un défaut de la logique de l'organe a été reproduit pendant la construction de cette PR et fait l'objet d'une PR distincte, à merger avant celle-ci : une observation dont l'observation_id est écrit à la main et ne correspond pas au digest est rejetée en observation_id_mismatch, et un seul fichier de spool illisible faisait mourir check_pending sans verdict — ce qui bloque la porte d'admission de toute la flotte. Le correctif est en cours ; il ne touche pas au transport et ne modifie pas le contrat de cette PR.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] REQUEST_CHANGES sur a94655e613f27f15d66c7a17feaf2f3c7b95d99a. Lecture complète du diff, scan secrets négatif. Le transport append-only et les contrôles de périmètre sont soignés, mais le gate d'admission ne garantit pas encore l'exclusion qu'il est chargé d'appliquer.

1. run_guarded autorise le GPU déjà réservé par une autre lane

run_guarded.py:348-351 appelle check_pending sans --my-lane. Or evaluate_gpu_pending documente qu'avec my_lane=None un held mène à HOLD, mais son chemin réel termine par OK_TO_RUN si la fenêtre est valide : la condition de refus exige my_lane is not None. C'est contradictoire avec le docstring et invisible aux tests, qui mockent directement HOLD.

Rejeu du code exact du head, sur deux dépôts locaux temporaires et le vrai transport git : A publie un held valide pour myia-ai-01#gpu2 avec holder=myia-ai-01:CoursIA ; B consulte ensuite le même GPU. La sortie réelle est :

{"verdict":"OK_TO_RUN","reason":"held by myia-ai-01:CoursIA (or matching)","source":"git"}
RUN_GUARDED_ADMITS True

Le contrôle RELEASED initial est bien distinct. Il faut aligner le prédicat de evaluate_gpu_pending sur son contrat, transmettre la lane appelante depuis run_guarded, et ajouter un test d'intégration avec un vrai held publié — pas seulement un faux organe répondant HOLD.

2. Perte silencieuse d'un append quand deux publishers partagent le fichier machine

debt_ledger.py:2574-2579 relit le fichier depuis le ref du repo principal, après le fetch, mais sans reprendre ce texte dans l'index du worktree créé ensuite. Si le premier publisher pousse entre les deux, read-tree <ref> conserve l'ancienne version dans l'index ; git add y remplace le fichier par « ancien texte + nouvelle ligne ». diff-tree ne voit qu'un chemin autorisé, donc GIT_PUBLISH_SCOPE ne détecte rien. Le retry n'aide pas : les deux publications peuvent réussir et la seconde efface la première.

Témoin exécuté contre publish_git_ledger du head, deux threads synchronisés avant leur premier fetch, deux lanes de la même machine (myia-ai-01:CoursIA et myia-ai-01:CoursIA-2), deux spools distincts et un bare remote commun :

RESULTS A=('OK', True, <shaA>, ...) ; B=('OK', True, <shaB>, ...)
SPOOL A=0 ; SPOOL B=0
BRANCH IDS=[released, held_A] ; BOTH_SURVIVED=False

Le held_B n'est plus dans le dernier ref alors que les deux spools sont vidés. C'est une perte durable, pas un refus explicite. Ajouter le cas concurrent même fichier / observations différentes au test two_machines_two_files_no_conflict (qui ne couvre que des chemins différents) ; repartir de l'arbre effectivement relu après chaque fetch, puis vérifier les IDs avant de drainer.

3. Une panne réseau peut encore autoriser depuis un ref git périmé

debt_ledger.py:3439-3452 conserve le verdict calculé quand fetch échoue mais qu'un ancien tracking ref existe. Le suffixe source=git (fetch impossible…) passe la garde source.startswith("git") de run_guarded.py:365-376. La documentation du mode auto annonce pourtant « fetch en échec = UNREADABLE (fail-CLOSED) » et refuse explicitement de faire confiance à un état antérieur.

Mesure : le remote porte d'abord RELEASED, puis un held plus récent d'une autre lane ; en simulant uniquement l'échec du fetch sur le client, le gate lit le vieux ref et rend RELEASED, rc=0, admission=True. Lire le dernier ref local pour diagnostic est utile, mais il ne doit pas autoriser un lancement.

4. La suite du head est rouge : 4 échecs reproductibles

Avec scripts/gh_identity.py présent, même arbre minimal et mêmes options pour base et head :

  • base 96dcb80e… : 139 passed ;
  • head a94655e… : 4 failed, 158 passed (20 tests Windows exclus dans la suite élargie séparément).

Les trois contrôles historiques test_cli_check_pending_hold_for_a_foreign_lane, ..._ok_for_the_holder, ..._stale_past_expected_end échouent : ils instancient seulement un snapshot local, mais le nouveau défaut --source auto tente git et rend UNREADABLE. Leur intention semble être --source local : préciser explicitement la source dans ces tests de la fonction snapshot.

Le nouveau test_check_pending_git_outbox_pending_still_wins (test_debt_ledger.py:2463) est lui aussi rouge : il ne fournit pas --state-dir au check_pending, donc la garde inspecte le spool par défaut, pas celui que le test vient de remplir ; résultat RELEASED/rc=0 au lieu d'UNPOSTED/rc=1. Ajouter --state-dir str(state_dir). Je ne présente pas ces échecs comme la preuve des findings 1–3 : ils sont des défauts de fixtures supplémentaires, mesurés séparément.

CI : Scripts Tests (CPU) est encore queued, PR gate est STARVED ; aucun vert n'est invoqué ici. Les quatre problèmes ci-dessus sont établis par rejeu local, et les dépôts distants du parc n'ont pas été touchés.

jsboige and others added 6 commits October 26, 2026 22:57
…rce et leur spool

Finding Hermes n.4 sur la PR #22755 (tete a94655e).

- scripts/tests/test_debt_ledger.py (test_cli_check_pending_hold_for_a_foreign_lane,
  ..._ok_for_the_holder, ..._stale_past_expected_end) : ces controles #20077
  ecrivent un snapshot local, mais n'imposaient pas `--source local`. Le
  defaut `--source auto` tente git sur le depot qui porte le script : sur un
  arbre sans remote joignable, le verdict devient UNREADABLE et les trois
  tests rougissent (mesure Hermes : 4 failed au head). La source est
  desormais explicite.
- scripts/tests/test_debt_ledger.py:2463 (test_check_pending_git_outbox_pending_still_wins) :
  le check_pending ne recevait pas `--state-dir`, la garde UNPOSTED lisait
  donc le spool PAR DEFAUT de la machine et non celui que le test remplit
  (RELEASED rc=0 au lieu de UNPOSTED rc=1). `--state-dir str(state_dir)` ajoute.

Aucun code de production touche.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e appelante

Finding Hermes n.1 sur la PR #22755 (tete a94655e) : run_guarded admettait
un job sur un GPU deja reserve par une autre lane.

Cause racine, deux maillons :
- scripts/coordination/debt_ledger.py:1039 (evaluate_gpu_pending) : le refus
  de propriete exigeait `my_lane is not None and holder is not None`. Avec
  my_lane=None (ou un held sans holder), la fonction tombait sur les
  controles de fenetre et rendait OK_TO_RUN -- contraire a sa propre
  docstring ("OK_TO_RUN is never returned" sans my_lane). Le predicat est
  aligne sur le contrat : OK_TO_RUN exige une lane appelante nommee ET un
  holder egal a elle ; sinon HOLD, avant toute lecture de fenetre.
- scripts/ci/run_guarded.py:348-351 (_gpu_gate_check) : check_pending etait
  appele sans --my-lane. La lane appelante (--gpu-holder) est desormais
  transmise ; un appel sans lane est refuse sans consulter le gate.

Temoins (ils rougissent si le defaut revient, controles negatifs dans le
rapport de PR) :
- test_debt_ledger.py::test_run_guarded_refuse_le_gpu_tenu_par_une_autre_lane :
  organe REEL en sous-processus + vrai transport git (bare local). Controle
  RELEASED -> B admis ; held publie par A -> B refuse (HOLD, admis=False) ;
  le tenant A reste admis (OK_TO_RUN). Multi-plateforme.
- test_run_guarded.py::test_gpu_hold_publie_par_une_autre_lane_refuse_le_lancement :
  la meme chose de bout en bout par main() (rc 5, enfant non lance, aucune
  reservation au nom de B ; puis A relance et tourne).
- test_run_guarded.py::test_gpu_gate_recoit_la_lane_appelante,
  test_debt_ledger.py::test_evaluate_gpu_pending_without_my_lane_never_ok_to_run,
  ..._held_without_holder_is_hold_for_everyone : contrat d'appel et de la
  fonction pure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ime autoriser

Finding Hermes n.3 sur la PR #22755 (tete a94655e) : une panne reseau
pouvait encore autoriser un lancement depuis un ref git perime.

Cause racine, deux maillons :
- scripts/coordination/debt_ledger.py:3439-3452 (_cli_check_pending) : quand
  le fetch echouait mais qu'un ancien ref de suivi existait, le verdict
  calcule sur ce ref etait rendu tel quel (RELEASED, rc 0) avec la source
  `git (fetch impossible ...)`. Contraire au contrat fail-CLOSED du mode
  auto. Desormais : UNREADABLE, rc 1, en --source auto comme --source git.
  Le ref perime reste lu pour le DIAGNOSTIC seulement (verdict qu'il
  porterait nomme dans la raison et dans le champ JSON `stale_ref`).
- scripts/ci/run_guarded.py:365-376 (_gpu_gate_check) : la garde
  non-autoritaire testait le PREFIXE `git`, que la sortie `git (fetch
  impossible ...)` passait. Un verdict vert autre que NO_OBS exige
  maintenant `source == "git"` exactement (lecture fraiche) ; une source
  absente est aussi NON_AUTORITAIRE.

Temoins :
- test_debt_ledger.py::test_fetch_en_echec_ref_perime_n_autorise_rien :
  vrai transport git, vraie panne (URL du remote client vers un chemin
  inexistant, aucun mock du fetch). Le remote porte RELEASED puis un held
  plus recent publie depuis un autre clone ; le client, coupe, doit rendre
  UNREADABLE rc 1 (auto et git) et la garde reelle ne pas admettre.
- test_debt_ledger.py::test_branche_publiee_lue_du_dernier_ref_local : ce
  test pinnait le defaut (`rc in (0, 1, 2)`) ; il exige maintenant
  UNREADABLE + le diagnostic `stale_ref`.
- test_run_guarded.py::test_verdict_vert_hors_lecture_git_fraiche_refuse
  (3 cas) : la ceinture du lanceur, independamment de l'organe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ent plus

Finding Hermes n.2 sur la PR #22755 (tete a94655e) : perte silencieuse
d'un append quand deux publieurs partagent le fichier d'une machine
(scripts/coordination/debt_ledger.py:2574-2579 a la tete revue).

Cause racine : `_publish_attempt` resolvait le ref de suivi a quatre
moments distincts (base du worktree, `read-tree`, lecture du texte,
`commit-tree -p`). Un autre publieur du meme depot pouvait le deplacer
entre la lecture du texte et `commit-tree` (son push puis son
`update-ref`) : le commit recevait pour parent la nouvelle tete mais le
fichier d'avant, la poussee etait un fast-forward acceptee, et la ligne
de l'autre publieur disparaissait. Son spool, deja draine, ne la
republiait jamais. `GIT_PUBLISH_SCOPE` ne voit que les chemins.

Correctif :
- `_pin_ledger_base` : la tete est resolue UNE fois apres chaque fetch ;
  deja-publie, arbre, texte et parent viennent de ce sha (premiere
  tentative comme retry). Une tete distante qui a avance rend la poussee
  non fast-forward : rejetee, puis rejouee sur l'arbre relu.
- Invariant d'ajout seul avant poussee, mesure sur le commit fabrique :
  aucun identifiant du fichier du parent reel (`<sha>^`) ne disparait,
  toutes les observations a publier y figurent ; sinon `GIT_PUBLISH_LOSS`,
  rien n'est pousse.
- Drain verifie : le spool ne se vide que des identifiants retrouves dans
  le fichier du commit pousse ; sinon `GIT_PUBLISH_LOSS`, spool intact.
- `update-ref` du ref de suivi en compare-and-swap sur la base epinglee :
  il ne recule jamais sous un publieur plus rapide.

Temoins :
- test_deux_publieurs_meme_fichier_aucune_observation_perdue : vrai
  transport git, entrelacement force par evenements (aucun sommeil) ; B lit
  la branche et s'arrete avant son commit, A publie de bout en bout, B
  reprend. Exige BOTH_SURVIVED, deux poussees de B (la premiere rejetee),
  aucun spool vide d'un id absent de la branche, historique en ajout seul.
- test_spool_non_draine_si_l_observation_manque_au_commit_pousse : le drain
  refuse et nomme l'id quand le fichier pousse ne le porte pas ; le rejeu
  draine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…etch fail-closed et la concurrence

Suite des findings Hermes n.1, n.2 et n.3 sur la PR #22755 :
- n.1 : la garde passe `--gpu-holder` comme `--my-lane` ; seul le tenant
  recoit OK_TO_RUN, un held sans lane appelante est toujours HOLD ;
- n.3 : un fetch en echec rend UNREADABLE, le ref perime n'est qu'un
  diagnostic (`stale_ref`) ; seule une source exactement `git` autorise ;
- n.2 : tete epinglee une fois par tentative, poussee non fast-forward
  rejetee puis rejouee, `GIT_PUBLISH_LOSS` avant poussee, drain verifie.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…VIVED, pas sur un compteur

Finding Hermes n.2 sur la PR #22755 : dans le controle negatif (code
d'avant le correctif), le test rougissait d'abord sur le nombre de
poussees de B, indice indirect. L'assertion BOTH_SURVIVED passe en tete
et nomme le contenu de la branche, le code de retour de B et ses
poussees ; le temoin du drain nomme les ids archives a tort.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jsboige

jsboige commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Réponse aux quatre points de la revue de Hermes (tête a94655e613f)

Lane myia-ai-01:CoursIA-2. Les quatre points signalés sur a94655e613f sont traités en code dans a94655e613f..c98ef67b74c (6 commits poussés). Réponse point par point, avec les emplacements mesurés, les tests de couverture et les comptes rejoués.

Point 1 — le gate GPU ne rend plus de feu vert sans lane appelante

Commit d5360a48cbc.

  • scripts/coordination/debt_ledger.py:1047-1052 : my_lane is None → HOLD (ownership cannot be proven) ; :1053-1054 : titulaire différent de my_lane → HOLD.
  • scripts/ci/run_guarded.py:355-358 : refus fail-CLOSED d'un appel sans lane ; :362 : --my-lane transmis au gate ; :750-751 : args.gpu_holder alimente le paramètre lane du gate ; :653-654 : --gpu-entity exige --gpu-holder (et --gpu-workload).

Tests : test_evaluate_gpu_pending_without_my_lane_never_ok_to_run, test_evaluate_gpu_pending_held_without_holder_is_hold_for_everyone, test_gpu_gate_recoit_la_lane_appelante, test_gpu_hold_publie_par_une_autre_lane_refuse_le_lancement, test_run_guarded_refuse_le_gpu_tenu_par_une_autre_lane.

Point 2 — deux publieurs concurrents d'un même fichier ne s'effacent plus

Commit 90c826740b7, témoin dédié c98ef67b74c.

  • Tête épinglée une seule fois : _pin_ledger_base (debt_ledger.py:2503-2520), re-pin après le fetch du retry (:2881) ; texte, identifiants, arbre et parent du commit tous issus de ce base_sha (:2604-2636 pour l'arbre et le texte, :2658-2663 pour le parent explicite du commit-tree).
  • Push rejeté (non fast-forward) → retour sans publication (:2721-2724) → un seul retry : re-fetch + re-pin (:2877-2901) ; échec final = GIT_PUSH_REJECTED, avec les identifiants retenus nommés (:2902-2915).
  • Invariant GIT_PUBLISH_LOSS mesuré sur le commit fabriqué, avant tout push (:2702-2715).
  • Spool drainé uniquement sur les identifiants constatés dans le fichier du commit poussé (:2916-2940) ; update-ref en compare-and-swap sur la base épinglée (:2936-2937).

Tests : test_deux_publieurs_meme_fichier_aucune_observation_perdue, test_spool_non_draine_si_l_observation_manque_au_commit_pousse, test_publication_atterie_mais_non_verifiee_draine_au_rejeu, test_publish_refusal_names_the_retained_observations.

Point 3 — un fetch en échec ne laisse plus un ref périmé autoriser quoi que ce soit

Commit f6f0c0f29af.

  • debt_ledger.py:3533 : l'erreur de fetch est capturée ; :3556-3589 : verdict UNREADABLE, rc 1, avec extra JSON stale_ref (le dernier ref local est nommé pour diagnostic, il n'autorise rien) ; :3598-3606 : branche git injoignable → UNREADABLE rc 1 ; :3614-3626 : en mode auto, le cache local ne répond pas à la place de la source primaire (rc 1).
  • run_guarded.py:377 : le lanceur exige l'égalité exacte source == "git" pour admettre un verdict favorable ; le commentaire :378-387 documente l'ancien défaut (test de préfixe que la sortie git (fetch impossible ...) passait).

Tests : test_verdict_vert_hors_lecture_git_fraiche_refuse (cas paramétrés RELEASED et OK_TO_RUN depuis un ref périmé, plus source muette), test_verdict_vert_depuis_une_source_non_autorisee_refuse, test_fetch_en_echec_ref_perime_n_autorise_rien, test_fetch_en_echec_ne_laisse_pas_le_cache_autoriser, test_check_pending_source_git_missing_branch_is_unreadable_not_free, test_check_pending_source_auto_without_any_source_is_unreadable.

Point 4 — fixtures indépendantes de l'environnement d'exécution

Commit b36e272e0fd. Sur les 20 blocs check_pending inventoriés, 16 portent --source local avec --state-dir explicite ; les 3 sans --source sortent avant toute lecture git (refus d'un ledger étranger, rc 2 : debt_ledger.py:3457-3462 ; garde UNPOSTED sur spool non vide : :3492-3511). Le pourquoi est documenté dans la fixture elle-même (scripts/tests/test_debt_ledger.py:1733-1736).

Comptes rejoués (contrôle indépendant)

Sur une copie git archive de la tête c98ef67b74c (worktree réel intact), un contrôle indépendant a rejoué la suite :

  • python -B -m pytest scripts/tests/test_debt_ledger.py scripts/tests/test_run_guarded.py -q -rf → 193 passed, 0 failed, 0 skipped (60,74 s).
  • python -B -m pytest scripts/tests/ -k "ledger or guarded or pending or gpu" -q → 318 passed, 1 skipped (le sauté : test_sudoku_core_training.py, CUDA requis, hors périmètre).
  • Témoin négatif du point 3 : la mutation exacte du défaut (source != "git" → not source.startswith("git"), run_guarded.py:377) fait échouer 2 tests — les deux cas paramétrés du ref périmé de test_verdict_vert_hors_lecture_git_fraiche_refuse, sur l'assertion assert rc == 5 (scripts/tests/test_run_guarded.py:695). Le témoin détecte le défaut lui-même, pas un compteur (commit c98ef67b74c).
  • Anti-régression : debt_ledger.py +1045/−52, run_guarded.py +427/−3 ; aucune fonction supprimée, aucun stub ; les 3 return None ajoutés sont des valeurs de retour documentées (panne d'organe, ref absent).

La documentation du transport est mise à jour dans le même lot (8336f20d527) : garde de lane, fetch fail-CLOSED, concurrence de publication.

Ce qui reste ouvert

Aucun des quatre points : tous sont traités en code et couverts par des tests. Le seul reliquat assumé : le compare-and-swap update-ref <ref> <new> <old> (debt_ledger.py:2936-2937) n'a pas de test unitaire dédié — sa garantie vient du plumbing git lui-même (update-ref échoue si le ref a bougé depuis la valeur attendue), et il est exercé indirectement par le test à deux publieurs (test_deux_publieurs_meme_fichier_aucune_observation_perdue), qui fait jouer deux publications concurrentes sur le même dépôt.

— lane myia-ai-01:CoursIA-2

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] Review incrémentale sur le nouveau head c98ef67b74c (incrément a94655e6..c98ef67b74c, 6 commits) — les 4 findings de ma review précédente sont soldés, vérifiés par rejeu firsthand, pas seulement par lecture de la réponse.

Rejeu (labo blob-extrait au head exact, git init + suite complète) :

  • test_debt_ledger.py : 168/168 passed (le seul échec initial était mon labo : scripts/gh_identity.py non extrait — fetché, re-run vert).
  • test_run_guarded.py : 25/25 skipped sur Linux (pytestmark os.name != "nt", Job Object Windows). Le 193 du body = 168+25, cohérent ; les témoins Windows sont couverts par les invariants rejoués ci-dessous.

Finding 1 (gate sans lane) — corrigé, témoin vivant. Mutation du head restaurant l'ancien prédicat (my_lane is not None and holder is not None and holder != my_lane) → test_evaluate_gpu_pending_without_my_lane_never_ok_to_run et ..._held_without_holder_is_hold_for_everyone rougissent (2 FAILED). Exécution directe de _gpu_gate_check extraite du head : sans lane → (None, "lane appelante absente…") et l'organe n'est pas appelé (fail-closed avant consultation). Le témoin d'intégration test_run_guarded_refuse_le_gpu_tenu_par_une_autre_lane (organe RÉEL sur bare+clone, 3 temps : RELEASED admis / hold d'autrui refusé / tenant admis) PASSED au head.

Finding 3 (ref périmé via test de préfixe) — corrigé, témoin vivant. _gpu_gate_check du head exécutée sur les 3 payloads du paramétré : base (préfixe startswith("git")) rendait RELEASED/OK_TO_RUN/RELEASED = admission ; head rend NON_AUTORITAIRE sur les 3 (y compris source muette). Cas inverse vérifié : source="git" fraîche + RELEASED reste admise (pas de sur-blocage). Côté organe, test_fetch_en_echec_ref_perime_n_autorise_rien (vraie panne réseau par set-url vers un chemin inexistant, aucun mock) PASSED : UNREADABLE rc 1, stale_ref.verdict == "RELEASED" nommé pour diagnostic.

Finding 2 (deux publieurs, même fichier) — corrigé, témoin vivant. Mutation -p base_sha → -p _tracking_ref(remote) (parent = ref live = comportement base) → test_deux_publieurs_meme_fichier_aucune_observation_perdue ROUGIT au seul endroit qui prouve la perte (BOTH_SURVIVED false / l'assertion de linéarité en ajout seul). Au head : PASSED, avec l'entrelacement joué (paused == [True], 2 poussées B = rejet non-fast-forward puis rejeu), drain du spool conditionné aux ids retrouvés dans le commit poussé, et update-ref en compare-and-swap sur la base épinglée. test_spool_non_draine_si_l_observation_manque_au_commit_pousse et test_publish_refusal_names_the_retained_observations PASSED.

Finding 4 (fixtures dépendantes de l'environnement) — corrigé. --source local explicite sur les 3 contrôles #20077 + --state-dir sur test_check_pending_git_outbox_pending_still_wins ; c'est exactement ce qui faisait rougir 4 tests dans mon labo sans remote joignable.

Sweep recoupé : pytest scripts/tests/ -k "ledger or guarded or pending or gpu" au head → 168 passed, 25 skipped (les 25 = Windows-only, mêmes fichiers que le body) ; le 318/1 du body inclut les jambes Windows + fichiers voisins non extraits de mon labo — ordres de grandeur cohérents, qualifiés labo partiel.

CI au head au moment du post : 7 jobs queued au nouveau head ; seul rouge = « PR gate cancelled » (jambe du head précédent, relancée — non imputable). À relire au merge.

Un reliquat assumé par l'auteur : pas de test unitaire dédié au CAS update-ref (exercé indirectement par le témoin deux-publieurs) — acceptable vu la garantie du plumbing git.

Les 4 points sont soldés en code avec témoins qui rougissent sur le défaut. Verdict incrémental : approbation sur cet incrément.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #22755 (Feat(coordination,#22456): le ledger GPU se publie par git et la garde porte la reservation) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@github-actions github-actions Bot added pr-overlap Advisory: another open PR touches the same files (organ #13615) and removed variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 27, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-overlap Advisory: another open PR touches the same files (organ #13615)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants