Skip to content
Back to skills

Coordinate Adjoint

BSecurity

Cycle horaire du coordinateur adjoint myia-po-2025:CoursIA-2. Réduit la file, répond aux ASK CoursIA-2, publie des preflights COMMENTED et dispatche les lanes CoursIA-2, sans merge ni arbitrage réservé à ai-01.

  • 16 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 28, 2026
educationpythonbashgitapi

Works with

  • terminal
  • cli
  • api

Security analysis

B75/100
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned October 5, 2026

npx -y skills add jsboige/CoursIA --skill coordinate-adjoint --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Coordinate Adjoint?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Coordinate Adjoint
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/jsboige-coordinate-adjoint/badge)](https://www.skillsdirectory.com/skills/jsboige-coordinate-adjoint)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: coordinate-adjoint
description: Cycle horaire du coordinateur adjoint myia-po-2025:CoursIA-2. Réduit la file, répond aux ASK CoursIA-2, publie des preflights COMMENTED et dispatche les lanes CoursIA-2, sans merge ni arbitrage réservé à ai-01.
---

# 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

1. **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 ».
2. **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](../../../docs/reference/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`).
3. Traiter d'abord les handovers, ASK et bloqueurs actifs.
4. Lire les PRs ouvertes pertinentes : body complet, commentaires, reviews et diff avant tout preflight/commentaire.
5. Préparer les décisions réservées à ai-01 sous forme de synthèse courte : PR, état vérifié, preuve, action recommandée.
6. 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.
7. Ne jamais laisser une lane terminalement idle : fournir une deep-queue ou un prochain grain DEEP/MED de CONTENU vérifié.
8. **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.
9. 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.

## 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

```bash
# 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).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…