diff --git a/.claude/skills/coordinate-adjoint/SKILL.md b/.claude/skills/coordinate-adjoint/SKILL.md index 5500e460f5..87ab302d0e 100644 --- a/.claude/skills/coordinate-adjoint/SKILL.md +++ b/.claude/skills/coordinate-adjoint/SKILL.md @@ -93,18 +93,23 @@ La proposition `act_kind` est donc **mise en attente mesurée**, pas ajoutée au - **Dérive de schéma** : cette section disait `--ledger pr-actions` + `kind` + `audited_head_sha` — drift silencieux. Le code aurait rejeté à chaque cycle avec `unknown_field`. Corrigé par lecture firsthand de `debt_ledger.py` ligne par ligne. - **Journalisation manquée** : 33 `[ADJOINT CLOSE]` publiés sur GitHub n'ont **pas** été traduits en observations ledger valides (le CLI était absent de main, et la skill disait du faux). Backfill = geste ultérieur, après merge phase A — pas un skip silencieux. +- **`spool` n'est pas une sous-commande** : le CLI en déclare **trois** (`init`, `append`, `reduce`). Le *spool* est le **répertoire** désigné par le flag `append --out-dir` (défaut local `%LOCALAPPDATA%\CoursIA\debt-ledgers\\spool\`). Une demande de « `spool --status` » se traite donc par un **audit du répertoire**, pas par un refus : compter les enveloppes, et recouper chacune contre les surfaces du dashboard. +- **Un dashboard a TROIS surfaces, et « absent de l'intercom vivant » ne dit rien des deux autres** : l'intercom vivant, l'**archive de condensation**, et le fichier Drive. Mesuré le 2026-09-26 : une enveloppe spoolée (`obs-5e28f86031aaae2e`) que j'avais déclarée « jamais postée » était en fait **postée puis archivée** par la condensation de 02:36:41Z — l'archive la portait, entière, avec son `observation_id`. Avant de conclure « jamais postée », lire l'archive (`action:"read_archive"`). Même classe que « un `find` qui ne rend rien n'est pas une absence » : un instrument muet ne mesure pas une absence, il mesure son propre silence. +- **Ne pas récupérer par acquittement ce qu'on n'a pas vérifié** : la conclusion « 1 enveloppe manquante » a produit un append de récupération qui s'est avéré un **doublon** d'un message déjà archivé (et qui a levé un `[FORK SUSPECTÉ]` en pure perte). Le contrôle qui l'aurait évitée coûtait un appel : `read_archive` **avant** de re-poster — la règle de l'organe lui-même (« relire le canonique avant tout retry ») nomme l'intercom, mais le canonique inclut l'archive. ## Émission de dossiers — garde-fous obligatoires - **Instrument de mesure des checks (arbitrage ai-01 2026-09-21 + RECTIF 12:09Z)** : lire `commits//check-runs` — **jamais** `actions/runs` (un `attempt=2` y garde l'ancien id plus petit : organe `dedupe_latest`, `scripts/pr_gate.py` l.537, mesure #11416). Dédup **obligatoire** (17 noms dupliqués mesurés sur #16263) par clé canonique `(started_at, id)` dans cet ordre — jamais `created_at`, jamais `id` seul — et **paginer** (`--paginate` : total_count 101 > per_page 100 mesuré sur #16263). -- **DWELL = minuteur, pas un défaut de contenu** : un rouge `PR gate: DWELL -- ... ecoule a Z. Rien a corriger dans le code` ne se répare PAS par push (chaque push ré-arme le plancher 120 min depuis la nouvelle tête) ; un dossier BLOCKED qui le nomme est un livrable valide, le merge suit l'échéance. `gh pr update-branch` ne ré-arme PAS le plancher depuis #16149. Corollaire : `statusCheckRollup` ment sur ~20 % des candidates (mesuré ai-01 2026-09-21) — ne jamais en faire un verdict. +- **DWELL = minuteur, pas un défaut de contenu** : un rouge `PR gate: DWELL -- ... ecoule a Z. Rien a corriger dans le code` ne se répare PAS par push (chaque push ré-arme le plancher 120 min depuis la nouvelle tête) ; un dossier BLOCKED qui le nomme est un livrable valide, le merge suit l'échéance. `gh pr update-branch` ne ré-arme PAS le plancher depuis #16149 ; en revanche une **résolution de conflit** (merge de `main` dont l'arbre diffère de l'auto-merge des parents) le **ré-arme** — mesuré le 2026-09-26 sur #17800, où la résolution d'ai-01 a repoussé la stabilisation de `PR gate` de 2 h. C'est ce qui produit la forme la plus déroutante : `PR gate` **reste en vol longtemps après que tout le reste est vert**, parce qu'il attend l'échéance *puis* juge — son `started_at` récent n'est pas un signe de lenteur de la PR. Corollaire : `statusCheckRollup` ment sur ~20 % des candidates (mesuré ai-01 2026-09-21) — ne jamais en faire un verdict. +- **Un pliage partiel n'est pas un `latest-wins-green`** : le fold est une photo de ce qui **existe**, pas de ce que la CI **produira**. Avant tout verdict vert, exiger **0 jambe non-`completed`** *et* un compte stable entre deux relevés. Mesuré le 2026-09-26 sur #17800 : 30 noms, **19 verts et 5 en vol** — dont le seul composite `PR gate` — et `residual_reds: []` sur ce relevé ne disait rien. Un `residual_reds` vide est un plancher, jamais un acquittement. - **Le gate en échec imprime sur STDOUT** : sans `--json`, l'échec de `check_adjoint_prevalidation.py` rend `UNKNOWN -- {erreur}` sur stdout — une redirection `--template > file.md` capture cette ligne comme template. Avant tout post : (1) rc=0 du template, (2) `head -1` du fichier = `[ADJOINT PREFLIGHT]`, (3) placeholders `REPLACE_WITH` présents dans le template source. `grep -c REPLACE_WITH = 0` est un **faux-OK** sur une ligne d'erreur. - **Le gate ne lit pas l'état de merge** : un dossier READY exige la vérification `mergeable` côté attestant (CONFLICTING → BLOCKED conflit ; UNKNOWN → HOLD re-mesure). - **Fenêtre rate-limited** : après un refus GraphQL (`rate limit already exceeded` avec buckets pleins = limite secondaire), le fallback REST `gh api repos/.../issues/N/comments --input payload.json` passe (payload `{"body": "..."}` construit hors shell — la forme `-f body=` est interdite, `gh-posting-hygiene` HARD 1) — mais le template doit être **régénéré après** la fenêtre, jamais réutilisé. - **Dossier posé EN DERNIER** : toute prose postée après le dossier le périmé (surfaces-sha256). - **Auto-attestation refusée (HARD)** : le gate refuse un dossier dont la `lane` est **celle de la PR porteuse**, lue dans son tag `Grain:` — une lane ne peut pas attester la PR qu'elle porte. Contre-exemple mesuré : #17422 accepté (porteuse tierce) vs #17051 refusé (porteuse = la mienne), même cycle. Conséquence d'action : une PR verte sans dossier dont la lane porteuse est la mienne **n'est prévalidable que par ai-01** — la nommer pour lui, ne pas la contourner. - **Un dossier tiers à la même tête rend le mien inutile** : avant de poster, vérifier le **dernier commentaire**. Si un `[ADJOINT PREFLIGHT]` intact d'une autre lane porte déjà la tête vive, poster le mien le périme sans rien apporter (mesuré sur #17835) — vérifier que le gate rend déjà 0, et passer le relais à ai-01. -- **`NO-DOSSIER` a deux lectures** : « aucun dossier » (il en faut un) et « dossier existant devenu invalide » (head ≠ tête vive, ou surfaces modifiées). Un dossier dont la tête n'est plus vive compte comme **nit non levé** — la sortie est de **ré-émettre**, pas d'argumenter (§B.0). Lire `errors[]`, jamais `head -1`. +- **`NO-DOSSIER` a trois lectures** : « aucun dossier » (il en faut un) ; « dossier existant devenu invalide » (head ≠ tête vive, ou surfaces modifiées) ; et — mesuré le 2026-09-26 — un dossier **valide** (tête vive, surfaces intactes, `errors[]` vide) dont le **champ bloquant déclaré** n'est plus soutenu par la mesure. Un dossier dont la tête n'est plus vive compte comme **nit non levé** — la sortie est de **ré-émettre**, pas d'argumenter (§B.0). Lire `errors[]`, jamais `head -1`. Le troisième cas se ré-émet **à la même tête** : le gate rend alors 0. Cas type : `checks: blocked` alors que la jambe bloquante a terminé **verte à tête inchangée** (mesuré sur #17831 — `PR gate` success à 03:52:41Z, 80 min après le dossier de 02:32Z ; aucun commit poussé entre les deux). +- **Le paquet READY fait QUATRE organes, et `domain` est le quatrième** — cette skill ne décrivait pas le champ que le contrat de dossier exige (mesuré le 2026-09-26 : `grep -c domain` rendait **0** sur le fichier). Les quatre : `checks` (pliage *latest-wins*, 0 jambe en vol) · `b0` (`check_unaddressed_nits.py`, rc) · `perimeter` (`check_pr_perimeter.py` — **le gate ne le calcule pas**, c'est le seul organe qui attrape un desserrement de baseline sans `--baseline-justified`) · `domain` (crible de **contenu**). `domain` ne se recopie **jamais** de `checks` : recopier des checks verts est exactement le claim non mesuré qui a produit un READY faux (#16953 — identifiants accentués et ré-exécution sans clé passés sous READY). Trois valeurs : `pass` (cribles verts) · **`not-applicable`** (le diff ne touche **aucun carnet** — les cribles mesurent la prose et les sorties de carnets, ils n'ont pas d'objet : le dire est la réponse exacte) · `fail` (un crible trouve un défaut réel). Un `fail` sur un carnet **nouveau** est invisible à la CI : sans jumeau, l'advisory de désaccentuation ne juge rien (#17623) — mesuré le 2026-09-26 sur #17825, où `domain: fail` portait sur 2 caractères accentués sur 5620 contre 305 sur 12054 au contrôle de la même famille, toutes les jambes vertes par ailleurs. - **Ledger : poster `d["content"]` avec `messageId = observation_id`** — l'idempotence du CLI est portée par cet id ; un id dérivé à la main crée un doublon silencieux. - **Une note postée APRÈS le dossier lui est invisible** : `_strip_adjoint_dossier` coupe le corps **jusqu'à sa fin**, donc un commentaire — ou un `CHANGES_REQUESTED` — collé après un bloc dossier ne compte pas comme réserve et l'organe rend `rc=0`. Le pire des deux mondes : perdue pour le gate, lue par l'humain. Une observation qui ne doit ni lever ni réserver se poste dans un commentaire **séparé, AVANT** le dossier. @@ -122,7 +127,7 @@ La proposition `act_kind` est donc **mise en attente mesurée**, pas ajoutée au ## Amélioration continue (mandat user 2026-09-21) -« Gardez sous le coude l'amélioration continue, et mettez à jour vos skills régulièrement. » Chaque tell fondateur mesuré en cycle (garde-fou manquant, anti-pattern, instrument faux) est consigné sur le dashboard **puis** reporté dans cette skill par PR dédiée — pas d'édition directe de `main`. Trois défauts muets à chercher en priorité : un instrument qui réimplémente un organe existant (`git grep` le geste dans `scripts/` avant d'écrire du jq de verdict), une absence observée sur un échantillon prise pour une propriété de l'API, une forme d'appel gh non canonique (`-f body=` interdit, `gh-posting-hygiene` HARD 1). +« Gardez sous le coude l'amélioration continue, et mettez à jour vos skills régulièrement. » Chaque tell fondateur mesuré en cycle (garde-fou manquant, anti-pattern, instrument faux) est consigné sur le dashboard **puis** reporté dans cette skill par PR dédiée — pas d'édition directe de `main`. Quatre défauts muets à chercher en priorité : un instrument qui réimplémente un organe existant (`git grep` le geste dans `scripts/` avant d'écrire du jq de verdict), une absence observée sur un échantillon prise pour une propriété de l'API, une forme d'appel gh non canonique (`-f body=` interdit, `gh-posting-hygiene` HARD 1), et une **condition de reprise qu'aucun instrument ne mesure** — elle ne peut jamais être constatée atteinte, donc elle diffère indéfiniment contre la volonté de qui l'a posée. Cas mesuré le 2026-09-26 : une lane attendait un « arbitrage d'ordre de merge » d'ai-01 pendant qu'un `gh pr view --json state,mergedAt` aurait montré que la PR d'en face était mergée depuis ~23 h. Rendre la condition **observable** est plus petit que le travail différé, et le débloque entièrement. ## Cron