# Secrets hygiene & coordinator discipline — detail



Detail des deux regles HARD :
- [.claude/rules/secrets-hygiene.md](../../.claude/rules/secrets-hygiene.md)
- [.claude/rules/coordinator-discipline.md](../../.claude/rules/coordinator-discipline.md)



## 1. Secrets hygiene — content-based, pas path-based



### Incident recurrent



2026-05-14 : `scripts/whisper_int8_comparison.py` (commit `b34e3a05` direct main par po-2023) — cle API GenAI service hardcoded comme **fallback `os.getenv("KEY", "<literal-secret>")`**. NanoClaw l'a attrape post-hoc sur PR #1071, mais la push direct main avait deja landed le secret. **Recurring, pas isole.**



### Pourquoi `.gitignore` ne suffit pas



`.gitignore` du repo : `.env*`, `.secrets/`, fichiers tokens nommes — tous ignores. Mais protege uniquement les **fichiers dedies au secret**.



Un agent ecrivant un secret **a l'interieur d'un `.py`/`.ipynb` normal** contourne `.gitignore` entierement : le fichier lui-meme n'est pas un fichier secret. Pas de pre-commit hook ni CI scanner a date 2026-05-14 (pas de gitleaks/trufflehog/detect-secrets, pas de `.pre-commit-config.yaml`).



Conclusion : path-based `.gitignore` + review bot post-hoc = structurellement insuffisant. Ne peut pas attraper les literaux inline avant qu'ils landent, surtout sur push direct main.



### Anti-patterns interdits



Tous ces patterns sont des secrets inline (interdits) :
- `API_KEY = "sk-..."`, `TOKEN = "ghp_..."`, `KEY = "AIza..."`
- `os.getenv("KEY", "<actual-secret-as-fallback>")` — fallback DEFAULT ne doit JAMAIS contenir le secret
- URLs avec credentials integres : `https://user:password@host/...`
- Tokens en commentaires, meme en exemple : `# example token: sk-...`



### Pattern correct



```python
key = os.getenv("API_KEY")
if not key:
    raise ValueError("API_KEY environment variable required")
```



### Si secret commit accidentel detecte



- **Ne PAS faire `git revert`** (l'historique contient toujours le secret).
- Creer une nouvelle branche clean depuis avant le commit fautif via cherry-pick des autres commits.
- **Rotater immediatement la cle compromise** chez le provider (le secret est sur le repo public, considere comme leak).
- Documenter dans dashboard + issue + memo postmortem agent responsable.



### Postmortem responsable



Obligatoire pour l'agent ayant commit le secret :
1. Analyse de comment le secret est arrive dans le code (etourderie ? methodologie defaillante ?)
2. L'agent construit lui-meme le garde-fou (ajout au pre-commit, doc, exemple corrige)
3. Review independante par un autre agent ou par le bot reviewer



### Fix structurel en cours



Tracking GitHub issue : ajouter un **content-based secret scanner**
- (a) pre-commit hook (gitleaks ou detect-secrets)
- (b) CI workflow gate qui bloque merge



Scan **tous** les fichiers trackes (pas juste les paths suspects).



### Detection manuelle pre-commit



```bash
git diff --cached | grep -E "(sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36}|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|eyJ[A-Za-z0-9_-]{20,}\.eyJ)"
```



Si match : NE PAS commiter, identifier le secret, deplacer en `.env`, ajouter `.env` au `.gitignore` si pas deja, rotater la cle si push deja fait.



### 1.6 Stop & Repair — JAMAIS hand-editer une SORTIE de cellule (regle 6, mandat user 2026-06-22)



Scrubber / redacter / maquiller a la main une **sortie de cellule** (le champ `outputs` d'une cellule code = compte-rendu de ce que le code a reellement produit) est **BANNI** : c'est **falsifier la preuve d'execution = malhonnete**. A la prochaine re-execution le materiel resaute de toute facon. Quoi que contienne l'output indesirable (secret, prefixe de cle, chemin machine, render casse, bruit), la **seule** voie honnete est d'en **corriger la cause** puis de **RE-EXECUTER** (regle ancestrale Stop & Repair, cf CLAUDE.md section F).



La cause tombe dans l'un de TROIS cas (le scanner ne les distingue PAS — c'est a l'agent de trier la CAUSE, pas de toucher l'output) :



- **(A) env / cwd** — chemins filesystem benins imprimes dynamiquement (racine de checkout, AppData, pip "already satisfied", ipykernel/nuget regeneres). → **re-executer dans un cwd normalise** (papermill `--cwd <dir-du-notebook>`) ou corriger la source pour imprimer le `basename`/chemin relatif. Pas de scrub manuel de la sortie.
- **(B) outil manquant / cellule cassee** — le chemin est dans un **message d'erreur / exception / "introuvable"** (`process 'dot' ... introuvable` + `.gv save path` quand Graphviz absent, `FileNotFoundException`, assembly conflict). → cellule en ERREUR maquillee. **INSTALLER l'outil + re-exec** (RECOVERABLE-LOCAL, cf sota-not-workaround §F/§H), jamais scrubber le chemin dans l'exception.
- **(C) source-leak** — code source qui **imprime ou hardcode** du materiel (`print(f"...{key[:8]}...")`, `print(f"KEY={key}")`, `Console.WriteLine($"...{filePath}")`, `os.environ.get("VAR", "<machine-path-default>")`). → **corriger la source** (masquer : `'configuree' if key else '(non configuree)'`, jamais `key[:N]`/`key[-N:]` ; imprimer le `basename`, pas le chemin absolu ; supprimer le default machine-path) **puis re-exec**.



**Seules normalisations manuelles tolerees (PAS un scrub d'output)** : (1) **`metadata.papermill.input/output_path`** — c'est de la **metadata notebook, PAS une sortie de cellule** ; la mettre au `basename` est OK (ideal : la produire propre a l'exec via input/output relatifs). (2) **Quantbooks QC** — le MCP qc-mcp ne permet toujours PAS de les executer, ils passent par le Research Assistant (Playwright / lean-cli), la manipulation manuelle des sorties y est la realite acceptee. (3) **`probeAddresses` banner strip** post-re-execution .NET Interactive — l'enumeration `System.Net.NetworkInformation.NetworkInterface` que le runner emet en banniere **leak les interfaces reseau de la machine** ; elle se retire par l'organe `scripts/notebook_tools/strip_probe_banner.py --apply <path>`, a passer **systematiquement apres toute re-exec .NET, avant commit** (cf L532 en memoire locale). Ce n'est pas un scrub de resultat : la banniere n'est pas une sortie du code de la cellule, c'est du bruit d'infrastructure injecte par le kernel. **Pour tout le reste, execution reelle obligatoire.**



**Tell infaillible** : sortie de cellule indesirable → corrige la CAUSE (A env/cwd, B installe l'outil, C corrige la source) + **re-exec**, jamais hand-edit. `metadata.papermill` ou quantbook QC → seules normalisations manuelles tolerees.



**Critere bot-review (HARD)** : toute PR qui **hand-edite une sortie de cellule** (scrub d'un secret / prefixe-de-cle / chemin machine / render casse) hors `metadata.papermill` et hors quantbook QC = `CHANGES_REQUESTED`. Une redaction-output **seule**, sans correction de la cause + re-exec, = falsification = refus. Un `APPROVED` dessus = complaisance (cf pr-review-discipline §H).



**Incidents fondateurs** : masque `key[:N]` lignee redaction-only #3903 corrigee par #3913 ; scrub-output-paths Probas/Infer + ML.Net #3952/#3953/#3955/#3956 (Graphviz `dot` jamais installe + `WriteLine($"...{filePath}")`) corriges par re-exec reel #3958/#3959/#3960 (2026-06-22). L'echec 2026-06-22 = coordination (dispatch scope "scrub N fichiers" au lieu de "corriger les scripts / l'env") **ET** bots ayant laisse passer la lignee redaction-only.



---





### 1.7 Pourquoi le numero de version gitleaks ne se recopie pas — incident #10143 (2026-08-10)



> Deporte de `.claude/rules/secrets-hygiene.md` le 2026-08-21 (issue #12051). La **prescription** reste dans
> la rule ; ce qui suit est la justification datee qui l'explique.



La ligne de la rule a porte `v8.21.2` longtemps apres que les deux surfaces reelles soient passees a
**8.24.3** (`.pre-commit-config.yaml` `rev:` et `GITLEAKS_VERSION:` dans le workflow, que le workflow compare
lui-meme et fait echouer en cas de drift). Consequence mesuree le 2026-08-10 sur #10143 : un scan lance sous
8.21.2 en le croyant « la version epinglee » a renvoye **0 finding** la ou 8.24.3 en trouvait **37** deux
jours plus tot — dont, a l'epoque, ~7 credentials reellement fuites. Un scan sous un binaire different n'est
pas une mesure de ce que la CI applique : c'est une mesure d'autre chose, et elle **paraissait rassurante**.



## 2. Coordinator discipline (ai-01)



### 2.1 Merge actif (directement sous `myia-ai-01`)



Le compte `myia-ai-01` **a** le droit `MergePullRequest` (GraphQL) sur `jsboige/CoursIA` — verifie firsthand cycle 2026-08-08 : 6 merges consecutifs reussis sous `myia-ai-01` sans aucun `gh auth switch` (#10087, #10086, #10055, #10015, #10061, #10080). La protection de branche renvoie `404` (pas `403`) sous `myia-ai-01` (cf #9991) : le compte admin `jsboige` reste requis pour **lire/modifier cette protection de branche**, PAS pour merger.



Workflow batch merge quand 1+ PR(s) CLEAN MERGEABLE + APPROVED s'accumulent :



```bash
# 1. Confirmer le compte actif
gh auth status   # doit montrer myia-ai-01



# 2. Audit pre-merge G.6 + merge DIRECTEMENT sous myia-ai-01
gh pr merge <N> --squash   # x N PRs



# 3. Post dashboard ack (liste merges + commits hash)
```



`gh auth switch -u jsboige` est reserve aux operations qui l'exigent reellement : lecture/ecriture de la protection de branche, et le cas des PRs etudiantes (cf [student-pr-reviews.md](../../.claude/rules/student-pr-reviews.md)).



**Anti-pattern interdit** : laisser une PR "pending user merge" dans todo sans tenter le merge soi-meme. Le coordinateur (ai-01) merge — regle CLAUDE.md A. Ne pas confondre "permissions GitHub absent" avec "interdiction de merger".



**Note historique** : une version anterieure affirmait que `myia-ai-01` n'avait pas le droit `MergePullRequest` (constate absent le 2026-05-19 sur #1278/#1279/#1281 ; user feedback 2026-05-19 02:24Z : "Fais un switch pour merger toi meme stp") et prescrivait `gh auth switch -u jsboige` -> merge -> switch back. Le droit a ete accorde entre temps ; le switch mute un etat global au process `gh` qui entre en course avec toute autre session/sous-agent (cf [model-delegation.md](../../.claude/rules/model-delegation.md) regle 6) — le prescrire inutilement imposait un risque de corruption d'auth sans contrepartie.



**Exception** : PR notebook → regle H.4 (`git checkout` + Papermill local + verify `execution_count`) AVANT merge.



### 2.2 Aucune demande user ne pourrit > 1 cycle



Source incident : User 2026-05-16 ~09:45Z apres ma proposition (1ere fois) de cloner repo upstream mmaaz-git :



> "Oui bien sur, je t'ai demande ca depuis plusieurs jours."



L'action (`git clone --depth 1` + `cp` + commit) prenait ~5 min. Echec de coordination.



### Triage en 3 categories



| Categorie | Critere | Action |
|-----------|---------|--------|
| **Trivial** | < 30 min, action locale | Executer dans la session courante. Pas d'excuse "on verra prochain cycle". |
| **Dispatchable** | 30 min - quelques heures | Creer immediatement dispatch agent + post dashboard `[INFO]` + acker user dans tour suivant ("dispatche a X, ETA Y") |
| **Strategique** | Multi-day, vraie reflexion architecturale | Creer immediatement GitHub issue avec scope + acceptance + label + ack user |



### Anti-patterns explicites interdits



- **"Je note pour prochain cycle"** sans tracking concret (issue/dispatch/dashboard line) — oubli deguise en planification
- **Recycler la demande a travers plusieurs `MEMORY.md` updates** sans action — cosmetique
- **Attendre que user re-formule pour agir** — complaisance, le user a deja paye le cout d'attention



### Verification fin de session



Avant `[DONE]` dashboard, parcourir les N derniers messages user et confirmer que chaque demande a soit :
- (a) ete executee
- (b) eu un dispatch concret trace
- (c) eu une issue ouverte
- (d) ete explicitement reportee avec justification ecrite acceptee par user



**Bonus culture du doute** (G.9) : se demander explicitement "qu'est-ce que le user a demande que je n'ai pas encore fait ?" est un meilleur reflexe que "qu'est-ce qui reste a faire dans mon plan ?". Le plan du coord ne survit pas au feedback user qui sait mieux ce qui est prioritaire.



### 2.3 Coordonner CHAQUE lane independamment (Regle 3)



Source : mandat user 2026-06-14.



Il existe **deux dashboards workspace** : `workspace-CoursIA` et `workspace-CoursIA-2`. **Aucun n'est "le dashboard du coordinateur"** — ce sont deux lanes co-egales. Un `lane` = **machine x workspace** : chaque machine worker qui a une lane CoursIA-2 a **AUSSI** une lane CoursIA :



| Machine | Lane CoursIA | Lane CoursIA-2 |
|---------|--------------|----------------|
| po-2026 | Lean (Conway/Knot) | Grothendieck |
| po-2024 | QuantConnect | MGS/.NET |
| po-2025 | Python/ML | .NET/Argumentum |



Chaque cycle `/coordinate`, ai-01 doit **LIRE *et* POSTER une coordination lane-specific sur LES DEUX**, independamment :
- **Lire** `section:"all"` des deux dashboards — sinon les ASKs/blockers d'une lane restent invisibles.
- **Poster** un contenu **propre a chaque lane** — **jamais de broadcast miroir** (copier-coller identique).
- **Anti-pattern interdit** : traiter `workspace-CoursIA` comme « le mien » et `workspace-CoursIA-2` comme « celui des workers ». Les deux portent du travail worker.
- **Piege duplication cross-lane** : un worker peut dedoubler une livraison sur ses deux lanes (ex #2950 sur CoursIA + #2952 sur CoursIA-2 pour le meme #1027). Reconcilier explicitement : 1 canonique mergee, l'autre disposee.



**Incident origine** : un cycle entier ou seule la lane CoursIA-2 a ete coordonnee (broadcast miroir), ratant l'ASK greenlight PR1.5 Knot #2874 de po-2026:CoursIA et le rapport C.2 de #2953 (po-2025:CoursIA).



### 2.4 JAMAIS sanctionner une lane idle — deep-queue + fallback perenne (Regle 4)



Source : incident 2026-06-16 — pendant une session Sudoku-13 longue, ai-01 a poste « po-2026 honest-idle legit / po-2024 await dispatch / po-2025 nearing exhausted ». Les workers ont **cite cette sanction** comme licence pour stand-down. User (verbatim) : « tu n'as pas prevu de deep queue comme on a dit, ni d'idle lane pour qu'il n'y ait jamais rien a faire ». S'applique **meme quand ai-01 est mobilise par le user** sur une autre tache longue.



1. **Interdit de sanctionner l'idle.** ai-01 ne poste **JAMAIS** un statut de lane terminal-idle (« honest-idle legit », « lane exhausted/closed », « await dispatch ») **comme etat final**. Une lane idle = **echec ai-01**, pas un etat acceptable du worker (cf [[never-close-a-lane-feed-deep-queue]]).
2. **Deep queue = ~5-10 steps ordonnes par lane, sur le dashboard.** Assez profonde pour couvrir plusieurs cycles meme si ai-01 est absent plusieurs heures. Pre-autorisation explicite : « brule-les **dans l'ordre**, NE m'attends PAS entre steps, ASK seulement sur **blocker reel** ou **queue vide** » (cf [[feedback-dispatch-fill-spare-capacity]]).
3. **Fallback perenne never-empty par famille.** Quand la deep-queue Epic d'une lane est epuisee, le worker **NE rapporte PAS idle** : il tombe sur le fallback perenne de SA famille. **Un tracker FERME ne ferme pas le rollout** : le travail repo-wide famille-partitionne persiste meme quand l'issue-Epic de suivi est CLOSED — ne JAMAIS lire « issue closed » comme « fallback indisponible » (incident recurrent 2026-06-25 : 3 lanes ont rapporte idle en citant #2651/#2161 CLOSED, alors que prose et 3-exercices restent a faire notebook par notebook). **Quand un tracker LIVE est ferme a son tour, ai-01 le remplace par son successeur OPEN — ce point ne pointe JAMAIS vers une issue CLOSED.** Rollouts perennes LIVE (a re-verifier `gh issue view` avant citation, ils derivent) :

   - **#3973 feuilles-READMEs ascendantes** (filles par famille : #3974 GenAI+Python, #3975 SymbolicAI-non-Lean+Sudoku+GameTheory+CaseStudies, #3976 QuantConnect, #3978 SymbolicAI/Lean, #3979 Knot/Grothendieck). Successeur LIVE de #2651 (prose pedagogique, CLOSED).
   - **#4212 finition editoriale** (aeration prose + visuels Mermaid/GenAI, Part of #4208).
   - **#3870 backfill FR des READMEs EN** (#1650 Phase 0.5, ~15 READMEs, preserve `README.en.md`).
   - **#2876 accents manquants** dans les READMEs francophones (repo-wide).
   - **Convention 3 exercices/notebook** (`.claude/rules/three-exercises-per-notebook.md`, tracker #2161 CLOSED mais rollout a faire notebook par notebook).
   `[CLAIMED] <item> — <machine:workspace> <ts>` avant de commencer (anti-double-claim).
4. **Token economy ne ferme jamais une lane Lean/research.** L'economie de tokens est **Anthropic-only** (cf [[feedback-token-economy-anthropic-only]]) ; sur les workers z.ai/GLM/MiniMax elle n'est PAS une contrainte. Un worker qui HOLD du Lean/proving en citant « token economy » se trompe — decomposer les sorries jusqu'au noyau dur irreductible (1-2 genuinement intractables OK a laisser, documentes). ai-01 ne valide pas un HOLD Lean motive par l'economie.



### 2.5 Le STEER doit ATTEINDRE le worker, etre VRAI, et DECIDER (Regle 5)



Source : mandat user 2026-06-26 (« je ne veux PLUS JAMAIS trouver de workers oisifs ... tes dispatchs longue queue ne tiennent pas, tes taches idle ne tiennent pas, ou ta facon de coordonner n'est pas la bonne »). Diagnostic firsthand : un worker mature firsthand-sature, qui refuse correctement de fabriquer du filler (G.9 + anti-filler), reste oisif quand le coordinateur (a) poste ses steers sur l'intercom dashboard (qui **auto-condense/archive chaque cycle**), (b) les redige depuis le **status CONDENSE** (qui hallucine : confond un `[DONE]` avec un merge, pointe des issues CLOSED), et (c) **defere** les design-gates. Resultat = steers **phantom** (4 cycles de suite), le worker brule ses cycles a les refuter. **L'idle est un echec COORDINATEUR, jamais worker.**



Les 4 mecanismes correctifs (chaque cycle, par lane) :



1. **Livrer la decision en DIRECT MESSAGE** (`roosync_messages send`/`reply` vers `machine:workspace`), **pas seulement** un append dashboard. Le worker lit `inbox status:unread` EN PREMIER ; un message direct y atterrit et **survit a la condensation**. Un steer qui ne vit que dans l'intercom est archive avant le prochain wakeup = il « ne tient pas ». Le dashboard reste le backstop persistant + la **sonnette** `[DISPATCH->inbox]` (cf [[feedback-double-dm-with-dashboard-notif]]), **pas** le canal de decision.
2. **Grounder chaque grain firsthand AVANT de dispatcher.** `gh issue view N` / `gh pr view N --json state,mergeStateStatus` sur CHAQUE issue/PR cite — **jamais** depuis le status condense (cf [[verify-before-claiming]], `stale-steer`). Si le worker a firsthand-rapporte une saturation, **le croire** et fournir un grain HORS de ce scope, pas re-pointer le scope sature.
3. **DECIDER les design-gates, ne pas deferer.** Quand un worker surface des options de design avec son investigation firsthand, **trancher dans le cycle** — un greenlight nomme + acceptance. Deferer = fabriquer de l'idle.
4. **La premisse « fallback perenne jamais vide » FAILE pour les familles MATURES.** Les rollouts mecaniques famille-partitionnes (§2.4) **s'epuisent reellement** par famille — un worker peut etre genuinement 100% conforme (verifiable G.1). Quand c'est le cas, **ne pas re-pointer le fallback** (= phantom §2.5.2). Deux sources never-empty restantes :

   - **(a) CREATION SUBSTANTIELLE** scopee-coordinateur : nouveaux lakes Lean (#4038), audits axe-2 SOTA (#3801), consolidation lakes (#4362), nouveaux notebooks. ai-01 maintient cette queue **stockee en avance**.
   - **(b) DIVERSITE DU BACKLOG EN SOUFFRANCE** (mandat 2026-06-26, cf [[diversity-backlog-aged-issues]]) : dizaines d'issues anciennes/priority-low/EPICs partiels a **avancer au compte-goutte** (#16, #417, #569, #1206, #1210, #2137, #3436…). **Piocher une tranche concrete par cycle** (pas l'EPIC entier), **varier genres ET familles**, ne JAMAIS tunneliser. Grounder firsthand avant de dispatcher.



**Tell d'auto-detection** (avant de poster un steer) : « (a) ce grain est-il verifie OPEN/non-sature firsthand a l'instant ? (b) la decision atteint-elle l'inbox du worker ? (c) ai-je tranche, ou defere ? ». Trois oui requis. Sinon = phantom, le worker idlera.

### 2.6 Garde d'identite de lane — recit, arbitrage et pieges (2026-09-11)

> Deporte de [`.claude/rules/coordinator-discipline.md`](../../.claude/rules/coordinator-discipline.md)
> le 2026-09-13 (#15204, slimming du harnais auto-charge). **La garde n'a PAS ete deportee** : la table
> des trois mesures, l'invocation de l'organe, l'avertissement sur `exit 0` et la table de decision
> restent dans la rule, ou elles sont operatoires. Ce qui suit est le recit de l'incident et la
> justification des defauts d'arbitrage — le contexte qui explique *pourquoi* la garde a cette forme,
> pas la garde elle-meme. L'organe reel est `scripts/check_coordinator_identity.py` et ses tests : ils
> portent les deux premieres mesures et **ne coutent rien au contexte**.

**Incident fondateur (2026-09-11).** Un reboot machine a tue la session coordinateur et son cron
(`CronCreate` est session-only, L740). Deux sessions CoursIA se sont retrouvees vivantes sur
`myia-ai-01` sans qu'aucun signal ne dise laquelle devait coordonner — `ListAgents` listait des **noms
de session**, pas des lanes. L'arbitrage s'est regle **par accord** au premier aller-retour : le defaut
deterministe n'a pas eu a jouer, et il ne faut pas lire cet episode comme son precedent. Ce qui a
departage est ce que la table de decision nomme desormais — une session portait le cron arme, l'autre
avait un `CronList` vide.

**Pourquoi les deux criteres de defaut sont asymetriques.** « Celle qui detient deja un cron arme »,
puis « la session demarree le plus tot » : chaque session peut rendre son `CronList` et son heure de
demarrage, donc les deux criteres sont **lisibles des deux cotes**. « Celle qui a detecte la collision
cede » ne l'est pas — une detection **simultanee** ferait ceder les deux et ne laisserait **aucun**
coordinateur, precisement ce que le defaut existe pour empecher.

**La session qui cede change d'arbre, pas seulement de cadence.** Deux sessions sur la meme lane **et
le meme clone** partagent HEAD, l'index et le stash. Le double-cron n'est qu'un probleme de cadence ;
l'arbre de travail partage est un probleme de **corruption silencieuse** — un `checkout` / `rebase` /
`stash` d'un cote pendant une lecture de l'autre ne fait rougir aucune garde
([[concurrent-sessions-share-the-working-tree]]). La session qui cede passe donc en
`git worktree add`, et pas seulement sous `/continue`.

**Ce que la garde ne discrimine pas — mesure du 2026-09-11.** `clone_ok` ne separe que des clones
*distincts*. Deux sessions lancees depuis `D:/CoursIA` rendent toutes deux `exit 0` et le role
COORDINATOR : mesure faite entre `coursia-1c` et `coursia-0f`. Ce qui tranche est **l'aller-retour de
la mesure 3**, jamais le code de sortie de l'organe — lequel le dit de lui-meme en rendant
`uniqueness_measured: false`. Il documente cet angle mort, il ne le resout pas.

**Piege 1 — deux clones partagent une lane.** Sur ai-01, `D:/CoursIA` et `D:/dev/CoursIA` rendent tous
deux `myia-ai-01:CoursIA` : la chaine de lane ne les discrimine pas, seul le chemin le fait. L'organe
porte la racine canonique et **retrograde en worker** (fail-CLOSED) une session lancee depuis le
jumeau.

**Piege 2 — `CronList` est session-locale** ([[session-local-view-read-as-global]]). Une liste vide ne
prouve rien au-dela de la session courante — surtout pas qu'aucun cron de coordination ne tourne
ailleurs sur la machine. **La recurrence est mesuree, et elle n'a pas la forme de la premiere** : le
2026-09-13, deux cadences de coordination tournaient simultanement sur `myia-ai-01`, aveugles l'une a
l'autre — un cycle 4 h arme depuis la lane `CoursIA`, un cycle 6 h arme depuis la lane
`roo-extensions`. La collision n'opposait donc pas deux sessions d'un meme workspace, mais **deux
workspaces d'une meme machine** : la chaine `machine:workspace` les distingue correctement, et
c'est la **vue** qui manquait, pas la mesure. Resolu par une cadence unique. L'ironie vaut d'etre
inscrite : #15648 est la PR qui construit la garde contre exactement cela, et l'incident s'est
reproduit sur son auteur pendant qu'elle attendait d'etre mergee.

Enchainer avec [[handover-must-disarm-outgoing-cron]] : **desarmer la cadence sortante d'abord**,
armer ensuite.

### 2.7 Registre re-parcouru des arbitrages user en attente (Regle 7)

Source : convergence des harnais roo-extensions <-> CoursIA (Epic roo-extensions#3111 candidat n°5, VERIFIE passe 3 du 2026-08-20 ; livraison roo-extensions#3677, 2026-09-15). Le raffinement de l'audit : ce n'est pas un formalisme qui manquait a CoursIA, c'est un **registre re-parcouru**.

**Preuve mesuree** (audit passe 3, VERIFIE — dashboards live 20/08 + archives 13-15/08) :

- roo-extensions : section recurree par cycle coordinateur « En attente d'arbitrage user (aucune action prise) » — ~31 % des archives dashboard en portent une mention ; un item ne peut pas disparaitre silencieusement.
- CoursIA : le coordinateur auto-arbitre d'abord (atout de debit, CONSERVE — non-but explicite de la convergence) mais la trace des demandes user en attente vivait en dette personnelle en texte libre. Verbatim ai-01 (archives 13-14/08) : « les 4 arbitrages restes en l'air (#7257, #7266, #7298, #6711) — un [ASK USER] jamais arrive pre-merge [...] c'est ma dette, pas celle des lanes ». Un [ASK USER] « jamais arrive » est precisement la panne que le format roo-ext previent.

**Le mandat user du 15/09 demandait deja la moitie du patron** — donne dans CE workspace (machine ai-01) : « je prefere que tu gardes tes questions pour la fin de session, et si jamais le cron reprend, que tu les gardes tant qu'elles sont pas repondues dans une memoire que tu dois restituer en fin de session. Ca va demander une MAJ du harnais global en coordination avec roo-extensions » (roo-extensions#3656). Cote roo-ext, la moitie registre est codifiee (registre open-questions, roo-extensions#3657) ; cette regle en est le portage CoursIA, augmentee de la moitie roo-ext historique : la **liste re-parcourue par cycle**.

#### Les 3 invariants

1. **Registre persistant** — fichier `.claude/local/arbitrations-user.md` sur ai-01 (gitignore : `.claude/local/`). Format d'entree :

   ```markdown
   ## 2026-09-15 — sign-off regle convergence candidat n°5
   - Demande : [ASK] poste cycle du 15/09 (dashboard CoursIA + DM user)
   - Bloque : merge de la PR concernee (gate pre-merge) ; 2 options A/B
   - Relances : 15/09 · 16/09
   - Sortie : TRANCHEE option B le 17/09 (reponse DM user) — archivee
   ```

2. **Re-parcours chaque cycle** — section recurree du bilan `/coordinate`, sur LES DEUX dashboards (Regle 3) :

   ```markdown
   ### En attente d'arbitrage user (aucune action prise)
   - sign-off regle X (depuis 15/09, 2 cycles) · arbitrage sauvegarde Y (depuis 13/09, 5 cycles)
   ```

   Le dashboard se re-condense ; le registre est la **source**, la section n'en est que la representation. La demande survit a la condensation parce qu'elle ne vit pas uniquement dedans.

3. **Sortie sur reponse user explicite uniquement** — jamais auto-expiree (l'age d'un item est un signal de RELANCE, pas de purge : plus il vieillit, plus le re-parcours le met sous les yeux du user), jamais retiree silencieusement. La reponse est datee et tracee dans l'entree, puis l'entree descend en fin de fichier (section archive). Rien n'est efface.

#### Frontiere exacte (non-buts)

- **L'auto-arbitrage coordinateur reste la norme** (Regle 5.3 : decider, ne pas deferer). Le registre ne couvre QUE les decisions qui EXIGENT le user : sign-offs de regles, arbitrages de politique, approbations que le mandat reserve au user, blocages budget/permissions. Tout le reste se tranche dans le cycle, comme avant.
- La Regle 2 couvre l'entrant (demandes user -> action) ; la Regle 7 couvre le sortant (escalades coord -> user). Ensemble : « aucune demande user ne pourrit > 1 cycle » **dans les deux sens**.

#### Verification apres N cycles (critere R2)

- **Chaque fin de cycle (rapide)** : comparer la section « En attente d'arbitrage user » du bilan precedent au registre — tout ecart non justifie par une reponse user tracee = violation.
- **Convergence (apres N cycles d'application)** : dans les archives dashboard, aucun item ne doit disparaitre de la section sans ligne de sortie datee dans le registre. C'est le critere de reussite de roo-extensions#3677 (checkbox 3) : a verifier et rapporter sur cette issue.





## 3. Secrets via RooSync — recits et justification datee



> Deporte de `.claude/rules/secrets-roosync-policy.md` le 2026-08-21 (issue #12051, slimming vague 2),
> quand cette rule a ete fusionnee dans [`.claude/rules/secrets-hygiene.md`](../../.claude/rules/secrets-hygiene.md).
> **Les prescriptions n'ont PAS ete deportees** : elles vivent toutes dans `secrets-hygiene.md`, section
> « Transmission d'un secret — canal RooSync prive ». Ce qui suit est du recit d'incident et de la
> justification datee — le contexte qui explique *pourquoi* ces regles existent, pas les regles elles-memes.



### 3.1 Provenance & honnetete (note d'audit)



**Statut de la decision** : ACTIVE. Decision user 2026-07-02, **reaffirmee en session directe 2026-07-03**.



La decision de lever l'interdit (2026-07-02) est corroboree par deux sources contemporaines : le message de
challenge de po-2023 (`msg-20260702T115323-dzxy6q`, qui rapporte firsthand « le user a defendu l'usage ») et
la reponse ai-01 (`msg-20260702T120049-p79z01`). Une version anterieure de cette policy citait un « verbatim
user » horodate avec une precision que ai-01 **ne peut pas attester** (aucune memoire cross-session +
incoherence d'horodatage) ; ce faux-verbatim a ete **retire**. La decision reste valide — elle est reaffirmee
par le user en session directe le 2026-07-03.



### 3.2 Incident fondateur de la clause anti-stonewall — blocage Kokoro/OWUI 2026-07-02→03



Incident fondateur de la clause anti-stonewall : blocage Kokoro/OWUI 2026-07-02→03. La regle absolue
« JAMAIS secrets via RooSync » + une clause d'abus « user override = refuser » avaient ete empilees de sorte
qu'un worker pouvait refuser indefiniment un relais pourtant legitime. La regle absolue est **levee** ; la
clause d'abus **retiree** ; seul subsiste le noyau conserve dans `secrets-hygiene.md` (escalade rapide au
user, pas de stonewall).



### 3.3 Incident fondateur du quorum de contreseing — 2026-07-14



**Incident fondateur 2026-07-14** : ai-01 (provision + HTTP 200) + po-2023 (corrobore `master.env` present +
HTTP 200) + po-2026 (provision + HTTP 200) = initiateur + 2 contreseings = **quorum deja atteint**. po-2024 a
gate sur le user — defendable sous l'ancienne regle (« relais non verifiable »), mais le contreseing-quorum
remplace ce gate : sur 2 contreseings firsthand, l'agent ecrit. Le user a tranche en interactif (« circulez
les cles, avec prudence mais de facon determinee ») **et** pose ce mecanisme.



### 3.4 Sources historiques



- Incident 2026-06-02 (`feedback_no_secrets_roosync.md`, memory) : la vraie lecon = « utiliser `master.env`
  quand il couvre la cible », **pas** « jamais RooSync ».
- Pipeline `master.env` : `secrets-centralized-management-3160.md` (memory).



### 3.5 Mecanique de l'attachment — etat verifie c.647 (#10333)

> Deporte de `.claude/rules/secrets-hygiene.md` (issue #15204, Levier B). La **prescription** reste dans la
> rule (« preferer attachment + `destruct_after` ») ; ce qui suit est l'etat mesure de l'API, qui date de sa
> verification et se relit avant de s'appuyer dessus.

**Aller-retour fonctionnel** : `send` avec `attachments` → `attachments_list(message_id=X)` → `attachments_get(message_id, filename)`.

C'est le troisieme appel qui **ferme la boucle** : aucun `uuid` n'est decouvrable via la liste, donc le couple
`(message_id, filename)` est la voie nominale. `attachments_get(uuid=...)` reste valide par compatibilite.
Le ciblage O(1) des refs passe par `MessageManager.updateMessageAttachments`, depuis la PR
`jsboige/jsboige-mcp-servers#1039` (MERGED 2026-08-25).

**`destruct_after` ne couvre PAS l'attachment.** Il s'applique au **message** (`MessageManager`, champ
`expires_at`, L983/L1013) — verifie c.647, corps `roo-extensions#933` MERGED 2026-08-25. L'attachment vit dans
`AttachmentManager`, qui herite d'un cleanup **par anciennete** (4 semaines par defaut), pas d'un TTL de
message. Consequence a ne pas inverser : poser un `destruct_after: 30m` sur un message porteur de secret ne
fait **pas** disparaitre la piece jointe en 30 minutes — c'est une reduction d'empreinte, pas une garantie
d'effacement, et c'est bien pour cela que la rule la qualifie d'hygiene recommandee et **non bloquante**.

