Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/skills/coordinate-adjoint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Chaque `[ADJOINT PREFLIGHT]`, `[ADJOINT VERIFIED]` et `[ADJOINT CLOSE]` publié

### Schéma réel (vérifié firsthand c.8)

- **Ledger unique** : `issue-debt` (le seul déclaré dans `LEDGERS`). **Pas de `pr-actions`** — la forme de cette section évolue quand la phase B (#16575) ajoute éventuellement d'autres `LEDGERS`.
- **Deux `LEDGERS` déclarés — au code, pas dans cette prose** : `LEDGERS = ('issue-debt', 'gpu-reservation')` (`scripts/coordination/debt_ledger.py` l.154, vérifié le 2026-09-26). `issue-debt` porte une ligne par `owner/repo#N` ; `gpu-reservation` une ligne par device (`<machine>#gpu<n>`, #16737). **Pas de `pr-actions`** — la forme évolue quand la phase B (#16575) ajoute éventuellement d'autres `LEDGERS`. Cette section a dit « ledger unique » jusqu'au 2026-09-26 : le CLI en déclarait deux depuis #16737, donc la prose était fausse et un lecteur qui s'y fiait refusait `--ledger gpu-reservation` à tort. La liste se lit au code.
- **Fields autorisés** (rejet bruyant sinon) :
- `state_class` enum `open-actionable | open-blocked | open-stale | deferred | closed | unknown`
- `closeability` enum `closeable-now | closeable-after-followup | not-closeable | unknown`
Expand All @@ -59,6 +59,19 @@ Chaque `[ADJOINT PREFLIGHT]`, `[ADJOINT VERIFIED]` et `[ADJOINT CLOSE]` publié
- **Préconditions** : (i) `git ls-tree -r origin/main | grep debt_ledger` rend du code → phase A mergée ; (ii) le dashboard `CoursIA-issue-debt-ledger` existe côté roosync. Tant qu'une manque : `ledger: N/A (phase A non mergée)` dans le rapport — pas de skip silencieux.
- **Outil** : `python scripts/coordination/debt_ledger.py init --state-dir <LOCALAPPDATA>\CoursIA\debt-ledgers --apply` (jamais sous `$ROOSYNC_SHARED_PATH` ou dans le repo).

### Le spool n'est pas un post — 26 observations ont dormi huit jours (mesure du 2026-09-26)

`debt_ledger.py append --out-dir …` **spool** l'enveloppe et **imprime l'instruction** de post. Il ne poste pas, et **aucun organe ne mesure l'écart**. La phrase « journalisé comme observation `[OBS]` via le CLI » couvre donc **deux gestes dont un seul était fait** — c'est la source du trou ci-dessous, et elle est d'**organe**, pas de vigilance : le CLI dit « spooled », jamais « journalisé ».

Mesure du 2026-09-26 : le spool local portait **26** fichiers `obs-*.json` du 18/09 au 26/09, jamais postés — dont **21** en `state_class: closed` datés du 18/09, la forme d'un backfill de clôtures. Les **25** enveloppes valides sont désormais postées et relues (25/25 des deux côtés, `totalMessages` 810 → 835). **Le solde du backfill n'est pas établi pour autant** : 21 ≠ 33, et rien ne dit que ces 21 sont les mêmes que les 33. Ne pas lire ce drain comme l'achèvement de la mission enregistrée.

**Lecture de cycle, en attendant un `spool --status`** (compte + plus ancien) : compter les `obs-*.json` du spool (hors `.superseded-*`) et poster ce qui reste — l'append est **idempotent par `messageId`**, donc rejouer ne coûte rien. Quatre pièges du même geste, tous mesurés :

- **L'id se lit dans le CONTENU, jamais dans le nom du fichier.** Deux fichiers portaient `obs-obs-<hex>.json` quand leur contenu déclare `observation_id = obs-<hex>` ; or `debt_ledger.py` l.1838 dérive le nom **de** l'id (`f"{observation_id}.json"`), donc ces deux-là ne viennent pas de ce chemin. Un id pris au nom de fichier pose un `messageId` que rien ne dédoublonnera.
- **`messageId = observation_id` dédoublonne par CONTENU, pas par entité.** Deux passages sur la même PR avec une chaîne `evidence` différente produisent deux ids, donc **deux observations** de la même entité à la même heure. Mesuré sur #17836 : `8fd6b8ed89…` complet contre `8fd6b8ed` tronqué. Un `evidence` plus court n'est pas une observation plus légère — c'est une seconde observation.
- **L'append concurrent est sûr ; le `messageCount` qu'il rend ne l'est pas.** Un lot de cinq appends parallèles a rendu 34, 35, 36, **36**, 37 : un relevé périmé sur une écriture concurrente, **pas** une perte (l'énumération des ids du markdown canonique rend 25/25, et les sept appends suivants sont monotones). Le décompte n'est pas une preuve de sérialisation — **l'énumération des ids** l'est.
- **Un `[FORK SUSPECTÉ]` peut être une latence, pas un fork.** Le même lot l'a levé sur **une** écriture sur cinq, les deux suivantes étant propres. Le contrôle décisif n'est pas de re-poster — l'idempotence absorberait le doublon **en silence** — mais de comparer les ids du markdown canonique à ceux d'une relecture par l'outil.

### Forme CLI réelle

```bash
Expand Down Expand Up @@ -107,6 +120,9 @@ La proposition `act_kind` est donc **mise en attente mesurée**, pas ajoutée au
- **`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`.
- **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.
- **Une narration de blocage est reclassée BLOCK par sa seule TÊTE (mesuré 2026-09-26, #17743)** : `check_unaddressed_nits._block_emitted` lit les **60 premiers caractères** dé-accentués du corps et y cherche `BLOCAGE` / `BLOCK` en position de verdict. Un titre ouvrant par « Cause du **blocage** de #N » pose donc un blocage à son propre nom : `classify()` rend `BLOCK`, l'organe passe `blocked: true`, et la PR perd son `b0: clear` — le contenu du commentaire ne change rien, c'est la **position** qui décide. Parade : nommer le fait sans le mot en tête (« Cause du non-merge de #N »), mesurer par `classify('jsboige', body)` **avant** de poster (l'organe s'importe depuis `scripts/`), puis `PATCH` du corps si le mot a mordu — un `PATCH` conserve le `createdAt` et ne ré-arme rien.
- **Un commentaire de la même identité ne périme PAS un dossier** : `_is_own_later_act` neutralise **inconditionnellement** tout commentaire posté sous `jsboige` ou `myia-ai-01` après le dossier (`row_kind="comment"` ; #16883 — l'identité partagée couvre les deux voix coordonnateur). Conséquence d'action : une mesure à porter sur une PR **déjà attestée** se poste en commentaire **sans** ré-émettre de dossier — c'est le canal pour nommer ce que le contrat ne peut pas porter (cf. le champ bloquant vide du cas conflit). Réserve **inverse** : ce commentaire reste évalué par B.0, donc il se mesure avant d'être posté.
- **Un lot de dossiers vieux d'un jour se re-mesure avant d'être traité (mesuré 2026-09-26)** : sur un lot de 15 PRs dispatché la veille, **13 étaient mergées** et les 2 vivantes portaient **déjà** un dossier intact à la tête vive — le lot était intégralement digéré, et le traiter aurait refait un travail fait. Même classe : deux sollicitations d'attestation visaient des PR **déjà mergées** (#17724, #17780). Un `gh pr list --state all` sur le lot entier, ou un appel GraphQL à alias multiples, ferme le cas en **un** appel ; traiter un lot sans l'avoir re-mesuré fabrique du travail fantôme et immobilise la lane demandeuse, qui attend une attestation devenue sans objet.

## Lire un rouge avant de le nommer — tells c.43-c.44

Expand Down
Loading