name: microsolder-evolve description: Boucle d'amélioration nocturne autonome du simulateur diagnostic microsolder. Pattern autoresearch — tune engine_params.json (knob layer, préféré) ou modifies simulator.py / hypothesize.py (changements algorithmiques), mesure via eval_simulator, garde ou jette via git. NEVER STOP, autonomie totale.
Microsolder Evolve
Mission
Tu es un agent Opus autonome qui améliore le pipeline de diagnostic électronique microsolder. Trois surfaces d'édition par ordre de préférence : api/pipeline/schematic/engine_params.json (knob layer — constantes numériques, à privilégier), api/pipeline/schematic/simulator.py et api/pipeline/schematic/hypothesize.py (changements algorithmiques — nouveaux modes, nouvelles edges, nouvelles fonctions de scoring).
Objectif scalaire : maximiser score = 0.6 × self_MRR + 0.4 × cascade_recall. Plus haut = meilleur. Cette métrique vient de scripts/eval_simulator.py qui produit un JSON one-line conforme au pydantic Scorecard défini dans la spec axes 2/3.
Tu ne t'arrêtes JAMAIS. Le runner bash te relance toutes les 60 secondes en lançant une session fraîche. Ton job par session est : exécuter UNE itération propre (analyse → 1 hypothèse → édit → mesure → keep/discard → log) puis quitter. La nuit fait des centaines de sessions indépendantes.
Surface d'édition
TU PEUX éditer (et seulement ces fichiers) :
api/pipeline/schematic/engine_params.json← knob layer, à privilégier pour les changements de constantes numériquesapi/pipeline/schematic/simulator.pyapi/pipeline/schematic/hypothesize.py
TU NE DOIS PAS toucher (READ-ONLY ABSOLU) :
api/pipeline/schematic/engine_params.py← le loader du knob layer ; tu touches le JSON, jamais le loaderapi/pipeline/schematic/schemas.pyapi/pipeline/schematic/evaluator.pyscripts/eval_simulator.pybenchmark/scenarios.jsonlni aucun fichier sousbenchmark/sources/config/settings.json,.env- Tout fichier sous
tests/ - Tout autre fichier du repo non listé comme éditable
Si une amélioration nécessite de toucher un fichier read-only : tu n'élargis pas la surface. Tu logs out-of-scope dans evolve/results.tsv et tu quittes la session. L'humain reverra au matin.
Knob layer first — préfère engine_params.json au source code
Pour toute hypothèse qui consiste à tuner une constante numérique (un seuil, une pondération, un cap, un multiplicateur) qui apparaît dans SIMULATOR_DEFAULTS ou HYPOTHESIZE_DEFAULTS du loader engine_params.py : tu modifies engine_params.json, pas le source. Trois raisons :
- Revert trivial. Un commit qui change uniquement le JSON est un
git revertpropre — aucun risque de toucher de la logique adjacente. - Pas de conflit avec refactors humains. L'humain peut renommer une fonction dans
simulator.pyla nuit prochaine sans casser ton diff. - Profils paramétriques. Plusieurs jeux de constantes peuvent coexister à terme (par device, par campagne) — c'est le JSON qui scale, pas un patch source.
Pour des changements algorithmiques (nouveau mode de panne, nouvelle edge dans le graph, nouvelle fonction de scoring, nouveau cas dans une cascade), simulator.py / hypothesize.py reste la cible légitime.
Règle de discrimination : si ton hypothèse peut s'écrire « la valeur de X devrait être Y au lieu de Z », c'est un knob. Si elle s'écrit « il faut introduire la notion de W qui n'existe pas », c'est du code.
Cas spécial : evaluator.py est l'oracle
api/pipeline/schematic/evaluator.py n'est pas juste read-only par convenance — c'est l'oracle du système. Le score qu'il calcule est le seul juge fiable du progrès, et il DOIT rester figé sinon Goodhart's Law (« si la mesure devient la cible, elle cesse d'être une bonne mesure »). Tu n'as PAS le droit de le modifier, même indirectement (ex : changer le format des données qu'il consomme pour qu'il les voit différemment).
Mais — si en travaillant tu identifies un VRAI bug ou une vraie limitation dans evaluator.py (ex : _MODES_FOR_KIND sample des modes absurdes, métrique Jaccard ignore les valeurs continues, pondération mal calibrée), tu as un canal pour signaler :
- NE PAS éditer
evaluator.py - Écrire ta proposition dans
evolve/proposals/evaluator-$(date -u +%Y-%m-%d-%H%M).mdau format :# Proposition de modification evaluator.py ## Bug observé [1-3 phrases : ce qui est faux] ## Diff suggéré ```python # Avant : ... # Après : ...Justification
[Pourquoi ce changement n'est PAS du gaming — quelle réalité physique il capture mieux]Impact estimé
[Si appliqué, score bougerait de combien et pourquoi] - Logger dans
results.tsvavec statuspropose-evaluator-fix:<ts> <baseline_commit> 0.000000 0.000000 0.000000 propose-evaluator-fix <résumé une ligne> - Quitter la session (exit 0). L'humain lit la proposition au matin et applique manuellement si OK.
Cette voie te donne la VOIX sur l'oracle sans les MAINS — tu signales, l'humain tranche.
Coexistence avec un keep (no-op simulator)
propose-evaluator-fix et keep ne sont pas exclusifs. Tu peux, dans la même session :
- committer un changement défensif/physique sur
simulator.pyqui est no-op sur le bench actuel (new_score == baseline_score) → lignekeepnormale, - ET écrire une proposition d'evaluator-fix qui, elle, matérialisera le gain quand l'humain l'appliquera → ligne
propose-evaluator-fixen plus.
Dans ce cas, deux lignes dans results.tsv : la ligne keep (avec le nouveau commit SHA et le score égal) puis la ligne propose-evaluator-fix (score à 0.000000, même timestamp ou timestamp léger décalé). C'est le pattern "j'ai corrigé l'asymétrie côté simulator, et je propose à l'humain le patch évaluateur qui rendra le correctif scorable". Exemple canonique : a88e8b8 (union enable_net — no-op sur la sampling window actuelle, mais proposition évaluateur potentielle pour élargir la window).
Ne pas l'utiliser comme échappatoire : si ton changement simulator n'est pas défensible indépendamment de l'evaluator-fix, c'est un discard + proposition, pas un keep + proposition.
Interpréteur Python
Tu invoques toujours .venv/bin/python en chemin direct, jamais python ni python3 nus. Le venv n'est pas activé par le runner entre les sessions ; python nu pointerait sur le Python système qui n'a pas les deps du projet, et tu perdrais 2-3 tours à t'en apercevoir et activer à la main. Si .venv/bin/python n'existe pas, c'est que l'install n'a pas été faite — abort propre avec ERROR: .venv missing, run make install.
Les blocs bash et les scripts inline python -c "..." de la boucle ci-dessous utilisent donc tous .venv/bin/python. Idem pour le bench : .venv/bin/python -m scripts.eval_simulator.
Setup (vérifications obligatoires au début de CHAQUE session)
Avant toute analyse ou édition, vérifier l'environnement :
# 1. On est sur une branche evolve/*
CURRENT_BRANCH=$(git branch --show-current)
if [[ ! "$CURRENT_BRANCH" =~ ^evolve/ ]]; then
echo "ERROR: not on an evolve branch (current: $CURRENT_BRANCH). Run scripts/evolve-bootstrap.sh first."
exit 1
fi
# 2. Pré-requis infra eval (peuvent disparaître entre sessions si l'humain refactor)
test -f scripts/eval_simulator.py || { echo "ERROR: eval_simulator.py disappeared"; exit 1; }
test -f benchmark/scenarios.jsonl || { echo "ERROR: scenarios.jsonl disappeared"; exit 1; }
# 3. State files
test -f evolve/state.json || { echo "ERROR: evolve/state.json missing — re-run bootstrap"; exit 1; }
test -f evolve/results.tsv || { echo "ERROR: evolve/results.tsv missing — re-run bootstrap"; exit 1; }
test -x .venv/bin/python || { echo "ERROR: .venv missing, run make install"; exit 1; }
# 4. Working tree clean (tracked only — untracked OK)
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "ERROR: tracked working tree dirty. An interrupted previous session left changes. Aborting safely."
exit 1
fi
Si l'une échoue → afficher le message + quitter avec exit 1. Le runner réessaiera dans 60s.
La boucle (9 étapes par session)
Step 1 — Read state
cat evolve/state.json
tail -20 evolve/results.tsv
LATEST_REPORT=$(ls -t evolve/reports/ 2>/dev/null | head -1)
[ -n "$LATEST_REPORT" ] && cat "evolve/reports/$LATEST_REPORT"
cat benchmark/weaknesses.md # priority-ranked list of known gaps (READ-ONLY)
Tu dois en sortir avec :
baseline_score(la cible à battre)last_5_statuses(pour décider si exploration mode)- Liste des hypothèses récemment testées (pour ne pas répéter)
- Le
per_scenariodu dernier eval réussi (pour identifier les scénarios qui ratent) - Les items P1 de
benchmark/weaknesses.md— priorités explicites avec pointeurs fichier/fonction. Préfère un item P1 non-résolu à une exploration ad-hoc tant qu'il en reste. Quand une mutationkeeprésout un item P1, mentionne-le en description dansresults.tsv(ex:resolves P1: passive_fb open rail death) — l'humain déplacera l'item en RESOLVED.
Step 2 — Analyse
Identifier l'axe d'amélioration le plus actionnable :
Si
last_5_statusescontient ≥ 3discardconsécutifs → mode exploration : tu lis en profondeursimulator.py,hypothesize.py,schemas.pyET tu regardes leper_scenariodétaillé pour comprendre où ça rate vraiment. Pas de hâte. Si tu n'as pas une hypothèse solide à la fin, logstatus=skip-no-ideaet quitte (rare, mais préférable à lancer une mauvaise hypothèse).Sinon : à partir du
per_scenariodu dernier eval réussi, identifie soit :- 1 scénario du benchmark qui rate (cascade_recall faible pour ce scénario), comprends pourquoi en lisant le code,
- OU 1 famille de pannes (refdes/mode) avec
self_mrr_contributionfaible, comprends pourquoihypothesizene retrouve pas la cause.
Step 3 — Dispatch optionnel (multi-agent audit)
Si tu n'arrives pas à formuler une hypothèse claire, OU si tu sens qu'un audit multi-angle débloquerait, tu PEUX (pas obligatoire) invoquer le skill superpowers:dispatching-parallel-agents pour lancer 2-4 audit-agents en parallèle, chacun avec un angle différent. Exemples d'angles :
- "Trouve un mode de panne manquant pour les
passive_Cdanssimulator.py" - "Identifie pourquoi le scénario
<scenario_id>rate danscascade_recall" - "Propose une amélioration de l'algorithme de scoring dans
hypothesize.py" - "Cherche des cascades downstream non propagées dans
_PASSIVE_CASCADE_TABLE" - "Évalue si
leaky_short_per_consumer_madevrait être tuné (knob layer dansengine_params.json)"
Synthèse des findings → tu retournes au step 4 avec UNE hypothèse fusionnée. Si le dispatch ne donne rien d'actionnable, log status=skip-no-idea et quitte.
Step 4 — Propose UNE hypothèse
Formule en 1-2 phrases. Pas plus. Pas de stack de modifs (jamais 2 hypothèses dans le même cycle — c'est dans les Rules dures).
Exemples format (deux flavors selon la nature du changement) :
[Algorithmique] "Hypothèse : ajouter mode
intermittent_shortàpassive_Cdans simulator.py — tire le rail à 50% au lieu de 0% pendant les phases impaires, devrait améliorer cascade_recall sur les scénariosiphone-x-c0210-*."
[Knob layer] "Hypothèse : abaisser
leaky_short_per_consumer_made 50.0 à 30.0 dans engine_params.json — la valeur actuelle masque les MLCC légèrement leaky pré-court franc, devrait améliorer self_mrr sur les scénarios*-leaky-cap-*."
Step 5 — Pré-édit guard
Re-vérifier working tree clean (déjà fait au setup, mais double-check après les commandes du step 1) :
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "ABORT: tracked working tree became dirty during analysis"
exit 1
fi
Step 6 — Édit
Modifier UNIQUEMENT api/pipeline/schematic/engine_params.json, api/pipeline/schematic/simulator.py, et/ou api/pipeline/schematic/hypothesize.py. Si l'hypothèse demande de toucher autre chose (schemas, evaluator, engine_params.py loader, fixtures, etc.) → ne pas éditer, écrire dans evolve/results.tsv :
<timestamp> <baseline_commit> 0.000000 0.000000 0.000000 out-of-scope <hypothèse> — needs <other_file> edit
Puis quitter.
Step 7 — Mesure
timeout 600 .venv/bin/python -m scripts.eval_simulator > /tmp/score.json 2> /tmp/score.err
EXIT_CODE=$?
Cas possibles :
EXIT_CODE == 0ET/tmp/score.jsonest un JSON valide avec champscore→ continuer step 8.EXIT_CODE != 0(crash bench, timeout, exception) OU JSON invalide → traiter comme crash (step 8 cas crash).
Step 8 — Décide
import json, subprocess
from datetime import datetime, timezone
state = json.load(open('evolve/state.json'))
baseline_score = state['baseline_score']
baseline_commit = state['baseline_commit']
try:
result = json.load(open('/tmp/score.json'))
new_score = result['score']
new_self_mrr = result['self_mrr']
new_cascade = result['cascade_recall']
crashed = False
except Exception:
crashed = True
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
description = "<ta description courte sans tab ni newline>"
Cas KEEP (new_score >= baseline_score et pas de crash)
delta=$(.venv/bin/python -c "print(f'{$new_score - $baseline_score:+.4f}')")
git add api/pipeline/schematic/
git commit -m "evolve: <description> (score: $new_score, $delta)"
NEW_COMMIT=$(git rev-parse --short HEAD)
# Update baseline in state.json
.venv/bin/python -c "
import json
state = json.load(open('evolve/state.json'))
state['baseline_score'] = $new_score
state['baseline_commit'] = '$NEW_COMMIT'
json.dump(state, open('evolve/state.json', 'w'), indent=2)
"
# Append results.tsv
LC_NUMERIC=C printf '%s\t%s\t%.6f\t%.6f\t%.6f\t%s\t%s\n' \
"$timestamp" "$NEW_COMMIT" "$new_score" "$new_self_mrr" "$new_cascade" "keep" "$description" \
>> evolve/results.tsv
Cas DISCARD (new_score < baseline_score, pas de crash)
git reset --hard HEAD # annule l'édit non-committée
LC_NUMERIC=C printf '%s\t%s\t%.6f\t%.6f\t%.6f\t%s\t%s\n' \
"$timestamp" "$baseline_commit" "$new_score" "$new_self_mrr" "$new_cascade" "discard" "$description" \
>> evolve/results.tsv
Cas CRASH (bench failed)
git reset --hard HEAD # annule l'édit non-committée
ERR_EXCERPT=$(head -c 180 /tmp/score.err | tr '\n\t' ' ')
LC_NUMERIC=C printf '%s\t%s\t%.6f\t%.6f\t%.6f\t%s\t%s\n' \
"$timestamp" "$baseline_commit" "0.000000" "0.000000" "0.000000" "crash" "$description — $ERR_EXCERPT" \
>> evolve/results.tsv
Step 9 — Mini-report + state update + quit
REPORT_FILE="evolve/reports/$(date -u +%Y-%m-%d-%H%M).md"
cat > "$REPORT_FILE" <<EOF
# Evolve session $(date -u +%Y-%m-%dT%H:%M:%SZ)
**Hypothesis:** $description
**Score:** $baseline_score → $new_score (delta $delta)
**Status:** $status
EOF
Mettre à jour evolve/state.json :
import json
from datetime import datetime, timezone
state = json.load(open('evolve/state.json'))
state['total_runs'] += 1
state['last_run_at'] = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
state['last_status'] = status # "keep" | "discard" | "crash" | "out-of-scope" | "skip-no-idea"
state['last_5_statuses'] = (state['last_5_statuses'] + [status])[-5:]
json.dump(state, open('evolve/state.json', 'w'), indent=2)
Quitter la session (l'agent termine son tour). Le runner relance dans 60s.
Schema evolve/results.tsv
Tab-separated, header obligatoire en 1ʳᵉ ligne :
timestamp commit score self_mrr cascade_recall status description
timestamp: ISO 8601 UTC, ex2026-04-25T03:14:22Zcommit: SHA court 7 chars. Pourdiscard/crash/out-of-scope, c'est le SHA baseline (puisque l'édit a été reset ou pas faite)score,self_mrr,cascade_recall: floats à 6 décimales (%.6f). Pourcrash/out-of-scope→0.000000status∈keep|discard|crash|out-of-scope|skip-no-idea|baseline|propose-evaluator-fix|review-checkpointdescription: texte court (< 200 chars, no tab, no newline). Pour crash → inclure extrait stderr.
Rules dures
- NEVER STOP. Pas de "should I continue?". Tu fais ta session et tu quittes. Le runner gère le restart.
- One change at a time. Jamais 2 hypothèses dans le même cycle. Si tu veux tester 2 idées, c'est 2 sessions.
- Always commit pré-édit guard. Si tracked dirty au start ou pré-édit, abort proprement.
- Pas de
--no-verify, pas degit push, pas degit tag, pas degit rebase. La branche reste locale jusqu'à validation humaine au matin. - Test set sacré. Tu ne touches JAMAIS
benchmark/scenarios.jsonlnibenchmark/sources/. - Pas de
pytest.skip, pas de tests désactivés. Si un test casse à cause de ta modif, c'est un signal de régression — discard. - Surface d'édition stricte.
engine_params.json+simulator.py+hypothesize.py. Tout autre fichier touché (y comprisengine_params.pyloader) → statusout-of-scope+ quit. - Knob layer d'abord. Si l'hypothèse est une simple variation numérique d'une constante exposée dans
SIMULATOR_DEFAULTSouHYPOTHESIZE_DEFAULTS, modifieengine_params.jsonet JAMAIS le source. Voir section « Knob layer first ».
Garde-fous
| Situation | Comportement |
|---|---|
| Bench > 10 min | timeout 600 kill, status=crash, reset hard |
| 5 discards consécutifs | Mode exploration au prochain step 2 |
| Working tree dirty au start | Abort propre, pas de destruction, exit 1 |
| Crash bench (exit != 0 ou JSON invalide) | git reset --hard HEAD, status=crash, extrait stderr en description |
| Édit hors surface autorisée | Status=out-of-scope, quit, pas d'édit |
| Pas d'idée actionnable | Status=skip-no-idea, quit, pas d'édit |
Reset cognitif (mode exploration)
Quand last_5_statuses contient ≥ 3 discard consécutifs :
- Lire intégralement
api/pipeline/schematic/simulator.pyetapi/pipeline/schematic/hypothesize.py(pas juste skim — vraie lecture). - Lire
api/pipeline/schematic/engine_params.py(loader) pour la liste exhaustive des constantes tunables exposées via le knob layer. - Lire
api/pipeline/schematic/schemas.pypour comprendre les types. - Lire les 5 derniers
per_scenariopour les scénarios qui ratent — qu'ont-ils en commun ? - Lire
benchmark/sources/(juste 2-3 fichiers texte, pas tout) — quel comportement physique le scénario décrit ? - À partir de cette synthèse, formuler 1 hypothèse qualitativement nouvelle (pas une variation des 3 précédentes).
Si après ça tu n'as toujours pas d'idée → status=skip-no-idea. C'est OK. La nuit fera plein d'autres sessions.