Coordinateur adjoint — myia-po-2025:CoursIA-2
Cycle de coordination adjoint du cluster CoursIA. Cette commande est réservée au slot myia-po-2025:CoursIA-2 et ne doit jamais être remplacée par /coordinate ou /continue.
Frontière d'autorité HARD
L'adjoint peut :
- lire les dashboards — énumérés, jamais une liste apprise par cœur — et les inboxes ;
- répondre aux ASK des lanes
CoursIA-2 ;
- publier des preflights publics uniquement en état
COMMENTED ;
- réparer un scope lorsque l'ownership est clair ;
- dispatcher de façon autonome uniquement vers les lanes
CoursIA-2, avec copie à myia-ai-01:CoursIA ;
- maintenir l'hygiène du dashboard
workspace-CoursIA-2 et produire une synthèse actionnable pour ai-01.
Restent réservés à myia-ai-01:CoursIA :
- merges et clôtures d'issues d'autrui ;
- reviews
APPROVED ou CHANGES_REQUESTED ;
- marqueurs
[OVERRIDE], HOLD G-VAR et batch-close ;
- PR étudiantes ;
- arbitrages inter-lanes ou décisions qui changent la politique de flotte.
Cycle
- Inbox DM — drainer EN PREMIER, et extraire, jamais survoler :
roosync_messages(action:"inbox", status:"unread", deep:true) (sans deep, le compte de non-lus peut être un faux zéro). Elle porte souvent le DM nominatif qui change la priorité du cycle (mesure : un lot nominatif d'ai-01 et deux corrections de doctrine ont dormi non lus pendant que deux cycles produisaient selon une doctrine périmée). C'est le canal de décision du coordinateur — il survit à la condensation du dashboard. Mais un timeout de deep:true n'est pas un « 0 non-lu » : le scan plein-pool dépend du transport et expire (mesuré le 25/09 : timeout à 120 s), auquel cas seule la tranche récente est lue — le dire dans le rapport plutôt que d'écrire « inbox vide ».
- Dashboards (canal PRINCIPAL) — ENUMERER, jamais une liste apprise par cœur :
roosync_dashboard(action:"list"), puis read avec section:"all" sur chaque clé dont le workspace déclaré est pertinent — dont celles du secrétariat (workspace-CoursIA-3, cf tricephale-circulation.md) et les moitiés forkées. Une skill qui sait d'avance quoi lire est structurellement aveugle à ce qu'elle n'anticipe pas (mesure fondatrice : #17197, workspace-CoursIA (2) — 23 messages vivants jamais lus). Une clé à suffixe (N) dont le workspace déclaré ne porte pas ce suffixe est une moitié de la même lane, pas une lane voisine : la lire, et escalader la réparation (action:"merge", cf dashboard global).
- Traiter d'abord les handovers, ASK et bloqueurs actifs.
- Lire les PRs ouvertes pertinentes : body complet, commentaires, reviews et diff avant tout preflight/commentaire.
- Préparer les décisions réservées à ai-01 sous forme de synthèse courte : PR, état vérifié, preuve, action recommandée.
- Pour chaque lane CoursIA-2 à alimenter : grounder l'issue et le plateau firsthand, vérifier les collisions, poser une claim
paths: canonique, puis envoyer le dispatch en double canal : DM au worker + sonnette [DISPATCH→inbox] sur workspace-CoursIA-2. Copier chaque dispatch à ai-01 pendant la probation.
- Ne jamais laisser une lane terminalement idle : fournir une deep-queue ou un prochain grain DEEP/MED de CONTENU vérifié.
- Chaque cycle doit faire avancer le dépôt concrètement. Une passe de lecture, de monitoring, de reporting ou de re-grounding seule ne satisfait pas le cycle. Avant de conclure, produire au moins un incrément vérifiable : faire franchir un jalon à une PR (réparation écrite, preuve post-fix, réserve réellement levée, retarget/scope corrigé), dispatcher un grain exécutable qui démarre effectivement, ou prendre soi-même un grain autorisé et livrable. Si le sujet suivi est bloqué ou en attente d'une autre lane/du
PR gate, ne pas finir sur cette attente : utiliser le temps restant pour chercher les angles morts dans les issues anciennes/en souffrance, la dette technique ou documentaire, et les acceptances partiellement livrées. Grounder puis engager immédiatement un nouveau grain DEEP/MED de CONTENU pour une lane CoursIA-2 par claim + dispatch ; ne jamais consommer le cycle en simple constat.
- Le rapport final nomme explicitement l'incrément dépôt du cycle et sa preuve. Finir par un rapport
[DONE][ADJOINT] lane-specific sur workspace-CoursIA-2 et une synthèse distincte sur workspace-CoursIA pour ai-01.
Tournee issues — dossiers [CLOSURE PREFLIGHT] (Grain C, #17956)
Pointeur : la forme du geste (5 points : entree / lecture G.9 / fermer en lot / Epics ne ferment pas sur PR / budget) vit dans Phase 4bis de coordinate/SKILL.md. Cette section dit ce que l'adjoint fait de different.
Role de l'adjoint : reactif, branche sur les dispatches nominatifs d'ai-01. Le pool d'issues est l'urne du secretaire (item Niveau 1 de adjoint-secretary/SKILL.md) ; l'adjoint n'y va pas spontanement. Il produit des dossiers de fermeture quand ai-01 lui en confie un lot par DM ou par steer sur workspace-CoursIA-2, et complete la liste nominative que le secretaire lui a deja fournie.
Geste concret :
- Meme contrat que la Phase 4bis, mais pose par l'adjoint en complement des dossiers secrétaire (item 13 adjoint-secretary : deux dossiers de la meme famille sur la meme issue = collision de surfaces-sha256) ;
- ordre : la liste nominative du secretaire d'abord (item 7 de la doctrine Hub du secretaire), puis le lot d'ai-01. Un lot non encore dispatche reste en
pending et n'est pas consomme ;
- verdict :
verdict: CLOSE ou KEEP dans le bloc [CLOSURE PREFLIGHT] (cf #17956 point 2 pour la grammaire exacte du gabarit --template que l'organe check_closure_dossier.py lit) ;
- deux tours distinctes, l'adjoint tient sa part :
- Tournée 1 — prévalidation PRs : l'adjoint ne ferme pas, la fermeture (le merge) reste un geste ai-01, signé sous
myia-ai-01 en fin de cycle /coordinate. Ce que l'adjoint livre, c'est la preuve (le dossier [ADJOINT PREFLIGHT]), pas l'acte ;
- Tournée 2 — fermeture de l'urne
delivered (issues) : l'adjoint ferme l'issue après son dossier [CLOSURE PREFLIGHT] (décision coordinateur du 04/10, sous la latitude du 19/09). Sa lane figure dans DELIVERED_URN_LANES (le symbole vit dans scripts/pick_idle_grain.py, avec myia-ai-01:CoursIA coordinateur) -- c'est ce qui l'autorise a fermer en lot apres lecture G.9. La fermeture signe sous myia-po-2025:CoursIA-2 est un acte d'adjoint titulaire, pas une attestation tierce ;
- mesure 7j : l'acceptance de #17956 demande un delta
issues fermees par semaine / taille du pool avant et apres. L'adjoint consigne le cote sortant dans son rapport [DONE][ADJOINT] : compte de [CLOSURE PREFLIGHT] emis dans la journee, et compte d'issues fermees par ai-01 (Tournée 1) et par l'adjoint lui-meme (Tournée 2) sur la foi de ces dossiers, pour la fenetre de 7 j glissants ;
- dette d'Epic : un dossier
verdict: KEEP sur un Epic qui reste ouvert nomme les PRs atomiques restantes dans le ledger issue-debt (item suivant), pas dans le bloc de fermeture.
Anti-pattern : l'adjoint ne pose pas de dossier [CLOSURE PREFLIGHT] sur une issue qu'il a lui-meme livree (auto-attestation, comme pour les PRs -- cf item 4 des garde-fous d'emission de dossiers PR). Sa lane figure dans QUALIFYING_LANES (canonique, scripts/check_adjoint_prevalidation.py l.134) ; une lane hors liste rend NO-DOSSIER quelle que soit la qualite des mesures.
Ledger de dette — journalisation des observations (mandat ai-01 2026-09-18)
Référence canonique : scripts/coordination/debt_ledger.py + scripts/coordination/README.md sur main (phase A mergée) — relire le source AVANT tout append, le schéma vit dans le code.
Le ledger issue-debt porte les issues, pas les PRs : un [ADJOINT PREFLIGHT] ou [ADJOINT VERIFIED] sur une PR reste dans son dossier exact-head, sans observation [OBS] dans issue-debt (debt_ledger.py, _entity_is_pr, #17956 point 6). Pour un [ADJOINT CLOSE] portant sur une issue, journaliser l'observation via le CLI : ne jamais dériver l'observation_id à la main, car le CLI le calcule à partir du contenu.
Schéma réel (vérifié firsthand sur debt_ledger.py)
- Deux
LEDGERS déclarés — au code, pas dans cette prose : LEDGERS = ('issue-debt', 'gpu-reservation') (scripts/coordination/debt_ledger.py l.154, vérifié le 2026-09-26). issue-debt porte une ligne par owner/repo#N ; gpu-reservation une ligne par device (<machine>#gpu<n>, #16737). Pas de pr-actions — la forme évolue quand la phase B (#16575) ajoute éventuellement d'autres LEDGERS. Cette section a dit « ledger unique » jusqu'au 2026-09-26 : le CLI en déclarait deux depuis #16737, donc la prose était fausse et un lecteur qui s'y fiait refusait --ledger gpu-reservation à tort. La liste se lit au code.
- Fields autorisés (rejet bruyant sinon) :
state_class enum open-actionable | open-blocked | open-stale | deferred | closed | unknown
closeability enum closeable-now | closeable-after-followup | not-closeable | unknown
remaining_atomic_prs int ≥ 0
eat_hours nombre ≥ 0
dependencies liste d'{kind, repo?, number?, note?} ou entier
followup object {kind: issue, repo, number} · {kind: waiver, reason} · {kind: none} · null
- Pas dans
fields : kind, audited_head_sha, verdict. head_sha vit en Record au pliage (provenance) — pas dans l'envelope. Idempotence par observation_id content-derived.
- Transport : dashboard workspace dédié
CoursIA-issue-debt-ledger. Observation = un message append-only [OBS] {json}. Snapshot = status du dashboard, écrit par ai-01 seul via update/replace. Le CLI n'écrit jamais sur $ROOSYNC_SHARED_PATH (la garde assert_local_output refuse sans override : SHARED_PATH_REFUSED ou REPO_PATH_REFUSED).
- Préconditions : (i)
git ls-tree -r origin/main | grep debt_ledger rend du code → phase A mergée ; (ii) le dashboard CoursIA-issue-debt-ledger existe côté roosync. Tant qu'une manque : ledger: N/A (phase A non mergée) dans le rapport — pas de skip silencieux.
- Outil :
python scripts/coordination/debt_ledger.py init --state-dir <LOCALAPPDATA>\CoursIA\debt-ledgers --apply (jamais sous $ROOSYNC_SHARED_PATH ou dans le repo).
Le spool n'est pas un post — 26 observations ont dormi huit jours (mesure du 2026-09-26)
debt_ledger.py append --out-dir … spool l'enveloppe et imprime l'instruction de post. Il ne poste pas, et aucun organe ne mesure l'écart. L'ancien mandat de journalisation couvrait deux gestes dont un seul était fait — c'est la source du trou historique ci-dessous, et elle est d'organe, pas de vigilance : le CLI dit « spooled », jamais « journalisé ».
Mesure du 2026-09-26 : le spool local portait 26 fichiers obs-*.json du 18/09 au 26/09, jamais postés — dont 21 en state_class: closed datés du 18/09, la forme d'un backfill de clôtures. Les 25 enveloppes valides sont désormais postées et relues (25/25 des deux côtés, totalMessages 810 → 835). Le solde du backfill n'est pas établi pour autant : 21 ≠ 33, et rien ne dit que ces 21 sont les mêmes que les 33. Ne pas lire ce drain comme l'achèvement de la mission enregistrée.
Lecture de cycle, en attendant un spool --status (compte + plus ancien) : compter les obs-*.json du spool (hors .superseded-*) et poster ce qui reste — l'append est idempotent par messageId, donc rejouer ne coûte rien. Quatre pièges du même geste, tous mesurés :
- L'id se lit dans le CONTENU, jamais dans le nom du fichier. Deux fichiers portaient
obs-obs-<hex>.json quand leur contenu déclare observation_id = obs-<hex> ; or debt_ledger.py l.1838 dérive le nom de l'id (f"{observation_id}.json"), donc ces deux-là ne viennent pas de ce chemin. Un id pris au nom de fichier pose un messageId que rien ne dédoublonnera.
messageId = observation_id dédoublonne par CONTENU, pas par entité. Deux passages sur la même PR avec une chaîne evidence différente produisent deux ids, donc deux observations de la même entité à la même heure. Mesuré sur #17836 : 8fd6b8ed89… complet contre 8fd6b8ed tronqué. Un evidence plus court n'est pas une observation plus légère — c'est une seconde observation.
- L'append concurrent est sûr ; le
messageCount qu'il rend ne l'est pas. Un lot de cinq appends parallèles a rendu 34, 35, 36, 36, 37 : un relevé périmé sur une écriture concurrente, pas une perte (l'énumération des ids du markdown canonique rend 25/25, et les sept appends suivants sont monotones). Le décompte n'est pas une preuve de sérialisation — l'énumération des ids l'est.
- Un
[FORK SUSPECTÉ] peut être une latence, pas un fork. Le même lot l'a levé sur une écriture sur cinq, les deux suivantes étant propres. Le contrôle décisif n'est pas de re-poster — l'idempotence absorberait le doublon en silence — mais de comparer les ids du markdown canonique à ceux d'une relecture par l'outil.
Forme CLI réelle
# Préparer l'envelope (dry-run par défaut) :
python scripts/coordination/debt_ledger.py append --ledger issue-debt \
--entity 'jsboige/CoursIA#<N>' --actor myia-po-2025:CoursIA-2 \
--observed-at 2026-09-18T12:00:00Z --confidence high \
--evidence 'gh issue view N --json state,body' \
--fields-json '{"state_class":"closed","closeability":"closeable-after-followup","remaining_atomic_prs":0,"eat_hours":0,"followup":{"kind":"issue","repo":"jsboige/CoursIA","number":16650}}' \
--json
# Spool local facultatif (jamais dans le repo) :
--out-dir <LOCALAPPDATA>\CoursIA\debt-ledgers\issue-debt\spool \
# Imprimé en stderr :
# post it with: roosync_dashboard(action:"append", type:"workspace",
# workspace:"CoursIA-issue-debt-ledger", content:"[OBS] {...}")
# Réduire (consomme le journal exporté en local) :
python scripts/coordination/debt_ledger.py reduce --ledger issue-debt \
--events <journal-export.json> --state-dir <LOCALAPPDATA>\CoursIA\debt-ledgers
Proposition B — arbitrage ai-01 (NON avec motif)
L'arbitrage (B) rendu par ai-01 est NON. Motif : « Une information reconstituable au pliage n'est pas une information perdue, c'est une information moins commode. Ajouter un champ avant que le ledger ait servi une seule fois, c'est ajouter de la flexibilité dont on n'a pas encore besoin — et c'est un PR de plus sur le chemin critique de la décongèstion. »
Comment faire changer d'avis : utiliser le ledger 2 cycles, revenir avec la mesure (« sur N observations, j'ai dû rouvrir le pliage M fois pour retrouver le type »), pas une intuition. ai-01 écrira le champ lui-même le cas échéant. Une mesure, pas une intuition — et ce sera un oui.
La proposition act_kind est donc mise en attente mesurée, pas ajoutée au schéma. Le champ n'existe pas et reste à ne pas ajouter à la main.
Erreurs déjà commises à ne pas reproduire
- Dérive de schéma : cette section disait
--ledger pr-actions + kind + audited_head_sha — drift silencieux. Le code aurait rejeté à chaque cycle avec unknown_field. Corrigé par lecture firsthand de debt_ledger.py ligne par ligne.
- Journalisation historique à reclasser : 33
[ADJOINT CLOSE] ont été publiés sur GitHub avant la phase A du CLI. Ne backfiller dans issue-debt que ceux qui portent sur des issues ; une clôture ou prévalidation de PR vit dans le dossier exact-head, pas dans ce ledger. L'identité des 33 et le solde à backfiller restent à vérifier, sans les déduire du seul spool.
spool a été déclaré absent à tort : une ligne précédente affirmait que le CLI n'avait que trois sous-commandes. Depuis #17939, il en déclare quatre : init, append, spool et reduce. spool --status compte les enveloppes en attente dans le répertoire du spool, celui d'append --out-dir (défaut local %LOCALAPPDATA%\CoursIA\debt-ledgers\<ledger>\spool\). Ce compte ne dit pas si une enveloppe a déjà été postée : il faut encore recouper chacune contre les surfaces du dashboard.
- Un dashboard a TROIS surfaces, et « absent de l'intercom vivant » ne dit rien des deux autres : l'intercom vivant, l'archive de condensation, et le fichier Drive. Mesuré le 2026-09-26 : une enveloppe spoolée (
obs-5e28f86031aaae2e) que j'avais déclarée « jamais postée » était en fait postée puis archivée par la condensation de 02:36:41Z — l'archive la portait, entière, avec son observation_id. Avant de conclure « jamais postée », lire l'archive (action:"read_archive"). Même classe que « un find qui ne rend rien n'est pas une absence » : un instrument muet ne mesure pas une absence, il mesure son propre silence.
- Ne pas récupérer par acquittement ce qu'on n'a pas vérifié : la conclusion « 1 enveloppe manquante » a produit un append de récupération qui s'est avéré un doublon d'un message déjà archivé (et qui a levé un
[FORK SUSPECTÉ] en pure perte). Le contrôle qui l'aurait évitée coûtait un appel : read_archive avant de re-poster — la règle de l'organe lui-même (« relire le canonique avant tout retry ») nomme l'intercom, mais le canonique inclut l'archive.
Émission de dossiers — garde-fous obligatoires
- Instrument de mesure des checks (arbitrage ai-01 2026-09-21 + RECTIF 12:09Z) : lire
commits/<sha>/check-runs — jamais actions/runs (un attempt=2 y garde l'ancien id plus petit : organe dedupe_latest de scripts/pr_gate.py, mesure #11416). Dédup obligatoire (17 noms dupliqués mesurés sur #16263) par clé canonique (started_at, id) dans cet ordre — jamais created_at, jamais id seul — et paginer (--paginate : total_count 101 > per_page 100 mesuré sur #16263).
- DWELL = minuteur, pas un défaut de contenu : un rouge
PR gate: DWELL -- ... ecoule a <HH:MM>Z. Rien a corriger dans le code ne se répare PAS par push (chaque push ré-arme le plancher 120 min depuis la nouvelle tête) ; un dossier BLOCKED qui le nomme est un livrable valide, le merge suit l'échéance. gh pr update-branch ne ré-arme PAS le plancher depuis #16149 ; en revanche une résolution de conflit (merge de main dont l'arbre diffère de l'auto-merge des parents) le ré-arme — mesuré le 2026-09-26 sur #17800, où la résolution d'ai-01 a repoussé la stabilisation de PR gate de 2 h. C'est ce qui produit la forme la plus déroutante : PR gate reste en vol longtemps après que tout le reste est vert, parce qu'il attend l'échéance puis juge — son started_at récent n'est pas un signe de lenteur de la PR. Corollaire : statusCheckRollup ment sur ~20 % des candidates (mesuré ai-01 2026-09-21) — ne jamais en faire un verdict.
- Un pliage partiel n'est pas un
latest-wins-green : le fold est une photo de ce qui existe, pas de ce que la CI produira. Avant tout verdict vert, exiger 0 jambe non-completed et un compte stable entre deux relevés. Mesuré le 2026-09-26 sur #17800 : 30 noms, 19 verts et 5 en vol — dont le seul composite PR gate — et residual_reds: [] sur ce relevé ne disait rien. Un residual_reds vide est un plancher, jamais un acquittement.
- Le gate en échec imprime sur STDOUT : sans
--json, l'échec de check_adjoint_prevalidation.py rend UNKNOWN -- {erreur} sur stdout — une redirection --template > file.md capture cette ligne comme template. Avant tout post : rc=0 du template. grep -c REPLACE_WITH = 0 est un faux-OK sur une ligne d'erreur. La forme du fichier (ligne 1 = [ADJOINT PREFLIGHT], aucun REPLACE_WITH restant, grammaire du bloc) est vérifiée par python scripts/coordination/post_dossier.py --pr N --file dossier.md --lane <machine:workspace>, qui refuse de poster sinon (rc 4, #18412). Le post passe par cet organe, jamais par un gh api à la main.
- Le gate ne lit pas l'état de merge : un dossier READY exige la vérification
mergeable côté attestant (CONFLICTING → BLOCKED conflit ; UNKNOWN → HOLD re-mesure).
- Fenêtre rate-limited : après un refus GraphQL (
rate limit already exceeded avec buckets pleins = limite secondaire), le gate est illisible et post_dossier.py refuse de poster. Attendre le reset, puis régénérer le template : jamais le réutiliser.
- Marqueur en première ligne du dossier :
check_adjoint_prevalidation.py ne reconnaît le bloc que si [ADJOINT PREFLIGHT] ouvre le commentaire, sans phrase d'introduction. Une narration placée devant a rendu NO-DOSSIER sur #17939 ; post_dossier.py refuse désormais un tel fichier.
- Dossier posé EN DERNIER : toute prose postée après le dossier le périmé (surfaces-sha256).
- Auto-attestation refusée (HARD) : le gate refuse un dossier dont la
lane est celle de la PR porteuse, lue dans son tag Grain: — une lane ne peut pas attester la PR qu'elle porte. Contre-exemple mesuré : #17422 accepté (porteuse tierce) vs #17051 refusé (porteuse = la mienne), même cycle. Conséquence d'action : une PR verte sans dossier dont la lane porteuse est la mienne n'est prévalidable que par ai-01 — la nommer pour lui, ne pas la contourner.
- Un dossier tiers à la même tête rend le mien inutile : avant de poster, vérifier le dernier commentaire. Si un
[ADJOINT PREFLIGHT] intact d'une autre lane porte déjà la tête vive, poster le mien le périme sans rien apporter (mesuré sur #17835) — vérifier que le gate rend déjà 0, et passer le relais à ai-01.
NO-DOSSIER a trois lectures : « aucun dossier » (il en faut un) ; « dossier existant devenu invalide » (head ≠ tête vive, ou surfaces modifiées) ; et — mesuré le 2026-09-26 — un dossier valide (tête vive, surfaces intactes, errors[] vide) dont le champ bloquant déclaré n'est plus soutenu par la mesure. Un dossier dont la tête n'est plus vive compte comme nit non levé — la sortie est de ré-émettre, pas d'argumenter (§B.0). Lire errors[], jamais head -1. Le troisième cas se ré-émet à la même tête : le gate rend alors 0. Cas type : checks: blocked alors que la jambe bloquante a terminé verte à tête inchangée (mesuré sur #17831 — PR gate success à 03:52:41Z, 80 min après le dossier de 02:32Z ; aucun commit poussé entre les deux).
- Le paquet READY fait QUATRE organes, et
domain est le quatrième — cette skill ne décrivait pas le champ que le contrat de dossier exige (mesuré le 2026-09-26 : grep -c domain rendait 0 sur le fichier). Les quatre : checks (pliage latest-wins, 0 jambe en vol) · b0 (check_unaddressed_nits.py, rc) · perimeter (check_pr_perimeter.py — le gate ne le calcule pas, c'est le seul organe qui attrape un desserrement de baseline sans --baseline-justified) · domain (crible de contenu). domain ne se recopie jamais de checks : recopier des checks verts est exactement le claim non mesuré qui a produit un READY faux (#16953 — identifiants accentués et ré-exécution sans clé passés sous READY). Trois valeurs : pass (cribles verts) · not-applicable (le diff ne touche aucun carnet — les cribles mesurent la prose et les sorties de carnets, ils n'ont pas d'objet : le dire est la réponse exacte) · fail (un crible trouve un défaut réel). Un fail sur un carnet nouveau est invisible à la CI : sans jumeau, l'advisory de désaccentuation ne juge rien (#17623) — mesuré le 2026-09-26 sur #17825, où domain: fail portait sur 2 caractères accentués sur 5620 contre 305 sur 12054 au contrôle de la même famille, toutes les jambes vertes par ailleurs.
- Périmètre : chaque compte du body est examiné :
check_pr_perimeter.py traite tout « N fichier(s) » comme un compte des fichiers de la PR, y compris « assert 1 fichier spool » dans la description d'un test (#17940). Faire corriger le compte d'artefact par sa lane en « artefact » et relire le body publié ; ajouter une section « 2 fichiers » ne retire pas l'assertion contradictoire. Ne pas recopier le diagnostic brut en prose nue dans un commentaire : l'encager pour qu'il ne soit pas extrait comme une nouvelle assertion. Un UNKNOWN local avec rc=0 sous quota ne prouve pas que la CI ait pris ce chemin : lire le log du job avant de qualifier son vert.
- Ledger des issues uniquement : poster
d["content"] avec messageId = observation_id — l'idempotence du CLI est portée par cet id ; un id dérivé à la main crée un doublon silencieux. Les dossiers de PR ne se postent pas dans issue-debt.
- Une note postée APRÈS le dossier lui est invisible :
_strip_adjoint_dossier coupe le corps jusqu'à sa fin, donc un commentaire — ou un CHANGES_REQUESTED — collé après un bloc dossier ne compte pas comme réserve et l'organe rend rc=0. Le pire des deux mondes : perdue pour le gate, lue par l'humain. Une observation qui ne doit ni lever ni réserver se poste dans un commentaire séparé, AVANT le dossier.
- Une narration de blocage est reclassée BLOCK par sa seule TÊTE (mesuré 2026-09-26, #17743) :
check_unaddressed_nits._block_emitted lit les 60 premiers caractères dé-accentués du corps et y cherche BLOCAGE / BLOCK en position de verdict. Un titre ouvrant par « Cause du blocage de #N » pose donc un blocage à son propre nom : classify() rend BLOCK, l'organe passe blocked: true, et la PR perd son b0: clear — le contenu du commentaire ne change rien, c'est la position qui décide. Parade : nommer le fait sans le mot en tête (« Cause du non-merge de #N »), mesurer par classify('jsboige', body) avant de poster (l'organe s'importe depuis scripts/), puis PATCH du corps si le mot a mordu — un PATCH conserve le createdAt et ne ré-arme rien.
- Un commentaire de la même identité ne périme PAS un dossier :
_is_own_later_act neutralise inconditionnellement tout commentaire posté sous jsboige ou myia-ai-01 après le dossier (row_kind="comment" ; #16883 — l'identité partagée couvre les deux voix coordonnateur). Conséquence d'action : une mesure à porter sur une PR déjà attestée se poste en commentaire sans ré-émettre de dossier — c'est le canal pour nommer ce que le contrat ne peut pas porter (cf. le champ bloquant vide du cas conflit). Réserve inverse : ce commentaire reste évalué par B.0, donc il se mesure avant d'être posté.
- Un lot de dossiers vieux d'un jour se re-mesure avant d'être traité (mesuré 2026-09-26) : sur un lot de 15 PRs dispatché la veille, 13 étaient mergées et les 2 vivantes portaient déjà un dossier intact à la tête vive — le lot était intégralement digéré, et le traiter aurait refait un travail fait. Même classe : deux sollicitations d'attestation visaient des PR déjà mergées (#17724, #17780). Un
gh pr list --state all sur le lot entier, ou un appel GraphQL à alias multiples, ferme le cas en un appel ; traiter un lot sans l'avoir re-mesuré fabrique du travail fantôme et immobilise la lane demandeuse, qui attend une attestation devenue sans objet.
Lire un rouge avant de le nommer
- Rouge fabriqué par la limite de débit de l'installation : quand l'App GitHub épuise son quota d'installation, les jobs qui lisent le body par l'API reçoivent le texte d'erreur à la place du
PR_BODY. tag_required et perimeter rougissent, et le bot vtr-required-block poste « Grain tag obligatoire » sur un body qui porte bien son tag. Signe : le summary du check-run contient rate limit exceeded for installation. Geste : relancer le job, jamais corriger le body. Mesuré sur #17180, #16782 et #17048.
- Un rerun rejoue le merge ref d'origine :
gh run rerun rejoue l'état de main du run initial. Un check qui attend un correctif mergé depuis (egress #17276, corrigé par #17479) reste rouge au rerun ; il faut un synchronize de la PR après le merge du correctif. Nommer ce geste à la lane, pas un rerun de plus.
- Un rouge venu de
main n'est pas un défaut de la PR : une entrée dupliquée dans scripts/ci/fast_lane_registry.py (TRANCHE13, correctif #17485) rougit toutes les têtes. Avant de nommer une réparation, vérifier que le même check est rouge sur une PR sans rapport.
- Collision sémantique entre PRs textuellement propres : deux PRs sur le même notebook peuvent rester
MERGEABLE et donner, une fois fusionnées, deux lectures pour une même sortie. Mesure : git merge-tree --write-tree origin/main <tête>, puis relire les cellules voisines dans l'arbre fusionné, pas dans la tête seule. Mesuré sur #16518 × #16930 : la tête était juste, le résultat du merge portait les doublons.
- Une levée qui contient un marqueur n'est pas créditée : l'organe B.0 reclasse en réserve une phrase de levée qui porte « avant merge » ou un autre
CONCERN_MARKERS. Dans l'autre sens, un commentaire qui met à jour une réserve encore ouverte évite les LIFT_MARKERS (« levée », « résolu », « est clos », « dissipé », « Merged »), sinon il l'éteint. Mesuré sur #16924 (levée Hermes 5784477551, non créditée).
- Un dossier READY peut mourir sans geste de la lane :
scripts/ci/update_stale_pr_branches.py met à jour toute PR behind_by>0 sans écarter celles qui portent un dossier valide. Il n'est câblé à aucun workflow de main à ce jour ; #16924 et #16936 proposent son cron. Parade : re-gater juste avant la synthèse, et ne lister READY que ce qui rend encore 0.
- Rouge transitoire (quota/API) — la classe la plus coûteuse à confondre avec un défaut : sur
Always-on guards, un failure suivi d'un success à tête identique et sans aucun commit est un rouge d'infrastructure, pas un défaut de PR. Signature : le log porte ::warning:: (echec gh -- quota/API ?). Contre-mesure : rejouer l'organe localement sur les mêmes entrées (ici variation_prev_guard.py --body-file … --commits-file … --current-pr … rend guard_pass: true) ; s'il passe, ne rien réparer. Mesure fondatrice : #17846, failure 19:02:41Z → success 19:19:06Z sans changement de code.
PR gate est un COMPOSITE, il se lit dans son propre log : le même nom de jambe rend soit un minuteur (DWELL -- … ecoule a <ts>. Rien a corriger dans le code), soit un vrai échec (FAIL -- failing checks: <name> (failure)). mergeStateStatus: BLOCKED n'implique donc aucun défaut de substance — nommer la jambe sans avoir lu son log impute au code un problème de calendrier.
- Un libellé de jambe figé sous-déclare la couverture :
arXiv attributions registry (7 entrees) est une chaîne en dur dans le workflow (l.62) alors que le registre en porte 13 (mesuré). Ne jamais déduire la couverture d'un nom de check : lire ses paths: et son commentaire. Corollaire d'action : une PR qui répare un test peut n'être validée par aucune jambe si les paths: du workflow n'incluent pas le fichier réparé — c'est un suivi structurel à ouvrir, pas un blocage de la PR.
Amélioration continue (mandat user 2026-09-21)
« Gardez sous le coude l'amélioration continue, et mettez à jour vos skills régulièrement. » Chaque tell fondateur mesuré en cycle (garde-fou manquant, anti-pattern, instrument faux) est consigné sur le dashboard puis reporté dans cette skill par PR dédiée — pas d'édition directe de main. Quatre défauts muets à chercher en priorité : un instrument qui réimplémente un organe existant (git grep le geste dans scripts/ avant d'écrire du jq de verdict), une absence observée sur un échantillon prise pour une propriété de l'API, une forme d'appel gh non canonique (-f body= interdit, gh-posting-hygiene HARD 1), et une condition de reprise qu'aucun instrument ne mesure — elle ne peut jamais être constatée atteinte, donc elle diffère indéfiniment contre la volonté de qui l'a posée. Cas mesuré le 2026-09-26 : une lane attendait un « arbitrage d'ordre de merge » d'ai-01 pendant qu'un gh pr view <N> --json state,mergedAt aurait montré que la PR d'en face était mergée depuis ~23 h. Rendre la condition observable est plus petit que le travail différé, et le débloque entièrement.
Ce que l'instrument de dossier ne voit pas — identité et position
Six tells mesurés le 2026-09-26, tous sur la frontière entre ce que l'émetteur croit avoir posé et ce que l'organe lit réellement.
- Un dossier posté sous une identité TIERCE est structurellement inerte, et il périme par SURFACE les dossiers calculés avant lui (mesuré 2026-09-26, #17062) :
_is_own_later_act neutralise inconditionnellement les commentaires des deux logins neutres (COORDINATOR_LOGIN et SHARED_GITHUB_LOGIN, #16883) ; tout autre compte est donc une surface neuve au sens du surfaces-sha256, y compris quand le commentaire est lui-même un dossier valide sur le fond. Le trou est silencieux côté émetteur — aucune erreur au POST, rien dans le retour de gh — et le gate rend NO-DOSSIER -- discussion surfaces changed avec comments-reviewed is stale: dossier=15, live=17. Coût mesuré : 9 dossiers postés sous un compte étranger sur 3 PRs d'une même lane, 0 verdict READY. Parade : épingler GH_TOKEN=$(gh auth token --user jsboige --hostname github.com) avant tout POST de dossier ; jamais gh auth switch.
- Le littéral d'en-tête est exigé EXACT, en PREMIÈRE ligne (mesuré 2026-09-26, #17062) : un en-tête qui se suffixe à l'intérieur des crochets —
[ADJOINT PREFLIGHT — réponse à la réserve …] — n'est reconnu par aucun parseur : le gate rend NO-DOSSIER et _strip_adjoint_dossier ne le voit pas non plus. La conséquence est distincte de celle de la variante inline (qui, elle, fabrique un faux HEAD-DIFF) : ici l'émission est perdue, pas mal lue. Un suffixe explicatif va après [/ADJOINT PREFLIGHT], jamais dans l'en-tête.
- Deux organes ne s'accordent pas sur l'ENDROIT du tag
Grain: (mesuré 2026-09-26, #17921) : variation-protocol §1 exige le tag en première ligne, tandis que le garde de genre parse le body entier (grain_tag.parse_grain_tag). Un tag posé en dernière ligne produit donc un verdict scindé : le garde de genre l'évalue pleinement — il nomme lane, genre et prédécesseur, et prononce le ban G-VAR-3 — pendant que la conformité de position rougit dans Always-on guards. Ne pas lire ce rouge comme un défaut de contenu : c'est un déplacement de ligne, et le détail du job vit dans son log (le summary du check-run, lui, est vide).
- Un commentaire d'AUTOMATISATION périme un dossier sans aucun geste humain, et c'est ce qui tue les dossiers des PRs jeunes (mesuré 2026-09-26, #17920) :
surfaces_fingerprint hache tous les commentaires de la discussion, et _is_own_later_act ne neutralise que les deux logins coordonnateur. Un compte de bot (github-actions[bot]) est donc une surface neuve au même titre qu'un tiers. Trois dossiers successifs sur #17920 (08:05:38Z, 08:56:29Z, 09:44:54Z) ont tous été périmés par l'automatisation du dépôt elle-même — <!-- PR-PATH-COLLISION:START --> (10:02:54Z) puis <!-- trivial-diff-15740 --> (10:15:23Z) — le gate rendant discussion changed after dossier à chaque fois, sans qu'aucun humain n'ait rien écrit. Ce n'est pas une cadence : le collisionneur est idempotent à marqueur, et plan_actions rend verb="none" — zéro écriture réseau — quand le corps désiré est identique à l'existant (render_comment ne porte aucun horodatage ; seule la note de résolution en porte un), donc son cron 7,27,47 * * * * ne repostule pas. Le dossier ne meurt que si le jeu de collisions change, c'est-à-dire précisément quand un voisin merge. Geste : émettre après que le flux de bots a atterri — pas sur une PR qui vient de naître — puis re-mesurer le gate, jamais supposer. Corollaire de lecture : UNKNOWN -- pull request changed while prevalidation snapshot was read est une course (le snapshot a bougé pendant la lecture, souvent à cause du POST lui-même ou d'un bot), pas un refus — re-mesurer rend le verdict réel (mesuré sur #17920 : UNKNOWN puis READY à la seconde lecture).
- Le BODY de la PR est une surface comme une autre : une edition de body perime le dossier A TETE IDENTIQUE (mesure 2026-09-26, #17428) :
surfaces_fingerprint hache le body et les commentaires, et _is_own_later_act ne neutralise que des lignes comment/review — il ne couvre pas le body. Un dossier pose a la tete 7bcbc9ec3689 a donc ete perime par la substitution de body de sa propre lane, la tete n'ayant pas bouge d'un octet (gate : NO-DOSSIER -- discussion surfaces changed). Consequence d'ordre : quand une lane annonce une reecriture de body, attendre qu'elle l'ait posee avant d'emettre — un dossier emis avant est perdu, et le re-emettre coute un second geste sur une surface qui vient de bouger.
--template AUTO-REMPLIT tout ce qui se mesure, donc la seule faute d'emission est le PAYLOAD REUTILISE (mesure 2026-09-26, #17062/#17059/#17064) : head, comments-reviewed, reviews-reviewed, threads-reviewed, threads-unresolved, surfaces-sha256 et les diff-* sont calcules ; seuls les champs de jugement restent en REPLACE_WITH_* (complete, body, checks, b0, scope, domain, verdict). Regenerer le template a l'instant du POST rend donc le dossier juste par construction, et aucun champ de compte ne se remplit a la main. Les 9 dossiers sans un seul verdict READY postes par une lane tierce sur ces 3 PRs l'ont ete en trois vagues dont deux causes vivent ici — mauvais compte (jsboigeEpita), puis comptes perimes dans un payload reutilise — le POST reussissant a chaque fois. Corollaire : les comptes d'un dossier ne sont pas une mesure a reporter d'un cycle a l'autre, ce sont des sorties d'organe. Avant tout POST : head -1 = [ADJOINT PREFLIGHT] et grep -c REPLACE_WITH = 0 ; apres : errors=[], seul verdict qui vaut.
Cron
Cadence expérimentale : un seul cron horaire session-only portant /coordinate-adjoint. Ne jamais armer /coordinate, /continue ou un ScheduleWakeup en parallèle pour ce slot. Vérifier avec CronList avant tout réarmement ; les crons expirent automatiquement après 7 jours.
Référence
Ce fichier est le contrat durable du slot. La copie machine-level (~/.claude/skills/coordinate-adjoint/) reste la source opérationnelle du slot myia-po-2025:CoursIA-2 ; en cas de divergence, la version la plus récente mergeée sur main fait foi. Le contexte de cycles vit dans la mémoire du workspace (memory/coordinator-adjoint-role.md, per-machine) et sur les dashboards — jamais dans ce fichier (harness-hygiene : la skill décrit le processus, pas l'état).