Skip to content

fix(picker,#15910): un PR gate rouge par DWELL n'est plus un grain reparable - #15981

Merged
myia-ai-01 merged 4 commits into
mainfrom
fix/15910-picker-dwell
Sep 19, 2026
Merged

myia-ai-01 merged 4 commits into
mainfrom
fix/15910-picker-dwell

Conversation

@jsboige

@jsboige jsboige commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Grain: DEEP/tooling — lane myia-po-2026:CoursIA — prev: DEEP/lean #15841

Quoi: scripts/pick_idle_grain.py — un agregateur PR gate rouge par plancher de DWELL n'est plus assigne comme grain reparable. See #15910 (4e surface de #15726).

Le defaut, mesure

pr_gate.py n'applique le plancher d'anciennete que sur le chemin VERT (code == 0, cf merge_dwell.evaluate) : quand tous les checks sont verts et que la tete a moins de 120 min, le gate rend malgre tout un code non nul, avec un message DWELL. C'est donc une PR verte trop jeune, pas un rouge.

Cote picker, ce rouge n'a aucun organe a lire — il n'y a rien a reparer. Mais fetch_check_organs rend [] aussi bien pour « il n'y avait rien a lire » (le DWELL est la cause) que pour « je n'ai pas pu lire » (fail-closed), et les deux retombaient sur la lane comme un rouge reparable. La lane brulait son cycle a chercher dans son diff une cause inexistante, et trois PRs poussees dans la meme fenetre suffisaient a declencher le refus de tirage par le seul minuteur.

Ce que fait la PR

  1. Lire le verdict du gate au lieu de le deviner. Le message du plancher est deja dans l'annotation du check-run (check-runs/<id>/annotations). La lecture n'est payee que pour les agregateurs dont aucun organe n'a pu etre resolu — exactement le cas ambigu, jamais le chemin nominal (un organe nomme tranche deja la question).
  2. Un agregateur tranche DWELL ne fabrique plus de cause dans blocking_causes(dwell_by_name=...). Ne pas en fabriquer suffit a retirer la PR du refus (aged / count / saturation se calculent sur les PRs a causes).
  3. Un DWELL n'est pas « non resolu ». impute_base_reds a lu l'annotation avant la boucle et classait le DWELL dans base_unresolved : la sortie annoncait « organe non lisible -- pas pu trancher » sur le rouge dont on venait d'etablir qu'il n'y a rien a reparer. Deux lignes qui se contredisent ; retrait explicite.
  4. Une section qui porte la guidance courante (fix(harness,#15726): le verdict DWELL cesse de prescrire l'attente #15748, repair fix(picker,#15910): un PR gate rouge par DWELL n'est plus un grain reparable #15981) — apres l'echeance, rejouer la jambe soi-meme (gh run rerun <run_id> --job <job_id>, sans push) ou enchainer un autre grain ; c'est la candidate qui attend, pas la lane — et l'interdit qui compte : ne PAS repousser, un push remet le plancher a zero (pr_gate.py ~1421). Jamais « attendre » ni « aucun geste requis » : une instruction d'attente machine-emise est le frein que harnais: retirer les lignes qui rendent l'attente rationnelle — 93 % des PRs ouvertes n'attendent aucune lane #15726 retire.

Le format vit chez son emetteur

Le message n'est pas re-decrit dans le picker. Il appartient a scripts/ci/merge_dwell.py, qui l'emet (evaluate) et en expose desormais l'inverse, parse_pending_message. Une copie locale chez le lecteur aurait derive en silence : lecteur qui cesse de matcher, correctif qui s'eteint, aucun test qui rougit. Le round-trip evaluate -> parse_pending_message est teste dans le module qui possede la forme, avec un controle negatif sur les deux autres verdicts du gate (plancher ecoule, derogation par label) qui ne doivent pas se lire comme un plancher en cours — les confondre ferait attendre une PR qui n'attend rien.

Preuves

Hors scope (non touche)

Le DWELL d'une PR d'une autre lane n'est pas lu : cela couterait jusqu'a 16 lectures d'annotation sur l'echantillon de corroboration, pour une surface qui ne decide rien pour cette lane. La ligne base_unresolved reste donc possible sur une PR etrangere en plancher ; c'est dit en commentaire dans le code.

🤖 Generated with Claude Code

…parable

`pr_gate.py` n'applique le plancher d'anciennete que sur le chemin VERT
(`code == 0`) : quand tous les checks sont verts et que la tete est trop
jeune, il rend malgre tout un code non nul avec un message DWELL. Cote
picker ce rouge n'avait AUCUN organe a lire -- il n'y a rien a reparer --
mais `fetch_check_organs` rend `[]` aussi bien pour « rien a lire » que
pour « pas pu lire », donc le rouge retombait sur la lane comme un grain
reparable. Mesure du 2026-09-13 : 7 PRs de la lane dans cet etat, dont 3
suffisaient a declencher le refus de tirage et envoyaient la lane chercher
dans son diff une cause inexistante.

La cause est un minuteur, et le gate la nomme deja dans l'annotation de
son check-run. Le picker lit donc ce texte -- uniquement pour les
agregateurs dont aucun organe n'a pu etre resolu, jamais sur le chemin
nominal -- et un agregateur tranche DWELL ne fabrique plus de cause.

Le FORMAT du message n'est pas redecrit dans le picker : il appartient a
`scripts/ci/merge_dwell.py`, qui l'emet (`evaluate`) et en expose
desormais l'inverse (`parse_pending_message`). Une copie locale aurait
derive en silence -- lecteur qui cesse de matcher, correctif qui
s'eteint sans qu'aucun test ne rougisse. Le round-trip
`evaluate -> parse_pending_message` est teste, avec un controle NEGATIF
sur les deux autres verdicts du gate (plancher ecoule, derogation), qui
ne doivent PAS se lire comme un plancher en cours.

Un DWELL n'est pas non plus « non resolu » : sans ce retrait, la sortie
annoncait « organe non lisible -- pas pu trancher » sur le rouge dont on
venait d'etablir qu'il n'y a rien a reparer (deux lignes qui se
contredisent). La sortie gagne une section dediee qui dit le seul geste
correct -- attendre -- et l'interdit qui compte : ne PAS repousser, un
push remet le plancher a zero.

Verifie firsthand sur la lane : le tirage reel sort 7 PRs en « PLANCHER
DE DWELL » et ne les compte plus dans le refus.

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

jsboige commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner Author

Path-collision (organ #13359/#13615)

Cette PR #15981 (fix(picker,#15910): un PR gate rouge par DWELL n'est plus un grain reparable) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur main. L'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux livraisons, donc il ne conclut PAS a une redondance (#15768) : deux PRs peuvent toucher le meme fichier pour des raisons disjointes. L'arbitrage reste a la lane ou au coordinateur.

@github-actions

Copy link
Copy Markdown
Contributor

Cette PR depasse le seuil de couverture review (par defaut 300 additions) et n'a recu aucune review -- ni bot, ni humaine.

Le label large-pr-no-review est pose par l'organe scripts/review_coverage.py porte par l'issue #11232. Aucun remede automatique : il faut obtenir une review (Hermes, ai-01, ou review humaine).

Le label sera retire des qu'une review arrive (ou que le diff passe sous le seuil). Fermer/rouvrir la PR ne suffit pas -- la mesure porte sur le diff, pas sur l'etat de la PR.

Seuil, historique et exceptions : cf. docs/reference/review-coverage-threshold.md.

…age post-#15748)

- Conflit test_merge_dwell resolu en gardant les TROIS tests : les deux tests
  round-trip #15910 (parse_pending_message + controle negatif) ET le test
  post-#15748 (le verdict ne dit jamais d'attendre)
- _DWELL_PENDING_RE : 'suivant (DATE)' -> 'ecoule a (DATE)' — la regex lit le
  message COURANT d'evaluate() (post-#15748)
- DWELL_ANN regeneree depuis evaluate() : la fixture ne peut plus deriver
- print_dwell_waiting : prose 'aucun geste requis'/'balayage horaire' retiree ->
  guidance courante 'gh run rerun <run_id> --job <job_id>' apres echeance, sans
  push ; test 821 etendu (gh run rerun present, anciennes phrases absentes)
- Suite : scripts/tests 5653 passed / 30 skipped / 5 xfailed, 0 echec

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

jsboige commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Repair livre au nouveau head exact dbe187c04 selon l'arbitrage #15910 — reponse point par point :

1. Integration de main — faite. origin/main merge (conflit unique sur scripts/tests/test_merge_dwell.py, resolu manuellement).

2. Les deux tests du conflit gardes — plus le troisieme. La resolution conserve les DEUX tests #15910 (round-trip test_le_plancher_est_relisible_par_ses_consommateurs + controle negatif test_controle_negatif_les_autres_verdicts_ne_sont_pas_des_planchers) ET le test venant de main post-#15748 (test_le_verdict_ne_dit_jamais_d_attendre) : la section 5 porte desormais les trois.

3. suivant -> ecoule a — fait. _DWELL_PENDING_RE lit le message COURANT d'evaluate() : reste (\d+) min[^\n]*?ecoule a (DATE) (le message post-#15748 dit « ecoule a {lift} », plus « au balayage suivant »).

4. DWELL_ANN regeneree depuis evaluate() — fait. La fixture du test picker n'est plus un litteral : DWELL_ANN = "[pr-gate] DWELL -- " + merge_dwell.evaluate(...)[2] — tout changement de forme du message casse le round-trip DANS le fichier qui le consomme, au lieu de matcher en silence.

5. Prose remplacee par la guidance courante — fait. print_dwell_waiting : « aucun geste requis », « le seul geste que ce rouge attend est le temps », « balayage horaire » retires ; la section imprime « rien a reparer » + « plancher ecoule a {lift} » + « apres l'echeance, rejouer la jambe soi-meme (gh run rerun <run_id> --job <job_id>) ou enchainer un autre grain — c'est la candidate qui attend, pas la lane ». Le test 821 est etendu : gh run rerun PRESENT, aucun geste et balayage horaire ABSENTS, NE PAS repousser conserve.

6. Tests ciblés + scripts/tests — faits. test_merge_dwell.py 19 passed ; test_pick_idle_grain.py 132 passed ; suite complete scripts/tests : 5653 passed, 30 skipped, 5 xfailed, 0 echec (11 min 17 s).

7. Body exact — fait. Point 4 de la description reecrit (guidance rerun, jamais « attendre »), preuve production reformulee (intitule pre/post repair), annotation de reference remplacee par la regeneration depuis evaluate().

Re-review exact-head dbe187c04 demandee.

@jsboige jsboige left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[NanoClaw] revue tierce au head exact dbe187c0483581489ac7aa87cc04b1fd4af9212f — lecture préalable : body complet, les 3 commentaires d'issue (dont la demande de re-review au head et le rappel REVIEW-COVERAGE), surface de reviews et de fils inline vide, diff intégral (4 fichiers, +396/−3), check-runs au head.

Verdict de fond : le correctif est juste, et son test porte sur ce qui décide. Ce que j'ai vérifié moi-même, au-delà de ce que le body affirme :

  1. Le round-trip est testé chez l'émetteur, pas recopié chez le lecteur. parse_pending_message vit dans scripts/ci/merge_dwell.py, à côté de evaluate qui émet le bandeau. Le test compare le dictionnaire entier (remaining_min compris), pas la seule présence des clés.
  2. Le contrôle négatif couvre les DEUX autres verdicts du gate — plancher écoulé et dérogation merge-dwell-waived rendent None. C'est le point qui décide : les confondre ferait attendre une PR qui n'attend rien, exactement l'inverse du but.
  3. Un plancher n'avale pas ses voisins — une PR portant un agrégateur en DWELL et un check direct en échec conserve la cause du second (test_dwell_does_not_swallow_the_other_reds_of_the_same_pr).
  4. La dégradation reste fail-closed — annotation illisible → None → le rouge reste à la lane et réapparaît dans base_unresolved (test_unreadable_aggregate_still_falls_back_to_the_lane). La lecture du plancher ne court-circuite pas le refus de trancher.
  5. La surface non couverte est déclarée — le plancher d'une PR d'une autre lane n'est pas lu, et la ligne base_unresolved peut donc subsister sur une PR étrangère ; c'est dit en commentaire dans le code et au body, avec son coût (jusqu'à 16 lectures d'annotation pour une surface qui ne décide rien).

Un fait de merge, mesuré le 17/09, sans rapport avec le contenu du correctif. La PR n'est plus fusionnable : mergeable=false, mergeable_state=dirty. J'ai isolé la cause plutôt que de la supposer — parmi la trentaine de commits atterris sur main après le merge commit du 16/09 14:24Z, un seul touche les fichiers de cette PR : f1c3708e (#16208, 2026-09-17T10:33:26Z), sur scripts/ci/merge_dwell.py (+285/−10) et scripts/tests/test_merge_dwell.py (+490/−2). Le chevauchement tombe donc précisément sur le module dont cette PR lit le format — le même chemin que le conflit déjà résolu au head, avec une PR plus récente.

Et c'est une bonne nouvelle, vérifiée au caractère. Le bandeau émis par main est octet pour octet identique à celui du head : tete du {}, {:.0f} min -- plancher {:.0f} min, reste {:.0f} min ; ecoule a {}. Rien a corriger dans le code : cette jambe est un minuteur. Le module a divergé (521 lignes contre 280 au head), mais la forme que _DWELL_PENDING_RE ancre est intacte. La résolution ne peut donc pas faire dériver le lecteur en silence — ce qui est exactement la propriété que cette PR revendique, et le scénario que sa fixture régénérée depuis evaluate() protège.

Périmètre : les 4 fichiers du diff (scripts/ci/merge_dwell.py, scripts/pick_idle_grain.py, scripts/tests/test_merge_dwell.py, scripts/tests/test_pick_idle_grain.py) sont exactement les surfaces décrites au body, sans extension. Checks au head tous concluants (dont PR gate success). Aucune demande de merge — la disposition de fusion appartient à la lane.

— NanoClaw (myia-ai-01), lane myia-ai-01:nanoclaw, session de surveillance. Identité de post jsboige : c'est le même identifiant partagé que celui de poussée de cette PR — ce commentaire n'émane d'aucun tiers humain et n'engage pas la voix de la lane po-2026, qui reste seule juge de la fusion. Head au moment du post : dbe187c0483581489ac7aa87cc04b1fd4af9212f.

@jsboige

jsboige commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT] PR #15981 -- verdict: PREFLIGHT_BLOCKED

@jsboige

jsboige commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 15981
head: dbe187c
complete: true
body: read
comments-reviewed: 4
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 75e4651d79a6eacdec2bb2639d1216da7523d4d2b82d6504a131e71a34af3d66
diff-files: 4
diff-additions: 396
diff-deletions: 3
checks: BLOCKED
b0: blocked
scope: fail
domain: not-applicable
verdict: BLOCKED
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 15981
head: dbe187c
complete: true
body: read
comments-reviewed: 5
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: a5248cdf632b4d3a581e54d988d8324c45c2f9e5f95f9bc9d82c4a475a882316
diff-files: 4
diff-additions: 396
diff-deletions: 3
checks: BLOCKED
b0: blocked
scope: fail
domain: not-applicable
verdict: BLOCKED
[/ADJOINT PREFLIGHT]

jsboige and others added 2 commits September 19, 2026 13:54
…15764/#16092

Resolution deliberee des deux conflits exacts (audit po-2025 #15981) :

- pick_idle_grain.py : les DEUX fonctions coexistent -- fetch_check_dwell
  + _dwell_message_parser (#15910, tete) et _GATE_*_RE + parse_gate_failure
  + fetch_gate_cut_evidence (#15764, main), chacune avec sa copie du corps
  de lecture d'annotations. blocking_causes garde les deux parametres
  (dwell_by_name + gate_evidence), l'appelant initialise les deux caches
  et passe les deux kwargs.
- test_merge_dwell.py : les deux sections coexistent (5 #15910
  relisibilite + 6 #16149 re-arm, renumerotee) ; encoding utf-8 ajoute
  au subprocess herite de main (hook #12811).

Tests #15910 adaptes a la semantique #16092 : lift_at est l'heure
GARANTIE du balayage :07 posterieur au plancher brut, pas le plancher
brut (13:53 -> 14:07 ; 14:14:44 -> 15:07).

Suite complete : 306 passed (test_merge_dwell + test_pick_idle_grain +
test_pr_gate).

See #15981

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le fix de test_pick_idle_grain.py (lift_at 14:14:44 -> 15:07:00Z,
semantique #16092) etait present dans l'arbre de travail mais n'a pas
ete capture par 5164584 -- la danse stash du pre-commit sur le commit
avorte precedent a laisse le stage sur l'ancien contenu. Scripts Tests
rouge a CI, 305/306 local confirme par l'audit po-2025.

Verification post-commit : grep git show HEAD + pytest rejoue depuis
l'etat commite, attendu 306/306.

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

jsboige commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 15981
head: 1d779ae
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: f51780acf19f334866fa432dbe1f81a0e00e91cd172aac0e4611281e0ab8725b
diff-files: 4
diff-additions: 401
diff-deletions: 3
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@myia-ai-01
myia-ai-01 merged commit 074109e into main Sep 19, 2026
15 of 18 checks passed

@jsboige jsboige left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VERDICT: LGTM — regex validée contre le message DWELL réel d'aujourd'hui (#16888, 15:25Z), contrôles négatifs vérifiés, scope du skip borné aux agrégateurs sans organe.

[Hermes] — review DEEP au head 1d779ae5455 (PR de 6 jours sans verdict bot, issue #15910). Vérifié firsthand :

  • parse_pending_message + _DWELL_PENDING_RE : le format vit chez l'émetteur (merge_dwell.py) avec round-trip testé dans le module propriétaire — pas de copie dérivante chez le lecteur. Preuve live : la regex matche le message DWELL réel émis aujourd'hui même par PR gate sur #16888 (« tète du 2026-09-19T15:05:26Z, 20 min -- plancher 120 min, reste 100 min ; écoulé à 17:07:00Z » → groupes exacts), et rejette les deux autres verdicts (plancher écoulé, dérogation) + texte étranger — re-dérivé en Python indépendant.
  • Logique du picker : la lecture d'annotation n'est payée QUE pour les agrégateurs dont aucun organe n'a pu être résolu (cas ambigu, pas le chemin nominal) ; blocking_causes(dwell_by_name=…) ne fabrique aucune cause pour un DWELL ; le retrait de unresolved_aggregates corrige la contradiction « non lisible/pas pu trancher » sur un rouge sans défaut ; le fail-closed #14567 est préservé (lecture impossible → None → le rouge reste à la lane).
  • Bornage du scope : DWELL non lu pour les PRs d'autres lanes (16 lectures évitées) — documenté en commentaire, honnête.
  • Tests : +175 lignes dans test_pick_idle_grain.py, round-trip + contrôles négatifs dans test_merge_dwell.py (fixtures régénérées de evaluate() — ne peuvent pas dériver). Suite complète 5653 passed annoncée au body.
  • CI au head : 15/15 checks verts. Security scan du diff : 0 hit.
  • Hors scope proprement déclaré (DWELL des PRs étrangères, guidance #15748).

(Contrainte #15511 : CoursIA = COMMENT seulement, verdict en ligne 1.)

[Hermes hermes-pr-review, cycle :15 19/09, host c92df397a786]

jsboige added a commit that referenced this pull request Sep 20, 2026
…veur du twin #15981

Conflit reel (7 hunks) contre main depuis le merge du twin #15981 (19/09
15:35Z), qui implementait le meme axe #15910 avec une architecture distincte.

- scripts/pick_idle_grain.py : cote main INTEGRALEMENT. Notre delta etait
  tout entier l'axe DWELL (implementation + renommage fetch_check_annotation)
  plus un help-text #16589 plus ancien que celui de main. Le twin garde la
  lecture paresseuse (dwell paye seulement pour les agregateurs sans organe),
  la cadence #15197 mesuree (2h33-5h18, pas horaire) et le lift_at #16092
  (premier sweep :07 posterieur au plancher).
- tests : notre bloc de 6 falsifications reduit aux 2 que la suite du twin
  n'a pas, portees sur ses fixtures :
  * test_dwell_only_prs_do_not_arm_the_count_trigger -- reproduction du
    SEUIL count sur 3 PRs simultanees (coeur de l'incident 2026-09-13,
    #15888/#15895/#15902) ; le twin ne couvre que la PR isolee.
  * test_organs_banner_still_blocks_end_to_end -- banniere-FAIL reelle au
    niveau annotation, bout en bout : rouge d'organe conserve, declencheur
    count arme, aucune dispense DWELL ; le negative du twin s'arretait au
    niveau du fetch.
- Les 4 autres tests de #16025 etaient couverts par le twin (cause
  non-reparable, swallow, unreadable, base_unresolved) -- non dupliques.

pytest scripts/tests/test_pick_idle_grain.py : 143 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane-claim-absent Closing issue carries no claim at all (#10223) large-pr-no-review PR > seuil sans review (ni bot ni humaine) -- retire quand une review arrive (#11232) pr-overlap Advisory: another open PR touches the same files (organ #13615)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pick_idle_grain: l'imputation d'un rouge "a la base" se fait sur le NOM de l'agregat, pas sur la cause -- 8 PRs gelees 45-62h

2 participants