From cb8ca6966ef547f787fb5b162713c807e98523df Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 02:28:55 +0200 Subject: [PATCH 1/2] docs(harness,#12051): vague 2 -- slimming de proactive-coordination + variation-protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire la redite interne (deux paragraphes jumeaux portant le meme en-tete HARD sur l'urne umbrella), deplace deux recits mesures vers docs/ avec verification d'arrivee par grep, et raccourcit le changelog du durcissement #15793, deja integralement present dans docs/reference/variation-protocol-detail.md §12. Mesure : 201 113 -> 199 720 o auto-charges (-1 393 o, ~-619 tokens/session). Recompte des prescriptions imperatives sur les deux fichiers : 0 disparue -- le seul delta de marqueur (`jamais`) est un doublon du meme enonce, qui survit verbatim dans le paragraphe "Precondition toxique". La fusion physique des deux rules est evaluee puis ecartee, chiffre a l'appui : 92 occurrences du nom `variation-protocol` dans 30 fichiers hors docs/ (dont 8 scripts, 6 fichiers de tests, 2 workflows CI, le template de PR et 4 rules). Le nom est un contrat machine, pas un doublon ; l'analyse est dans le body. See #12051 Co-Authored-By: Claude Sonnet 5.5 --- .claude/rules/proactive-coordination.md | 216 +++++---- .claude/rules/variation-protocol.md | 240 ++++----- .../proactive-coordination-detail.md | 454 +++++++++--------- 3 files changed, 462 insertions(+), 448 deletions(-) diff --git a/.claude/rules/proactive-coordination.md b/.claude/rules/proactive-coordination.md index 0ff621acbb..3121184db8 100644 --- a/.claude/rules/proactive-coordination.md +++ b/.claude/rules/proactive-coordination.md @@ -1,109 +1,107 @@ -# Coordination proactive — plusieurs grains/wakeup plancher + pool global + never-idle - -S'applique à **tous les workers du cluster CoursIA** (po-2023/2024/2025/2026) et au **coordinateur ai-01**. Source : mandat user 2026-05-23, durci 2026-06-30 → 2026-07-19. - -**Détail (backlog 8 sources, cadence, anti-patterns, incidents, leçons L721/L1356 complètes, picker, veine)** : [docs/reference/proactive-coordination-detail.md](../../docs/reference/proactive-coordination-detail.md). - -## Règles HARD - -0. **Avant de TIRER un grain neuf : faire avancer tout ce qui peut avancer (HARD, mandat user 2026-09-19).** L'ordre de session est ferme — **(a)** toutes tes PRs ouvertes qui peuvent bouger : réserve à lever, nit à traiter, rebase, dossier `[ADJOINT PREFLIGHT]` à refabriquer, check rouge à réparer ; **(b) ensuite seulement** le tirage du picker. Cette règle est **en amont** de la règle 5 : elle ne remplace pas le tirage, elle le conditionne. - - **Pourquoi un ORDRE, et pas seulement un plancher.** La règle 1 compte les grains *livrés* — un compte qu'on satisfait en empilant du neuf par-dessus du non-fini. Mesure du 2026-09-19 sur **213 PRs ouvertes** : **104 CLEAN** (zéro conflit, mergeables sur-le-champ) et **174 sans aucune review**. Le dépôt ne souffre pas d'un manque de grains ouverts ; il souffre de grains ouverts qui n'atterrissent pas. Verbatim user : « *nombreuses sont celles qui n'auront pas besoin de plus de 4-5 commits de plus, donc si vous travaillez bien, les PRs en souffrance devraient pouvoir partir en moins de 24 h. Or ce n'est pas ce qu'on observe.* » - - **Trier par COÛT DE DOSSIER, pas par ancienneté seule.** Une PR **CLEAN et sans review** ne demande qu'une attestation tierce : c'est le grain le moins cher du dépôt. Une BLOCKED demande un diagnostic. L'ancienneté ordonne **à l'intérieur** du sous-ensemble le moins cher ; elle ne le choisit pas. - - **N PRs portant la MÊME classe de défaut = une cause, pas N grains.** Traiter la cause une fois plutôt que d'ouvrir N allers-retours. - - **Un script exécuté sur la branche remplace plusieurs allers-retours de commentaires.** Quand le défaut est **mécanique** — fixer canonique existant, geste déterministe, diff vérifiable — l'exécuter sur la branche de la PR plutôt que poster un commentaire qui coûte une session entière à son porteur. Quatre bornes, toutes requises : la PR est à **lane unique** (un `[CLAIMED]` vivant d'une autre lane renvoie au commentaire, cf [lane-claim-protocol.md](lane-claim-protocol.md)) · le commentaire ou le dossier **nomme le script et le commit** (un commit muet sur la branche d'autrui est indiscernable d'un détournement) · ça **ne lève aucune réserve de tiers** (B.0 : une phrase lève, pas un SHA) · si une cellule source de notebook est touchée, **la re-exécution redevient due** (C.2). - -1. **≥2 grains livrés entre 2 wakeups, dont ≥1 DEEP portant un genre de CONTENU = PLANCHER, jamais plafond.** Une PR livrée ne **clôt pas** la session : re-pioche **immédiatement** et enchaîne autant de PRs que la fenêtre le permet (débit nominal ~2 PR/h). S'arrêter après 1 PR alors qu'il reste du temps **et** 60+ issues ouvertes = **sous-régime**, pas « cycle terminé ». Le plancher est **pluriel et durci** (mandat user 2026-09-12) : un cycle dont le plat principal est MED ou META **n'a pas de plancher tenu**, même avec plusieurs PR livrées. La mesure qui motive ce durcissement — 15 % de DEEP sur 7 j, et sur 48 h le META passant devant le CONTENU — est déposée datée dans [détail, section Plancher durci](../../docs/reference/proactive-coordination-detail.md) ; le tier et le genre se tranchent en [variation-protocol.md](variation-protocol.md) (G-VAR-1). -2. **2 tracks en flight minimum** : une **track principale** (mission dispatchée, sinon la tête du tapis) + une **side-track autonome** que le worker avance **même si le coordinateur s'absente 1-2 jours**. -3. **Side-tracks → sous-agents spécialistes async (HARD).** Quand un specialist `.claude/agents/` couvre la side-track, la déléguer en **`run_in_background: true`** pendant que le worker interactif tient la main track. Roster : [docs/reference/subagents-reference.md](../../docs/reference/subagents-reference.md). -4. **Backlog pickup au wakeup vide (HARD).** Sans nouveau feedback / directive / tâche : **ne pas s'arrêter** — piocher dans le backlog et produire la PR du cycle. - -5. **Pool = TOUT l'ouvert, cross-lane, jamais siloté (HARD).** Cette règle **domine 1-4** : le backlog n'est PAS ta « famille » ni ta « lane », c'est **`gh issue list --state open` en entier**. Ta lane (`machine × workspace`) est une étiquette de **reporting**, PAS une frontière de travail. Tu **NE demandes PAS un grain au coordinateur** : tu **tires** (ci-dessous), prends **n'importe quelle issue techniquement exécutable** (autre famille, autre langage, autre lane — **rien n'est « le turf d'un autre »**), poses `[CLAIMED] <#N> — ` (anti-double-claim), livres. **Conclure « rien à faire » alors que `gh issue list` renvoie >0 = échec de méthode.** Sur-te-spécialiser = te starver toi-même. - -**Filtre `candidate-delivered` (#10466)** : une part du pool est du travail **déjà livré mais non fermé** (`See #N` même en résolution complète → GitHub ne ferme pas). Le label, posé par le workflow advisory (PR merged référençant l'issue + silence post-merge, EPICs exclus), **signale sans fermer** — ai-01 tranche en lecture body (G.9). Ne pas re-piocher une issue ainsi labellisée sans vérification firsthand. - -**Le pool ne se scanne plus à la main — il se TIRE, et le tirage est le PREMIER GESTE DE CHAQUE CYCLE (HARD, mandats user 2026-08-14 et 2026-08-20).** `gh issue list` plafonne à **30 résultats** triés par récence : un scan manuel ne voit que le récent et referme la boucle de monoculture — la troncature demande un organe, pas plus de vigilance (mesures : [détail, section Picker](../../docs/reference/proactive-coordination-detail.md)). Un steering nommé du coordinateur est **un grain de plus dans la main de la lane**, jamais une condition de départ : on le brûle s'il est déjà là, on ne constate pas son absence. Son équilibrage entre familles et genres reste dû au coordinateur (§4 de [variation-protocol.md](variation-protocol.md)). - -```bash -python scripts/pick_idle_grain.py --belt --lane -``` - -**Le tapis roulant** (`--belt`, #18832, mandat user 2026-10-02) sert l'ouvert entier par **dernière visite** : la plus ancienne en tête, une issue servie repart en queue, ni pondération ni refus. Les urnes restent un **filtre** : **grain** (issue unitaire → la livrer) · **umbrella** (un EPIC → piocher ou **créer un sous-grain dedans**, jamais claimer l'EPIC entier) · **delivered** (une `candidate-delivered` → **réservée au coordinateur et à l'adjoint**, mandat user 2026-09-07 [#15069, garde `DELIVERED_URN_LANES` dans `pick_idle_grain.py`] : une lane worker qui en rencontre une poste `[INFO] candidate-delivered` avec sa preuve et rend la main). Le tirage pondéré historique (âge de création + délaissement : [détail, section Picker](../../docs/reference/proactive-coordination-detail.md)) ne sert plus qu'au diagnostic. - -**Sur un tirage d'urne `umbrella`, confronter le body au réel AVANT de juger l'actionnabilité (#11900).** Le tirage pondère le **délaissement** ; or plus une EPIC est ancienne, plus son body a de chances d'être faux — **plus le picker fait ce qu'on lui demande, plus il remonte d'impasses apparentes**. Deux tirages consécutifs ont remonté deux EPICs entièrement actionnables dont le paragraphe bloquant avait survécu à sa propre résolution (PR déjà mergée ; décision déjà prise par le user le jour même). Conclure « bloquée sur autrui » sans mesurer, c'est reposer un grain disponible et croire le pool saturé. Le **geste** obligatoire et ses trois organes (l'artefact sur `main`, le plateau `gh pr list --search head:`, le commentaire de fermeture) sont portés par [verify-before-claiming.md](verify-before-claiming.md) règle 5, auto-chargée ; le corpus des cas et la **précondition toxique** — une condition de reprise que *rien ne mesure* ne peut jamais être constatée atteinte, et diffère indéfiniment contre la volonté de qui l'a posée — vivent dans [picker-delaisse-detail.md](../../docs/reference/picker-delaisse-detail.md). - -**Le tapis ordonne, l'agent ne re-trie pas.** Il prend les candidats dans l'ordre et pose son `[CLAIMED]` (`check_lane_claim.py` avant d'**éditer**, cf [lane-claim-protocol.md](lane-claim-protocol.md)) ; un candidat sauté l'est pour une barrière réelle (GPU-only, vision-only, second LIGHT du même genre d'affilée selon G-VAR-3 de [variation-protocol.md](variation-protocol.md)), dite dans le rapport de cycle. Les exclusions factuelles (`--exclude-issue`, labels, `--urns`) restent disponibles. **Aucun résultat vide filtré ne justifie un `[ASK coordinator]` ni un statut idle.** - -**Sur un tirage d'urne `umbrella`, confronter le body au réel AVANT de juger l'actionnabilité (HARD, #11900).** Un body est daté de sa rédaction, jamais de sa lecture — et le picker pondère le **délaissement**, donc plus une EPIC est ancienne, plus son body a de chances d'être faux : **plus le picker fait son travail, plus il remonte d'impasses apparentes.** Le paragraphe bloquant y a souvent survécu à sa propre résolution (mesuré sur #2874 et #7357, dont les deux conditions étaient levées depuis 68 j et 31 j). Sans ce réflexe, le régime produit des rerolls en boucle et la lane conclut à tort que le pool est saturé — l'inverse exact de l'intention de la règle 5. - -Trois surfaces mentent vite, et **deux sont décidables mécaniquement** : - -1. **un numéro de contenu** (`#N`) → l'organe résout son type et son état. Une EPIC dont **tous** les enfants cités sont CLOSED n'a plus rien à piocher : le picker a offert un **conteneur**, pas un grain ; -2. **un chemin de fichier** → vérifié contre l'arbre de travail (`find`, **jamais** `ls` : un `ls` sur un chemin périmé rend vide, et « rien trouvé » y est indiscernable de « pas regardé ») ; -3. **une demande de décision** → **non automatisable** : `gh issue view N --json comments`, la réponse est souvent le premier commentaire. - -**Organe** (la règle ne tient pas par la vigilance) : - -```bash -python scripts/ci/check_umbrella_freshness.py [...] # exit 1 = SATURATED, ne pas piocher ici -``` - -`SATURATED` (tous les enfants cités sont fermés) est le **seul** verdict qui prend la sortie **1** — c'est la seule lecture non ambiguë. Un chemin cité introuvable sort en **avertissement**, jamais en rouge : il a pu légitimement déménager. `UNRESOLVED` (aucun enfant cité) reste **advisory** et renvoie à la surface 3. - -**Ce que l'organe ne dit pas** : il mesure la vacuité du **conteneur**, pas la viabilité du **grain**. Une EPIC `FRESH` peut n'avoir que du résiduel `INTRINSIC` — mesuré le 2026-09-15 : les 8 `sorry` distincts de `knot_lean`, que #1453 désigne comme « le grain DEEP de cet EPIC », portent chacun, dans le fichier, l'annotation qui les déclare hors d'atteinte (`decades` away / *not in Mathlib*). La lecture du contenu reste due. - -**Précondition toxique** : une condition de reprise que *rien ne mesure* ne peut jamais être constatée atteinte — elle diffère indéfiniment contre la volonté de qui l'a posée. La rendre observable est **plus petit** que le travail différé, et le débloque entièrement. - -**Réparation d'un body périmé** : préfixer d'un bloc `> Etat mesure au ` (« ce que le body dit » / « ce qui est mesuré », préconditions et leur état, ce qu'une lane peut faire), **conserver l'historique intact en dessous**, écrire la **portée** de ce qui n'a pas été vérifié. Jamais de réécriture en place : le lecteur suivant doit voir ce qui a changé. - -**Reparer son propre rouge, dans le meme cycle que la production (HARD, mandat user 2026-08-22 ; l'ordre retire le 2026-09-12).** Le picker rend la reparation (sortie **0** — le grain rendu *est* la reprise) tant que la lane porte une PR **bloquee et ouverte depuis plus de 24 h** ; il nomme la liste, la cause et le geste. Ce n'est **pas un prealable a produire** : au debut de chaque session, la lane inventorie **toutes** ses PRs portant nits, reserves, `CHANGES_REQUESTED` ou threads inline non resolus, puis traite sequentiellement **tous** les points reparables qui la concernent — jamais un seul nit ou une seule PR. Chaque remarque recoit une correction reelle si necessaire et une reponse ecrite qui la nomme ; les attentes externes (`CI`, `DWELL`, re-review, merge ou dependance) **sortent du champ de vision de la lane** — elles appartiennent a la candidate, qui attend seule, et une candidate n'attend jamais avec sa lane. La file reparable ne precede pas le tirage : la lane tire son grain DEEP de contenu dans le **meme** cycle, sequentiellement, tant que la fenetre reste ouverte. - -- **Une PR reparee et mergee compte dans le plancher R1** : ce n'est pas un a-cote, c'est du travail deja ecrit porte a son terme. Elle ne le tient pas **seule** — le plancher exige ≥1 DEEP de CONTENU, et un REPAIR est au mieux MED ([variation-protocol.md](variation-protocol.md), §Grain REPAIR) : il compte comme grain, jamais comme plancher. Une PR seulement corrigee mais encore en attente externe ne compte pas encore ; la lane continue donc a produire. Elle garde son tag `Grain:` d'origine — la reparation ne re-qualifie pas le tier. -- **Ce qui compte comme a reprendre en premiere action** est ce qui **empeche vraiment le merge**, en **quatre** causes : un check **requis** en echec (champ GraphQL `isRequired`, pas un motif de nom), un conflit avec `main`, un `CHANGES_REQUESTED` non leve, et — **en tete des trois autres** — un **point de review non leve**. Il est propose a chaque cycle et n'interdit jamais le grain suivant. Un advisory rouge n'est **pas** un rouge ; `mergeStateStatus: BLOCKED` vaut aussi « en attente de review », que la lane ne peut pas lever — une telle PR n'est donc **pas** un grain de reparation, et sort du champ de la lane jusqu'a ce qu'un geste de lane redevienne possible. -- **Les points de review passent en premier** car ils sont structurellement aveugles aux trois surfaces de [§B.0](../../CLAUDE.md) (nits du user en issue comments, réserves d'Hermes en préfixe de body, threads inline) : une PR peut être verte, sans conflit, sans `CHANGES_REQUESTED` — et rester non mergeable. Le picker délègue la détection à `check_unaddressed_nits.analyse`, **le même organe que le merge-gate** — s'ils divergeaient, une lane serait autorisée à produire du neuf sur une PR que le merge-gate refusera. La cause est listée d'abord parce que ce qui lève une remarque est **une phrase, pas un SHA**. -- **Organe injoignable ≠ ardoise propre** : quand la lecture des points échoue, le picker tire quand même mais le **dit** — vérifier à la main (`python scripts/check_unaddressed_nits.py `) avant de produire. -- **Rouge non réparable par cette lane** (garde cassée sur `main`, dépendance d'une autre PR) : l'**écrire en commentaire sur la PR**, puis `--ignore-red`. L'échappatoire se justifie par écrit, elle ne se prend pas en silence. -- **Portée écrite** : une PR sans tag `Grain:` lisible n'est imputable à aucune lane et reste invisible à ce garde. Son tag manquant est lui-même le défaut à corriger — c'est au coordinateur de les reprendre. - -6. **Variété obligatoire — le tarissement est structurellement interdit (HARD).** Les règles 1-5 interdisent l'idle ; celle-ci interdit la **monotonie**, et pose l'auto-alimentation comme **principe**, pas comme rattrapage du coordinateur. Une lane ne PEUT PAS se tarir : le worker pioche **de lui-même**, **varié**, même si le coordinateur est absent plusieurs cycles. - - **Substance en plat principal** : chaque cycle, viser un grain de fond (preuve Lean, backtest/training, série notebook, moteur SOTA, sécu/infra). - - **Nettoyage/doc = à-côté plafonné** (budget G-VAR-2, cf [variation-protocol.md](variation-protocol.md)) : nécessaire, jamais le plat unique. Une journée entière sur un seul registre monotone = sous-régime à corriger **de soi-même**. - - **Rotation genres ET familles** : alterner Lean / .NET / Python / QC / GenAI / docs ; le pool global rend la variété toujours accessible ; ne jamais tunneliser un mono-thème. - -7. **Never-idle ancré — le « forensic-floor » n'est PAS un livrable (HARD).** Les workers ont contourné 1-6 en inventant un **vocabulaire d'idle-honnête** qui *sonne* comme du travail. Parce qu'un nouveau synonyme est toujours inventable, l'autorité n'est **pas une liste de mots** mais un **test de résultat** : - - > **Test de fin de cycle.** Ai-je, ce cycle, sorti un grain de **substance** du pool global et l'ai-je transformé en PR (ou fait avancer un livrable multi-cycle) ? **Si non, et que `gh issue list` renvoie >0** — *quel que soit le label* que je m'apprête à poster — **c'est un échec de méthode, pas un cycle honnête.** Un scan forensic qui trouve 0 défaut, une `candidate-delivered`, une édition de body seule, un HOLD ou une attente mécanique ne sont **pas** des livrables : ce sont des préludes au pick suivant, jamais leur substitution. - - **Trois évasions mortes** (labels bannis : [détail, section Vocabulaire](../../docs/reference/proactive-coordination-detail.md)) : - - **« Pas ma famille »** — FAUX : famille = préférence de **reporting**, pas frontière. **Seules deux vraies barrières** : (a) **GPU-only** ; (b) **vision-only** (→ lanes MiniMax/ai-01). Le reste est piochable partout. - - **« Tout ce qui reste est gated »** — un *gate* qualifie une **prochaine action**, pas une issue entière : énumérer chaque issue ouverte + son gate précis ; si UNE a un sous-grain exécutable (doc, notebook CPU, audit, test), **le prendre**. - - **« Les micro-fixes suffisent »** — non : nettoyage/tooling/doc plafonnés, jamais le plat principal ; le plancher exige un **DEEP de CONTENU** chaque cycle, le MED et le META venant au-delà (tiers et genres : [variation-protocol.md](variation-protocol.md)). - - **Mécanisme never-empty (ordre strict, inversé le 2026-08-20).** (1) **Tirer** — `pick_idle_grain.py`, au démarrage du cycle, systématiquement. (2) Un **steering nommé** du coordinateur (deep-queue, DM `[DISPATCH→inbox]`) **s'ajoute** au tirage quand il est déjà là : le brûler dans l'ordre, ne pas attendre entre items, `[CLAIMED]` avant chaque. Son absence ne se constate pas. (3) Ni tirage exploitable ni steering → le pool global reste la source : « rien à faire » demeure **structurellement impossible** tant que `gh issue list` renvoie >0. La deep-queue est un **bootstrap éphémère**, jamais la condition du travail. - -8. **Veine plafonnée = interdit ciblé (HARD, #11343 tranche 2 ; amendé 2026-08-20).** Au-delà de `vein_cap=2` PRs citant la même umbrella dans la journée de la lane, la **PR suivante** DOIT appeler le picker avec la commande exposée par `variation_light_cap.py --check-pr N --genre-signals` (`picker_command` du JSON) pour **écarter l'umbrella saturée**. **Le plafond ne bloque PAS la tranche en cours** (« on ne jette pas du travail écrit », amendement ai-01 2026-08-16) : c'est la PR suivante qui est contrainte. Historique : [détail, section Veine plafonnée](../../docs/reference/proactive-coordination-detail.md). - -9. **L'absence d'instruction n'est pas un etat (HARD, mandat user 2026-09-12).** Aucun geste du coordinateur — steer, review, capture, arbitrage, merge, reponse a un DM — n'est un **prealable** a produire. Une lane qui n'a rien recu **tire**, et c'est son regime normal, pas un mode degrade. Trois formulations disent la meme faute et sont bannies comme statut de cycle : « j'attends la re-review », « j'attends le merge », « j'attends la decision ». Ce qui attend, c'est la **candidate** ; la lane, elle, est deja sur le grain suivant. - - **Ce que le coordinateur ne peut pas te demander** : un message de sa part qui te suspend sans te dire quoi faire a la place est un **defaut de sa part, pas une consigne** — tu le signales en une ligne et tu tires. Lui non plus n'a pas le droit de l'ecrire ([coordinator-discipline.md](coordinator-discipline.md) R0). - -## Leçons ancrées (checks pré-`[DONE]`) - -- **L721 ★ — stale-tracker guard.** Avant tout claim « 0 PR / saturated / idle » : interroger le **tag de lane** (`Grain:`), **PAS** `--author ` (compte `jsboige` partagé, #13870) ; la requête se valide par ses **faux négatifs symétriques**. Repli : PR sans tag lisible → sweep `GRAIN-ORPHANS-SWEEP` (#13086). Forme canonique + mesures : [détail, section L721](../../docs/reference/proactive-coordination-detail.md). -- **L740 ★ — CronList 7-day verify.** Les crons `CronCreate` sont **session-only, expirent à 7 j** : avant `[DONE]`, vérifier `CronList` et **re-armer** — un cron expiré = lane-idle silencieuse. -- **L898 ★★★ — collision guard : avant d'ÉCRIRE, pas avant de pousser.** Avant `[CLAIMED]`/steer/édit/push : `git worktree list` + `gh pr list --search head:` + `--search ""` + `--state open --json files` sur le **chemin** visé. Relire `main` ne remplace pas ce check (`main` ne montre pas l'*ouvert*). Coût ~10 s ; omission = travail dupliqué + rétractation publique. -- **L1356 ★★★ — preflight de claim : `--state all`, jamais open (incidents #13562/#13608).** Une PR **MERGÉE** ayant livré en rider (sans `Closes #N`) est invisible aux filtres open : avant tout `[CLAIMED]`, (1) `gh pr list --state all --search ""`, (2) inspecter les **worktrees orphelins** nommés pour l'issue. Si livré : `[INFO] candidate-delivered` avec preuve — pas de réimplémentation, pas de close soi-même. Recette complète : [détail, section L1356](../../docs/reference/proactive-coordination-detail.md). - -## Règle de sélection - -Prendre **un item à la fois** (`[CLAIMED]` avant), livrer, **re-piocher aussitôt** — G.5 interdit N deep-tracks **parallèles**, pas N PRs **séquentielles**. **Anti-pattern interdit** : auditer la tranche étroite de sa famille puis poster un `[ASK coordinator]` alors que le pool offrait des grains cross-lane — c'est le silo (incident fondateur R5/R7, [détail §Incident fondateur R5/R7](../../docs/reference/proactive-coordination-detail.md)). **Le coordinateur n'est PAS un distributeur de grains** : il merge, scoper les issues, déconflitter les claims. - -## Voir aussi - -- [docs/reference/proactive-coordination-detail.md](../../docs/reference/proactive-coordination-detail.md) — backlog 8 sources, mapping, cadence, anti-patterns, incidents, leçons complètes, picker, veine -- [variation-protocol.md](variation-protocol.md) — **opérationnalise R6/R7** : tag `Grain:` + 3 gates + merge-gate + provisionnement -- [coordinator-discipline.md](coordinator-discipline.md) — ai-01 merge actif, no languishing -- [docs/reference/subagents-reference.md](../../docs/reference/subagents-reference.md) — roster spécialistes +# Coordination proactive — plusieurs grains/wakeup plancher + pool global + never-idle + +S'applique à **tous les workers du cluster CoursIA** (po-2023/2024/2025/2026) et au **coordinateur ai-01**. Source : mandat user 2026-05-23, durci 2026-06-30 → 2026-07-19. + +**Détail (backlog 8 sources, cadence, anti-patterns, incidents, leçons L721/L1356 complètes, picker, veine)** : [docs/reference/proactive-coordination-detail.md](../../docs/reference/proactive-coordination-detail.md). + +## Règles HARD + +0. **Avant de TIRER un grain neuf : faire avancer tout ce qui peut avancer (HARD, mandat user 2026-09-19).** L'ordre de session est ferme — **(a)** toutes tes PRs ouvertes qui peuvent bouger : réserve à lever, nit à traiter, rebase, dossier `[ADJOINT PREFLIGHT]` à refabriquer, check rouge à réparer ; **(b) ensuite seulement** le tirage du picker. Cette règle est **en amont** de la règle 5 : elle ne remplace pas le tirage, elle le conditionne. + + **Pourquoi un ORDRE, et pas seulement un plancher.** La règle 1 compte les grains *livrés* — un compte qu'on satisfait en empilant du neuf par-dessus du non-fini. Mesure et verbatim user (2026-09-19) : [détail, section R0](../../docs/reference/proactive-coordination-detail.md). + + **Trier par COÛT DE DOSSIER, pas par ancienneté seule.** Une PR **CLEAN et sans review** ne demande qu'une attestation tierce : c'est le grain le moins cher du dépôt. Une BLOCKED demande un diagnostic. L'ancienneté ordonne **à l'intérieur** du sous-ensemble le moins cher ; elle ne le choisit pas. + + **N PRs portant la MÊME classe de défaut = une cause, pas N grains.** Traiter la cause une fois plutôt que d'ouvrir N allers-retours. + + **Un script exécuté sur la branche remplace plusieurs allers-retours de commentaires.** Quand le défaut est **mécanique** — fixer canonique existant, geste déterministe, diff vérifiable — l'exécuter sur la branche de la PR plutôt que poster un commentaire qui coûte une session entière à son porteur. Quatre bornes, toutes requises : la PR est à **lane unique** (un `[CLAIMED]` vivant d'une autre lane renvoie au commentaire, cf [lane-claim-protocol.md](lane-claim-protocol.md)) · le commentaire ou le dossier **nomme le script et le commit** (un commit muet sur la branche d'autrui est indiscernable d'un détournement) · ça **ne lève aucune réserve de tiers** (B.0 : une phrase lève, pas un SHA) · si une cellule source de notebook est touchée, **la re-exécution redevient due** (C.2). + +1. **≥2 grains livrés entre 2 wakeups, dont ≥1 DEEP portant un genre de CONTENU = PLANCHER, jamais plafond.** Une PR livrée ne **clôt pas** la session : re-pioche **immédiatement** et enchaîne autant de PRs que la fenêtre le permet (débit nominal ~2 PR/h). S'arrêter après 1 PR alors qu'il reste du temps **et** 60+ issues ouvertes = **sous-régime**, pas « cycle terminé ». Le plancher est **pluriel et durci** (mandat user 2026-09-12) : un cycle dont le plat principal est MED ou META **n'a pas de plancher tenu**, même avec plusieurs PR livrées. La mesure qui motive ce durcissement — 15 % de DEEP sur 7 j, et sur 48 h le META passant devant le CONTENU — est déposée datée dans [détail, section Plancher durci](../../docs/reference/proactive-coordination-detail.md) ; le tier et le genre se tranchent en [variation-protocol.md](variation-protocol.md) (G-VAR-1). +2. **2 tracks en flight minimum** : une **track principale** (mission dispatchée, sinon la tête du tapis) + une **side-track autonome** que le worker avance **même si le coordinateur s'absente 1-2 jours**. +3. **Side-tracks → sous-agents spécialistes async (HARD).** Quand un specialist `.claude/agents/` couvre la side-track, la déléguer en **`run_in_background: true`** pendant que le worker interactif tient la main track. Roster : [docs/reference/subagents-reference.md](../../docs/reference/subagents-reference.md). +4. **Backlog pickup au wakeup vide (HARD).** Sans nouveau feedback / directive / tâche : **ne pas s'arrêter** — piocher dans le backlog et produire la PR du cycle. + +5. **Pool = TOUT l'ouvert, cross-lane, jamais siloté (HARD).** Cette règle **domine 1-4** : le backlog n'est PAS ta « famille » ni ta « lane », c'est **`gh issue list --state open` en entier**. Ta lane (`machine × workspace`) est une étiquette de **reporting**, PAS une frontière de travail. Tu **NE demandes PAS un grain au coordinateur** : tu **tires** (ci-dessous), prends **n'importe quelle issue techniquement exécutable** (autre famille, autre langage, autre lane — **rien n'est « le turf d'un autre »**), poses `[CLAIMED] <#N> — ` (anti-double-claim), livres. **Conclure « rien à faire » alors que `gh issue list` renvoie >0 = échec de méthode.** Sur-te-spécialiser = te starver toi-même. + +**Filtre `candidate-delivered` (#10466)** : une part du pool est du travail **déjà livré mais non fermé** (`See #N` même en résolution complète → GitHub ne ferme pas). Le label, posé par le workflow advisory (PR merged référençant l'issue + silence post-merge, EPICs exclus), **signale sans fermer** — ai-01 tranche en lecture body (G.9). Ne pas re-piocher une issue ainsi labellisée sans vérification firsthand. + +**Le pool ne se scanne plus à la main — il se TIRE, et le tirage est le PREMIER GESTE DE CHAQUE CYCLE (HARD, mandats user 2026-08-14 et 2026-08-20).** `gh issue list` plafonne à **30 résultats** triés par récence : un scan manuel ne voit que le récent et referme la boucle de monoculture — la troncature demande un organe, pas plus de vigilance (mesures : [détail, section Picker](../../docs/reference/proactive-coordination-detail.md)). Un steering nommé du coordinateur est **un grain de plus dans la main de la lane**, jamais une condition de départ : on le brûle s'il est déjà là, on ne constate pas son absence. Son équilibrage entre familles et genres reste dû au coordinateur (§4 de [variation-protocol.md](variation-protocol.md)). + +```bash +python scripts/pick_idle_grain.py --belt --lane +``` + +**Le tapis roulant** (`--belt`, #18832, mandat user 2026-10-02) sert l'ouvert entier par **dernière visite** : la plus ancienne en tête, une issue servie repart en queue, ni pondération ni refus. Les urnes restent un **filtre** : **grain** (issue unitaire → la livrer) · **umbrella** (un EPIC → piocher ou **créer un sous-grain dedans**, jamais claimer l'EPIC entier) · **delivered** (une `candidate-delivered` → **réservée au coordinateur et à l'adjoint**, mandat user 2026-09-07 [#15069, garde `DELIVERED_URN_LANES` dans `pick_idle_grain.py`] : une lane worker qui en rencontre une poste `[INFO] candidate-delivered` avec sa preuve et rend la main). Le tirage pondéré historique (âge de création + délaissement : [détail, section Picker](../../docs/reference/proactive-coordination-detail.md)) ne sert plus qu'au diagnostic. + +**Le tapis ordonne, l'agent ne re-trie pas.** Il prend les candidats dans l'ordre et pose son `[CLAIMED]` (`check_lane_claim.py` avant d'**éditer**, cf [lane-claim-protocol.md](lane-claim-protocol.md)) ; un candidat sauté l'est pour une barrière réelle (GPU-only, vision-only, second LIGHT du même genre d'affilée selon G-VAR-3 de [variation-protocol.md](variation-protocol.md)), dite dans le rapport de cycle. Les exclusions factuelles (`--exclude-issue`, labels, `--urns`) restent disponibles. **Aucun résultat vide filtré ne justifie un `[ASK coordinator]` ni un statut idle.** + +**Sur un tirage d'urne `umbrella`, confronter le body au réel AVANT de juger l'actionnabilité (HARD, #11900).** Un body est daté de sa rédaction, jamais de sa lecture — et le picker pondère le **délaissement**, donc plus une EPIC est ancienne, plus son body a de chances d'être faux : **plus le picker fait son travail, plus il remonte d'impasses apparentes.** Le paragraphe bloquant y a souvent survécu à sa propre résolution. Sans ce réflexe, le régime produit des rerolls en boucle et la lane conclut à tort que le pool est saturé — l'inverse exact de l'intention de la règle 5. Le **geste** obligatoire et ses trois organes (l'artefact sur `main`, le plateau `gh pr list --search head:`, le commentaire de fermeture) sont portés par [verify-before-claiming.md](verify-before-claiming.md) règle 5, auto-chargée ; le corpus des cas (mesure 2026-08-20) vit dans [picker-delaisse-detail.md](../../docs/reference/picker-delaisse-detail.md), avec la **précondition toxique** (ci-dessous). + +Trois surfaces mentent vite, et **deux sont décidables mécaniquement** : + +1. **un numéro de contenu** (`#N`) → l'organe résout son type et son état. Une EPIC dont **tous** les enfants cités sont CLOSED n'a plus rien à piocher : le picker a offert un **conteneur**, pas un grain ; +2. **un chemin de fichier** → vérifié contre l'arbre de travail (`find`, **jamais** `ls` : un `ls` sur un chemin périmé rend vide, et « rien trouvé » y est indiscernable de « pas regardé ») ; +3. **une demande de décision** → **non automatisable** : `gh issue view N --json comments`, la réponse est souvent le premier commentaire. + +**Organe** (la règle ne tient pas par la vigilance) : + +```bash +python scripts/ci/check_umbrella_freshness.py [...] # exit 1 = SATURATED, ne pas piocher ici +``` + +`SATURATED` (tous les enfants cités sont fermés) est le **seul** verdict qui prend la sortie **1** — c'est la seule lecture non ambiguë. Un chemin cité introuvable sort en **avertissement**, jamais en rouge : il a pu légitimement déménager. `UNRESOLVED` (aucun enfant cité) reste **advisory** et renvoie à la surface 3. + +**Ce que l'organe ne dit pas** : il mesure la vacuité du **conteneur**, pas la viabilité du **grain**. Une EPIC `FRESH` peut n'avoir que du résiduel `INTRINSIC` — la lecture du contenu reste due (cas mesuré : [détail, section organe d'EPIC](../../docs/reference/proactive-coordination-detail.md)). + +**Précondition toxique** : une condition de reprise que *rien ne mesure* ne peut jamais être constatée atteinte — elle diffère indéfiniment contre la volonté de qui l'a posée. La rendre observable est **plus petit** que le travail différé, et le débloque entièrement. + +**Réparation d'un body périmé** : préfixer d'un bloc `> Etat mesure au ` (« ce que le body dit » / « ce qui est mesuré », préconditions et leur état, ce qu'une lane peut faire), **conserver l'historique intact en dessous**, écrire la **portée** de ce qui n'a pas été vérifié. Jamais de réécriture en place : le lecteur suivant doit voir ce qui a changé. + +**Reparer son propre rouge, dans le meme cycle que la production (HARD, mandat user 2026-08-22 ; l'ordre retire le 2026-09-12).** Le picker rend la reparation (sortie **0** — le grain rendu *est* la reprise) tant que la lane porte une PR **bloquee et ouverte depuis plus de 24 h** ; il nomme la liste, la cause et le geste. Ce n'est **pas un prealable a produire** : au debut de chaque session, la lane inventorie **toutes** ses PRs portant nits, reserves, `CHANGES_REQUESTED` ou threads inline non resolus, puis traite sequentiellement **tous** les points reparables qui la concernent — jamais un seul nit ou une seule PR. Chaque remarque recoit une correction reelle si necessaire et une reponse ecrite qui la nomme ; les attentes externes (`CI`, `DWELL`, re-review, merge ou dependance) **sortent du champ de vision de la lane** — elles appartiennent a la candidate, qui attend seule, et une candidate n'attend jamais avec sa lane. La file reparable ne precede pas le tirage : la lane tire son grain DEEP de contenu dans le **meme** cycle, sequentiellement, tant que la fenetre reste ouverte. + +- **Une PR reparee et mergee compte dans le plancher R1** : ce n'est pas un a-cote, c'est du travail deja ecrit porte a son terme. Elle ne le tient pas **seule** — le plancher exige ≥1 DEEP de CONTENU, et un REPAIR est au mieux MED ([variation-protocol.md](variation-protocol.md), §Grain REPAIR) : il compte comme grain, jamais comme plancher. Une PR seulement corrigee mais encore en attente externe ne compte pas encore ; la lane continue donc a produire. Elle garde son tag `Grain:` d'origine — la reparation ne re-qualifie pas le tier. +- **Ce qui compte comme a reprendre en premiere action** est ce qui **empeche vraiment le merge**, en **quatre** causes : un check **requis** en echec (champ GraphQL `isRequired`, pas un motif de nom), un conflit avec `main`, un `CHANGES_REQUESTED` non leve, et — **en tete des trois autres** — un **point de review non leve**. Il est propose a chaque cycle et n'interdit jamais le grain suivant. Un advisory rouge n'est **pas** un rouge ; `mergeStateStatus: BLOCKED` vaut aussi « en attente de review », que la lane ne peut pas lever — une telle PR n'est donc **pas** un grain de reparation, et sort du champ de la lane jusqu'a ce qu'un geste de lane redevienne possible. +- **Les points de review passent en premier** car ils sont structurellement aveugles aux trois surfaces de [§B.0](../../CLAUDE.md) (nits du user en issue comments, réserves d'Hermes en préfixe de body, threads inline) : une PR peut être verte, sans conflit, sans `CHANGES_REQUESTED` — et rester non mergeable. Le picker délègue la détection à `check_unaddressed_nits.analyse`, **le même organe que le merge-gate** — s'ils divergeaient, une lane serait autorisée à produire du neuf sur une PR que le merge-gate refusera. La cause est listée d'abord parce que ce qui lève une remarque est **une phrase, pas un SHA**. +- **Organe injoignable ≠ ardoise propre** : quand la lecture des points échoue, le picker tire quand même mais le **dit** — vérifier à la main (`python scripts/check_unaddressed_nits.py `) avant de produire. +- **Rouge non réparable par cette lane** (garde cassée sur `main`, dépendance d'une autre PR) : l'**écrire en commentaire sur la PR**, puis `--ignore-red`. L'échappatoire se justifie par écrit, elle ne se prend pas en silence. +- **Portée écrite** : une PR sans tag `Grain:` lisible n'est imputable à aucune lane et reste invisible à ce garde. Son tag manquant est lui-même le défaut à corriger — c'est au coordinateur de les reprendre. + +6. **Variété obligatoire — le tarissement est structurellement interdit (HARD).** Les règles 1-5 interdisent l'idle ; celle-ci interdit la **monotonie**, et pose l'auto-alimentation comme **principe**, pas comme rattrapage du coordinateur. Une lane ne PEUT PAS se tarir : le worker pioche **de lui-même**, **varié**, même si le coordinateur est absent plusieurs cycles. + - **Substance en plat principal** : chaque cycle, viser un grain de fond (preuve Lean, backtest/training, série notebook, moteur SOTA, sécu/infra). + - **Nettoyage/doc = à-côté plafonné** (budget G-VAR-2, cf [variation-protocol.md](variation-protocol.md)) : nécessaire, jamais le plat unique. Une journée entière sur un seul registre monotone = sous-régime à corriger **de soi-même**. + - **Rotation genres ET familles** : alterner Lean / .NET / Python / QC / GenAI / docs ; le pool global rend la variété toujours accessible ; ne jamais tunneliser un mono-thème. + +7. **Never-idle ancré — le « forensic-floor » n'est PAS un livrable (HARD).** Les workers ont contourné 1-6 en inventant un **vocabulaire d'idle-honnête** qui *sonne* comme du travail. Parce qu'un nouveau synonyme est toujours inventable, l'autorité n'est **pas une liste de mots** mais un **test de résultat** : + + > **Test de fin de cycle.** Ai-je, ce cycle, sorti un grain de **substance** du pool global et l'ai-je transformé en PR (ou fait avancer un livrable multi-cycle) ? **Si non, et que `gh issue list` renvoie >0** — *quel que soit le label* que je m'apprête à poster — **c'est un échec de méthode, pas un cycle honnête.** Un scan forensic qui trouve 0 défaut, une `candidate-delivered`, une édition de body seule, un HOLD ou une attente mécanique ne sont **pas** des livrables : ce sont des préludes au pick suivant, jamais leur substitution. + + **Trois évasions mortes** (labels bannis : [détail, section Vocabulaire](../../docs/reference/proactive-coordination-detail.md)) : + - **« Pas ma famille »** — FAUX : famille = préférence de **reporting**, pas frontière. **Seules deux vraies barrières** : (a) **GPU-only** ; (b) **vision-only** (→ lanes MiniMax/ai-01). Le reste est piochable partout. + - **« Tout ce qui reste est gated »** — un *gate* qualifie une **prochaine action**, pas une issue entière : énumérer chaque issue ouverte + son gate précis ; si UNE a un sous-grain exécutable (doc, notebook CPU, audit, test), **le prendre**. + - **« Les micro-fixes suffisent »** — non : nettoyage/tooling/doc plafonnés, jamais le plat principal ; le plancher exige un **DEEP de CONTENU** chaque cycle, le MED et le META venant au-delà (tiers et genres : [variation-protocol.md](variation-protocol.md)). + + **Mécanisme never-empty (ordre strict, inversé le 2026-08-20).** (1) **Tirer** — `pick_idle_grain.py`, au démarrage du cycle, systématiquement. (2) Un **steering nommé** du coordinateur (deep-queue, DM `[DISPATCH→inbox]`) **s'ajoute** au tirage quand il est déjà là : le brûler dans l'ordre, ne pas attendre entre items, `[CLAIMED]` avant chaque. Son absence ne se constate pas. (3) Ni tirage exploitable ni steering → le pool global reste la source : « rien à faire » demeure **structurellement impossible** tant que `gh issue list` renvoie >0. La deep-queue est un **bootstrap éphémère**, jamais la condition du travail. + +8. **Veine plafonnée = interdit ciblé (HARD, #11343 tranche 2 ; amendé 2026-08-20).** Au-delà de `vein_cap=2` PRs citant la même umbrella dans la journée de la lane, la **PR suivante** DOIT appeler le picker avec la commande exposée par `variation_light_cap.py --check-pr N --genre-signals` (`picker_command` du JSON) pour **écarter l'umbrella saturée**. **Le plafond ne bloque PAS la tranche en cours** (« on ne jette pas du travail écrit », amendement ai-01 2026-08-16) : c'est la PR suivante qui est contrainte. Historique : [détail, section Veine plafonnée](../../docs/reference/proactive-coordination-detail.md). + +9. **L'absence d'instruction n'est pas un etat (HARD, mandat user 2026-09-12).** Aucun geste du coordinateur — steer, review, capture, arbitrage, merge, reponse a un DM — n'est un **prealable** a produire. Une lane qui n'a rien recu **tire**, et c'est son regime normal, pas un mode degrade. Trois formulations disent la meme faute et sont bannies comme statut de cycle : « j'attends la re-review », « j'attends le merge », « j'attends la decision ». Ce qui attend, c'est la **candidate** ; la lane, elle, est deja sur le grain suivant. + + **Ce que le coordinateur ne peut pas te demander** : un message de sa part qui te suspend sans te dire quoi faire a la place est un **defaut de sa part, pas une consigne** — tu le signales en une ligne et tu tires. Lui non plus n'a pas le droit de l'ecrire ([coordinator-discipline.md](coordinator-discipline.md) R0). + +## Leçons ancrées (checks pré-`[DONE]`) + +- **L721 ★ — stale-tracker guard.** Avant tout claim « 0 PR / saturated / idle » : interroger le **tag de lane** (`Grain:`), **PAS** `--author ` (compte `jsboige` partagé, #13870) ; la requête se valide par ses **faux négatifs symétriques**. Repli : PR sans tag lisible → sweep `GRAIN-ORPHANS-SWEEP` (#13086). Forme canonique + mesures : [détail, section L721](../../docs/reference/proactive-coordination-detail.md). +- **L740 ★ — CronList 7-day verify.** Les crons `CronCreate` sont **session-only, expirent à 7 j** : avant `[DONE]`, vérifier `CronList` et **re-armer** — un cron expiré = lane-idle silencieuse. +- **L898 ★★★ — collision guard : avant d'ÉCRIRE, pas avant de pousser.** Avant `[CLAIMED]`/steer/édit/push : `git worktree list` + `gh pr list --search head:` + `--search ""` + `--state open --json files` sur le **chemin** visé. Relire `main` ne remplace pas ce check (`main` ne montre pas l'*ouvert*). Coût ~10 s ; omission = travail dupliqué + rétractation publique. +- **L1356 ★★★ — preflight de claim : `--state all`, jamais open (incidents #13562/#13608).** Une PR **MERGÉE** ayant livré en rider (sans `Closes #N`) est invisible aux filtres open : avant tout `[CLAIMED]`, (1) `gh pr list --state all --search ""`, (2) inspecter les **worktrees orphelins** nommés pour l'issue. Si livré : `[INFO] candidate-delivered` avec preuve — pas de réimplémentation, pas de close soi-même. Recette complète : [détail, section L1356](../../docs/reference/proactive-coordination-detail.md). + +## Règle de sélection + +Prendre **un item à la fois** (`[CLAIMED]` avant), livrer, **re-piocher aussitôt** — G.5 interdit N deep-tracks **parallèles**, pas N PRs **séquentielles**. **Anti-pattern interdit** : auditer la tranche étroite de sa famille puis poster un `[ASK coordinator]` alors que le pool offrait des grains cross-lane — c'est le silo (incident fondateur R5/R7, [détail §Incident fondateur R5/R7](../../docs/reference/proactive-coordination-detail.md)). **Le coordinateur n'est PAS un distributeur de grains** : il merge, scoper les issues, déconflitter les claims. + +## Voir aussi + +- [docs/reference/proactive-coordination-detail.md](../../docs/reference/proactive-coordination-detail.md) — backlog 8 sources, mapping, cadence, anti-patterns, incidents, leçons complètes, picker, veine +- [variation-protocol.md](variation-protocol.md) — **opérationnalise R6/R7** : tag `Grain:` + 3 gates + merge-gate + provisionnement +- [coordinator-discipline.md](coordinator-discipline.md) — ai-01 merge actif, no languishing +- [docs/reference/subagents-reference.md](../../docs/reference/subagents-reference.md) — roster spécialistes diff --git a/.claude/rules/variation-protocol.md b/.claude/rules/variation-protocol.md index 8d796c3c45..635223ad2f 100644 --- a/.claude/rules/variation-protocol.md +++ b/.claude/rules/variation-protocol.md @@ -1,120 +1,120 @@ -# Protocole de variation — anti-monoculture, tag déclaré + merge-gate coordinateur - -S'applique à **tous les workers** (`po-*`) **et au coordinateur `ai-01`**. Source : mandat user 2026-07-21. Les concepts (tiers DEEP/MED/LIGHT, rotation des genres, never-idle) vivent dans [proactive-coordination.md](proactive-coordination.md) R6/R7 — la monoculture a persisté parce qu'ils étaient auto-évalués et invisibles ; ce fichier ajoute la mécanique qui les fait mordre : **tag auditable**, **merge-gate**, **obligation de provisionnement**. - -**Détail (justifications mesurées, incidents fondateurs, verbatims)** : [docs/reference/variation-protocol-detail.md](../../docs/reference/variation-protocol-detail.md). - -## 1. Le tag de grain (HARD) - -Tout `[CLAIMED]` **et** tout body de PR portent en **première ligne** : - -``` -Grain: / — lane — prev: / # -``` - -Ex. `Grain: DEEP/lean — lane myia-po-2026:CoursIA — prev: LIGHT/guard #8954`. - -`prev:` documente le grain précédent de la lane (adjacence G-VAR-3) et le lie à une PR relisable — le numéro rend la déclaration vérifiable ; les deux sont obligatoires. **Le genre déclaré dans `prev:` n'est PAS la clé d'adjacence** : l'organe [`scripts/ci/variation_adjacency_guard.py`](../../scripts/ci/variation_adjacency_guard.py) lit la **séquence mergée** de la lane et rend le genre déclaré dans un champ séparé (`declared_prev_genre`) ; le `prev:` n'est source de vérité qu'en repli. Ne pas dériver l'adjacence à la main depuis les `prev:` déclarés — deux HOLD faux en ont résulté le 2026-09-11. Mécanisme : [détail §2.1](../../docs/reference/variation-protocol-detail.md). - -Le guard ([`variation-tag-guard.yml`](../../.github/workflows/variation-tag-guard.yml)) matche par **mot-clé** (`Grain:`, `lane`), casse insensible, décoration markdown neutralisée : la **substance** seule est vérifiée (TIER par le litmus, GENRE dans l'énumération, `lane` présente). Pas de churn cosmétique sur un tag valide en substance. - -### TIER — test objectif, pas auto-évaluation - -| TIER | Litmus décisif | Exemples | -|---|---|---| -| **DEEP** | `main` contient-il désormais un **résultat/capacité qui n'existait pas**, dont la production a demandé du **raisonnement de domaine** ? | sorry Lean retiré + `lake build SUCCESS` · backtest/training avec verdict multi-seed · nouveau notebook exécuté (≥3 exos, outputs réels) · moteur SOTA branché (verdict SOTA-OK) · module de recherche à résultat falsifiable | -| **MED** | Étend de la substance existante **avec ré-exécution/vérification**, et **change quelque chose** (pas « 0 trouvé ») | enrichissement + ré-exec · audit borné dont le finding **change une décision** · exercice ajouté + exécuté · refactor avec tests verts · audit README fichier-entier corrigeant un drift structurel | -| **LIGHT** | **« Pourrais-je en générer une douzaine en scannant l'instance suivante ? »** → si oui : LIGHT, quel que soit le label | guard-tranche · path-fix · doc-resync · ledger append · accent/leak/FP · propagation de marqueur | - -Le litmus LIGHT est le **cœur anti-gaming** : guards, resyncs, ledger-entries, accents le passent tous. - -### GENRE — énumération CLOSE - -`lean` · `qc` · `training` · `genai` · `notebook-python` · `notebook-dotnet` · `notebook-lean` · `slides` · `docs` · `guard` · `refactor` · `ledger` · `readme` · `test` · `tooling` · `research-code`. - -**L'énumération se partitionne en deux, et la frontière porte G-VAR-1 :** - -| Classe | Genres | Ce qu'un grain y produit | -|---|---|---| -| **CONTENU** | `lean` · `qc` · `training` · `genai` · `notebook-python` · `notebook-dotnet` · `notebook-lean` · `slides` · `research-code` | une capacité, une preuve, un résultat, du matériel pédagogique — ce que le dépôt existe pour offrir | -| **META** | `guard` · `tooling` · `ledger` · `docs` · `readme` · `test` · `refactor` | l'outillage, les garde-fous et la prose *autour* du contenu — nécessaire, jamais suffisant | - -Un genre META n'est pas inférieur — un guard qui rougit au bon moment vaut mieux qu'un notebook de plus — mais une flotte qui ne produit que du META construit un atelier sans rien y fabriquer (mesure fondatrice : [détail §11](../../docs/reference/variation-protocol-detail.md)). - -**`slides` reste CONTENU** quand le grain écrit ou enrichit le contenu du deck (12 slides de cours neuves = CONTENU, pas `docs`) ; un grain slides qui fait autre chose garde son genre de type de travail — `guard` pour un gate CI de build Slidev, `refactor` pour un script, `tooling` pour un convertisseur. - -Un genre hors liste est un **alias** que le merge-gate normalise : pas une violation, le worker n'est ni repris ni HOLD. **Le GENRE est le TYPE DE TRAVAIL, jamais la famille où vivent les fichiers** (test : *si le prochain grain tombait dans une autre famille, changerais-je le GENRE ?*) : le composé `-` (`lean-ci`, `cjk-ci`, `audit-tooling`) **se réduit toujours à sa tête** ; les synonymes (`test-coverage` → `test`, `documentation` → `docs`, `data` → `ledger`, `slidev` → `slides`) se normalisent. Table complète : [détail §Alias](../../docs/reference/variation-protocol-detail.md). - -**`guard` vs `tooling` — le discriminant est « est-ce que ça peut rougir ».** Un check susceptible de passer au rouge est `guard` ; un script/helper/convertisseur sans statut d'échec propre est `tooling`. - -**Entrer dans la liste LIGHT de G-VAR-3 se mesure, jamais s'intuitionne** : un genre y entre dès **≥ 2 grains LIGHT mergés**. - -### Grain REPAIR — hérite du genre de la PR qu'il répare - -| PR réparée | Tag REPAIR | Genre | Plancher G-VAR-1 | -|---|---|---|---| -| `notebook-python` (ex #12141) | `MED/notebook-python` | **CONTENU** | grain de contenu **au-delà** du plancher | -| `lean` (ex #12252) | `MED/notebook-lean` | **CONTENU** | grain de contenu **au-delà** du plancher | -| `guard` (ex #11997) | `MED/guard` | **META** | ne tient **pas** le plancher, comme toute PR META | - -Le raisonnement : G-VAR-1 demande « qu'est-ce qui atteint `main` quand ce travail aboutit ? » — la réponse regarde ce qui **arrive sur `main`**, pas ce que le REPAIR a fait. **Cas négatif explicite** : un REPAIR d'une PR META reste META — une lane qui ne réparerait que ses PRs de tooling/docs ne tiendrait toujours pas G-VAR-1. **Forme** : le genre hérité est déclaré directement, sans annotation spéciale (le REPAIR se lit dans le titre `fix(` et le diff) ; une annotation ajoute du bruit sans information. **Tier** : litmus habituel — un REPAIR à ré-exécution complète + diagnostic de ratchet est `MED` ; un REPAIR d'une ligne de body reste `LIGHT` et consomme le budget G-VAR-2 (pas d'exception). Raisonnement complet et arbitrage #11815 : [détail §13](../../docs/reference/variation-protocol-detail.md). - -## 2. Les trois gates durs - -- **G-VAR-1 — Plat principal DEEP, dans un genre de CONTENU.** La PR-plancher du cycle (R1 de proactive-coordination) **DOIT** être DEEP **et** porter un genre de la classe CONTENU. **Une LIGHT ne satisfait JAMAIS le plancher ; un genre META non plus, quel que soit son tier ; un MED non plus.** Le MED et le META restent **bienvenus au-delà** du plancher (sous budget G-VAR-2 pour leurs composantes LIGHT) : ce qui est exigé, c'est qu'**au moins un** grain DEEP de contenu porte le cycle. Le pool global porte toujours du DEEP de contenu : la monoculture vient du choix du plus facile *disponible*, pas d'une absence de substance. - - **Durcissement #15793 (2026-09-12)** : le plancher passe de « DEEP **ou MED** » à **DEEP** — mesure motrice (15 % de DEEP sur 7 j, META devant CONTENU sur 48 h) et contre-poids anti-inflation (`TIER-INFLATION` + re-qualification par le merge-gate, §3) : [détail §12](../../docs/reference/variation-protocol-detail.md). Le litmus DEEP reste objectif. Le durcissement se paie en **lecture de tags par ai-01**, jamais en confiance. - - **La sécheresse se mesure — [`scripts/pick_idle_grain.py`](../../scripts/pick_idle_grain.py) — elle ne s'auto-évalue pas (#13086).** Le picker compte les **merges consécutifs sans genre CONTENU** de la lane et, au seuil (3 par défaut), **restreint le tirage aux genres CONTENU** : la lane reçoit un grain, et ce grain tient le plancher. L'échappatoire `--ignore-drought` (capability GPU-only/vision-only) **se justifie par écrit** ; une capability **se mesure sur les lanes sœurs du même modèle**, elle ne se déclare pas — tant qu'une lane sœur livre du contenu, « ma capability exclut le contenu » est réfuté. - -- **G-VAR-2 — Budget LIGHT proportionnel : `max(1, grains_mergés_du_jour // 3)`**, par lane et par jour, **toutes catégories LIGHT confondues**. Une lane à 1-5 grains garde le plafond d'une LIGHT ; à 6 elle en a deux, à 19 elle en a six. Au-delà : la LIGHT attend demain ou cède la place à du DEEP/MED. Le budget se **calcule** — [`scripts/variation_light_cap.py`](../../scripts/variation_light_cap.py) — il ne se déclare pas. - - Note d'arbitrage (#11154) : les PRs `DEFECT-ALIVE` (dette #11044) **consomment le budget LIGHT**, avec exception écrite + mesure de la dette résiduelle à chaque merge au cap. - -- **G-VAR-3 — Pas deux fois le même GENRE LIGHT consécutif.** Sur les genres LIGHT (`guard` · `ledger` · `docs` · `readme` · `test`) : bloqué dès 2 grains consécutifs de même genre, **sauf** exception mécanique. Les genres de CONTENU ne relèvent pas de G-VAR-3 — un spécialiste Lean qui enchaîne deux preuves DEEP distinctes n'est **pas** la monoculture visée. - - **Exception MED/DEEP mesurable (#14357)** : deux grains consécutifs d'un même genre LIGHT sont autorisés **ssi** (i) le **second** est MED ou DEEP et (ii) les deux PRs ne partagent **aucun fichier**. Le critère est mécanique — rendu par l'organe (`variation_light_cap.py`, clé `exempt_runs`) et consommé par le gate (`variation_adjacency_guard.py`, verdict `exempted`), jamais jugé à la main ; un run de ≥3 ne s'exempte que si **chaque paire adjacente** satisfait (i) et (ii) ; ce que l'organe ne peut pas lire ne s'exempte pas — fail-CLOSED. Tell décisif : le litmus LIGHT — générable en scannant l'instance d'à-côté → bloqué **même sous une étiquette DEEP**. - -## 3. Merge-gate coordinateur (ai-01) — les dents (HARD) - -Le protocole ne mord que si `ai-01` cesse de merger passivement. À chaque passe, pour chaque PR, **lire le tag** et croiser avec les grains récents de la lane : - -| Constat | Action | -|---|---| -| LIGHT d'une lane à budget épuisé (G-VAR-2) | **HOLD** : citer la sortie de `variation_light_cap.py` (`N` LIGHT pour `M` grains), pas une estimation | -| 2ᵉ même-GENRE consécutif (G-VAR-3) | **HOLD**, sauf exception mécanique #14357 rendue par l'organe (`exempt_runs`) | -| Plancher tenu par une LIGHT (G-VAR-1) | steer vers un grain **DEEP de contenu** du pool, **nommé** | -| Plancher tenu par un **MED** ou un genre **META**, même tagué DEEP (G-VAR-1) | le cycle n'a pas de plancher : merger la PR si elle est bonne, **et** nommer dans le même geste le grain DEEP de contenu qui portera le cycle suivant. Ne **pas** HOLD une PR META saine — c'est le **provisionnement** qui a manqué (§4) | -| Tag mal dérivé (tier sur-coté, genre pris sur la famille, alias/composé) | **re-qualifier le tag soi-même**, puis traiter selon le tag corrigé | -| `lane` absente | **HOLD** jusqu'à déclaration — un grain sans lane est **structurellement incomptable** | - -**Le tag déclaré n'est pas auto-exécutoire** : il rend le grain auditable, il ne le définit pas. Merger sans lire le tag laisse le protocole s'auto-certifier. - -**Ne jamais tenir une LIGHT plus d'une journée** : un hold prolongé fait réécrire le même travail par une autre lane. Passé 24 h : merger, ou fermer **en nommant le remplaçant**. - -Le HOLD est **attaché à la candidate**, jamais à la cadence de sa lane. Il ne sanctionne jamais l'idle ([coordinator-discipline.md](coordinator-discipline.md) R0/R4) et ne bloque jamais un nouveau grain **DEEP de contenu** : toujours accompagné d'un grain nommé du pool, en **double canal** (DM inbox + `[DISPATCH→inbox]` dashboard). HOLD sans remplacement, ou HOLD pour réduire les dispatchs, = échec coordinateur. - -## 4. Provisionnement d'exception — ce qui lie ai-01 (HARD) - -La cause racine est **autant** un défaut de provisionnement qu'un réflexe de facilité worker. Le provisionnement est une **exception nommée** : le coordinateur provisionne **quand il a une raison nommée** (sécheresse mesurée, dette de batch-close, veine à équilibrer), pas par quota de lane ni à chaque cycle — un quota par lane à tous les cycles rendrait le steering structurellement toujours premier sur le tirage, ce que la règle 5 de [proactive-coordination.md](proactive-coordination.md) interdit. Quand il provisionne, `ai-01` : - -0. **Le tirage est la voie par défaut** : une lane sans steering **tire** et n'attend rien. Un steering nommé s'ajoute au tirage quand il est déjà là (mécanisme never-empty, règle 7) — et doit être le plus équilibré possible : un steering qui répète le genre du cycle précédent fait pire que le tirage. -1. **Provisionne sur sa raison nommée**, **groundé firsthand** (`gh issue view`), varié en genre d'une lane à l'autre. -2. **Agrège les GENRES des merges récents avant de provisionner**, pas seulement leurs tiers — le tirage ne voit pas l'historique de genres de la flotte ([détail §9](../../docs/reference/variation-protocol-detail.md)). -3. **Un batch-close de famille crée une dette de provisionnement**, à honorer dans le même cycle. -4. **Varie la loterie** — le coordinateur applique G-VAR-3 à son propre dispatch. -5. **Dissocie admission et production** : les candidates en HOLD, `DWELL`, review ou attente de merge ne diminuent jamais le provisionnement dû. La queue d'admission se résorbe par une piste de digestion parallèle ; elle n'applique aucune backpressure globale aux producteurs. - -Sous-provisionner puis merger la monoculture qui en résulte est **le** manquement que ce protocole corrige. Ralentir la production pour accommoder la digestion en est un autre. - -## 5. Auto-détection - -Avant de claim / de merger : **« ce grain est-il générable-en-série (LIGHT) ET (budget épuisé OU même-genre-que-le-précédent) ? »** Si oui, c'est la monoculture — le worker pioche un **DEEP de contenu**, le coordinateur HOLD+redirige. - -Et la question que le tier seul ne posait pas, en fin de cycle : **« qu'est-ce que ce cycle a ajouté au dépôt qu'un lecteur ou un étudiant puisse utiliser ? »** Si la seule réponse honnête est « un détecteur de plus, un guard de plus, une doc de plus », le plancher n'est pas tenu — quel que soit le nombre de PR mergées. Côté worker : piocher un grain de contenu. Côté coordinateur : c'est un défaut de provisionnement (§4), pas une faute de lane. - -## Voir aussi - -- [docs/reference/variation-protocol-detail.md](../../docs/reference/variation-protocol-detail.md) — justifications mesurées, incidents #8056 / #8961, historique des applications -- [proactive-coordination.md](proactive-coordination.md) — R1, R5 (pool global), R6/R7 (variété, never-idle) -- [coordinator-discipline.md](coordinator-discipline.md) — R4 (jamais sanctionner l'idle), R5 (steer qui ATTEINT/VRAI/DÉCIDE) +# Protocole de variation — anti-monoculture, tag déclaré + merge-gate coordinateur + +S'applique à **tous les workers** (`po-*`) **et au coordinateur `ai-01`**. Source : mandat user 2026-07-21. Les concepts (tiers DEEP/MED/LIGHT, rotation des genres, never-idle) vivent dans [proactive-coordination.md](proactive-coordination.md) R6/R7 — la monoculture a persisté parce qu'ils étaient auto-évalués et invisibles ; ce fichier ajoute la mécanique qui les fait mordre : **tag auditable**, **merge-gate**, **obligation de provisionnement**. + +**Détail (justifications mesurées, incidents fondateurs, verbatims)** : [docs/reference/variation-protocol-detail.md](../../docs/reference/variation-protocol-detail.md). + +## 1. Le tag de grain (HARD) + +Tout `[CLAIMED]` **et** tout body de PR portent en **première ligne** : + +``` +Grain: / — lane — prev: / # +``` + +Ex. `Grain: DEEP/lean — lane myia-po-2026:CoursIA — prev: LIGHT/guard #8954`. + +`prev:` documente le grain précédent de la lane (adjacence G-VAR-3) et le lie à une PR relisable — le numéro rend la déclaration vérifiable ; les deux sont obligatoires. **Le genre déclaré dans `prev:` n'est PAS la clé d'adjacence** : l'organe [`scripts/ci/variation_adjacency_guard.py`](../../scripts/ci/variation_adjacency_guard.py) lit la **séquence mergée** de la lane et rend le genre déclaré dans un champ séparé (`declared_prev_genre`) ; le `prev:` n'est source de vérité qu'en repli. Ne pas dériver l'adjacence à la main depuis les `prev:` déclarés — deux HOLD faux en ont résulté le 2026-09-11. Mécanisme : [détail §2.1](../../docs/reference/variation-protocol-detail.md). + +Le guard ([`variation-tag-guard.yml`](../../.github/workflows/variation-tag-guard.yml)) matche par **mot-clé** (`Grain:`, `lane`), casse insensible, décoration markdown neutralisée : la **substance** seule est vérifiée (TIER par le litmus, GENRE dans l'énumération, `lane` présente). Pas de churn cosmétique sur un tag valide en substance. + +### TIER — test objectif, pas auto-évaluation + +| TIER | Litmus décisif | Exemples | +|---|---|---| +| **DEEP** | `main` contient-il désormais un **résultat/capacité qui n'existait pas**, dont la production a demandé du **raisonnement de domaine** ? | sorry Lean retiré + `lake build SUCCESS` · backtest/training avec verdict multi-seed · nouveau notebook exécuté (≥3 exos, outputs réels) · moteur SOTA branché (verdict SOTA-OK) · module de recherche à résultat falsifiable | +| **MED** | Étend de la substance existante **avec ré-exécution/vérification**, et **change quelque chose** (pas « 0 trouvé ») | enrichissement + ré-exec · audit borné dont le finding **change une décision** · exercice ajouté + exécuté · refactor avec tests verts · audit README fichier-entier corrigeant un drift structurel | +| **LIGHT** | **« Pourrais-je en générer une douzaine en scannant l'instance suivante ? »** → si oui : LIGHT, quel que soit le label | guard-tranche · path-fix · doc-resync · ledger append · accent/leak/FP · propagation de marqueur | + +Le litmus LIGHT est le **cœur anti-gaming** : guards, resyncs, ledger-entries, accents le passent tous. + +### GENRE — énumération CLOSE + +`lean` · `qc` · `training` · `genai` · `notebook-python` · `notebook-dotnet` · `notebook-lean` · `slides` · `docs` · `guard` · `refactor` · `ledger` · `readme` · `test` · `tooling` · `research-code`. + +**L'énumération se partitionne en deux, et la frontière porte G-VAR-1 :** + +| Classe | Genres | Ce qu'un grain y produit | +|---|---|---| +| **CONTENU** | `lean` · `qc` · `training` · `genai` · `notebook-python` · `notebook-dotnet` · `notebook-lean` · `slides` · `research-code` | une capacité, une preuve, un résultat, du matériel pédagogique — ce que le dépôt existe pour offrir | +| **META** | `guard` · `tooling` · `ledger` · `docs` · `readme` · `test` · `refactor` | l'outillage, les garde-fous et la prose *autour* du contenu — nécessaire, jamais suffisant | + +Un genre META n'est pas inférieur — un guard qui rougit au bon moment vaut mieux qu'un notebook de plus — mais une flotte qui ne produit que du META construit un atelier sans rien y fabriquer (mesure fondatrice : [détail §11](../../docs/reference/variation-protocol-detail.md)). + +**`slides` reste CONTENU** quand le grain écrit ou enrichit le contenu du deck (12 slides de cours neuves = CONTENU, pas `docs`) ; un grain slides qui fait autre chose garde son genre de type de travail — `guard` pour un gate CI de build Slidev, `refactor` pour un script, `tooling` pour un convertisseur. + +Un genre hors liste est un **alias** que le merge-gate normalise : pas une violation, le worker n'est ni repris ni HOLD. **Le GENRE est le TYPE DE TRAVAIL, jamais la famille où vivent les fichiers** (test : *si le prochain grain tombait dans une autre famille, changerais-je le GENRE ?*) : le composé `-` (`lean-ci`, `cjk-ci`, `audit-tooling`) **se réduit toujours à sa tête** ; les synonymes (`test-coverage` → `test`, `documentation` → `docs`, `data` → `ledger`, `slidev` → `slides`) se normalisent. Table complète : [détail §Alias](../../docs/reference/variation-protocol-detail.md). + +**`guard` vs `tooling` — le discriminant est « est-ce que ça peut rougir ».** Un check susceptible de passer au rouge est `guard` ; un script/helper/convertisseur sans statut d'échec propre est `tooling`. + +**Entrer dans la liste LIGHT de G-VAR-3 se mesure, jamais s'intuitionne** : un genre y entre dès **≥ 2 grains LIGHT mergés**. + +### Grain REPAIR — hérite du genre de la PR qu'il répare + +| PR réparée | Tag REPAIR | Genre | Plancher G-VAR-1 | +|---|---|---|---| +| `notebook-python` (ex #12141) | `MED/notebook-python` | **CONTENU** | grain de contenu **au-delà** du plancher | +| `lean` (ex #12252) | `MED/notebook-lean` | **CONTENU** | grain de contenu **au-delà** du plancher | +| `guard` (ex #11997) | `MED/guard` | **META** | ne tient **pas** le plancher, comme toute PR META | + +Le raisonnement : G-VAR-1 demande « qu'est-ce qui atteint `main` quand ce travail aboutit ? » — la réponse regarde ce qui **arrive sur `main`**, pas ce que le REPAIR a fait. **Cas négatif explicite** : un REPAIR d'une PR META reste META — une lane qui ne réparerait que ses PRs de tooling/docs ne tiendrait toujours pas G-VAR-1. **Forme** : le genre hérité est déclaré directement, sans annotation spéciale (le REPAIR se lit dans le titre `fix(` et le diff) ; une annotation ajoute du bruit sans information. **Tier** : litmus habituel — un REPAIR à ré-exécution complète + diagnostic de ratchet est `MED` ; un REPAIR d'une ligne de body reste `LIGHT` et consomme le budget G-VAR-2 (pas d'exception). Raisonnement complet et arbitrage #11815 : [détail §13](../../docs/reference/variation-protocol-detail.md). + +## 2. Les trois gates durs + +- **G-VAR-1 — Plat principal DEEP, dans un genre de CONTENU.** La PR-plancher du cycle (R1 de proactive-coordination) **DOIT** être DEEP **et** porter un genre de la classe CONTENU. **Une LIGHT ne satisfait JAMAIS le plancher ; un genre META non plus, quel que soit son tier ; un MED non plus.** Le MED et le META restent **bienvenus au-delà** du plancher (sous budget G-VAR-2 pour leurs composantes LIGHT) : ce qui est exigé, c'est qu'**au moins un** grain DEEP de contenu porte le cycle. Le pool global porte toujours du DEEP de contenu : la monoculture vient du choix du plus facile *disponible*, pas d'une absence de substance. + + **Durcissement #15793 (2026-09-12)** : le plancher est passé de « DEEP **ou MED** » à **DEEP** ; le litmus DEEP reste objectif, et le durcissement se paie en **lecture de tags par ai-01**, jamais en confiance — mesure motrice et contre-poids anti-inflation (`TIER-INFLATION` + re-qualification par le merge-gate, §3) : [détail §12](../../docs/reference/variation-protocol-detail.md). + + **La sécheresse se mesure — [`scripts/pick_idle_grain.py`](../../scripts/pick_idle_grain.py) — elle ne s'auto-évalue pas (#13086).** Le picker compte les **merges consécutifs sans genre CONTENU** de la lane et, au seuil (3 par défaut), **restreint le tirage aux genres CONTENU** : la lane reçoit un grain, et ce grain tient le plancher. L'échappatoire `--ignore-drought` (capability GPU-only/vision-only) **se justifie par écrit** ; une capability **se mesure sur les lanes sœurs du même modèle**, elle ne se déclare pas — tant qu'une lane sœur livre du contenu, « ma capability exclut le contenu » est réfuté. + +- **G-VAR-2 — Budget LIGHT proportionnel : `max(1, grains_mergés_du_jour // 3)`**, par lane et par jour, **toutes catégories LIGHT confondues**. Une lane à 1-5 grains garde le plafond d'une LIGHT ; à 6 elle en a deux, à 19 elle en a six. Au-delà : la LIGHT attend demain ou cède la place à du DEEP/MED. Le budget se **calcule** — [`scripts/variation_light_cap.py`](../../scripts/variation_light_cap.py) — il ne se déclare pas. + + Note d'arbitrage (#11154) : les PRs `DEFECT-ALIVE` (dette #11044) **consomment le budget LIGHT**, avec exception écrite + mesure de la dette résiduelle à chaque merge au cap. + +- **G-VAR-3 — Pas deux fois le même GENRE LIGHT consécutif.** Sur les genres LIGHT (`guard` · `ledger` · `docs` · `readme` · `test`) : bloqué dès 2 grains consécutifs de même genre, **sauf** exception mécanique. Les genres de CONTENU ne relèvent pas de G-VAR-3 — un spécialiste Lean qui enchaîne deux preuves DEEP distinctes n'est **pas** la monoculture visée. + + **Exception MED/DEEP mesurable (#14357)** : deux grains consécutifs d'un même genre LIGHT sont autorisés **ssi** (i) le **second** est MED ou DEEP et (ii) les deux PRs ne partagent **aucun fichier**. Le critère est mécanique — rendu par l'organe (`variation_light_cap.py`, clé `exempt_runs`) et consommé par le gate (`variation_adjacency_guard.py`, verdict `exempted`), jamais jugé à la main ; un run de ≥3 ne s'exempte que si **chaque paire adjacente** satisfait (i) et (ii) ; ce que l'organe ne peut pas lire ne s'exempte pas — fail-CLOSED. Tell décisif : le litmus LIGHT — générable en scannant l'instance d'à-côté → bloqué **même sous une étiquette DEEP**. + +## 3. Merge-gate coordinateur (ai-01) — les dents (HARD) + +Le protocole ne mord que si `ai-01` cesse de merger passivement. À chaque passe, pour chaque PR, **lire le tag** et croiser avec les grains récents de la lane : + +| Constat | Action | +|---|---| +| LIGHT d'une lane à budget épuisé (G-VAR-2) | **HOLD** : citer la sortie de `variation_light_cap.py` (`N` LIGHT pour `M` grains), pas une estimation | +| 2ᵉ même-GENRE consécutif (G-VAR-3) | **HOLD**, sauf exception mécanique #14357 rendue par l'organe (`exempt_runs`) | +| Plancher tenu par une LIGHT (G-VAR-1) | steer vers un grain **DEEP de contenu** du pool, **nommé** | +| Plancher tenu par un **MED** ou un genre **META**, même tagué DEEP (G-VAR-1) | le cycle n'a pas de plancher : merger la PR si elle est bonne, **et** nommer dans le même geste le grain DEEP de contenu qui portera le cycle suivant. Ne **pas** HOLD une PR META saine — c'est le **provisionnement** qui a manqué (§4) | +| Tag mal dérivé (tier sur-coté, genre pris sur la famille, alias/composé) | **re-qualifier le tag soi-même**, puis traiter selon le tag corrigé | +| `lane` absente | **HOLD** jusqu'à déclaration — un grain sans lane est **structurellement incomptable** | + +**Le tag déclaré n'est pas auto-exécutoire** : il rend le grain auditable, il ne le définit pas. Merger sans lire le tag laisse le protocole s'auto-certifier. + +**Ne jamais tenir une LIGHT plus d'une journée** : un hold prolongé fait réécrire le même travail par une autre lane. Passé 24 h : merger, ou fermer **en nommant le remplaçant**. + +Le HOLD est **attaché à la candidate**, jamais à la cadence de sa lane. Il ne sanctionne jamais l'idle ([coordinator-discipline.md](coordinator-discipline.md) R0/R4) et ne bloque jamais un nouveau grain **DEEP de contenu** : toujours accompagné d'un grain nommé du pool, en **double canal** (DM inbox + `[DISPATCH→inbox]` dashboard). HOLD sans remplacement, ou HOLD pour réduire les dispatchs, = échec coordinateur. + +## 4. Provisionnement d'exception — ce qui lie ai-01 (HARD) + +La cause racine est **autant** un défaut de provisionnement qu'un réflexe de facilité worker. Le provisionnement est une **exception nommée** : le coordinateur provisionne **quand il a une raison nommée** (sécheresse mesurée, dette de batch-close, veine à équilibrer), pas par quota de lane ni à chaque cycle — un quota par lane à tous les cycles rendrait le steering structurellement toujours premier sur le tirage, ce que la règle 5 de [proactive-coordination.md](proactive-coordination.md) interdit. Quand il provisionne, `ai-01` : + +0. **Le tirage est la voie par défaut** : une lane sans steering **tire** et n'attend rien. Un steering nommé s'ajoute au tirage quand il est déjà là (mécanisme never-empty, règle 7) — et doit être le plus équilibré possible : un steering qui répète le genre du cycle précédent fait pire que le tirage. +1. **Provisionne sur sa raison nommée**, **groundé firsthand** (`gh issue view`), varié en genre d'une lane à l'autre. +2. **Agrège les GENRES des merges récents avant de provisionner**, pas seulement leurs tiers — le tirage ne voit pas l'historique de genres de la flotte ([détail §9](../../docs/reference/variation-protocol-detail.md)). +3. **Un batch-close de famille crée une dette de provisionnement**, à honorer dans le même cycle. +4. **Varie la loterie** — le coordinateur applique G-VAR-3 à son propre dispatch. +5. **Dissocie admission et production** : les candidates en HOLD, `DWELL`, review ou attente de merge ne diminuent jamais le provisionnement dû. La queue d'admission se résorbe par une piste de digestion parallèle ; elle n'applique aucune backpressure globale aux producteurs. + +Sous-provisionner puis merger la monoculture qui en résulte est **le** manquement que ce protocole corrige. Ralentir la production pour accommoder la digestion en est un autre. + +## 5. Auto-détection + +Avant de claim / de merger : **« ce grain est-il générable-en-série (LIGHT) ET (budget épuisé OU même-genre-que-le-précédent) ? »** Si oui, c'est la monoculture — le worker pioche un **DEEP de contenu**, le coordinateur HOLD+redirige. + +Et la question que le tier seul ne posait pas, en fin de cycle : **« qu'est-ce que ce cycle a ajouté au dépôt qu'un lecteur ou un étudiant puisse utiliser ? »** Si la seule réponse honnête est « un détecteur de plus, un guard de plus, une doc de plus », le plancher n'est pas tenu — quel que soit le nombre de PR mergées. Côté worker : piocher un grain de contenu. Côté coordinateur : c'est un défaut de provisionnement (§4), pas une faute de lane. + +## Voir aussi + +- [docs/reference/variation-protocol-detail.md](../../docs/reference/variation-protocol-detail.md) — justifications mesurées, incidents #8056 / #8961, historique des applications +- [proactive-coordination.md](proactive-coordination.md) — R1, R5 (pool global), R6/R7 (variété, never-idle) +- [coordinator-discipline.md](coordinator-discipline.md) — R4 (jamais sanctionner l'idle), R5 (steer qui ATTEINT/VRAI/DÉCIDE) diff --git a/docs/reference/proactive-coordination-detail.md b/docs/reference/proactive-coordination-detail.md index 6eb8110168..6f16475340 100644 --- a/docs/reference/proactive-coordination-detail.md +++ b/docs/reference/proactive-coordination-detail.md @@ -1,219 +1,235 @@ -# Coordination proactive — detail - -Detail de [.claude/rules/proactive-coordination.md](../../.claude/rules/proactive-coordination.md). Voir aussi [subagents-reference.md](subagents-reference.md), [scripts-reference.md](scripts-reference.md). - -## Plancher durci — « plusieurs grains dont un DEEP de CONTENU » (mandat user 2026-09-12, #15793) - -R1 de [proactive-coordination.md](../../.claude/rules/proactive-coordination.md) et G-VAR-1 de [variation-protocol.md](../../.claude/rules/variation-protocol.md) portent un plancher **pluriel et durci** : **≥ 2 grains livrés entre 2 wakeups, dont ≥ 1 DEEP dans un genre de CONTENU**. Le MED et le META restent **bienvenus au-delà** du plancher — ils ne le tiennent plus. - -**Ce qui le motive — mesure du 2026-09-12T16:39Z**, attribution par le tag `Grain:` (jamais `--author` : l'identité de poussée `jsboige` est partagée, L721) : - -| Fenêtre | merges | DEEP | MED | LIGHT | CONTENU | META | -|---|---:|---:|---:|---:|---:|---:| -| 7 jours (depuis 09-05) | **583** (83,3/j) | 88 (**15 %**) | 400 | 88 | 314 | 260 | -| 48 heures | **196** (98,0/j) | 35 (**18 %**) | 113 | 45 | **82** | **109** | - -Le volume n'est pas le défaut. Deux choses le sont : (1) la **dureté** — 15 % de DEEP sur 7 j, le plancher R1 d'alors (« ≥ 1 PR ») étant tenu par n'importe quoi, massivement par du MED ; (2) la **bascule CONTENU/META** — sur 7 j le CONTENU tient encore (314 contre 260), sur 48 h le META passe devant (109 contre 82) : la flotte construit l'atelier plus vite qu'elle n'y fabrique. - -Le contraste par lane rend le mécanisme lisible : la lane au plus gros volume sur 7 j (`myia-po-2023:CoursIA`, 108 merges) est la plus faible en substance (39 CONTENU, **8 DEEP**), tandis que `myia-po-2027:CoursIA-2` rend 32 CONTENU et 10 DEEP sur 41 merges. La lane la plus META de la flotte est celle du coordinateur (`myia-ai-01:CoursIA` : 38 merges sur 7 j pour **1 DEEP** et 7 CONTENU — 0 DEEP et 1 CONTENU sur 48 h). - -**Contre-poids anti-inflation, nommé et non inventé.** Exiger un DEEP crée une incitation à **sur-coter le tier**. Ce qui la couvre est déjà en place : le signal bot `TIER-INFLATION`, et le merge-gate qui **re-qualifie lui-même un tag mal dérivé** (§3 de [variation-protocol.md](../../.claude/rules/variation-protocol.md), ligne « Tag mal dérivé »). Le litmus DEEP reste objectif — *`main` contient-il désormais un résultat ou une capacité qui n'existait pas, dont la production a demandé du raisonnement de domaine ?* Le durcissement se paie en **lecture de tags par ai-01**, jamais en confiance. - -**Ce durcissement normatif substantiel exigeait un sign-off user** conformément à CLAUDE.md §A : la PR #15793 qui l'a porté ne se self-merge pas. Les corrections non normatives suivent la même PR/review, sans sign-off supplémentaire. - -## Backlog pickup — sources autorisees (ordre de priorite decroissant) - -Wakeup sans dispatch neuf : prendre la **premiere source non-vide** et la consommer comme une **file sequentielle de grains atomiques**. Chaque grain garde sa PR propre ; une PR livree ne termine pas la session et la lane poursuit tant que sa fenetre est active. - -| # | Source | Action | Pourquoi cet ordre | -|---|--------|--------|---------------------| -| 1 | Side-track Epic dispatchée propre au worker (cf mapping) | Avancer ses sous-taches atomiques dans l'ordre | Engagement deja pris | -| 2 | Ses propres PRs OPEN en attente review/iter | Drainer feedback, conflits et CI propres avant production neuve | Pas laisser pourrir | -| 3 | Issues `priority-high` non assignees dans son domaine | Self-assign + livraisons atomiques sequentielles | Urgences declarees | -| 4 | PRs languishing > 3 jours sans reponse (siennes ou domaine) | Re-iter ou cloturer, puis poursuivre | Hygiene cluster | -| 5 | Issues `priority-medium` du domaine | Self-assign + livraisons atomiques sequentielles | Travail cataloguable | -| 6 | Low-priority Epics stockees (#1646 Grothendieck, #1647 Conway Phase 2, #1650 Translation, #1651 Conway Phase 3 FWT) | Une PR atomique par phase/pilier, plusieurs a la suite | Stock activable "fenetres calmes" | -| 7 | Audit findings non traites (NanoClaw #488 etc.) | Reassessment puis PR si CONFIRMED, et continuer | Backlog d'enquete | -| 8 | Consolidation/documentation de son domaine | PRs atomiques, sans plafond d'une par session | Wiki Karpathy / SDDD documentaire | - -## Anti-patterns wakeup vide (interdits) - -- "Rien de nouveau, je me rendors" → **REFUSE** ; pioche backlog avant tout ScheduleWakeup -- "J'attends le prochain dispatch coord" → **REFUSE** ; le coord n'est pas un distributeur flux tendu -- Fusionner plusieurs sujets dans une PR « pour faire du chiffre » → **REFUSE** ; plusieurs grains se livrent sequentiellement, chacun dans sa PR atomique -- Annoncer "BLOCKED" alors que sources 5-8 non explorees → **REFUSE** (G.7 stagnation cross-cycle = escalade) - -## Reporting wakeup vide - -```text -[INFO] Wakeup R vide (pas de feedback/dispatch nouveau). -Backlog pickup : source <#X> "" → item # "". -PR cible : # en cours. -``` - -Pas de `[BLOCKED]` ni `[DONE pas de travail]`. Wakeup vide = opportunite backlog, pas blocage. - -## Pourquoi il n'y a pas de mapping machine → track - -Ce fichier a porté jusqu'au 2026-09-12 un tableau « Mapping machine → track principale + side-track (**cycle courant**) ». Le « (cycle courant) » de son titre était le défaut : un fichier versionné ne peut pas porter l'état d'un cycle, et celui-là ne l'a pas porté. Sur ses **7 assignations numérotées, 5 pointaient une issue CLOSED** — #1409 (fermée le 2026-07-26), #1273 (2026-06-02), #1385 (2026-05-26), #1455 (2026-06-11), #999 (2026-05-23) ; trois des quatre lignes worker avaient une track principale fermée, et po-2023 comme po-2025 avaient **les deux colonnes** fermées. - -Un steer qui pointe une issue CLOSED est un **phantom** au sens de R5 de [coordinator-discipline.md](../../.claude/rules/coordinator-discipline.md) : le worker brûle son cycle à le réfuter au lieu de produire. Le tableau en fabriquait cinq, en permanence, depuis un fichier auto-référencé par le harnais. - -**Ce qui le remplace est le tirage**, déjà la voie par défaut depuis le mandat du 2026-08-20 (R5 de [proactive-coordination.md](../../.claude/rules/proactive-coordination.md)) : `python scripts/pick_idle_grain.py --lane --prev-genre `. Le steering nommé du coordinateur reste l'exception qui passe devant — mais il se **grounde firsthand au moment où il est posé** (`gh issue view N`), ce qu'un tableau écrit une fois ne peut structurellement pas faire. Le roster des spécialistes async, lui, n'a jamais eu besoin d'être partitionné par machine : il vit dans [subagents-reference.md](subagents-reference.md), et R3 dit de déléguer la side-track au specialist qui couvre le sujet, quelle que soit la lane. - -Cette section existe pour que le tableau ne soit pas **re-inventé** : son absence est un choix mesuré, pas un oubli de maintenance. - -## Cadence - -- ScheduleWakeup horaire (~3540-3555s jitter) re-arme a chaque tour coord/worker interactif (ping-pong) — cf CLAUDE.md global "Multi-Machine Ping-Pong". -- Le sous-agent async tourne **pendant** le sommeil entre wakeups : au reveil, recuperation notification + integre/PR. - -## Anti-patterns cycle normal (interdits) - -- Worker avec une seule track (BG seul, ou main seule sans side-track) → demander/prendre 2e track immediatement -- Refaire a la main une tache couverte par un specialist async (modernisation serie, batch notebooks, QC robustness…) -- Side-track sans Epic de rattachement (intracable, meurt a l'absence du coordinateur) -- Lancer 2 sous-agents editeurs sur le **meme** notebook/serie (collision) — read-only OK en parallele, editeurs = un par fichier - -## Vérification pré-cycle (L282 / L283 / L786 / L915) — leçons post-c.264 - -Consolidees depuis les incidents cross-machine c.264 / c.288-289 / c.797 / c.796 et le PR feedback upstream. **Dures** : à executer AVANT tout claim, pas après. - -**L282 — Mirror-lanes collision**. Une machine avec 2 lanes (CoursIA **et** CoursIA-2) = 2 workers distincts. Avant `gh issue claim` ou poster `[CLAIMED]`, verifie **le dashboard de la lane ciblee** : un doublon mirroir (incident fondateur #5640/#5641 sur #5635, 6 min apart, ai-01 arbitrage 13:51Z) est une **perte de cycle pour les deux workers** + overhead coordinateur. `roosync_dashboard(action:"read", type:"workspace", section:"all")` sur **les 2 dashboards** (workspace-CoursIA + workspace-CoursIA-2) avant de claim. - -**L283 — Confiance hierarchique = `gh API > git log > dashboard intercom > status condense`**. Avant de propager un claim d'etat (merge / OPEN / contenu main) : `gh pr view N --json state,mergedAt,mergeCommit` + `git ls-tree origin/main ` + `git log origin/main --oneline -- `. Le **status condense auto-92%** du dashboard peut **halluciner** un merge (po-2026 c.288 : "#5657 MERGED by jsboige" alors que PR OPEN CLEAN awaiting sweep). Source : condensation LLM hallucine des PRs sur 4 cycles consecutifs. - -**L786 — Honest-drain diagnostic AVANT claim "lane exhausted"**. Avant de poster un statut terminal-idle (forensic-floor, drained, saturé, no-change, loophole) : `gh issue list --state open | wc -l` > 0 ne suffit pas. **Identifier le grain executable pour ta capability** (CPU-only-po et lecture OK, GPU-only exclu) — test : pour chaque issue ouverte, peux-tu (a) la lire, (b) la decomposer en sous-grain non-GPU, (c) executer le sous-grain ? Si ≥1 sous-grain executable → claim, sinon re-arme. Incident : 4 lanes "drained" simultanement c.796, pool offrait ≥6 grains CPU frais non-claimés (cf c.797 dispatch). - -**L915 — PR OPEN MERGEABLE ≠ PR mergee**. Avant de claim un cycle N dependant d'une PR OPEN MERGEABLE upstream, verifie `gh pr view N --json mergedAt` (non-null = mergee). 2 options : (a) reporter c.N a `mergedAt` confirme ; (b) redécouper le grain en **substrate standalone** + c.N+1 (le livrable de N est comprehensible sans merger l'upstream). Source : worker-side claim triple phantom-dispatach (c.715 #7225, c.797 #8031, c.792 #7733) corrige par L715-L2. - -**C715-L2 — Cross-repo work detection AVANT claim**. Avant de prendre un grain sur issue #X, triple-verification : -``` -git log --all --oneline --grep "#" # 0 commit upstream -git ls-tree origin/main # 0 fichier upstream -gh pr list --search "#" --state all # 0 PR antérieure -``` -3 zeros = grain libre. ≥1 hit = verifier l'etat de l'upstream (`gh pr view N --json mergedAt`) et choisir (a) reporter, (b) redécouper, (c) rafraichir la substance. - -**C715-L2 — Incident fondateur #8835/#8836 (po-2024, 2026-07-29)**. Deux écarts au triple-check ci-dessus ont laissé passer un doublon **déjà mergé** : (a) recherche `--state open` au lieu de `--state all` — la PR #8836 (mergée 14:03Z par jsboige) était invisible à toute recherche de PRs ouverts ; (b) recherche par **mots-clés** du titre (`"GradeBook is_absolute"`) au lieu du **numéro d'issue** (`"#8835"`) — le titre de #8836 (« top-tree rule form-invariant ») ne matchait pas les mots-clés choisis. Le correctif identique (même règle `_classify`, mêmes 2 fichiers, mêmes 5 critères d'acceptation) fut livré ~30 min après → PR #8838 fermée comme doublon CONFLICTING. **Le triple-check canonique l'aurait attrappé** : `gh pr list --search "#8835" --state all` retourne #8836 (son tag scope `notebook-tools,#8835` matche), et `git log --all --grep "#8835"` montre `6446bd7bd`. Leçon : avant de pousser un fix sur une issue, le triple-check C715-L2 est **non-négociable**, la recherche se fait par **numéro d'issue** (pas par mots-clés), et `--state all` (pas `open`) — un doublon mergé est le cas de collision le plus facile à manquer parce qu'il n'a aucune PR ouverte à voir. - -**Routine verification first-and** (à coller dans `~/.claude/CLAUDE.md` personalisation ou alias bash) : - -```bash -# Avant tout claim de PR dependante d'une upstream -gh pr view N --json state,mergedAt,baseRefOid 2>/dev/null | head -5 -git merge-base --is-ancestor origin/main && echo "MERGED" || echo "NOT-MERGED" -``` - ---- - -## Vocabulaire d'idle-honnête — les labels bannis (R7, mandat user 2026-07-19) - -Les règles R1-R6 interdisent l'idle ; les workers l'ont contournée en inventant un vocabulaire qui *sonne* comme du travail. Liste constatée, **bannie comme état terminal de cycle** : - -`CLEAN_DONE 0-PR` · `0 grain frais` · `pool/lane exhausted/saturée` · `forensic-floor` · `drained-confirm` · `due-diligence contribution` · `NO-CHANGE-NEEDED honnête` · `Nᵉ cycle honnête` · `loophole applicable` · `rien à faire` · `en attente de dispatch`. - -Cette liste est **indicative, jamais l'autorité** : un worker peut toujours inventer un nouveau synonyme. L'autorité est le **test de résultat** de R7 (`.claude/rules/proactive-coordination.md`) — *ai-je sorti un grain de substance du pool et l'ai-je transformé en PR ?* — qui couvre tous les synonymes présents et futurs. - -## Incident fondateur R5/R7 — le silo qui se termine en ASK (2026-07-01, po-2025) - -Séquence constatée : le worker audite la tranche README de **sa seule famille**, la trouve cohérente, puis poste un `[ASK coordinator]` pour un grain frais — alors que `gh issue list --state open` offrait des dizaines de grains exécutables cross-lane (#3360 bug RL Python, #4039 gittins bi-track, #3968/#2876 cross-famille, entrées ML.Net manquantes). - -Un audit de tranche étroite qui se termine en ASK **est le silo, pas de la proactivité**. C'est l'origine de la formulation R5 « le pool n'est PAS ta famille » et de la clause « le coordinateur n'est PAS un distributeur de grains ». - -Exemple chiffré du même registre (2026-07-19) : la famille ICT offrait à elle seule **≥6 grains CPU frais non-claimés** pendant que **4 lanes** se déclaraient « drained ». - -## Picker — pondération, graine, mesures (organe #10853) - -Détail de la R5 « le pool se tire ». La règle porte l'usage et les trois urnes ; ce qui suit sert quand on veut comprendre ou ajuster le tirage. - -**Mesure fondatrice (2026-08-14).** Pool de 140 issues ouvertes, dont **89 créées dans les 7 derniers jours**. `gh issue list` plafonnant à 30 résultats triés par récence, la fenêtre visible s'arrêtait à **~6 jours** — le worker repiochait mécaniquement dans ce que le coordinateur venait de créer. Partition du même pool : **44 grains unitaires · 29 EPIC/umbrella · 67 `candidate-delivered`**. - -**Pourquoi trois urnes et pas une.** Au moment de la mesure, **aucun** grain unitaire n'avait plus de 30 jours : tout l'ancien (#1453 prover, #1454 training, #2159 Grothendieck, #2874 Knot, #1621 QC, #1206 Z3.Linq) était umbrella. Une urne unique ne l'aurait jamais montré, quel que soit le poids donné à l'ancienneté. L'urne `delivered` est ce qui fait *refluer* le compte sans batch-close aveugle — le batch-close endort une série (précédent ICT : 24 fermetures justifiées item par item, série ensuite dormante). - -**Pondération** — trois facteurs doux, tous imprimés à côté du candidat : - -| Facteur | Formule | Ce qu'il sert | -|---|---|---| -| ancienneté | `1 + log2(1 + jours/7)` | « faire refluer doucement » — la traîne est là où le compte s'accumule ; 6 mois pèsent ~4× une issue de la semaine | -| anti-répétition de session | `×0.25` si le genre appartient aux `--prev-genre` répétés/CSV | **G-VAR-3 au tirage** ; `guard → docs → guard` reste pénalisé | -| contenu | `×2` si le genre est CONTENU | **G-VAR-1 au tirage** | - -**Persistance des genres de session (#14591/#14704)** : `--prev-genre` est répétable et accepte les listes séparées par virgules. Le CSV par lane (`--csv-state ` + `--write-state candidate`) conserve l'ensemble sous `lane,last_genres,last_ts`, avec des genres séparés par `|`. L'ancien header `last_genre` et sa valeur scalaire restent lisibles comme un ensemble à un élément ; la prochaine écriture les migre sans perte. Chaque écriture ajoute le genre de la candidate aux genres déjà connus, au lieu d'écraser la mémoire par le dernier : la séquence `guard → docs → guard` reste donc pénalisée au troisième tirage. Le choix `--write-state=merged`, accepté mais sans effet, a été retiré. Toute `OSError` laisse le picker utilisable sans persistance pour le cycle. - -Tirage pondéré **sans remise** (Efraimidis-Spirakis : clé `u^(1/w)`, top-k). Plus de pondération reproduirait une monoculture avec des étapes en plus : on se limite à ce que les gates du variation-protocol demandent déjà. - -**Graine** = `sha256(lane | heure UTC | reroll)`. Deux lanes tirent des candidats différents à la même minute ; une même lane qui relance dans l'heure retrouve son tirage (idempotent, pas de thrash) ; `--reroll N` redistribue. Le genre affiché est **inféré** par regex sur titre+labels : c'est une aide au tri, pas un verdict — l'agent pose le vrai tag `Grain:` lui-même. - -### Cache de payloads et filtres factuels (#13920) - -Les trois lectures GitHub partageables sont mises en cache **avant dérivation** dans le cache utilisateur, jamais dans le dépôt : pool ouvert (10 min), PRs mergees de la fenêtre d'affluence (15 min), PRs avec fichiers de la saturation de séries (60 min). La clé encode dépôt, schéma, mesure et forme de requête ; les cutoffs temporels exacts restent dans le fetch, tandis que `age`, `idle`, citations et `saturation(prs)` sont recalculés à chaque lecture. Une écriture passe par un fichier temporaire puis `os.replace`; la rétention est bornée. `--cache auto|off|refresh`, `--cache-dir` et `--cache-status` rendent le mécanisme contrôlable. Un fallback stale n'est utilisé qu'après échec du refresh et reste signalé comme **NON MESURÉ** dans les verdicts qui en dépendent. - -Le cache ne touche jamais les organes minute-sensitive : garde des PRs rouges, checks/reviews/nits, claims, `recent_delivery`, sécheresse de contenu et écritures de commentaire restent live. - -Les rerolls successifs peuvent être remplacés par une requête plus expressive : - -```bash -python scripts/pick_idle_grain.py \ - --lane myia-po-2025:CoursIA-2 \ - --prev-genre tooling \ - --grains 8 --umbrellas 4 \ - --exclude-issue 13920,13924 \ - --exclude-label blocked \ - --min-idle-days 7 \ - --urns grain,umbrella \ - --cache-status -``` - -Filtres disponibles, tous locaux et combinables : `--exclude-issue` répétable ; `--require-label` (AND) ; `--exclude-label` (ANY) ; bornes inclusives `--min/max-age-days` et `--min/max-idle-days` ; sélection `--urns`. Les comparaisons de labels sont insensibles à la casse. Le funnel attribue chaque exclusion au premier filtre qui la retire et expose en JSON `filters.active`, `filters.excluded`, `filters.funnel`. Si ces filtres locaux vident la première passe, le picker les relâche automatiquement et l'annonce ; exclusions explicites et urnes restent fermes. Il ne prétend jamais que le pool global est vide. Les filtres de tier/genre sont volontairement absents, car le genre du picker est inféré et n'est pas un verdict. - -### Picker — les deux axes de pondération (complément) - -La pondération du tirage porte **deux axes indépendants** (mesurés non redondants, pearson r = 0,334) : l'**âge de création** (la traîne, là où le compte s'accumule) et le **délaissement** — jours depuis la dernière activité, colonne `inact`. Le second est ce qui atteint « les EPICs qui méritent d'être conduites à leur terme et sont souvent délaissées devant les sujets du moment » : un sujet intouché depuis 53 j pèse ~2,4× un sujet du jour — assez pour remonter, trop peu pour devenir la seule veine. - -Mesure du 2026-08-20 sur les 140 ouvertes : **91 avaient bougé dans les 24 h**, et sur les 12 les plus inactives, **9 étaient des EPICs**. C'est cette asymétrie qui a fait du tirage systématique le premier geste de chaque cycle (mandat user 2026-08-20) : tant que la sélection par défaut restait « ce que je vois », elle restait « ce qui est récent » — les sujets menés à leur terme étaient toujours ceux du moment, et les EPICs de fond ne l'étaient jamais. - -## L721 — stale-tracker guard (leçon complète) - -Avant **tout** claim de « 0 PR / saturated / idle » : la requête DOIT interroger le **tag de lane** (la même clé que `pick_idle_grain.py` lit), **PAS** `--author `. Forme canonique (variable shell, robuste Git Bash / PowerShell / CMD) : - -```bash -LANE="" # ex. myia-po-2026:CoursIA -gh pr list --state open --limit 100 --json number,title,body \ - --jq ".[]|select(.body|test(\"lane $LANE\"))|\"#\(.number) \(.title)\"" -``` - -**Pourquoi pas `--author `** (#13870) : les workers poussent sous le compte `jsboige` partagé, pas sous leur nom de lane — `--author myia-po-2026` rend **0** alors que la lane porte 25 PRs ouvertes (mesure 2026-09-01, 59 PRs pool). Quatre lanes sur cinq voient `0` de leur propre travail avec la requête naïve, et une lane a escaladé `[URGENT] HIGH` (`msg-20260831T181622-9fucbh`) en croyant n'avoir rien livré alors qu'elle portait 12 PRs — exactement l'état terminal faux-négatif que ce garde devait prévenir. - -**Contrôle positif obligatoire** : la requête corrigée se valide par ses **faux négatifs symétriques** — une lane connue pour avoir N PRs doit en rendre N, pas par le fait qu'elle rende un nombre. Mesure de référence du 2026-09-01 : `myia-po-2026:CoursIA` → 25, `myia-po-2024:CoursIA` → 11, `myia-ai-01:CoursIA` → 9, `myia-po-2027:CoursIA` → 5, `myia-po-2023:CoursIA` → 3 — toutes non-nulles (la mesure 2026-08-31 de l'issue #13870 incluait `myia-po-2025` à 1 PR qui a depuis été mergée). - -**Repli** : si une PR ne porte **pas** le tag `Grain:` lisible, elle reste invisible aux deux requêtes (tag + author) ; c'est l'angle mort partagé avec `GRAIN-ORPHANS-SWEEP` (#13086). Traiter par le sweep dédié, **pas** en relâchant la requête ici. - -## L1356 — preflight de claim, recette complète - -L898 vérifie ce qui est **ouvert** ; une PR **MERGÉE** qui a livré l'issue en rider (référence à un autre numéro, pas de `Closes #N`) est invisible à ce filtre, et l'issue reste OPEN sans PR liée — **« OPEN + zéro PR liée » n'est PAS une preuve de fraîcheur** (incidents #13562/#13608, corroboré po-2025 sur #12794). - -Avant tout `[CLAIMED]` : - -1. `gh search prs ""` (ou `gh pr list --state all --search ""`) — une PR merged sur le sujet = grain livré. -2. `ls -d /**` — un **worktree orphelin** nommé pour l'issue signale une session antérieure : la compaction efface le souvenir du travail, pas le disque ; inspecter sa branche (`git log -1`) avant de créer quoi que ce soit. - -Si livré : poster un commentaire `[INFO] candidate-delivered` avec preuve (commit + tests) pour fermeture par le coordinateur — **ne pas réimplémenter, ne pas closer soi-même**. - -Cas fondateur du point 1 : le doublon mergé #8835/#8836 (po-2024, 2026-07-29) — deux écarts au triple-check (recherche `--state open` au lieu de `all` ; recherche par mots-clés du titre au lieu du **numéro d'issue**) ont laissé passer un correctif déjà mergé 30 min plus tôt. La recherche se fait par **numéro d'issue**, en `--state all`. - -## Réparer son propre rouge — historique et mesures (R5, mandat 2026-08-22) - -Le picker a longtemps rendu « REFUS DE TIRAGE » + **sortie 2, aucun candidat** quand la lane portait une PR bloquée >24 h : le fond était le même (la reprise passe d'abord), la forme disait *l'outil n'a rien pour toi*. La forme actuelle — **sortie 0, le grain rendu *est* la reprise** — date de ce mandat. - -Pourquoi la reprise est assignée et pas négociable : **une PR rouge ne peut être réparée que par sa lane** — le coordinateur ne peut ni rebaser, ni corriger, ni lever un `CHANGES_REQUESTED` à sa place. Une lane à forte cadence accumule les PRs plus vite, déclenche `count`/`nits` plus tôt, et recevait donc l'ancien refus à **chaque** cycle — le boost causait le drain (incident lanes 2 sous crédit MiniMax, 2026-08-30). - -La mesure qui a fondé le mandat (2026-08-22) : débit de merge sain — 65 mergées dans la journée — et pourtant **16 PRs bloquées de plus de 24 h réparties sur 7 lanes**, une au moins par lane. Les PRs du jour mergent vite pendant que les vieilles s'accumulent : c'est le résidu que seule la reprise par la lane absorbe. - -## Veine plafonnée — historique et anti-pattern (R8, #11343 tranche 2) - -Avant que le tirage ne devienne systématique (R5), le plafond de veine portait une « condition d'appel » du picker. Depuis, il ne reste de lui que l'**interdit ciblé** : la PR suivante de la lane ne peut pas re-claimer l'umbrella qui vient de saturer (`vein_cap=2` PRs citant la même umbrella par jour de lane), quel que soit ce que le tirage propose — la commande `picker_command` exposée par `variation_light_cap.py` sert à écarter l'umbrella saturée. - -L'amendement ai-01 du 2026-08-16T22:53Z (verbatim : « on ne jette pas du travail écrit ») borne l'interdit : **le plafond ne bloque jamais la tranche en cours**, seulement la PR suivante. - -Anti-pattern fondateur : continuer à claimer `#11224`/`#11271` après la 3ᵉ tranche sans passer par le picker = monoculture auto-référence, le défaut structurel que la veine-mesure a justement rendu visible. +# Coordination proactive — detail + +Detail de [.claude/rules/proactive-coordination.md](../../.claude/rules/proactive-coordination.md). Voir aussi [subagents-reference.md](subagents-reference.md), [scripts-reference.md](scripts-reference.md). + +## Plancher durci — « plusieurs grains dont un DEEP de CONTENU » (mandat user 2026-09-12, #15793) + +R1 de [proactive-coordination.md](../../.claude/rules/proactive-coordination.md) et G-VAR-1 de [variation-protocol.md](../../.claude/rules/variation-protocol.md) portent un plancher **pluriel et durci** : **≥ 2 grains livrés entre 2 wakeups, dont ≥ 1 DEEP dans un genre de CONTENU**. Le MED et le META restent **bienvenus au-delà** du plancher — ils ne le tiennent plus. + +**Ce qui le motive — mesure du 2026-09-12T16:39Z**, attribution par le tag `Grain:` (jamais `--author` : l'identité de poussée `jsboige` est partagée, L721) : + +| Fenêtre | merges | DEEP | MED | LIGHT | CONTENU | META | +|---|---:|---:|---:|---:|---:|---:| +| 7 jours (depuis 09-05) | **583** (83,3/j) | 88 (**15 %**) | 400 | 88 | 314 | 260 | +| 48 heures | **196** (98,0/j) | 35 (**18 %**) | 113 | 45 | **82** | **109** | + +Le volume n'est pas le défaut. Deux choses le sont : (1) la **dureté** — 15 % de DEEP sur 7 j, le plancher R1 d'alors (« ≥ 1 PR ») étant tenu par n'importe quoi, massivement par du MED ; (2) la **bascule CONTENU/META** — sur 7 j le CONTENU tient encore (314 contre 260), sur 48 h le META passe devant (109 contre 82) : la flotte construit l'atelier plus vite qu'elle n'y fabrique. + +Le contraste par lane rend le mécanisme lisible : la lane au plus gros volume sur 7 j (`myia-po-2023:CoursIA`, 108 merges) est la plus faible en substance (39 CONTENU, **8 DEEP**), tandis que `myia-po-2027:CoursIA-2` rend 32 CONTENU et 10 DEEP sur 41 merges. La lane la plus META de la flotte est celle du coordinateur (`myia-ai-01:CoursIA` : 38 merges sur 7 j pour **1 DEEP** et 7 CONTENU — 0 DEEP et 1 CONTENU sur 48 h). + +**Contre-poids anti-inflation, nommé et non inventé.** Exiger un DEEP crée une incitation à **sur-coter le tier**. Ce qui la couvre est déjà en place : le signal bot `TIER-INFLATION`, et le merge-gate qui **re-qualifie lui-même un tag mal dérivé** (§3 de [variation-protocol.md](../../.claude/rules/variation-protocol.md), ligne « Tag mal dérivé »). Le litmus DEEP reste objectif — *`main` contient-il désormais un résultat ou une capacité qui n'existait pas, dont la production a demandé du raisonnement de domaine ?* Le durcissement se paie en **lecture de tags par ai-01**, jamais en confiance. + +**Ce durcissement normatif substantiel exigeait un sign-off user** conformément à CLAUDE.md §A : la PR #15793 qui l'a porté ne se self-merge pas. Les corrections non normatives suivent la même PR/review, sans sign-off supplémentaire. + +## Backlog pickup — sources autorisees (ordre de priorite decroissant) + +Wakeup sans dispatch neuf : prendre la **premiere source non-vide** et la consommer comme une **file sequentielle de grains atomiques**. Chaque grain garde sa PR propre ; une PR livree ne termine pas la session et la lane poursuit tant que sa fenetre est active. + +| # | Source | Action | Pourquoi cet ordre | +|---|--------|--------|---------------------| +| 1 | Side-track Epic dispatchée propre au worker (cf mapping) | Avancer ses sous-taches atomiques dans l'ordre | Engagement deja pris | +| 2 | Ses propres PRs OPEN en attente review/iter | Drainer feedback, conflits et CI propres avant production neuve | Pas laisser pourrir | +| 3 | Issues `priority-high` non assignees dans son domaine | Self-assign + livraisons atomiques sequentielles | Urgences declarees | +| 4 | PRs languishing > 3 jours sans reponse (siennes ou domaine) | Re-iter ou cloturer, puis poursuivre | Hygiene cluster | +| 5 | Issues `priority-medium` du domaine | Self-assign + livraisons atomiques sequentielles | Travail cataloguable | +| 6 | Low-priority Epics stockees (#1646 Grothendieck, #1647 Conway Phase 2, #1650 Translation, #1651 Conway Phase 3 FWT) | Une PR atomique par phase/pilier, plusieurs a la suite | Stock activable "fenetres calmes" | +| 7 | Audit findings non traites (NanoClaw #488 etc.) | Reassessment puis PR si CONFIRMED, et continuer | Backlog d'enquete | +| 8 | Consolidation/documentation de son domaine | PRs atomiques, sans plafond d'une par session | Wiki Karpathy / SDDD documentaire | + +## Anti-patterns wakeup vide (interdits) + +- "Rien de nouveau, je me rendors" → **REFUSE** ; pioche backlog avant tout ScheduleWakeup +- "J'attends le prochain dispatch coord" → **REFUSE** ; le coord n'est pas un distributeur flux tendu +- Fusionner plusieurs sujets dans une PR « pour faire du chiffre » → **REFUSE** ; plusieurs grains se livrent sequentiellement, chacun dans sa PR atomique +- Annoncer "BLOCKED" alors que sources 5-8 non explorees → **REFUSE** (G.7 stagnation cross-cycle = escalade) + +## Reporting wakeup vide + +```text +[INFO] Wakeup R vide (pas de feedback/dispatch nouveau). +Backlog pickup : source <#X> "" → item # "". +PR cible : # en cours. +``` + +Pas de `[BLOCKED]` ni `[DONE pas de travail]`. Wakeup vide = opportunite backlog, pas blocage. + +## Pourquoi il n'y a pas de mapping machine → track + +Ce fichier a porté jusqu'au 2026-09-12 un tableau « Mapping machine → track principale + side-track (**cycle courant**) ». Le « (cycle courant) » de son titre était le défaut : un fichier versionné ne peut pas porter l'état d'un cycle, et celui-là ne l'a pas porté. Sur ses **7 assignations numérotées, 5 pointaient une issue CLOSED** — #1409 (fermée le 2026-07-26), #1273 (2026-06-02), #1385 (2026-05-26), #1455 (2026-06-11), #999 (2026-05-23) ; trois des quatre lignes worker avaient une track principale fermée, et po-2023 comme po-2025 avaient **les deux colonnes** fermées. + +Un steer qui pointe une issue CLOSED est un **phantom** au sens de R5 de [coordinator-discipline.md](../../.claude/rules/coordinator-discipline.md) : le worker brûle son cycle à le réfuter au lieu de produire. Le tableau en fabriquait cinq, en permanence, depuis un fichier auto-référencé par le harnais. + +**Ce qui le remplace est le tirage**, déjà la voie par défaut depuis le mandat du 2026-08-20 (R5 de [proactive-coordination.md](../../.claude/rules/proactive-coordination.md)) : `python scripts/pick_idle_grain.py --lane --prev-genre `. Le steering nommé du coordinateur reste l'exception qui passe devant — mais il se **grounde firsthand au moment où il est posé** (`gh issue view N`), ce qu'un tableau écrit une fois ne peut structurellement pas faire. Le roster des spécialistes async, lui, n'a jamais eu besoin d'être partitionné par machine : il vit dans [subagents-reference.md](subagents-reference.md), et R3 dit de déléguer la side-track au specialist qui couvre le sujet, quelle que soit la lane. + +Cette section existe pour que le tableau ne soit pas **re-inventé** : son absence est un choix mesuré, pas un oubli de maintenance. + +## Cadence + +- ScheduleWakeup horaire (~3540-3555s jitter) re-arme a chaque tour coord/worker interactif (ping-pong) — cf CLAUDE.md global "Multi-Machine Ping-Pong". +- Le sous-agent async tourne **pendant** le sommeil entre wakeups : au reveil, recuperation notification + integre/PR. + +## Anti-patterns cycle normal (interdits) + +- Worker avec une seule track (BG seul, ou main seule sans side-track) → demander/prendre 2e track immediatement +- Refaire a la main une tache couverte par un specialist async (modernisation serie, batch notebooks, QC robustness…) +- Side-track sans Epic de rattachement (intracable, meurt a l'absence du coordinateur) +- Lancer 2 sous-agents editeurs sur le **meme** notebook/serie (collision) — read-only OK en parallele, editeurs = un par fichier + +## Vérification pré-cycle (L282 / L283 / L786 / L915) — leçons post-c.264 + +Consolidees depuis les incidents cross-machine c.264 / c.288-289 / c.797 / c.796 et le PR feedback upstream. **Dures** : à executer AVANT tout claim, pas après. + +**L282 — Mirror-lanes collision**. Une machine avec 2 lanes (CoursIA **et** CoursIA-2) = 2 workers distincts. Avant `gh issue claim` ou poster `[CLAIMED]`, verifie **le dashboard de la lane ciblee** : un doublon mirroir (incident fondateur #5640/#5641 sur #5635, 6 min apart, ai-01 arbitrage 13:51Z) est une **perte de cycle pour les deux workers** + overhead coordinateur. `roosync_dashboard(action:"read", type:"workspace", section:"all")` sur **les 2 dashboards** (workspace-CoursIA + workspace-CoursIA-2) avant de claim. + +**L283 — Confiance hierarchique = `gh API > git log > dashboard intercom > status condense`**. Avant de propager un claim d'etat (merge / OPEN / contenu main) : `gh pr view N --json state,mergedAt,mergeCommit` + `git ls-tree origin/main ` + `git log origin/main --oneline -- `. Le **status condense auto-92%** du dashboard peut **halluciner** un merge (po-2026 c.288 : "#5657 MERGED by jsboige" alors que PR OPEN CLEAN awaiting sweep). Source : condensation LLM hallucine des PRs sur 4 cycles consecutifs. + +**L786 — Honest-drain diagnostic AVANT claim "lane exhausted"**. Avant de poster un statut terminal-idle (forensic-floor, drained, saturé, no-change, loophole) : `gh issue list --state open | wc -l` > 0 ne suffit pas. **Identifier le grain executable pour ta capability** (CPU-only-po et lecture OK, GPU-only exclu) — test : pour chaque issue ouverte, peux-tu (a) la lire, (b) la decomposer en sous-grain non-GPU, (c) executer le sous-grain ? Si ≥1 sous-grain executable → claim, sinon re-arme. Incident : 4 lanes "drained" simultanement c.796, pool offrait ≥6 grains CPU frais non-claimés (cf c.797 dispatch). + +**L915 — PR OPEN MERGEABLE ≠ PR mergee**. Avant de claim un cycle N dependant d'une PR OPEN MERGEABLE upstream, verifie `gh pr view N --json mergedAt` (non-null = mergee). 2 options : (a) reporter c.N a `mergedAt` confirme ; (b) redécouper le grain en **substrate standalone** + c.N+1 (le livrable de N est comprehensible sans merger l'upstream). Source : worker-side claim triple phantom-dispatach (c.715 #7225, c.797 #8031, c.792 #7733) corrige par L715-L2. + +**C715-L2 — Cross-repo work detection AVANT claim**. Avant de prendre un grain sur issue #X, triple-verification : +``` +git log --all --oneline --grep "#" # 0 commit upstream +git ls-tree origin/main # 0 fichier upstream +gh pr list --search "#" --state all # 0 PR antérieure +``` +3 zeros = grain libre. ≥1 hit = verifier l'etat de l'upstream (`gh pr view N --json mergedAt`) et choisir (a) reporter, (b) redécouper, (c) rafraichir la substance. + +**C715-L2 — Incident fondateur #8835/#8836 (po-2024, 2026-07-29)**. Deux écarts au triple-check ci-dessus ont laissé passer un doublon **déjà mergé** : (a) recherche `--state open` au lieu de `--state all` — la PR #8836 (mergée 14:03Z par jsboige) était invisible à toute recherche de PRs ouverts ; (b) recherche par **mots-clés** du titre (`"GradeBook is_absolute"`) au lieu du **numéro d'issue** (`"#8835"`) — le titre de #8836 (« top-tree rule form-invariant ») ne matchait pas les mots-clés choisis. Le correctif identique (même règle `_classify`, mêmes 2 fichiers, mêmes 5 critères d'acceptation) fut livré ~30 min après → PR #8838 fermée comme doublon CONFLICTING. **Le triple-check canonique l'aurait attrappé** : `gh pr list --search "#8835" --state all` retourne #8836 (son tag scope `notebook-tools,#8835` matche), et `git log --all --grep "#8835"` montre `6446bd7bd`. Leçon : avant de pousser un fix sur une issue, le triple-check C715-L2 est **non-négociable**, la recherche se fait par **numéro d'issue** (pas par mots-clés), et `--state all` (pas `open`) — un doublon mergé est le cas de collision le plus facile à manquer parce qu'il n'a aucune PR ouverte à voir. + +**Routine verification first-and** (à coller dans `~/.claude/CLAUDE.md` personalisation ou alias bash) : + +```bash +# Avant tout claim de PR dependante d'une upstream +gh pr view N --json state,mergedAt,baseRefOid 2>/dev/null | head -5 +git merge-base --is-ancestor origin/main && echo "MERGED" || echo "NOT-MERGED" +``` + +--- + +## Vocabulaire d'idle-honnête — les labels bannis (R7, mandat user 2026-07-19) + +Les règles R1-R6 interdisent l'idle ; les workers l'ont contournée en inventant un vocabulaire qui *sonne* comme du travail. Liste constatée, **bannie comme état terminal de cycle** : + +`CLEAN_DONE 0-PR` · `0 grain frais` · `pool/lane exhausted/saturée` · `forensic-floor` · `drained-confirm` · `due-diligence contribution` · `NO-CHANGE-NEEDED honnête` · `Nᵉ cycle honnête` · `loophole applicable` · `rien à faire` · `en attente de dispatch`. + +Cette liste est **indicative, jamais l'autorité** : un worker peut toujours inventer un nouveau synonyme. L'autorité est le **test de résultat** de R7 (`.claude/rules/proactive-coordination.md`) — *ai-je sorti un grain de substance du pool et l'ai-je transformé en PR ?* — qui couvre tous les synonymes présents et futurs. + +## Incident fondateur R5/R7 — le silo qui se termine en ASK (2026-07-01, po-2025) + +Séquence constatée : le worker audite la tranche README de **sa seule famille**, la trouve cohérente, puis poste un `[ASK coordinator]` pour un grain frais — alors que `gh issue list --state open` offrait des dizaines de grains exécutables cross-lane (#3360 bug RL Python, #4039 gittins bi-track, #3968/#2876 cross-famille, entrées ML.Net manquantes). + +Un audit de tranche étroite qui se termine en ASK **est le silo, pas de la proactivité**. C'est l'origine de la formulation R5 « le pool n'est PAS ta famille » et de la clause « le coordinateur n'est PAS un distributeur de grains ». + +Exemple chiffré du même registre (2026-07-19) : la famille ICT offrait à elle seule **≥6 grains CPU frais non-claimés** pendant que **4 lanes** se déclaraient « drained ». + +## Picker — pondération, graine, mesures (organe #10853) + +Détail de la R5 « le pool se tire ». La règle porte l'usage et les trois urnes ; ce qui suit sert quand on veut comprendre ou ajuster le tirage. + +**Mesure fondatrice (2026-08-14).** Pool de 140 issues ouvertes, dont **89 créées dans les 7 derniers jours**. `gh issue list` plafonnant à 30 résultats triés par récence, la fenêtre visible s'arrêtait à **~6 jours** — le worker repiochait mécaniquement dans ce que le coordinateur venait de créer. Partition du même pool : **44 grains unitaires · 29 EPIC/umbrella · 67 `candidate-delivered`**. + +**Pourquoi trois urnes et pas une.** Au moment de la mesure, **aucun** grain unitaire n'avait plus de 30 jours : tout l'ancien (#1453 prover, #1454 training, #2159 Grothendieck, #2874 Knot, #1621 QC, #1206 Z3.Linq) était umbrella. Une urne unique ne l'aurait jamais montré, quel que soit le poids donné à l'ancienneté. L'urne `delivered` est ce qui fait *refluer* le compte sans batch-close aveugle — le batch-close endort une série (précédent ICT : 24 fermetures justifiées item par item, série ensuite dormante). + +**Pondération** — trois facteurs doux, tous imprimés à côté du candidat : + +| Facteur | Formule | Ce qu'il sert | +|---|---|---| +| ancienneté | `1 + log2(1 + jours/7)` | « faire refluer doucement » — la traîne est là où le compte s'accumule ; 6 mois pèsent ~4× une issue de la semaine | +| anti-répétition de session | `×0.25` si le genre appartient aux `--prev-genre` répétés/CSV | **G-VAR-3 au tirage** ; `guard → docs → guard` reste pénalisé | +| contenu | `×2` si le genre est CONTENU | **G-VAR-1 au tirage** | + +**Persistance des genres de session (#14591/#14704)** : `--prev-genre` est répétable et accepte les listes séparées par virgules. Le CSV par lane (`--csv-state ` + `--write-state candidate`) conserve l'ensemble sous `lane,last_genres,last_ts`, avec des genres séparés par `|`. L'ancien header `last_genre` et sa valeur scalaire restent lisibles comme un ensemble à un élément ; la prochaine écriture les migre sans perte. Chaque écriture ajoute le genre de la candidate aux genres déjà connus, au lieu d'écraser la mémoire par le dernier : la séquence `guard → docs → guard` reste donc pénalisée au troisième tirage. Le choix `--write-state=merged`, accepté mais sans effet, a été retiré. Toute `OSError` laisse le picker utilisable sans persistance pour le cycle. + +Tirage pondéré **sans remise** (Efraimidis-Spirakis : clé `u^(1/w)`, top-k). Plus de pondération reproduirait une monoculture avec des étapes en plus : on se limite à ce que les gates du variation-protocol demandent déjà. + +**Graine** = `sha256(lane | heure UTC | reroll)`. Deux lanes tirent des candidats différents à la même minute ; une même lane qui relance dans l'heure retrouve son tirage (idempotent, pas de thrash) ; `--reroll N` redistribue. Le genre affiché est **inféré** par regex sur titre+labels : c'est une aide au tri, pas un verdict — l'agent pose le vrai tag `Grain:` lui-même. + +### Cache de payloads et filtres factuels (#13920) + +Les trois lectures GitHub partageables sont mises en cache **avant dérivation** dans le cache utilisateur, jamais dans le dépôt : pool ouvert (10 min), PRs mergees de la fenêtre d'affluence (15 min), PRs avec fichiers de la saturation de séries (60 min). La clé encode dépôt, schéma, mesure et forme de requête ; les cutoffs temporels exacts restent dans le fetch, tandis que `age`, `idle`, citations et `saturation(prs)` sont recalculés à chaque lecture. Une écriture passe par un fichier temporaire puis `os.replace`; la rétention est bornée. `--cache auto|off|refresh`, `--cache-dir` et `--cache-status` rendent le mécanisme contrôlable. Un fallback stale n'est utilisé qu'après échec du refresh et reste signalé comme **NON MESURÉ** dans les verdicts qui en dépendent. + +Le cache ne touche jamais les organes minute-sensitive : garde des PRs rouges, checks/reviews/nits, claims, `recent_delivery`, sécheresse de contenu et écritures de commentaire restent live. + +Les rerolls successifs peuvent être remplacés par une requête plus expressive : + +```bash +python scripts/pick_idle_grain.py \ + --lane myia-po-2025:CoursIA-2 \ + --prev-genre tooling \ + --grains 8 --umbrellas 4 \ + --exclude-issue 13920,13924 \ + --exclude-label blocked \ + --min-idle-days 7 \ + --urns grain,umbrella \ + --cache-status +``` + +Filtres disponibles, tous locaux et combinables : `--exclude-issue` répétable ; `--require-label` (AND) ; `--exclude-label` (ANY) ; bornes inclusives `--min/max-age-days` et `--min/max-idle-days` ; sélection `--urns`. Les comparaisons de labels sont insensibles à la casse. Le funnel attribue chaque exclusion au premier filtre qui la retire et expose en JSON `filters.active`, `filters.excluded`, `filters.funnel`. Si ces filtres locaux vident la première passe, le picker les relâche automatiquement et l'annonce ; exclusions explicites et urnes restent fermes. Il ne prétend jamais que le pool global est vide. Les filtres de tier/genre sont volontairement absents, car le genre du picker est inféré et n'est pas un verdict. + +### Picker — les deux axes de pondération (complément) + +La pondération du tirage porte **deux axes indépendants** (mesurés non redondants, pearson r = 0,334) : l'**âge de création** (la traîne, là où le compte s'accumule) et le **délaissement** — jours depuis la dernière activité, colonne `inact`. Le second est ce qui atteint « les EPICs qui méritent d'être conduites à leur terme et sont souvent délaissées devant les sujets du moment » : un sujet intouché depuis 53 j pèse ~2,4× un sujet du jour — assez pour remonter, trop peu pour devenir la seule veine. + +Mesure du 2026-08-20 sur les 140 ouvertes : **91 avaient bougé dans les 24 h**, et sur les 12 les plus inactives, **9 étaient des EPICs**. C'est cette asymétrie qui a fait du tirage systématique le premier geste de chaque cycle (mandat user 2026-08-20) : tant que la sélection par défaut restait « ce que je vois », elle restait « ce qui est récent » — les sujets menés à leur terme étaient toujours ceux du moment, et les EPICs de fond ne l'étaient jamais. + +## L721 — stale-tracker guard (leçon complète) + +Avant **tout** claim de « 0 PR / saturated / idle » : la requête DOIT interroger le **tag de lane** (la même clé que `pick_idle_grain.py` lit), **PAS** `--author `. Forme canonique (variable shell, robuste Git Bash / PowerShell / CMD) : + +```bash +LANE="" # ex. myia-po-2026:CoursIA +gh pr list --state open --limit 100 --json number,title,body \ + --jq ".[]|select(.body|test(\"lane $LANE\"))|\"#\(.number) \(.title)\"" +``` + +**Pourquoi pas `--author `** (#13870) : les workers poussent sous le compte `jsboige` partagé, pas sous leur nom de lane — `--author myia-po-2026` rend **0** alors que la lane porte 25 PRs ouvertes (mesure 2026-09-01, 59 PRs pool). Quatre lanes sur cinq voient `0` de leur propre travail avec la requête naïve, et une lane a escaladé `[URGENT] HIGH` (`msg-20260831T181622-9fucbh`) en croyant n'avoir rien livré alors qu'elle portait 12 PRs — exactement l'état terminal faux-négatif que ce garde devait prévenir. + +**Contrôle positif obligatoire** : la requête corrigée se valide par ses **faux négatifs symétriques** — une lane connue pour avoir N PRs doit en rendre N, pas par le fait qu'elle rende un nombre. Mesure de référence du 2026-09-01 : `myia-po-2026:CoursIA` → 25, `myia-po-2024:CoursIA` → 11, `myia-ai-01:CoursIA` → 9, `myia-po-2027:CoursIA` → 5, `myia-po-2023:CoursIA` → 3 — toutes non-nulles (la mesure 2026-08-31 de l'issue #13870 incluait `myia-po-2025` à 1 PR qui a depuis été mergée). + +**Repli** : si une PR ne porte **pas** le tag `Grain:` lisible, elle reste invisible aux deux requêtes (tag + author) ; c'est l'angle mort partagé avec `GRAIN-ORPHANS-SWEEP` (#13086). Traiter par le sweep dédié, **pas** en relâchant la requête ici. + +## L1356 — preflight de claim, recette complète + +L898 vérifie ce qui est **ouvert** ; une PR **MERGÉE** qui a livré l'issue en rider (référence à un autre numéro, pas de `Closes #N`) est invisible à ce filtre, et l'issue reste OPEN sans PR liée — **« OPEN + zéro PR liée » n'est PAS une preuve de fraîcheur** (incidents #13562/#13608, corroboré po-2025 sur #12794). + +Avant tout `[CLAIMED]` : + +1. `gh search prs ""` (ou `gh pr list --state all --search ""`) — une PR merged sur le sujet = grain livré. +2. `ls -d /**` — un **worktree orphelin** nommé pour l'issue signale une session antérieure : la compaction efface le souvenir du travail, pas le disque ; inspecter sa branche (`git log -1`) avant de créer quoi que ce soit. + +Si livré : poster un commentaire `[INFO] candidate-delivered` avec preuve (commit + tests) pour fermeture par le coordinateur — **ne pas réimplémenter, ne pas closer soi-même**. + +Cas fondateur du point 1 : le doublon mergé #8835/#8836 (po-2024, 2026-07-29) — deux écarts au triple-check (recherche `--state open` au lieu de `all` ; recherche par mots-clés du titre au lieu du **numéro d'issue**) ont laissé passer un correctif déjà mergé 30 min plus tôt. La recherche se fait par **numéro d'issue**, en `--state all`. + +## Réparer son propre rouge — historique et mesures (R5, mandat 2026-08-22) + +Le picker a longtemps rendu « REFUS DE TIRAGE » + **sortie 2, aucun candidat** quand la lane portait une PR bloquée >24 h : le fond était le même (la reprise passe d'abord), la forme disait *l'outil n'a rien pour toi*. La forme actuelle — **sortie 0, le grain rendu *est* la reprise** — date de ce mandat. + +Pourquoi la reprise est assignée et pas négociable : **une PR rouge ne peut être réparée que par sa lane** — le coordinateur ne peut ni rebaser, ni corriger, ni lever un `CHANGES_REQUESTED` à sa place. Une lane à forte cadence accumule les PRs plus vite, déclenche `count`/`nits` plus tôt, et recevait donc l'ancien refus à **chaque** cycle — le boost causait le drain (incident lanes 2 sous crédit MiniMax, 2026-08-30). + +La mesure qui a fondé le mandat (2026-08-22) : débit de merge sain — 65 mergées dans la journée — et pourtant **16 PRs bloquées de plus de 24 h réparties sur 7 lanes**, une au moins par lane. Les PRs du jour mergent vite pendant que les vieilles s'accumulent : c'est le résidu que seule la reprise par la lane absorbe. + +## Veine plafonnée — historique et anti-pattern (R8, #11343 tranche 2) + +Avant que le tirage ne devienne systématique (R5), le plafond de veine portait une « condition d'appel » du picker. Depuis, il ne reste de lui que l'**interdit ciblé** : la PR suivante de la lane ne peut pas re-claimer l'umbrella qui vient de saturer (`vein_cap=2` PRs citant la même umbrella par jour de lane), quel que soit ce que le tirage propose — la commande `picker_command` exposée par `variation_light_cap.py` sert à écarter l'umbrella saturée. + +L'amendement ai-01 du 2026-08-16T22:53Z (verbatim : « on ne jette pas du travail écrit ») borne l'interdit : **le plafond ne bloque jamais la tranche en cours**, seulement la PR suivante. + +Anti-pattern fondateur : continuer à claimer `#11224`/`#11271` après la 3ᵉ tranche sans passer par le picker = monoculture auto-référence, le défaut structurel que la veine-mesure a justement rendu visible. + +## R0 — l'ORDRE de session, et pas seulement un plancher (mesure 2026-09-19, mandat user) + +La règle 1 compte les grains **livrés** — un compte qu'on satisfait en empilant du neuf par-dessus du non-fini. C'est cette faille qui a motivé R0 : rendre l'avancement du travail déjà écrit **condition** du tirage, au lieu de le laisser en concurrence avec lui. + +**Mesure du 2026-09-19** sur le plateau : **213 PRs ouvertes**, dont **104 CLEAN** (zéro conflit, mergeables sur-le-champ) et **174 sans aucune review**. Le dépôt ne souffrait pas d'un manque de grains ouverts ; il souffrait de grains ouverts qui n'atterrissaient pas. + +**Verbatim user** : « *nombreuses sont celles qui n'auront pas besoin de plus de 4-5 commits de plus, donc si vous travaillez bien, les PRs en souffrance devraient pouvoir partir en moins de 24 h. Or ce n'est pas ce qu'on observe.* » + +Trier par **coût de dossier** plutôt que par seule ancienneté en découle : une PR **CLEAN et sans review** ne demande qu'une attestation tierce — c'est le grain le moins cher du dépôt ; une `BLOCKED` demande un diagnostic. L'ancienneté ordonne *à l'intérieur* du sous-ensemble le moins cher, elle ne le choisit pas. + +## L'organe d'EPIC mesure la vacuité du conteneur, pas la viabilité du grain (mesure 2026-09-15) + +`check_umbrella_freshness.py` rend `SATURATED` quand **tous** les enfants cités d'une EPIC sont fermés — une lecture de **conteneur**. Le verdict inverse (`FRESH`) ne dit rien de la viabilité du **grain** : une EPIC fraîche peut n'avoir qu'un résiduel `INTRINSIC`. + +**Mesure du 2026-09-15** : les **8 `sorry` distincts** de `knot_lean`, que #1453 désigne comme « le grain DEEP de cet EPIC », portent chacun, **dans le fichier**, l'annotation qui les déclare hors d'atteinte (`decades` away / *not in Mathlib*). L'organe n'a aucun moyen de le voir — la lecture du contenu reste due, quel que soit son verdict. From 06dcf7551f1cde23e4edbdc2ad0f784ab6c91dc3 Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 04:32:54 +0200 Subject: [PATCH 2/2] docs(proactive,#12051): prose-counts -- predicat conserve, compteurs retires (0 fichier / 2 fichiers) Deux compteurs quantitatifs en prose flagges par check_prose_quantitative_claims : - annotation de commande ls-tree : '0 fichier upstream' -> 'absent upstream' (le predicat d'absence est garde, la mesure est retiree) ; - incident C715-L2 : 'memes 2 fichiers' -> 'les memes fichiers'. Co-Authored-By: Claude Sonnet 5.5 --- docs/reference/proactive-coordination-detail.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/proactive-coordination-detail.md b/docs/reference/proactive-coordination-detail.md index 6f16475340..b7ea87098e 100644 --- a/docs/reference/proactive-coordination-detail.md +++ b/docs/reference/proactive-coordination-detail.md @@ -90,12 +90,12 @@ Consolidees depuis les incidents cross-machine c.264 / c.288-289 / c.797 / c.796 **C715-L2 — Cross-repo work detection AVANT claim**. Avant de prendre un grain sur issue #X, triple-verification : ``` git log --all --oneline --grep "#" # 0 commit upstream -git ls-tree origin/main # 0 fichier upstream +git ls-tree origin/main # absent upstream gh pr list --search "#" --state all # 0 PR antérieure ``` 3 zeros = grain libre. ≥1 hit = verifier l'etat de l'upstream (`gh pr view N --json mergedAt`) et choisir (a) reporter, (b) redécouper, (c) rafraichir la substance. -**C715-L2 — Incident fondateur #8835/#8836 (po-2024, 2026-07-29)**. Deux écarts au triple-check ci-dessus ont laissé passer un doublon **déjà mergé** : (a) recherche `--state open` au lieu de `--state all` — la PR #8836 (mergée 14:03Z par jsboige) était invisible à toute recherche de PRs ouverts ; (b) recherche par **mots-clés** du titre (`"GradeBook is_absolute"`) au lieu du **numéro d'issue** (`"#8835"`) — le titre de #8836 (« top-tree rule form-invariant ») ne matchait pas les mots-clés choisis. Le correctif identique (même règle `_classify`, mêmes 2 fichiers, mêmes 5 critères d'acceptation) fut livré ~30 min après → PR #8838 fermée comme doublon CONFLICTING. **Le triple-check canonique l'aurait attrappé** : `gh pr list --search "#8835" --state all` retourne #8836 (son tag scope `notebook-tools,#8835` matche), et `git log --all --grep "#8835"` montre `6446bd7bd`. Leçon : avant de pousser un fix sur une issue, le triple-check C715-L2 est **non-négociable**, la recherche se fait par **numéro d'issue** (pas par mots-clés), et `--state all` (pas `open`) — un doublon mergé est le cas de collision le plus facile à manquer parce qu'il n'a aucune PR ouverte à voir. +**C715-L2 — Incident fondateur #8835/#8836 (po-2024, 2026-07-29)**. Deux écarts au triple-check ci-dessus ont laissé passer un doublon **déjà mergé** : (a) recherche `--state open` au lieu de `--state all` — la PR #8836 (mergée 14:03Z par jsboige) était invisible à toute recherche de PRs ouverts ; (b) recherche par **mots-clés** du titre (`"GradeBook is_absolute"`) au lieu du **numéro d'issue** (`"#8835"`) — le titre de #8836 (« top-tree rule form-invariant ») ne matchait pas les mots-clés choisis. Le correctif identique (même règle `_classify`, les mêmes fichiers, mêmes 5 critères d'acceptation) fut livré ~30 min après → PR #8838 fermée comme doublon CONFLICTING. **Le triple-check canonique l'aurait attrappé** : `gh pr list --search "#8835" --state all` retourne #8836 (son tag scope `notebook-tools,#8835` matche), et `git log --all --grep "#8835"` montre `6446bd7bd`. Leçon : avant de pousser un fix sur une issue, le triple-check C715-L2 est **non-négociable**, la recherche se fait par **numéro d'issue** (pas par mots-clés), et `--state all` (pas `open`) — un doublon mergé est le cas de collision le plus facile à manquer parce qu'il n'a aucune PR ouverte à voir. **Routine verification first-and** (à coller dans `~/.claude/CLAUDE.md` personalisation ou alias bash) :