diff --git a/.claude/commands/continue.md b/.claude/commands/continue.md index ea3ae6153f..26ed2841af 100644 --- a/.claude/commands/continue.md +++ b/.claude/commands/continue.md @@ -23,7 +23,7 @@ Reprendre le travail sur cette lane : lire les directives coordinateur, puis enc **Un seul geste ouvre le choix, et il etablit la priorite tout seul :** ```bash -python scripts/pick_idle_grain.py --lane --prev-genre [--prev-genre ] +python scripts/pick_idle_grain.py --belt --lane ``` **P0 — Reparer SON PROPRE rouge.** La commande rend en **sortie 0** une file de reparation quand cette lane porte des PRs **bloquees et ouvertes depuis plus de 24 h** : la premiere devient `grain`, la liste complete reste dans `backlog`. **C'est la premiere file de la session**, avant tout grain neuf ; la drainer ne termine pas la session. La raison est mecanique, pas disciplinaire : une PR rouge ne peut etre reparee **que par sa lane** — le coordinateur ne peut ni rebaser ni corriger a sa place — donc tant que la lane ne revient pas dessus, elle reste ouverte indefiniment pendant que les PRs du jour, elles, mergent. C'est exactement ce qui produit le residu de vieilles PRs. @@ -34,9 +34,9 @@ python scripts/pick_idle_grain.py --lane --prev-genre 0 issues ouvertes = echec de methode). +**P3 — Le tapis** (sortie 0) : `--belt` rend **tout l'ouvert, cross-lane**, trie par **derniere visite** (derniere PR mergee qui cite l'issue ; une issue jamais servie passe devant), la plus ancienne en tete. La lane est une etiquette de reporting, pas une frontiere de travail : rien n'est "le turf d'un autre". Prendre les candidats **dans l'ordre**, poser le claim avant chaque edition, livrer, puis passer au suivant sans attendre review, CI, DWELL ou merge du precedent ; une issue servie repart en queue d'elle-meme. Le tapis ne pondere pas et ne refuse jamais : pas de preference de famille, de serie ni d'EPIC, et pas de repli sur une EPIC "maison". Un candidat ne se saute que pour une barriere reelle (GPU-only, vision-only, second LIGHT du meme genre d'affilee selon G-VAR-3), dite dans le rapport de cycle, et l'issue sautee reste en tete pour le tirage suivant ; un claim vivant d'une autre lane est deja retenu par le tapis. Un EPIC en tete se sert par un sous-grain, jamais par un claim de l'EPIC entier. Le tirage pondere (sans `--belt`) reste un outil de diagnostic, il ne choisit plus le grain. Regles completes : [proactive-coordination.md](../rules/proactive-coordination.md) (plancher multi-grain, "rien a faire" avec >0 issues ouvertes = echec de methode). ### Phase 3 : Travailler et livrer diff --git a/.claude/rules/proactive-coordination.md b/.claude/rules/proactive-coordination.md index 337d9fb8ef..0ff621acbb 100644 --- a/.claude/rules/proactive-coordination.md +++ b/.claude/rules/proactive-coordination.md @@ -17,7 +17,7 @@ S'applique à **tous les workers du cluster CoursIA** (po-2023/2024/2025/2026) e **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** (dispatchée, Epic) + une **side-track autonome** que le worker avance **même si le coordinateur s'absente 1-2 jours**. +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. @@ -28,14 +28,14 @@ S'applique à **tous les workers du cluster CoursIA** (po-2023/2024/2025/2026) e **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 --lane --prev-genre +python scripts/pick_idle_grain.py --belt --lane ``` -Tirage pondéré dans **trois urnes** : **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). Pondération deux axes (âge de création + délaissement) : [détail, section Picker](../../docs/reference/proactive-coordination-detail.md). +**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 picker ne décide pas.** Il propose ; l'agent tranche selon les critères de variété de sa lane et pose son `[CLAIMED]` (`check_lane_claim.py` avant d'**éditer**, cf [lane-claim-protocol.md](lane-claim-protocol.md)). Plutôt que rejouer aveuglément : demander davantage de candidats et passer les exclusions factuelles (`--exclude-issue`, labels, bornes age/inactivité, `--urns`) ; `--reroll` reste le dernier recours ; le cache ne touche jamais les organes minute-sensitive ([détail §Cache](../../docs/reference/proactive-coordination-detail.md)). **Aucun résultat vide filtré ne justifie un `[ASK coordinator]` ni un statut idle.** +**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. @@ -69,7 +69,7 @@ python scripts/ci/check_umbrella_freshness.py [...] # exit 1 = SATURATE - **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 d'EPIC de fond (preuve Lean, backtest/training, série notebook, moteur SOTA, sécu/infra). + - **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. diff --git a/.claude/skills/coordinate/SKILL.md b/.claude/skills/coordinate/SKILL.md index a9a7458d29..21b00c12f4 100644 --- a/.claude/skills/coordinate/SKILL.md +++ b/.claude/skills/coordinate/SKILL.md @@ -44,7 +44,7 @@ Les phases ci-dessous s'executent sous le budget defini par la section `## Budge **Pourquoi enumerer, et pas nommer** : une skill qui sait d'avance quoi lire est **structurellement aveugle** a une cle qu'elle n'anticipe pas. Le 2026-09-21, `workspace-CoursIA (2)` — cle forkee par collision de noms Google Drive — portait **23 messages vivants** de po-2026 et po-2027, dont deux PRs debloquees en attente du merge-gate, pendant plusieurs jours sans qu'aucun cycle ne la voie. Une cle a suffixe ` (N)` dont le `workspace` declare **ne porte pas** ce suffixe est une moitie de la meme lane, pas une lane voisine : la lire, et escalader la reparation (`action:"merge"`, cf dashboard `global`). 2. **Inbox DM — drainer et EXTRAIRE, jamais survoler** : `roosync_messages(action:"inbox", status:"unread", deep:true)` — **sans `deep:true` le compte de non-lus est un faux zero**. Deux gestes, dans cet ordre. **(a) Purger les classes qui doublonnent une surface deja lue** — `bulk_mark_read(subject_contains:"Worker Report")` et `bulk_mark_read(subject_contains:"[MENTION] Dashboard")` : sans ca l'arriere se reconstruit a ~8 DM/h et noie le signal utile, qui pese moins de 10 % du volume. **(b) Extraire la liste nommee des PRs deja pre-machees** — marqueurs `[ADJOINT PREFLIGHT]`, `[ADJOINT VERIFIED]`, `[ADJOINT DECISION PACK]`, `preflight exact-head`. Cette liste est une **entree obligatoire de la Phase 3.3** : le pre-machage est produit qu'on le lise ou non ; non consomme, il est paye deux fois. -3. **GitHub** : `gh pr list --state open` (a merger) + le pool **tire, jamais scanne** — `python scripts/pick_idle_grain.py --lane myia-ai-01:CoursIA` (un `gh issue list` nu plafonne a 30, tries par recence : il ne montre que ce que je viens de creer, et c'est ce biais que le steering doit eviter de reproduire). +3. **GitHub** : `gh pr list --state open` (a merger) + le pool **tire, jamais scanne** — `python scripts/pick_idle_grain.py --belt --lane myia-ai-01:CoursIA` (un `gh issue list` nu plafonne a 30, tries par recence : il ne montre que ce que je viens de creer, et c'est ce biais que le steering doit eviter de reproduire). 4. **Cron** : `CronList` — si le job coordinateur a disparu (session-only), le re-armer a la cadence **courante**. Cette cadence est un **etat**, pas une regle : elle ne s'ecrit pas ici (decision user 2026-09-22 — l'etat courant se met a jour dans les memoires et les dashboards, jamais sous git). L'expression a re-armer se lit dans la memoire du coordinateur (`coordinator-handover`, section cron) et sur le dashboard. Ce qui reste durable : une minute off-`:00` (le jitter evite de frapper l'API a la meme seconde que le reste de la flotte), et une cadence unique, PAS de 2e cron ni ScheduleWakeup en plus — un cycle plus long que sa cadence annule deja ses propres declenchements, en empiler un second ne fait qu'ajouter de la conso. ### Phase 3 - Dispatchs, relances, memoire (LES 30 PREMIERES MINUTES) @@ -58,7 +58,7 @@ Les phases ci-dessous s'executent sous le budget defini par la section `## Budge 5. **Trancher les design-gates en attente** dans le cycle -- ne pas deferer une option deja investiguee. Regles : [coordinator-discipline.md](../../rules/coordinator-discipline.md) (R3 lanes independantes, R4 jamais sanctionner l'idle, R5 steer qui ATTEINT/VRAI/DECIDE). 6. **Grounder chaque grain firsthand** (`gh issue view N` / `gh pr view N`) AVANT de dispatcher -- jamais depuis un status condense. 7. **Double canal obligatoire** : DM `roosync_messages(action:"send", to:":", ...)` (le worker lit l'inbox en premier, le DM survit a la condensation) **+** pointeur `[DISPATCH->inbox]` sur le dashboard de la lane (sonnette persistante). -8. **Une lane sans grain = echec coordinateur** : deep-queue, fallback perenne par famille, ou pool global -- jamais un statut terminal-idle. Chaque worker draine **tous** ses nits et reserves reparables sur **toutes** ses PRs, puis enchaine plusieurs grains DEEP/MED ; une seule PR livree ne clot pas sa session. +8. **Une lane sans grain = echec coordinateur** : la tete du tapis (`pick_idle_grain.py --belt`), jamais un statut terminal-idle. Chaque worker draine **tous** ses nits et reserves reparables sur **toutes** ses PRs, puis enchaine plusieurs grains DEEP/MED ; une seule PR livree ne clot pas sa session. 9. **MAJ memoire maintenant, pas en fin de cycle** : `coordinator-durable-state.md` si l'etat durable a bouge. Repoussee a la fin, elle saute quand le cycle deborde -- et le cycle suivant re-derive ce qu'il savait deja. **Budget** : ces neuf points sont **clos avant** d'ouvrir la Phase 4. S'ils ne le sont pas a la fin des 30 minutes, ce sont eux qu'on termine -- pas le merge qu'on commence. diff --git a/scripts/pick_idle_grain.py b/scripts/pick_idle_grain.py index 581e625a6b..9eb335889d 100644 --- a/scripts/pick_idle_grain.py +++ b/scripts/pick_idle_grain.py @@ -3900,6 +3900,7 @@ def belt_sort_key(it: dict) -> tuple: def belt_filter( admitted: list[dict], args, + urns: set[str] | None = None, ) -> list[dict]: """Filtre le pool admissible pour le mode --belt. @@ -3907,11 +3908,20 @@ def belt_filter( urnes), sauf l'admissibilite par DWELL/zone -- le tapis ne refuse JAMAIS, par contrat (cf issue #18832). Les bornes du tapis sont uniquement celles que le caller passe en CLI. + + ``urns`` : urnes EFFECTIVES, deja passees par + ``apply_delivered_urn_gate`` (#15069). Le caller ``main`` les fournit + toujours ; relire ``args.urns`` brut rendrait l'urne ``delivered`` + (presente par defaut) a une lane worker, qui ne doit jamais la recevoir. + ``None`` garde la lecture de ``args.urns`` pour les appels sans lane. """ excluded_issues_set = {int(v) for v in _csv_values(args.exclude_issue)} required_labels_set = set(_csv_values(args.require_label)) excluded_labels_set = set(_csv_values(args.exclude_label)) - selected_urns_set = {v.casefold() for v in _csv_values([args.urns])} + if urns is None: + selected_urns_set = {v.casefold() for v in _csv_values([args.urns])} + else: + selected_urns_set = {v.casefold() for v in urns} def _keep(item: dict) -> bool: n = item["number"] @@ -5265,7 +5275,7 @@ def main(argv: list[str] | None = None) -> int: metrics = belt_report_metrics(pool, closed_7d=None) print_belt_report(metrics) return 0 - belt_pool = belt_filter(admitted, args) + belt_pool = belt_filter(admitted, args, urns=selected_urns) belt_pool.sort(key=belt_sort_key) # Verification des claims tenes par une autre lane : on regarde # plus large que `args.grains` pour tolerer un remplacement si diff --git a/scripts/tests/test_pick_idle_grain_belt.py b/scripts/tests/test_pick_idle_grain_belt.py index 981939bf70..74e0122fcb 100644 --- a/scripts/tests/test_pick_idle_grain_belt.py +++ b/scripts/tests/test_pick_idle_grain_belt.py @@ -464,6 +464,44 @@ def test_belt_report_metrics_computes_max_gap(): assert max_gap is not None and max_gap > 0 +# #15069 sous le tapis : l'urne `delivered` est presente dans le defaut de +# `--urns`. La voie ponderee la retire pour une lane worker via +# `apply_delivered_urn_gate` ; le tapis doit recevoir ces urnes EFFECTIVES, +# pas relire `args.urns` brut (sinon le mode par defaut de /continue sert +# des fermetures a des lanes qui ne ferment rien). + + +def test_belt_filter_honours_delivered_gate_for_worker_lane(): + pool = [ + _make_item(70, age_days=90, idle=30, klass="delivered"), + _make_item(71, age_days=80, idle=20, klass="grain"), + _make_item(72, age_days=70, idle=10, klass="umbrella"), + ] + args = _FakeArgs() + selected = {v.casefold() for v in pig._csv_values([args.urns])} + urns, notice = pig.apply_delivered_urn_gate( + "myia-po-2023:CoursIA", args.urns, "grain,umbrella,delivered", + selected) + assert notice is not None + kept = {it["number"] for it in pig.belt_filter(pool, args, urns=urns)} + assert kept == {71, 72} + + +def test_belt_filter_keeps_delivered_for_coordinator_lane(): + pool = [ + _make_item(70, age_days=90, idle=30, klass="delivered"), + _make_item(71, age_days=80, idle=20, klass="grain"), + ] + args = _FakeArgs() + selected = {v.casefold() for v in pig._csv_values([args.urns])} + urns, notice = pig.apply_delivered_urn_gate( + "myia-ai-01:CoursIA", args.urns, "grain,umbrella,delivered", + selected) + assert notice is None + kept = {it["number"] for it in pig.belt_filter(pool, args, urns=urns)} + assert kept == {70, 71} + + # ================================================================== # Tests #18866 : mode --belt --json = un seul document JSON. # La cle `repair` fusionne le rappel rouge/WIP qui etait sinon imprime @@ -589,4 +627,4 @@ def test_non_belt_json_red_still_emits_standalone_repair(monkeypatch, capsys): payload = json.loads(capsys.readouterr().out) # Le mode reste `repair`, pas `belt` : la volee ponderee est inchangee. assert payload["mode"] == "repair" - assert payload["grain"]["number"] == 18844 \ No newline at end of file + assert payload["grain"]["number"] == 18844