Changelog¶
Dernière release¶
3.54.0 — Plan produit 2026-Q4, banc à trois bras et phase 2 du nettoyage (mémoire, archétypes, cockpit, sécurité, code scanning)¶
- docs(security): nettoyage code scanning, lot 2 — triage (phase 2 du plan produit 2026-Q4, épic #552, suite de la PR #573). Le constat de départ (« ~50 alertes, presque toutes py/path-injection ») était largement dépassé : 402 alertes ouvertes au moment du triage, cause identifiée dans
.github/workflows/codeql-analysis.yml—queries: security-and-qualitycharge ~250 requêtes de qualité/style (imports inutilisés,except: pass, retours mixtes…) déjà couvertes par le gateruff/mypy --strictbloquant de cette CI, en plus des ~150 alertes de sécurité réelles ; le bruit est ce qui rend le check illisible. Narrowing verssecurity-extended(garde toutes les requêtes de sécurité, retire les requêtes de qualité).docs/security/code-scanning-triage-2026-09.md: méthode et preuve du triage despy/path-injectionrestants — lecture directe de chaque fichier signalé, constat de trois mécanismes de confinement de chemin déjà en place et non reconnus par CodeQL comme sanitizers (resolve_within_allowed/allowed_rootsdansproject_registry.py,_blueprint_path/SLUG_REdansforge_server.py,_ensure_inside_rootdansagentic_standard.py), et pour les fichiers sans ces helpers, un paramètre racine systématiquement dérivé de la CLI/config locale de l'opérateur, jamais d'un réseau distant. Alertespy/path-injectionrestantes dismisséesfalse positiveavec justification précise ; alertes de qualité/style dismisséeswon't fix(hors périmètre sécurité, doublon du gate existant). Alerte historique #223 (py/command-line-injection,cmd_cockpit.py) déjà dismissée le 2026-08-27 par Guilhem, rien à faire. Objectif de fait : 0 alerte ouverte non justifiée après ce lot, check CodeQL vert sur cette même PR. - perf(cockpit): la fiche Piloter faisait encore HUIT appels réseau à son premier rendu, dominée par
doctor(issue #548, phase 2 du plan produit 2026-Q4, épic #552, lot 2 — suite de #542/#547). Cause du cacheproposalsinopérant CONFIRMÉE en lisantsync_proposals()(src/grimoire/proposals.py) : les brancheskeep_rejectedetrefresh_pendingappelaient_save_proposal()INCONDITIONNELLEMENT à chaque appel, même quand rien n'avait changé — cette écriture bouge la mtime du dossier_grimoire-output/proposals/, or c'est exactement ce que surveille la signature deproposals_view()(grimoire.tools.view_cache) : auto-invalidation à chaque lecture, le cache ne pouvait jamais faire hit (mesuré 0,46s→0,52s→0,49s, jamais 0,00s). Corrigé : les deux branches comparent désormais leProposalrecalculé à l'existant (frozen=True→ égalité de valeur native) avant d'écrire ; nouveaux tests rouges avant/verts après danstests/unit/test_proposals.py(mtimes des fichiers de propositions identiques avant/après une resynchronisation sans rien de nouveau) et au niveau vue danstests/unit/test_workspace_api.py. Mesuré ensuite (micro-benchmarktime.perf_counter, projet réel) :kit_alignment()(project_health.py) domine/api/healthà elle seule (~120ms, sha256 par fichier shipé du kit à CHAQUE appel) ;doctor_view()(workspace_exec.py, relance ungrimoire doctorcomplet en sous-processus) est en réalité la route la PLUS chère des sept, ~350ms — jamais mesurée jusqu'ici. Les deux gagnent le même patron de cache queagents_view(view_cache, signature de mtimes des dossiers pertinents —_grimoire/kitpourhealth, config/agents/skills/kit/overrides/wrappers VS Code/policies pourdoctor) avec?probe=1pour forcer un recalcul frais (même câblage quememory_link_view). Nouvelle route agrégéeGET /api/workspace/sheet?project=(workspace_api.sheet_view, câblée dansworkspace_routes.py::GET_ROUTES) : santé, mémoire (mode rapide), agents, propositions, dernier run du wizard, dernier run de flowproject-upgradeet nom du projet (registre) calculés en parallèle (ThreadPoolExecutor, même principe quefleet_status) en une seule réponse —doctorvolontairement absent (mesuré comme le vrai coût des sept, l'onglet Problèmes le rend séparément, à la demande).web/workspace/spaces/piloter.js::loadSheet()remplacé par un uniquectx.api.sheet(slug); le nom du projet (cockpit) vient désormais desheet.name, plus de second appelprojects(). Mesures avant/après (harnais e2etests/e2e/test_workspace_piloter_perf.py, projets jetables) : fiche Piloter 8 appels réseau → 1 (/api/workspace/sheet), interactive en ~811-849ms → ~110ms (seuil du test : < 400ms, critère d'arrêt de l'issue). - fix(security): nettoyage code scanning, lot 1 — correctifs (phase 2 du plan produit 2026-Q4, épic #552). (a)
ForgeAPI.blueprint_compile(src/grimoire/tools/forge_server.py) construisaitartifact_rel/artifact_pathà partir deblueprint["id"](issu du corps d'une requête HTTP cockpit,POST /api/blueprints/<id>/compile) avant le premier appel à_blueprint_path, seule fonction du module à valider un id de blueprint (SLUG_RE+ confinement sous.github/prompts/) — un id contenant../écrivait le mission pack hors du projet (CodeQL py/path-injection, alertes réelles, pas un faux positif). Même gardeSLUG_REappliquée dès l'extraction debp_id, réutilisée telle quelle plutôt que dupliquée.blueprint_validate/blueprint_lint(mêmes routes cockpit, ouvertes en lecture sur toute la flotte sans garde_HOME_SLUG) laissaient aussi unrefde node (artifact/composite/schéma de gate) avec../transformer une vérification d'existence en oracle de fichier arbitraire sur la machine —_schema_resolves, la vérification d'artefact et celle de sous-blueprint confinent désormais le chemin résolu sousproject_rootavant tout.exists()/.is_file()(même message « absent » pour un chemin manquant et pour un chemin qui s'échappe, aucune fuite de bit). (b)_maybe_register_cockpit(src/grimoire/cli/cmd_init.py) journalisaittarget(nom de dossier fourni par l'appelant) avec%s— un nom de dossier contenant un retour à la ligne pouvait forger une fausse entrée de log (py/log-injection) ;%r(repr) échappe désormais\n/\r. (c)ensure_go_toolchain(scripts/bench/three_arms.py) appelaittarfile.extractall()sans filtre sur l'archive Go téléchargée (py/tarslip) — même une archive officielle pinnée reste vulnérable en cas de compromission de la source ou de MITM ; extraction factorisée dans_extract_go_archiveavecfilter="data"(refuse tout membre..//absolu), désormais testable sans réseau. Deux constantes_NODE_REmortes retirées (tests/unit/test_flows_composite.py,tests/unit/test_flows_extract.py, py/unused-global-variable). Tests rouges avant/verts après pour les trois correctifs de sécurité (tests/unit/tools/test_forge_server.py::test_compile_rejects_path_traversal_id,tests/test_cmd_init.py::TestMaybeRegisterCockpitLogging,tests/unit/test_bench_three_arms.py::test_extract_go_archive_rejects_a_path_traversal_member) ; suite complète revérifiée verte, aucun changement de comportement pour un chemin/id légitime. Triage documenté des alertes restantes (lot 2, PR séparée) :docs/security/code-scanning-triage-2026-09.md. - feat(bench): harnais du banc à trois bras (issue #551, plan produit 2026-Q4 phase 1 lot 1.1) —
scripts/bench/three_arms.pycompare Claude Code nu, Claude Code + ecc (github.com/affaan-m/ECC, MIT, plugin scope projet) et Claude Code + grimoire-kit (grimoire init --backend local --no-cockpit+host sync) sur un sous-ensemble reproductible du benchmark polyglot d'Aider (20 exercices Exercism, 5 par langue sur Python/JavaScript/Go/Rust, graine fixée551, k=3 rejeux, ordre des bras tiré au sort par tâche). Garde-fous par run (timeout 15 min, détection « tourne en rond » sur commandes Bash répétées via le fluxstream-json), critère d'arrêt statistique (IC bootstrap disjoints entre bras sur le succès), rapportreport.md/report.json(succès, pass^k, temps médian, coût médian par tâche résolue, IC95%).--dry-runprépare les 60 dépôts de tâche sans appel modèle ;--pilot(2 tâches x 3 bras x 1 run) écrit le coût attendu avant--full(20 x 3 x k=3) ; alerte (sans bloquer) si le coût réel dépasse 3x l'attendu. Isolation :HOMEjetable par bras avec les identifiants Claude Code du poste recopiés le temps d'un run puis effacés dans unfinally(jamais lus/affichés, jamais laissés à demeure — balayage de fin de campagne), jamais d'écriture dans~/.grimoireni leHOMEréel. Tests unitaires sans LLM ni réseau (tests/unit/test_bench_three_arms.py) : tirage reproductible, préparation de dépôt de tâche (tests cachés), détection de succès sur tests verts/rouges, calcul de pass^k et de son IC, détection de boucle, nettoyage des identifiants sur succès/exception. Protocole complet dansdocs/bench-three-arms.md. - fix(status):
kit_alignment()(src/grimoire/tools/project_health.py) affichait « aligné sur 3.46.0 » pour un projet à jour en 3.51.1 (issue #519) —aligneddate un contenu encore présent à sa première introduction au catalogue de digests, pas au dernier passage réel de l'outil ; un projet dont l'essentiel n'a pas changé depuis une vieille release se lisait donc comme en retard.grimoire init/up(ProjectScaffolder) écrit désormais un marqueur_grimoire/kit/.up-version(tier kit, régénéré à chaqueupcomme le reste de cette arborescence — jamais figé commeproject-context.yaml) avec la version de l'outil qui vient de tourner ;kit_alignment()le lit dans un nouveau champupVersion, seule source ajoutée (jamais une seconde estimation).tool_version_gap()(doctortool_version, #515) compare désormais l'outil courant àupVersionen priorité — correctif de fond, pas seulement d'affichage : un outil dont la version tombe entre la première introduction d'un contenu et le vrai dernierupse disait à tort « à jour » en comparant à l'ancienne référence. La fiche Piloter (web/workspace/spaces/piloter.js) affiche « à jour (kit X) » quandupToDateetupVersionsont connus, avec « contenu inchangé depuis Y » en détail secondaire sialigneddiffère ; repli inchangé (« aligné sur X, installé Y », #288) sur un projet sans marqueur connu. Tests rouges avant (tests/unit/test_project_health.py,tests/test_scaffold.py) :KeyError: 'upVersion'sans le marqueur, faux « pas en retard » avec un outil intermédiaire entre les deux références ;tests/unit/cli/test_doctor_tool_version.pyet l'e2etest_piloter_kit_reel_a_jour_dit_a_jour_avec_le_kit_du_dernier_up(tests/e2e/test_workspace_lot4_spaces.py) mis à jour pour la nouvelle référence. - fix(cockpit):
POST /api/blueprints/<id>/compilemanquait de_CockpitHandler(src/grimoire/cli/cmd_cockpit.py), issue #546, même défaut quePUTavant #543 :do_POSTn'avait aucune branche pour/compile, réponse 404 « route inconnue » avant même d'atteindre une garde — sur le seul serveur qui tourne réellement pourgrimoire serveetgrimoire cockpit servedepuis #351. Câblée avec la même garde que/api/setupetPUT:blueprint_compileécrit un artefact.prompt.mdsur disque et persiste la sectioncompileddans le blueprint d'origine, ce n'est pas un calcul comme/validate//simulate— seul le projet de lancement direct (_HOME_SLUG) peut donc compiler, 403 sinon./validateet/simulateétaient déjà câblés et volontairement sans cette garde (calcul pur) : confirmé avec un vrai handler plutôt que supposé.grimoire blueprint evalsvérifié comme commande CLI, jamais une route HTTP ni sur le cockpit ni sur l'atelier — rien à câbler. Nouveautests/unit/cli/test_cmd_cockpit_blueprint_writes.py, paramétré sur les trois routes (200 home, 404 blueprint inconnu, 403 hors home pour/compileseulement, 400 blueprint bloqué) contre un vrai_CockpitHandler— rouge avant, vert après.docs/cockpit-coverage-matrix.mdmis à jour (/compileetPUTpassent « oui », étiquette « atelier only » dePUTcorrigée, périmée depuis #543) et résumé régénéré parscripts/cockpit-coverage.py(Routes API 17→18, Concevoir 15→16, total 77→78 %). - fix(archetypes):
minimaletagentic-standard(archetype.dna.yaml) référençaientproject-navigatoretmemory-keeper(issue #550), retirés du paquet par la refonte meta #387 (commitf636633c) sans mise à jour des DNA en aval —grimoire init --archetype minimaldéclarait deux agents qu'il ne pouvait pas installer. Vérifié surgit log -S project-navigator -- archetypes: retrait volontaire (savoir-faire porté depuis par les skillsmeta-project-navigation/meta-memory-qualityattachés àagent-optimizer), donc retrait des références plutôt que rétablissement.checked_by: memory-keeperréaligné suragent-optimizerdans les deux DNA. Nouveau testtests/unit/registry/test_archetype_dna_agents.py, paramétré sur tous lesarchetype.dna.yamllivrés : échoue si un agent référencé n'a pas de fichier.mdlivré (rouge avant ce correctif, vert après) — attrape aussi tout futur cas. - docs(veille): lots 1.2 et 1.3 de la phase 1 du plan produit 2026-Q4 (issue #551).
docs/veille/anthropic-direction-2026-09.md: verdict chevaucher/envelopper/ignorer par capacité Anthropic récente (Agent Teams, Managed Agents, subagents, skills, plugins/marketplace, hooks, mémoire, sessions/reprise, tâches en arrière-plan, sandbox, computer use, prompt caching, MCP Apps), sources officielles lues le 2026-09-16 (code.claude.com/docs,platform.claude.com/docs,github.com/anthropics/skills, changelog Claude Code) — chevauche déjà Agent Teams via le genre de flowfanout(src/grimoire/flows/genres.py:246, preuve et coût que l'expérimental Anthropic n'a pas).docs/veille/idees-a-recolter-2026-09.md: instruction des huit idées du plan (§6) avec fichier source et licence vérifiés dans chaque dépôt concurrent (affaan-m/ECC 292 skills/68 agents, SuperClaude, cline/cline, langchain-ai/langgraph, mastra-ai/mastra, bmad-code-org/BMAD-METHOD), deux corrections (licence Mastra Apache-2.0 et non Elastic-2.0 ; le gategrimoire-skill-analyzercité par le plan est un outil de la Forge, absent desrc/grimoire/— écart réel de la phase 5.5, pas une capacité déjà livrée), une neuvième idée ajoutée (gate de confiance pré-implémentation, skillconfidence-checkde SuperClaude). Section « Révision 2026-09-16 » deframework/agentic-industry-reference.mdcomplétée avec un lien vers les deux documents. - fix(memory):
memory up --profileet le schéma parlaient deux vocabulaires (issue #527, phase 2 du plan produit 2026-Q4, épic #552). (a) La CLI acceptaitlexical|vector|full(grimoire.tools.memory_setup) pendant que le schéma etgrimoire.memory.profilesparlentlexical|standard|graphe|complet(layer_profile) —memory upn'écrivait jamaislayer_profileet laissaitretrieval_modesur son défaut (vector) même en profilfull, faisant croire au cockpit et àmemory statusqu'un projet passé en compositioncomplettournait encore enstandard.vector/fullsont maintenant des alias résolus pargrimoire.memory.profiles.ALIASESversstandard/complet;build_memory_planécrit toujourslayer_profile/retrieval_mode/vector_databased'après ce que la machine sert réellement (_finalize_layers, jamais d'après le profil demandé — uncompletsans Neo4j ni Redis s'écritstandard, sans aucun service vectoriel s'écritlexical). (b)memory migrate planretombait surcfg.memory.backend(le backend COURANT) faute demigration_source_backend— bug nommé par l'issue : un plan ne peut pas migrer un backend dans lui-même.memory up --applylaisse maintenant une trace du backend qu'il quitte (_grimoire/_memory/migration/previous-backend.json,write_previous_backend_breadcrumb) ;planla lit et l'expose (source_known). Nouvelle commandememory migrate run [--source] [--apply]: lit n'importe quel backend Memory OS via l'API publique du manager (get_all) et réécrit dans le backend courant (store_many, qui recalcule ses propres embeddings) — remplace le couple manuelmemory export+memory import, seul chemin qui fonctionnait jusqu'ici pour un lexical → vecteur. (c)memory graph sync-memoriesest additif seulement : un souvenir supprimé du store, ou un essai antérieur écrit sous une autre collection Weaviate, laisse des nœudsGrimoireMemoryorphelins pour toujours (cas réel observé sur la Forge : store=75, graph=3782). Nouvelle commandememory graph purge-orphans(dry-run par défaut,--applypour supprimer), bornée à la collection du projet et à l'ancienne collection génériqueGrimoireMemoryd'avant les profils (Neo4jMemoryGraph.find_orphan_memory_nodes/purge_memory_nodes) — jamais un scan non borné qui risquerait un autre projet partageant la même instance Neo4j. (d)RedisHotMemorynamespace déjà ses clés parcollection_prefix, mais un projet resté sur le défaut partagé (grimoire) collisionnait avec tout autre projet dans le même cas sur une instance Redis mutualisée (constat réel : Forge surredis://localhost:6379/0partagé avec un autre projet du poste) —_redis_namespace(grimoire.memory.manager) retombe désormais sur le slug du nom de projet quand le préfixe est encore générique, etmemory uple rapporte (plan.notes, nouveau champ). Tests rouges avant (backends mockés — driver Neo4j factice,store_manyfactice, aucun service réel) :tests/unit/tools/test_memory_setup.py,tests/unit/memory/test_migration.py,tests/unit/memory/test_neo4j_graph.py,tests/unit/memory/test_projections.py,tests/unit/memory/test_manager.py,tests/unit/cli/test_cmd_memory.py,tests/unit/cli/test_cmd_memory_ops.py(nouveau). - fix(tests):
test_status_non_resident_declenche_un_chargement_sans_generer_de_texte(tests/unit/test_source_assist.py) était instable en CI (assert 2 == 1, issue #529). Cause confirmée par une reproduction isolée (frontière exacte entre la fin d'un test et le début du suivant) :_trigger_warm_up(src/grimoire/tools/source_assist.py) démarre un thread démon fire-and-forget que niassist_statusni les tests ne joignent ; un test précédent qui déclenche un chargement (running = ()) puis rend la main avant la fin de ce thread laisse ce dernier écrire dans_FakeOllama.generate_calls(classvar partagée entre tous les tests) pendant le test SUIVANT, une fois la liste réinitialisée par la fixturefake_ollama— d'où tantôt 1 tantôt 2 entrées vues par l'assertion suivante, selon l'ordonnancement. Code de production non modifié :_trigger_warm_upne fait déjà qu'un seul appel réel par déclenchement (garde_WARM_UP_INFLIGHT), vérifié en lisant le code. Correctif : nouvelle fonction_drain_warm_up()dans le test, qui attend que_WARM_UP_INFLIGHTse vide avant que la fixture ne réinitialise l'état partagé et n'arrête le serveur — plus aucun thread en vol ne peut polluer le test suivant. Preuve : harnais de reproduction isolé montrant la contamination à 10/10 sans le correctif et 0/10 avec ; suite complète relancée 15 fois sans échec après correctif. - docs(plan): axes phase/finition et délégation optimale dans le plan produit 2026-Q4 (décisions Guilhem du 2026-09-16 matin, suite à #558).
docs/plan-2026-q4.md§2 : métriques 4 et 5 deviennent des métriques de délégation (part des tâches résolues au palier de départ, escalades justifiées vs subies, temps avant demande d'aide, boucles détectées), le coût par tâche et pass^k passent en métrique de conséquence (4bis) — le banc compare qualité puis temps puis coût. §3 : le banc à trois bras s'arrête sur clarté statistique (IC disjoints ou n suffisant), alerte (jamais un blocage) à 3× le coût attendu, jamais un plafond en dollars. §4 phase 4 réordonnée (préalable #521/#559 unification des tâches, puis phase et finition déclarées #560, propagation aux tâches et au kanban #561, pilote adaptatif #562, boucle de finition + juge #563, politique de question #564, reçu portable, rejeu diff, skills, cascade coût visible) ; deux nouvelles sections "Phase et finition" (deux axes, tableau phase × finition par défaut, cinq moments où l'utilisateur choisit) et "Délégation optimale" (signaux/actions du pilote, formes d'équipe). Issue #551 mise à jour avec la définition complète du banc (bras Claude Code nu/+ecc/+grimoire-kit, jeu de tâches Aider polyglot/Exercism, 20 tâches extensible, arrêt statistique). Six nouvelles issues #559-#564 (une par lot 4.1-4.6). - docs(plan): plan produit 2026-Q4 (issue #557, epics #551-#556). Cible développeur seul/entreprise/équipe, promesse « preuve, coût, portabilité », dix métriques de succès mesurées sur un projet jetable le 2026-09-16 (
grimoire --help: 49 commandes de premier niveau ;grimoire-hookPreToolUse : ~50 ms ;grimoire init: 0,24 s ; SessionStartadditionalContext: 1409 caractères ≈ 352 tokens ; PreToolUseadditionalContextpour Claude Code : 0 par construction,src/grimoire/hosts/runtime.py:166-174; 2 émetteurs d'hôtes riches sur 5 hôtes cibles ; 20 skills et 15 agents livrés dans les 10 archétypes), sept phases (0 à 6) et un transversal, matrice de parité reprise de la veille avec colonne phase, registre des risques (waivers chromadb corrigés à2027-02-28,.github/security/dependency-waivers.yaml), section « Révision 2026-09-16 » ajoutée àframework/agentic-industry-reference.mdrecentrant la référence sur le segment développeurs-sur-un-hôte.
Releases précédentes¶
3.53.0 — Cockpit plus rapide, couvert par une matrice de tests, panneau Preuves et blueprints enregistrables¶
- perf(cockpit): moitié front de la perf Piloter (issue #541, suite du backend #542 déjà mergé). (1)
loadFleet()(web/workspace/spaces/piloter.js) appelait encoreprojects()PUIShealth()/memoryStatus()par projet du registre — remplacé par un unique appelctx.api.fleet()(GET /api/fleet). (2)mount()/draw()appelaitctx.api.projects()(nom du projet, cockpit) en SÉRIE avantloadSheet()— les deux partent désormais en même temps (Promise.all). (3)draw()peignait un écran vide jusqu'à la résolution complète des appels — une coquille « Chargement… » s'affiche maintenant tout de suite, retirée avant le rendu réel (jamais empilée dessous). (4) Undraw()déclenché pendant qu'un tour précédent tourne encore rejoint désormais la même promesse (inFlight) plutôt que d'en relancer un second en concurrence — prouvé par un test qui chevauche deux rafraîchissements forcés au niveau DOM (rouge sans la garde : 2 appels/api/fleet; vert avec : 1) ; corrigé en cours de route pour ne jamais avaler un changement de cible légitime survenu pendant ce tour (clic de zoom Flotte/Projet), attrapé par le harnais générique de bascule de segments : un rendu frais s'enchaîne désormais après coup UNIQUEMENT si la cible a réellement changé, jamais pour un second déclenchement identique. (5)ctx.api.memoryStatus()accepte désormais{ probe: true }(GET /api/memory/status?probe=1) ; nouveau bouton « Sonder la mémoire » dans la fiche Piloter, seul déclencheur d'une sonde réseau fraîche (jamais desetInterval), mise à jour ciblée (pas de redessin de toute la fiche). Complément mineur côté backend (pas une réouverture de #542) :_fleet_entry()(project_health.py) manquaitnameetmanaged— la Flotte (renderFleet/watchReasons) les lit sur chaque entrée et n'a plus d'autre appel pour les obtenir depuis queloadFleetne fait plus qu'UN appel réseau. Le cache 60s de la Flotte n'était pas non plus invalidé après création d'un projet depuis ce même écran — corrigé.
| Scénario (harnais e2e, 3 projets jetables) | Avant | Après |
|---|---|---|
| Flotte — appels réseau (registre à froid) | 7 (projects + 3×health+memoryStatus) |
1 (fleet) |
| Flotte — temps interactif | ~269 ms | ~262 ms |
| Fiche Piloter — appels réseau | 8 | 8 (inchangé, hors périmètre — voir ci-dessous) |
| Fiche Piloter — étalement des départs de requête (parallélisme réel) | ~12,7 ms | ~1,3 ms |
| Fiche Piloter — temps interactif | ~811-849 ms | ~813-850 ms (dominé par le backend, voir ci-dessous) |
Rafraîchissement Flotte chevauché (forcé) — appels /api/fleet |
2 | 1 |
Mesures par tests/e2e/test_workspace_piloter_perf.py (rouge sur le code d'avant, vert après), pas inventées. Deux écarts documentés par rapport à la cible initiale de l'issue, les deux hors périmètre d'une PR front-only : le budget d'appels de la fiche Piloter reste à HUIT (loadSheet en porte SEPT depuis #513, un chantier antérieur — le ramener à SIX demanderait d'y retirer un appel, pas d'en changer l'ordonnancement) ; le seuil d'interactivité de la fiche vise 700 ms dans l'issue mais plafonne à ~810 ms sur ce harnais quel que soit l'ordonnancement front, la réponse la plus lente des huit endpoints (doctor/health, IO disque réelle) dominant le total — le gain mesuré ici est la disparition du petit aller-retour série (projects() avant loadSheet), pas le temps total.
- test(cockpit): lot B de couverture systématique — raccourcis et palette (issue #536). Dix tests e2e nouveaux (tests/e2e/test_workspace_shell_palette_actions.py) : les six raccourcis ⌘1–⌘6 (changement d'espace, jamais pressés réellement — seul GrimoireWorkspace.goto direct l'était), le raccourci ` (ouvre la Console du dock), et l'exécution réelle (pas seulement l'ouverture) des trois sections de palette restées non exercées : « Tâches » (grimoire task show), « Workflows » (navigation Concevoir), « Projets » (rechargement ?project=). Constat en cours de route, pas un bug : buildPalette() (shell.js) ne se reconstruit qu'au chargement de la page, jamais à l'ouverture — une tâche/un blueprint créé après coup n'apparaît dans la palette qu'après rechargement (documenté dans la matrice). Résumé chiffré régénéré : 163 lignes, 124 couvertes (76 %, coque à 97 %).
- fix(cockpit): Piloter n'affichait ni ne rafraîchissait jamais les propositions en attente après une mise à jour confirmée qui échoue en upgraded-but-failed (issue #538, xfail(strict=True) posé par le lot A de couverture ci-dessous). project_update.py::_run_upgrade_flow joint déjà report/proposals sur cet état précis, mais le handler du bouton « Confirmer la mise à jour » (piloter.js) gardait l'affichage du nombre de propositions ET son rafraîchissement (refreshProposals()) derrière result.ok — or state: "upgraded-but-failed" implique toujours ok: false (_derive_state), donc les deux blocs restaient systématiquement morts sur cet état, alors qu'ils fonctionnent pour completed/upgraded-checkpoint-pending. Les deux gardes portent désormais sur la présence réelle du champ (Array.isArray(result.proposals)), indépendamment de result.ok ; markKitCheckpointPending/checkpointPendingRunId restent réservés au succès (le nœud destructive n'est jamais atteint sur un run interrompu à apply). Retire le xfail de test_confirmer_upgraded_but_failed_devrait_rafraichir_les_propositions — rouge avant, vert après.
- test(cockpit): lot A de couverture systématique — écritures et décisions (issue #536). Six tests e2e nouveaux pour les six artifact_type de proposition (tests/e2e/test_workspace_proposals_types.py, issue #490) : jusqu'ici seul le type agent du déclencheur passait par un vrai clic Piloter + la route POST /api/workspace/proposals/<slug>/accept, les cinq autres (skill, repair avec/sans substitution, override-migration, memory-link, needs-hosts) n'étaient prouvés qu'au niveau moteur. Un test pour le repli « presse-papiers indisponible » du bouton « Revoir dans l'IDE » (tests/e2e/test_workspace_piloter_review_button.py) et un pour l'aperçu en échec de la mise à jour (tests/e2e/test_workspace_cockpit_upgrade_flow_states.py). Un bug confirmé et marqué xfail(strict=True) en attendant correction : Piloter n'affiche ni ne rafraîchit jamais les propositions en attente après une mise à jour confirmée qui échoue en upgraded-but-failed, alors que la route les renvoie déjà (#538). Deux corrections de la matrice (deux lignes marquées à tort « non » en PR précédente : la garde 404 sur projet hors registre et les erreurs de création de projet sont déjà couvertes au niveau route). Résumé chiffré régénéré : 162 lignes, 116 couvertes (72 %, +8 points).
- perf(cockpit): l'espace Piloter était lent à charger (mesuré sur le cockpit réel : /api/memory/status 0,88s, /api/workspace/agents 1,58s, /api/workspace/proposals 0,46s ; aucun cache n'existait nulle part, côté serveur comme côté client). (1) memory/status : MemoryManager.health_check() sondait Weaviate/backend, Redis et Neo4j SÉQUENTIELLEMENT, sans timeout — désormais en parallèle (ThreadPoolExecutor), chaque sonde bornée à 300ms (DEFAULT_PROBE_TIMEOUT_SECONDS) : un service injoignable ne fait plus jamais attendre la route plus que ce délai. memory_link_status() gagne un mode rapide par défaut (probe=False) qui ne sonde JAMAIS le réseau : il rend le dernier statut sondé (nouveau cache process grimoire.memory.health_cache, TTL 25s, invalidé sur toute écriture mémoire — store/upsert/update/delete/store_many/facts/diary) avec probedAt/probed/stale explicites ; probe=True (GET /api/memory/status?probe=1) force une sonde fraîche. Trouvé en profilant sur un projet jetable avec Neo4j réellement injoignable : la CONSTRUCTION du client Neo4j (Neo4jMemoryGraph.__init__ appelle ensure_schema(), un vrai aller-retour Cypher) peut à elle seule bloquer ~30s via le propre backoff du driver, avant même que health_check() ne s'exécute — MemoryManager.from_config() la borne désormais aussi (_create_memory_graph_bounded, 3s), un no-op pour tout appel en mode rapide puisque celui-ci ne construit jamais de MemoryManager. Corrige au passage un bug latent trouvé par ce même profilage : RedisHotMemory.health_check() ne rattrapait que les exceptions builtin (ConnectionError/TimeoutError/OSError) — redis.exceptions.ConnectionError n'en dérive pas, et un Redis réellement injoignable crashait toute la route au lieu de rendre healthy: False. (2) agents_view()/proposals_view() (grimoire.tools.workspace_api) relisaient et re-résolvaient toute leur surface (agents/skills/overrides, propositions) à chaque appel — nouveau cache générique par signature de mtimes (grimoire.tools.view_cache), invalidation naturelle dès qu'un fichier surveillé change. (3) Nouvelle route agrégée GET /api/fleet (grimoire.tools.project_health.fleet_status, câblée dans forge_routes.py) : santé + mémoire (mode rapide) de tous les projets du registre en une seule réponse, calculés en parallèle — remplace les 2×N appels HTTP que web/workspace/spaces/piloter.js::loadFleet() faisait un par un et par projet. Mesuré sur un projet jetable (backend local, Redis/Neo4j pointés vers une IP non routable) : memory/status 3108ms → 2,6ms (mode rapide, après une première sonde) ; agents_view 58ms → 0,3ms ; proposals_view 4,4ms → 0,0ms.
- docs(cockpit): matrice de couverture contrôles × états (issue #536). Inventaire exhaustif construit depuis le code (web/workspace/*.js, workspace_routes.py, forge_routes.py, project_update.py, cmd_cockpit.py, proposals.py), confronté aux tests réels (tests/e2e/*.py, tests/unit/test_workspace_*.py, tests/unit/cli/test_cmd_cockpit*.py, tests/unit/test_project_upgrade.py) — docs/cockpit-coverage-matrix.md, résumé chiffré régénéré (jamais estimé à la main) par scripts/cockpit-coverage.py, gardé synchronisé par tests/unit/test_cockpit_coverage_matrix.py. Deux régressions confirmées sur origin/main en construisant l'inventaire, chacune avec son issue : le bouton de rail « Preuves » (touche 3) n'est enregistré par aucun espace (#534), et l'ajout d'un nœud depuis la Bibliothèque de Concevoir ne persiste jamais sur disque malgré le message de succès affiché (#535).
- fix(cockpit): le bouton « Preuves » du rail (touche 3) n'avait aucun effet, sur aucun des six espaces (issue #534, trouvé par la matrice de couverture contrôles × états ci-dessus). triggerRailAction('evidence', …) (web/workspace/shell.js) cherchait un handler que seul un espace pouvait enregistrer via ctx.rail.on(…) — comme « 2 »/bibliothèque, propre à Concevoir — mais aucun des six espaces n'enregistrait 'evidence' : bouton mort et raccourci mort partout, contraire à la doctrine du cockpit (« aucun contrôle mort visible »). Contrairement à « 2 », « Preuves » n'a pas de sens propre à un espace : evidence rejoint désormais PANELS aux côtés d'Explorateur/Inspecteur, un panneau tenu par la coque elle-même — pas par ce qui est monté au centre — donc utilisable identiquement depuis les six écrans, avec la mécanique générique déjà existante des trois états (replié/entrouvert/épinglé, largeur mémorisée par espace et par projet). Nouveau panneau #panel-evidence (index.html) listant les tâches du standard gouverné, l'état de leurs gates et le chemin de leur pack — via GET /api/workspace/evidence (nouveau, workspace_routes.py/workspace_api.py::evidence_view), qui rejoue grimoire.core.agentic_standard.check_evidence_gates (la même lecture que grimoire standard verify/grimoire_standard_gate), jamais une relecture parallèle des fichiers ; enrolled: false nomme grimoire standard init quand le projet n'en a pas. Cliquer une tâche ouvre son pack dans Source (goto('source', { file })) — ce qui a exigé un quatrième étage Source, evidence (_grimoire-output/evidence/, lecture seule) : sans lui, file_view refusait (403) tout chemin sous _grimoire-output, le même échec que le bouton qu'il corrige. Nouveaux tests unitaires (tests/unit/test_workspace_api.py, nouvelle fixture governed_project dans tests/conftest.py — real_project est mutée par project_with_task dans d'autres tests du même fichier, ce qui aurait rendu ceux-ci dépendants de l'ordre de collecte) et e2e (tests/e2e/test_workspace_shell.py : la touche 3 ouvre le panneau depuis Piloter, le clic sur une tâche ouvre Source — rouge avant le correctif, vert après).
- fix(cockpit): dans Concevoir, ajouter un nœud depuis la Bibliothèque ne persistait jamais malgré le message de succès affiché (issue #535, trouvé par la matrice de couverture ci-dessus).
addNode()(concevoir.js) ne mutait questate.blueprinten mémoire ; corrigé en deux moitiés distinctes, trouvées l'une après l'autre. Client :state.dirty, posé paraddNodeet remis àfalseau chargement/à l'enregistrement — indicateur « modifié — non enregistré » dans la barre d'outils du graphe, bouton « Enregistrer » (saveBlueprint(), désactivé sans modification) qui appellectx.api.blueprintPut(jamais automatique), et confirmation avant de rouvrir un blueprint modifié (zoomToWorkflow, même mécanisme quesource.js::openFile). Serveur, trouvé en écrivant le test e2e : même le client corrigé échouait encore —_CockpitHandler(cmd_cockpit.py), le seul serveur qui tourne réellement pourgrimoire serveetgrimoire cockpit servedepuis la fusion #351, ne définissait aucundo_PUT:http.serverrépondait 501 avant d'atteindre la moindre garde, y compris sur l'atelier (readOnly: false).forge_http.py::do_PUT(couvert partests/unit/tools/test_forge_server.py) est un chemin mort en pratique — jamais celui qu'emprunte un navigateur réel. Nouveau_CockpitHandler.do_PUT:PUT /api/blueprints/<id>routé versblueprint_put, même garde_HOME_SLUGque/api/setup(un projet du registre qu'on ne fait que regarder reste en lecture seule, #356).blueprint_put(forge_server.py) gagne aussi une garde structurelle — un corps dontnodes/edgesn'est pas une liste d'objets levait uneAttributeErrornon rattrapée (500) au lieu d'un refus explicite (400) — sans devenir une validation de schéma stricte : un nœud tout juste ajouté a unrefvide, « à compléter dans Propriétés », un brouillon légitime que le lint (informatif, jamais bloquant) laisse déjà passer. Nouveaux tests :tests/unit/tools/test_forge_server.py(structure),tests/unit/cli/test_cmd_cockpit_blueprint_put.py(200 sur le projet de lancement, 403 sur un autre projet du registre, 400 sur un corps invalide, 404 hors/api/blueprints/),tests/e2e/test_workspace_concevoir.py(nouvelle fixtureconcevoir_scratch_blueprint— jamaisworkspace-demo, partagé et jamais réinitialisé pour toute la session e2e : ajouter-et-enregistrer un nœud réel l'aurait pollué pour tous les autres tests du module) — indicateur affiché, persistance après rechargement, confirmation avant d'écraser un brouillon ; rouge avant chaque moitié du correctif, vert après.
Releases précédentes¶
3.52.1 — Le cockpit gagne de la couleur, de façon intelligente (deux thèmes)¶
- feat(cockpit): thème plus coloré « de façon intelligente » (retour direct de Guilhem — « ça manque de couleur, ça paraît terne/sombre » en sombre, « trop sobre, sans vie, teintes trop proches » en clair). Les cinq niveaux de surface (
--bg/--e1/--bar/--e2/--e3,tokens.css) avaient un rapport de luminance de 1,06 à 1,17 entre voisins — sous le seuil de lisibilité d'une superposition — dans les deux thèmes ; rouverts à ~1,30, encre secondaire (--ink2/--ink3) retenue en retour pour garder l'AA.--acc(interaction : survol, focus, onglet actif, ligne sélectionnée) valait--ink— aucune teinte propre, cause principale du rendu terne — reçoit désormais la teinte de la série 1 (bleu froid), distincte de--pri(toujours l'unique orange par écran). Nouveaux vocabulaires de couleur, toujours informatifs — jamais décoratifs, toujours point + mot ou filet fin : six teintes d'identité d'espace (--id-piloter…--id-source, filet sous l'onglet actif et liseré de panneau) et des badges sémantiques pour le genre de blueprint (Concevoir), lerisk_profiledes tâches (Exécuter) et le type de proposition — six valeurs distinctes,agent/skill/repair/memory-link/override-migration/needs-hosts— jusqu'ici confondues sous un unique point orange (Piloter). Corrige au passage un contraste limite préexistant :.cv-node .kind/.cv-prim .d(survol, Concevoir) passent de--ink3à--ink2. Nouveau testtests/e2e/test_workspace_shell.py::test_les_surfaces_voisines_s_ecartent_assez_pour_se_distinguer(rouge sur les tokens d'avant, vert après) mesure l'écart de surface sur le DOM rendu, dans les deux thèmes. - feat(cockpit): thème, seconde marche plus franche (suite directe de la précédente entrée — accord de Guilhem : « accepté tel quel, puis une seconde marche plus franche »). Quatre changements, chacun mesuré : (1) les écarts de surface repassent de ~1,30 à 1,26-1,42 selon la paire, avec un vrai palier entre panneaux (
--e1) et cartes (--e2) pour que ces dernières se détachent nettement (sombre :--bar/--e21,30→1,41 ; clair :--bar/--e21,42→2,06) ;--bgsombre reçoit une teinte froide (bascule R/B, luminance inchangée) et--bgclair remonte franchement près du blanc chaud. Coût documenté : l'écart--ink/--ink2(sombre) se resserre de 1,32 à 1,21. (2)--accse voit désormais sur chaque élément actif, pas seulement une bordure fine : onglet (fond--accsoft+ filet 3 px), vue (.seg > button[aria-pressed], fond plein + nouveau--onfill), ligne sélectionnée (.tree > .sel, liseré 3 px ajouté), bouton par défaut (nouveau.btn[aria-pressed="true"], fond plein), champ focus (.input:focus-within, nouveau). (3) Les statuts kit/CI/doctor/mémoire (Inspecteur et Flotte), le type de proposition et le nœud du déroulé passent du point + mot à la pastille pleine (.chip.pill.<état>, nouvelle primitive shell.css, fond saturé +--onfill— mesuré ≥4,5:1 sur les six couleurs, marge la plus juste 5,72:1 sur--bad) ; genre de blueprint etrisk_profilerestent en point + mot, hors du périmètre demandé. (4) Le filet d'identité d'espace devient un bandeau plein (panel-head, 2 px → 4 px) et le panneau qui a le focus clavier gagne un liseré élargi (:focus-within, 3 px, pas d'état posé côté JS).web/DESIGN-SPEC-workspace-2026-09.mdettests/e2e/test_workspace_lot4_spaces.py(sélecteur.dot→.chip.pill, même assertion) mis à jour ; harnais de contraste inchangé (texte AA 4,5:1, surfaces 1,25:1 minimum). - feat(cockpit): thème clair, fond franchement blanc (troisième retouche, clair seulement — malgré des rapports de surface déjà conformes,
--bgclair mesurait L* 94,7, encore perçu comme gris sur les captures des deux passes précédentes).--bgclair remonte à L* 96,5 (luminance 0,912, teinte chaude inchangée) ;--e1/--bar/--e3recomposés par le pas le plus court qui tienne 1,25:1 à chaque paire adjacente — pas un simple report proportionnel, pour rester clair plutôt que retomber dans le gris (--bg/--e11,34→1,27,--e1/--bar1,34→1,27,--bar/--e22,06→1,77,--e2/--e31,43→1,27, toutes ≥1,25).--e2reste l'unique blanc pur.--ink2/--ink3inchangées : la surface la plus sombre qu'elles touchent (--bar) est désormais plus claire qu'avant, donc plus de marge — 6,67:1 et 5,44:1 mesurés sur--bar, contre 5,74:1/4,68:1 avant cette retouche. Thème sombre et pastilles (issues des deux passes précédentes) non touchés. - feat(cockpit): thème clair, pile inversée (quatrième retouche, clair seulement). Remonter
--bgtrois fois n'avait rien changé à l'écran :--bgne se voit qu'aux marges (#canvas), le gris perçu venait de--e1(.pl-kpi/.pl-watch-row/.pl-table— les cartes de contenu, L* 87) et--bar(.pl-table th— les en-têtes, L* 79). La pile est inversée comme un thème clair sobre :--e1/--e2(cartes) blancs ou quasi (L* 99/100),--bg/--bar/--e3(fond de page et barres) un gris clair proche (L* 90), chacun ≥1,25:1 de son voisin déclaré (--bg/--e11,27,--e1/--bar1,26,--bar/--e21,30,--e2/--e31,27).--barne peut pas atteindre la bande visée à l'origine (L* 93-95) sans redescendre sous 1,25:1 face à--e1quasi blanc — contrainte algébrique documentée danstokens.cssplutôt que forcée.--lineclair monte de .13 à .16 (bordures de carte plus marquées, sans le secours d'un fond gris derrière).--ink2/--ink3inchangées, marge élargie (surface la plus sombre qu'elles touchent,--bar, plus claire qu'avant : 9,08:1 et 7,40:1 contre 6,67:1/5,44:1). Sombre et pastilles non touchés.
Releases précédentes¶
3.52.0 — Revue de mise à jour dans le cockpit (prompt et bouton), contrôles réparés et tests d'interface systématiques¶
- fix(cockpit): cinq contrôles inertes ou muets de la vue de travail, trouvés par un audit factuel (issue #523). (1) Observer (
spaces/observer.js) appelaitctx.docbar.setViews([...], 'runtime')sans le callbackonPick(3e argument) — Runtime/Activité/RTK/Bench ne réagissaient à aucun clic ; les trois onglets secondaires n'avaient de toute façon aucun contenu prévu (dead code), ils rendent désormais un flux d'événements réel (Activité) ou l'admettent explicitement (RTK/Bench, aucune route serveur). (2) Mémoire (spaces/memoire.js) appelaitsetViewsune seule fois, hors dedraw(): cliquer « Couches » changeait le contenu mais laissait « Store » visuellement actif —setViewsest désormais rappelé à chaque rendu, commeexecuter.js/concevoir.js. (3)#project-chipn'avait aucun gestionnaire de clic ; il ouvre désormais la palette sur une section « Projets » mise en tête, et la palette (⌘K) est restructurée en sections titrées (Projets, Espaces, Commandes, Tâches, Workflows, Fichiers) au lieu d'une liste plate de 30+ entrées — le chip gagne aussi curseur/survol/chevron. (4) Le bouton « Suggérer » (spaces/source-editor.js) n'était expliqué qu'au survol (title) ; il porte désormais une aide permanente, et l'état « modèle configuré mais indisponible » (Ollama injoignable, modèle absent…) — qui faisait disparaître bouton ET statut en entier, sans le moindre indice — affiche maintenant la vraie raison rendue par le serveur. (5) Les états vides de Concevoir et Exécuter (pas de blueprint / pas de Mission Ledger) faisaient disparaître les boutons de vue par early-return ; ils restent désormais visibles (désactivés) et l'état vide cite la commande CLI exacte qui produit la première donnée. Nouveau test générique (tests/e2e/test_workspace_controls.py) : clique chaque bouton de chaqueview-seg/zoom-seg, deux fois, dans deux ordres différents, et vérifiearia-pressedET le style calculé — rouge avant le correctif sur Observer/Mémoire, vert après ; couvre aussi le chip projet, l'aide de Suggérer et les états vides. - feat(upgrade-review): revue de mise à jour — un mission pack pour décider ce qu'un
grimoire upgrade-flow runa laissé en attente (issues #490/#506/#510, #520). Nouvelle commandegrimoire upgrade-flow review [--json](lecture seule) : rend l'écart outil/kit (tool_version, issue #515 — refuse de tourner s'il est en retard), le dernier runproject-upgrade(grimoire.tools.flow_runs.list_flow_runs) et les propositions en attente (grimoire.proposals.list_proposals, lecture pure disque), sans jamais rien appliquer. Nouveau skillupgrade-review(archétypemeta, attaché au concierge) et prompt mission packgrimoire-upgrade-review(projeté parhost sync: commande Claude Code, prompt Copilot) qui portent le même protocole — une ligne par proposition, questions posées par lot, décision appliquée uniquement viagrimoire proposals accept|rejectetgrimoire flow resume --result(jamais une écriture directe). Doc :docs/upgrade.md#revoir-les-décisions. - feat(cockpit): bouton « Revoir dans l'IDE » dans Piloter (issue #520, lot 2). À côté de « Mettre à jour », présent dans la page seulement s'il y a une proposition en attente ou un checkpoint destructif en attente (
GET /api/workspace/proposals/flows/runs, déjà chargés par la fiche — aucun nouvel appel réseau). Au clic : copie dans le presse-papiers (repli affiché si indisponible) un texte prérempli/grimoire-upgrade-review+ l'id du dernier runproject-upgrade+ les slugs des propositions en attente, et montregrimoire upgrade-flow reviewà lancer dans le projet — aucun mécanisme d'ouverture d'IDE n'existe dans ce dépôt (aucun schémavscode://, aucun champ de config d'éditeur), donc aucun lien de ce type n'est inventé..pl-previewrenommé.pl-review-previewpour ce nouveau bloc — le sélecteur.pl-previewdes tests e2e existants doit continuer à ne trouver que celui du bouton « Mettre à jour ».
Releases précédentes¶
3.51.1 — Compléments de la boucle de mise à jour du cockpit après la validation finale (écart outil/kit, état et badge persistants, flotte sans fan-out)¶
- fix(cockpit): trois restes de la validation finale de la boucle de mise à jour (issues #506/#510, PR #511/#513). (1)
POST /api/projects/updaterendaitstate: nullsur tout run qui n'était pas le refus d'apply— y compris un run complet 9/9 arrêté, non décidé, au checkpointdestructive. Nouvelle fonction_derive_state()(tools/project_update.py), dérivée des mêmes champs structurés quenodes/stoppedAt(done,stopped_at,ok,dry_run), jamais du texte libre :preview-only,upgraded-checkpoint-pending,upgraded-but-failed(relayé tel quel),failed,completed. (2) Le badge Kit « mis à niveau, checkpoint destructif en attente » (Piloter) était un état purement client, posé une fois après le clic « Confirmer » — perdu à la première navigation Flotte → Projet.grimoire.tools.flow_runs.list_flow_runs()pose désormaisstatus/currentNode(depuis le kernel réel,FlowEngine.status()) sur chaque run déjà retenu ; la fiche Piloter dérive le badge, à chaque rendu, du dernier runproject-upgradedu projet dont le nœud courant estdestructiveencheckpointed, avec sonrun_id.api.flowRuns()accepte désormais unprojectexplicite, nécessaire à la fiche d'un projet consulté depuis la Flotte. (3) Le Dock de l'espace Observer citaitgrimoire upgrade-flow status, une commande qui n'existe pas (grimoire upgrade-flow --helpn'a pas de sous-commandestatus) — remplacée par la vraie commande,grimoire flow status <run_id>, avec l'id du run le plus récent. - perf(cockpit): la Flotte de Piloter ne rebalaye plus tout le registre à chaque changement de projet (issue #510 point 5). Cas réel : chaque changement de projet dans le cockpit lançait
GET /api/health+GET /api/memory/statuspour CHAQUE projet du registre (34 requêtes pour 17 projets, 20 pour 10) — le fan-out vivait dansloadFleet()(web/workspace/spaces/piloter.js), seul point d'entrée à balayer tout le registre ; sélectionner un projet ne l'a jamais touché, une seule lecture ciblée (loadSheet) suffisait déjà.loadFleet()gagne un cache mémoire par slug (60 s) : un retour sur la vue Flotte dans la fenêtre sert les données déjà lues plutôt que de tout relire. « Rafraîchir la flotte » (nouveau bouton,renderFleet) et une mise à jour de projet confirmée (invalidateFleetCache, dansrenderSheet) restent les deux seules façons de le contourner — la seconde pour que la ligne du projet qu'on vient de mettre à niveau ne reste jamais périmée jusqu'à 60 s. - feat(doctor,cockpit): écart entre l'outil CLI de l'utilisateur et le kit du projet, désormais nommé plutôt que tu (issue #510 point 4). Cas réel : un homelab piloté par un
grimoirepipx en 3.50.1 alors que le cockpit (un process séparé, 3.50.2) avait mis le projet à niveau — nidoctorni la fiche projet du cockpit ne le disaient, le badge Kit ne comparait la version alignée du projet qu'au serveur cockpit. Nouveau contrôledoctortool_version(grimoire.cli.app) : WARN — jamais FAIL — quand l'outil qui exécute la commande est plus vieux que le kit aligné du projet (grimoire.tools.project_health.tool_version_gap), nommantpipx upgrade grimoire-kit/pip install -U grimoire-kit.project_health.kit_alignment()gagneprojectTool: la version que CE projet déclare comme la sienne (.venv/bin/grimoireà la racine, outool:dansproject-context.yaml), distincte du process qui répond — jamais devinée via$PATH. La fiche projet du cockpit (Piloter, Inspecteur « Kit ») affiche cette troisième version quand elle diverge des deux autres, ou « inconnu, vérifiezgrimoire --versiondans le projet » quand rien n'est déclaré.
Releases précédentes¶
3.51.0 — La boucle de mise à jour se ferme dans le cockpit, le flow survit à ses défauts préexistants (troisième rejeu réel)¶
- fix(upgrade-flow): projections hôte fantômes retirées par
orphans, overrides pleins « fresh » proposés à la conversion (issue #510, points 2 et 6 — troisième rejeu réel, neuf projets). (1)host syncn'écrit que ce que son plan actuel résout, etfind_orphans()partait delayout.installed_agents: une projection hôte managée (.claude/agents/x.md,.github/agents/x.agent.md) dont la source avait disparu de toutes les tiers (kit/overrides/custom, manifeste) n'était donc jamais vue par personne, alors quegrimoire doctorla signalait déjà (agents_referenced) — c'est exactement la forme trouvée sur la Forge. Nouvelle fonction_ghost_managed_projections()(tools/project_upgrade.py) : toute projection managée dont le nom n'a aucun installé correspondant devient un orphelin comme un autre, archivée sous_archive/.../orphans/— jamais un fichier hôte sans le marqueurgrimoire:managed, jamais une suppression. Choisi dans le nœudorphansplutôt quehost sync: le mécanisme d'archivage y vit déjà, et ce nœud tourne avantapply. (2)propose_override_migrations()sautait tout override en statut de dérive"fresh"(le kit n'a pas bougé depuis la prise), y compris un override plein dont le corps est resté une copie exacte du kit — la forme la plus simple à convertir, puisque « fresh » signifie justement que rien n'a changé côté kit. Un homelab réel avait sept overrides pleins de cette forme, chacun ne portant qu'uncontext:, sans qu'aucun ne reçoive de proposition. Chaque override plein reçoit désormais un essai de conversion (convert_override --dry-run) quel que soit son statut de dérive ; seul un override déjà partiel (extends: kit) est sauté, sans condition — c'est la forme déjà convertie. - fix(upgrade-flow): ligne de base de
previewétendue à tous les contrôlesgrimoire doctor, rapport explicite quandapplyrefuse aprèsup(issue #510, points 1 et 3 — troisième rejeu réel, neuf projets). (1) La ligne de base quepreviewenregistre (doctor-baseline.json) ne couvrait quepaths_resolve(#502) : sur la Forge,applya refusé sur un FAILagents_referencedpréexistant (une projection hôte managée sans source dans aucune tier), sans proposition. Elle couvre désormais tout contrôle doctor — une signature"<contrôle> : <détail>"par check (other_failures, à côté dedead_references, déjà par ligne). Un contrôle déjà en échec au moment depreviewne fait plus refuserapply: il devient une propositionrepairnommant le contrôle (propose_doctor_repairs,category: "doctor-preexisting") ; seule une régression apparue depuispreviewéchoue encore. (2) Quandapplyrefuse malgré tout,grimoire upgrade-flow run --jsonrendaitdone: [],stoppedAt: null, sans proposition ni piste — alors queupavait déjà tourné. Il rend désormaisdone(les nœuds réellement exécutés),stopped_at: "apply",state: "upgraded-but-failed",failing_checks(le ou les contrôles en cause) etbackup_path; un_grimoire-output/upgrade/<date>/report.mdest écrit avant le refus, commençant par « Mis à niveau, flow en échec sur<contrôle>. ».grimoire.tools.project_update.update_project()(atelier/cockpit) porte les mêmesstate/backupPathet relit ce rapport.ApplyResultgagnefailing_checks; nouvelles fonctionspropose_doctor_repairs/write_apply_failure_report(tools/project_upgrade.py). - fix(tests): le registre cockpit réel de la machine (
~/.grimoire/cockpit/registry.json) contenait sept entrées mortesx/x-2…x-7pointant vers des répertoirestempfile.mkdtemp()disparus (/tmp/tmpXXXXXXXX/...), en plus de l'entréeprobedéjà trouvée (#492). Reproduction exhaustive (suite complètetests/unit,tests/test_*.py,tests/integrationrejouée sous un$HOMEsimulé instrumenté pour refuser toute écriture hors de l'isolation) : aucune fuite dans les suites pytest telles qu'elles s'exécutent aujourd'hui — l'isolationHOME/GRIMOIRE_COCKPIT_HOMEdetests/conftest.py(#153) tient. La forme des chemins retrouvés (tempfile.mkdtemp()direct, pas letmp_pathimbriqué de pytest) désigne ungrimoire initmanuel sur un dossier jetable, jamais nettoyé — un smoke test ou une session d'agent, sans--no-cockpit. Durcissement à la source plutôt qu'un correctif de test isolé :_maybe_register_cockpit(cli/cmd_init.py, chemin partagé parinit/up) refuse désormais silencieusement (tracelogging.DEBUG) d'enrôler un projet dont le chemin résolu vit à un ou deux niveaux sous la racine temporaire du système (tools/project_registry.py::is_scratch_path, nouveau) — assez large pour couvrir/tmp/tmpXXXXXXXX/<nom>et/tmp/<nom>, assez étroit pour ne jamais toucher letmp_pathisolé de pytest (toujours ≥3 niveaux sous cette racine). Les actions explicites (grimoire cockpit add/create,grimoire serve --project-root) restent inchangées. Le garde de session existant (tests/conftest.py::pytest_sessionfinish, #339) couvre désormais aussi l'état cockpit (cockpit.json), pas seulement le registre. - feat(cockpit): décider une proposition (Accepter/Refuser, espace Piloter) fonctionne désormais pour n'importe quel projet du registre depuis le cockpit, pas seulement le projet de lancement direct (issue #506) — première moitié de « fermer la boucle de mise à jour » après le premier test réel du flow
upgrade-flow.POST /api/workspace/proposals/<slug>/accept|rejectouvrait la même garde_HOME_SLUGque le reste des écritures de la vue de travail, alors quePOST /api/projects/update(le bouton « Mettre à jour » juste au-dessus, dans le même écran) écrit déjà dans n'importe quel projet du registre — décider une proposition (jamais la créer automatiquement) est le geste que le cockpit existe pour rendre possible sur toute la flotte. Nouvelle fonctionworkspace_routes.is_proposal_decision()nomme la dérogation ; le reste des écritures (fichier, tâche, override) reste strictement réservé au projet de lancement. Distingue aussi côté UI le typerepair(issue #502) : une substitution évidente s'accepte comme les autres propositions, une réparation sans substitution évidente n'offre plus que « Refuser » et « Ouvrir le fichier » (lien vers l'espace Source à la ligne citée, avec saut de curseur). Corrige au passage un bug trouvé pendant le test réel :withProject()(web/workspace/api.js) ajoutaitprojectà la query string même quand l'appelant en avait déjà posé un explicite, produisant un paramètre dupliqué (?project=terraform&project=grimoire-forge) qui pouvait faire lire la garde d'écriture sur le mauvais projet. - feat(cockpit): montrer ce que le flow de mise à jour a fait — deuxième moitié de « fermer la boucle de mise à jour » (issue #506).
/api/projects/updaterend désormais un statut par nœud (nodes—fait/proposition/sauté/erreur/checkpoint en attente,project_update.py::_node_statuses) et le chemin de la sauvegarde (backupPath, jusqu'ici jeté après avoir servi dedetailau contrat de flow —cmd_upgrade_flow.py,_fail_runporte aussidone/failed_nodesur un refus, perdus jusqu'ici par le_failgénérique). Piloter affiche ce déroulé nœud par nœud, un état « en cours » pendant l'appel (jusqu'à une minute), et rendpreview.mden Markdown structuré (nouveau module partagéweb/workspace/markdown.js, extrait de l'éditeur Source) plutôt qu'en texte brut tronqué à 2000 caractères sans indicateur ; le badge Kit passe à « mis à niveau, checkpoint destructif en attente » après un run confirmé, et nomme les fichiers en retard. Corrige un bug trouvé en testant :options.refresh(), appelé juste après avoir montré le résultat de la confirmation, redessinait toute la fiche et l'effaçait aussitôt (« l'Inspecteur revient silencieusement à l'état initial ») — retiré du chemin de confirmation, remplacé par un rafraîchissement ciblé des propositions. Corrige aussi « Ouvrir ce projet » (rechargeait toute la page et retombait sur la vue Flotte au lieu d'ouvrir l'écran Projet demandé) en navigation interne (ctx.goto), et ajouteGET /api/workspace/flows/runs(nouveau modulegrimoire.tools.flow_runs) pour qu'Observer cesse de dire « TraceLedger vide » quand un run de flow existe bel et bien, juste ailleurs que dans le TraceLedger.
Releases précédentes¶
3.50.2 — Le flow de mise à jour corrigé après son second rejeu réel sur dix projets (orphelins, hook, réparations)¶
- fix(upgrade-flow): second rejeu réel du flow
upgrade-flowsur dix projets (issue #502) — le nœudorphansarchivait encore des agents vivants, 1/3/13 par projet, quand ils vivaient dans une tier possédée par le projet (_grimoire/_config/custom/agents/, legacy) sans être déclarés sousagents.custom_agents:project_upgrade.find_orphans()ne posait pas la question dans le bon ordre, un agent absent du roster frais ET non déclaré ET hors overrides passait pour orphelin même hors tier kit. Un candidat orphelin doit désormais vivre dans la tier kit elle-même (_grimoire/kit/agents/) — la seule que ce nœud archive — en plus des critères existants (roster frais, déclaration).grimoire doctorgagne un nouveau checkagents_referenced(core/integrity.py) : un agent queagent-manifest.csvou une projection hôte gérée (.claude/agents/,.github/agents/, …) nomme mais dont le fichier source a disparu est désormais un FAIL nommé — un projet réel était passé à 25/25 malgré 13 agents manifestés disparus. La génération du manifeste (core/scaffold.py) exclut au passage le gabarit viergecustom-agent.md(name: "{{agent_tag}}"non rendu) qu'elle listait par erreur comme un agent installé. - fix(upgrade-flow): suite du second rejeu réel (issue #502) — deux défauts supplémentaires. (1)
probe_hook(project_upgrade.py) jugeait le hook SessionStart en erreur dès que la sous-chaîne « erreur »/« error » apparaissait n'importe où dans le JSON rendu ; un projet dont le rappel de tâche mémoire mentionnait une erreur passée (cas réel : TTS-Voice) faisait échouerapplysur un hook parfaitement sain. La détection ne porte plus que sur des signaux structurés — cléerrorde premier niveau, ou le marqueur exact[Grimoire] hook <id> en erreur(hosts/decisions/__init__.py::_failed_decision) en tête d'un des blocs de texte réellement rendus (additionalContext,systemMessage,reason) — jamais une recherche libre dans le texte. (2)applyrefusait sur des références_grimoire/...périmées déjà présentes AVANT la mise à niveau (_memory/decisions-log.md,.github/copilot-instructions.md,.claude/skills/*/SKILL.md), laissant trois projets réels « mis à niveau mais flow en échec » sans proposition. Le nœudpreviewenregistre désormais une ligne de base des références mortes (_grimoire-output/upgrade/<date>/doctor-baseline.json) ;applyn'échoue plus que sur une régression (référence absente de cette ligne de base) — chaque référence déjà connue devient une propositionrepair(propose_repairs/_accept_repair, nouveauartifact_type), nommant une substitution vers la tier kit quand elle est évidente (_suggest_repair_substitution, jamais une automatique) et une revue humaine sinon. Le rapport de fin du flow (grimoire upgrade-flow apply/run) dit désormais clairement « mis à niveau, N défaut(s) préexistant(s) en proposition » quand c'est le cas.
Releases précédentes¶
3.50.1 — Le flow de mise à jour corrigé après son premier rejeu réel (orphelins, sauvegarde, aide)¶
- fix(upgrade-flow): premier rejeu réel du flow
upgrade-flow(#490/#491/#492) sur un projet réel (archétypeinfra-ops+fix-loop, featurevector-memory) — trois défauts, issue #499. (1) Le nœudorphansarchivait des agents encore vivants (fix-loop-orchestrator,vectus), cassant le projet jusqu'à réparation manuelle :cmd_up._infer_resolved()codaitfeature_agents=()en dur malgré son propre docstring, gelant tout agent de feature (vectus) surupet le faisant passer pour orphelin — corrigé par_available_features(), qui l'infère depuis le disque commestack_agentsl'était déjà.project_upgrade.find_orphans()ignorait en plusagents.custom_agents(project-context.yaml) et la tier des overrides (un agent fully-custom sans base kit) — les deux sont désormais des motifs d'exclusion explicites, en plus du roster frais ; en cas de doute, jamais d'archivage. (2)backup_project()écrasait silencieusement le tarball/manifeste d'un run précédent le même jour dont le contenu avait changé depuis (dry-run puis run réel) : un digest SHA-256 du contenu réel décide désormais réutilisation (identique) vs nouveau fichier suffixé-2/-3/… (différent) — le nom canonique, une fois écrit, n'est plus jamais réécrit. (3)grimoire upgrade-flow --help/run --helpdécrivent désormais l'ordre des 9 nœuds, la distinction V0 (mécanique)/V1 (proposition)/V2 (checkpoint) et l'emplacement de la sauvegarde.
Releases précédentes¶
3.50.0 — Mettre à jour un projet est un flow (sauvegarde, orphelins, propositions), pas un up nu¶
- feat(cockpit): mettre à jour un projet depuis le cockpit lance le flow, pas
upseul (#490).POST /api/projects/update(src/grimoire/tools/project_update.py, deux hôtes —cmd_cockpit.pyetforge_server.py, câblage inchangé) appelle désormaisgrimoire upgrade-flow runen sous-processus : aperçu par défaut (--dry-run, la réponse porte le contenu exact depreview.md, jamais un résumé reformulé),confirm: truepour le flow complet sous--executor interactive(mécanique, arrêté au checkpointdestructivecomme en CLI — la réponse porte alorsreport.mdet les propositions encorepending). Projet non enregistré ou chemin hors registre : refusé (404), jamais un repli sur le projet servi. Le bouton « Mettre à jour — aperçu » de l'espace Piloter garde son nom ; les propositions qu'un run complet écrit apparaissent dans sa section Propositions dès le redessin qui suit la confirmation, sans mécanisme neuf.docs/upgrade.mdgagne la section « Depuis le cockpit ». - feat(upgrade): mettre à jour un projet est un flow, pas un
upnu (issue #490).registry/blueprints/project-upgrade.blueprint.json: sauvegarde (tarball + manifeste SHA-256 de_grimoire/_memory/) -> aperçu (up/host sync --dry-run) -> orphelins (agents/wrappersgrimoire:managedque le kit installé ne livre plus, archivés sous_archive/<date>-pre-<version>/orphans/, jamais supprimés — avantapply, sinon la garde de distinction deup/host syncrefuse) -> application (up, acceptance : doctor vert + hook SessionStart sans erreur) -> deux nœuds de jugement V1 qui ne font jamais qu'écrire une proposition via le mécanismeproposalsexistant (nouveauxartifact_typeoverride-migrationetmemory-link, jamais d'écriture directe) : overrides en dérive (convertir en partiel si le corps est identique au kit, sinon revue humaine avec diff) et fiches mémoire non raccordées à uncontext:-> besoins/hôtes non déclarés (propositionneeds-hosts) -> vérification (rapport final, manifeste recomparé) -> checkpoint forcé (kind: "checkpoint") pour tout retrait au-delà d'un archivage. Nouvelle commandegrimoire upgrade-flow(nom distinct degrimoire upgrade, déjà pris par la migration de structure v2→v3) :run [--dry-run] [--executor interactive|dispatch], plus les utilitairesbackup/preview/apply/orphans/propose/verify/check/probe-hookque les acceptances du blueprint invoquent elles-mêmes.grimoire.cli.cmd_up.fresh_kit_agent_roster(nouveau) diffuse le plan du scaffolder sans rien écrire, pour que l'orphelinage se détecte sans dépendre d'unupdéjà passé.grimoire.proposalsgagnecreate_manual_proposal(propositions hors déclencheur de non-choix) et le traitementaccept_proposaldes trois nouveauxartifact_type(override-migration,memory-link,needs-hosts) : unmemory-linkaccepté sur un agent kit sans override lui en crée un partiel (extends: kit,kit_source_hash, issue #427) ne portant quecontext:— jamais un refus pour absence d'override, seul un porteur non trouvé refuse encore ; un agent déjà overridé voit sa fiche ajoutée aucontext:existant.needs-hostsécrit désormaishosts.enableddansproject-context.yamlpar round-trip (grimoire.tools._common.load_yaml_roundtrip/save_yaml, #430 : commentaires intacts) quand la proposition porte une valeur mécanique réelle (détection sur disque) ;needs.commandsreste, sciemment, un refus nommé — un besoin non résolu n'a par définition aucune commande à écrire, en inventer une contrediraitgrimoire.core.execution_needs. Corrige au passagegrimoire blueprint validate: sa liste dekindconnus n'avait pas suivi les sept genres de node (#207/#488), refusant tout blueprint qui en déclare un comme « unknown kind » malgré un schéma qui l'acceptait déjà. - fix(tests):
real_project(fixture) était sondable par un vrai backend mémoire réseau sur l'hôte qui lance la suite —_init_real_project(tests/conftest.py) appelaitgrimoire initsans--backend, doncauto:detect_memory_backend()sonde de vrais ports localhost (Weaviate:8080, Qdrant:6333, Ollama:11434), hors du périmètre que_isolate_user_stateprotège. Sur un poste où l'un de ces backends répond réellement,TaskService.recall()remontait desdecisions/failuresdu poste sans rapport avec le projet jetable du test :test_le_rappel_d_une_tache_neuve_est_honnetement_videet son jumeau e2e échouaient en local, jamais en CI (ports injoignables). Épingle--backend local(mémoire fichier) dans_init_real_project; garde de régressiontest_le_projet_reel_n_a_pas_de_backend_memoire_sondable_sur_l_hote. Aucun changement côtésrc/(#494, closes #493).
3.49.0 — Le moteur de flows se compose (sept genres de nœud, sous-flows liés), mémoire multi-projets au cockpit, budget de session réparé¶
- feat(flows): les sept genres de node que le statique ne sait pas exprimer — lot 4 de l'épic moteur de flows (#207).
kindgagnefanout(un node produit N éléments viafanout_items, N sous-flows instanciés, résultats recollés, N borné parpilot.max_fanout_n— absent, refus),verify-panel(k vérificateurs indépendants sous angles imposés,config.verifyPanel.k/.angles, majorité requise, coût = k tentatives toujours, jamais un vert sur 1/k),loop-until-dry(relance jusqu'àconfig.loopUntilDry.maxRoundstours sansnovelty_keyneuve, dédupliquée contre tout le vu),judge(N tentatives sous angles imposés,config.judge.n/.angles, le node lui-même redispatché comme juge V1 qui désignejudge_winner, la trace des N survit dans leurs propres runs),checkpoint(approve/reject/amend, toujours une décision d'hôte forcée — jamais auto-décidée par la cascade,rejectbloque en nommant le motif dans l'événement de refus du kernel),budget(config.budget.maxCostUsd/.passes— première passe obligatoire, les suivantes abandonnées avant dépassement, jamais tentées au-delà) etreplay-diff(même flow rejoué deux fois sur la même entrée,replay_divergedexpose une divergence sans faire échouer le node). Six des sept genres généralisent le lancement de sous-flow déjà posé parcomposite(#206,_launch_child, factorisé) ;checkpointseul n'a pas deref. Paramétrage refusé nommément au chargement (blueprint_loader._validate_genre_config) ;flow extractne les aplatit jamais. Nouveau moduleflows/genres.py, compagnon deflows/dispatch_executor.py: le cliquet de code (scripts/check-code-ratchet.py, R2, seuil 1500 lignes) a refusé de laisserdispatch_executor.pygrossir au-delà (1695 lignes) — même geste que la scission historique deforge_server.py. Aucun changement Rust : orchestration Python (E/S, runs enfants), hors périmètre des fonctions pures déjà portées pargrimoire-flows-core. - feat(flows): un flow est un node — un node
kind: "composite"(déjà réservé par le schéma pour un sous-flow, jusqu'ici jamais exécuté par le moteur) lance sonref(.blueprint.json, id de registre local, ou refus nommé pouruse-case:— réservé au Studio) comme un run enfant à part entière, lié au parent (FlowRunMeta.parent_run_id/parent_node_id), dont le coût est remonté et plafonné par le pilote sur le total du sous-flow ; profondeur de composition bornée à 3, cycle ou référence introuvable refusés au chargement, jamais au node.flow extractne l'aplatit jamais (kind/refpréservés,extraction.child_run_idtracé).grimoire.flows.registry.describe_flow+flow listaffichent, par flow, sa version, sa plage de compatibilité kit (kitMin/kitMax), l'empreinte SHA-256 du fichier et l'union des besoins d'exécution (run_need) du flow et de ses sous-flows ;flow list --require-measurecouvre un sous-flow sans code neuf (même cléblueprint_idque #473). Aucun changement Rust (#206). - fix(policies): rechute du 2026-09-12 (#463), constatée le 2026-09-14 sur Grimoire-Forge : une session dont
per_session.max_duration_min(block, sanstool_pattern) avait dépassé sa fenêtre refusaitgit status(lecture) et — surtout —grimoire policies reset-session, la commande que son propre message de refus recommandait. L'exemption de réparation (_is_repair_exempt, miroir Rustis_repair_exempt) couvrait déjà tout appel non mutant et l'édition de_grimoire/standard/policies.yaml/_grimoire-output/.runs/session-*.json, mais pas l'invocation degrimoire policieselle-même : son premier mot (grimoire) n'est reconnu ni lecture seule (is_read_only_command) ni comme ciblant l'un des deux fichiers exemptés. Reconnaît désormais l'invocation degrimoire policies(status,reset-session) dans ses trois formes documentées (grimoire policies …,<venv>/bin/grimoire policies …,python -m grimoire policies …), en n'inspectant que le premier mot de chaque segment shell — jamais une commande qui se contente de la nommer (echo grimoire policies …reste refusé).grimoire doctorsignale désormais (WARN,policy_budget_duration_guard) toute règleper_sessionsanstool_patternenblockdontmax_duration_minest inférieur à 2880 min (48h), une session Claude Code pouvant rester ouverte plusieurs jours. (#481) - feat(cockpit): agrégation mémoire multi-projets — dernier volet de l'issue #172 (« Cockpit — du générateur statique au portefeuille actif », Refs #468).
GET /api/workspace/memory/overview?projects=all|<slugs>(src/grimoire/tools/workspace_memory.py) rend, par projet du registre de la machine, backend configuré/résolu, nombre d'entrées, dernière écriture et état de l'index lexical (primaire/compagnon/absent) — réutilisememory_link_status()etMemoryManager, aucun nouveau lecteur ; un projet dont la mémoire est absente ou illisible est listé avec sa raison, jamais compté comme un store vide en silence (#264).GET /api/workspace/memory/search?q=...&projects=...interroge la même chaîne quegrimoire memory search(run_memory_search, hybride si le projet a un compagnon lexical) projet par projet et fusionne seulement la LISTE de résultats, étiquetés parprojectSlug, ordre stable (score puis slug) — jamais de fusion des stores. L'espace Mémoire (web/workspace/spaces/memoire.js) gagne un onglet Flotte : zoom docbar « Ce projet / Tous les projets », tableau agrégé, recherche croisée dont les résultats s'ouvrent dans le panneau d'inspection avec leur projet d'origine. Lecture seule ;cmd_cockpit.pynon modifié (les deux routes passent par la tableworkspace_routes.GET_ROUTESdéjà partagée par l'atelier et le cockpit).
Releases précédentes¶
3.48.0 — Moteur de flows complet (besoins, extraction, mesures, pilote de coût), gardes fail-open du framework fermées, Windows soutenu (grimoire.sh, install.sh)¶
- feat(flows): un node de flow déclare un besoin d'exécution (
{"run_need": "test-runner"}) plutôt qu'une commande en dur — lot 2 de l'épic moteur de flows (#205). Catalogue fixe (test-runner,lint,typecheck,build,migration-tool,format,grimoire.core.execution_needs), résolu en deux temps au chargement du blueprint :needs.commandsdéclaré dansproject-context.yaml(schéma/validateur/Rust en parité,rust/grimoire-schema-core/) l'emporte toujours sur la détection par marqueur de projet (pyproject.toml,package.json,Cargo.toml,go.mod). Un besoin non résolvable refuse le chargement du blueprint entier en le nommant, avant que le premier node soit présenté à l'hôte — jamais une installation qui échoue au troisième node.grimoire needs resolveaffiche le verdict par besoin et sa source. Rétrocompatible : les blueprints à commandes en dur restent valides ;grimoire flow runavertit (jamais un refus) quand une commande en dur correspond à un besoin résolu pour ce projet. Aucun changement côtérust/grimoire-flows-core/: la résolution d'un besoin est une lecture disque (config + marqueurs), donc hors périmètre des fonctions pures déjà portées (check_output_against_contractne voit que les pins, jamais l'acceptance). - feat(flows):
grimoire flow extract <run-id> [--out <fichier>]— un flow s'extrait d'un run, il ne se dessine pas (transversale T2, issue #210). Joint les métadonnées du run (FlowRunMeta) au Mission Ledger (node_dispatch_history) pour produire un blueprint brouillon dans le format.blueprint.jsonexistant : l'acceptance d'un node complété par la cascade est remplacée par la commande réellement exécutée, réinférée en{"run_need": ...}sur correspondance caractère pour caractère avec un besoin résolu (#205) — jamais sur une correspondance partielle (une commande observée avec des arguments en plus reste{"run": ...}verbatim, deviner la coupure serait une invention). Un node jamais dispatché (classe V2, ou exécuté par un hôte interactif) garde son acceptance d'origine verbatim ; un node jamais atteint (run incomplet ou abandonné) est marquénot_reached. Chaque node du brouillon porte un objet"extraction"(status,note) traçant lequel de ces trois cas s'est produit — jamais une inférence depuis la prose. Le brouillon extrait est rejouable :flow run/resumele chargent comme n'importe quel blueprint et reproduisent la même séquence de nodes que le run source. Aucun changement côtérust/grimoire-flows-core/: l'extraction ne fait que lireFlowRunMeta/le Mission Ledger via l'API publique déjà existante deFlowEngine(run_meta,status), aucune nouvelle fonction pure candidate au portage. - feat(flows): le flow porte sa preuve, le registre local l'affiche — lot 5 réduit de l'épic moteur de flows (#208).
TraceLedger.dispatch_outcome_stats()gagne une troisième ventilationby_flow(en plus deby_class/by_provider, #442) : le préfixeblueprint_iddu tagreplay:<blueprint_id>:<node_id>qu'un node de flow porte déjà (#205/#210), jamais un dispatch hors flow (qui retombe sur l'id de tâche, sans:).grimoire dispatch statsaffiche cette ventilation ; nouvelle commandegrimoire flow list [--require-measure <blueprint-id>]— le registre local des flows (runs connus groupés par blueprint, dernier run, nœuds résolus/coût/escalade quand une mesure de dispatch existe) ;--require-measurerefuse (sortie 1) si le blueprint désigné n'a aucune mesure. Calculé en Python dans les deux backends (jamais un sixième port Rust : une dimension de plus sur les mêmes enregistrements queby_class/by_provider, pas une fonction assez chaude pour le justifier) — vérifié :to_dict()reste identique entreGRIMOIRE_TRACES_BACKEND=pythonet=rustsur le corpus de parité. Pas de nouveau registre publié (#206 reste verrouillé) : une lecture locale, jamais un second calcul. - feat(flows): le pilote — la fonction qui décide combien dépenser par node, pas une couche (transversale T1, issue #209). Nouveau module
flows/pilot.py:decide()nomme et rassemble la décision déjà éparpillée entremissions.dispatch.start_tier_for/_tier_chain(inchangées, toujours pures, toujours portées Rust —grimoire-dispatch-core) et le refus V2 deflows.dispatch_executor; une politique de projet optionnelle_grimoire/standard/pilot.yaml(start_tierpar classe,max_escalations,max_cost_usd_per_node) la paramètre, absente elle ne change rien.missions.dispatch.run_dispatchgagne le mécanisme réellement neuf : un paramètremax_cost_usdqui arrête l'escalade entre deux paliers (jamais au milieu d'une tentative, jamais après un vert déjà acquis) avec un refus nommé (DispatchReport.cost_capped, même mécanique queunrunnable— aucun changement Rust requis).DispatchExecutorcharge la politique une fois par run ;--max-tierexplicite de la CLI reste prioritaire sur le plafond d'escalade de la politique. Aucune CLI nouvelle. Unpilot.yamlmalformé refuse nommément le chargement plutôt que de retomber en silence (contrairement àorchestration-policy.yaml: un plafond de coût est un mécanisme de sécurité). - fix(framework): cinq parseurs mémoire de
framework/tools/avalaientOSError/UnicodeDecodeErroret rendaient[]/None/continue— un fichier illisible (encodage étranger, verrouillé, tronqué) disparaissait du résumé, de la synchro Qdrant et de l'index RAG sans une ligne,"errors": []en tête.SectionParser.parse_file(context-summarizer.py),MemoryParser.parse_file(memory-sync.py) etChunkingStrategy.chunk_file(rag-indexer.py) prennent désormais unerrors: list[str] | Noneoptionnel, nommant le fichier et la cause dansreport.errorscôtésummarize()/push()/index_collection()(chemins déjà outillés d'un rapport).ToolDiscoverer._inspect_python_tool/_inspect_shell_tool/_inspect_markdown_tool(tool-registry.py) et le fallback fichier derag-retriever.py(aucun rapport structuré sur ces chemins) écrivent la même information sur stderr. Diff net nul sur chacun des cinq fichiers (framework/gelé, shrink-only) : le garde anti-boucle est fusionné avec lereturn/continued'origine sur la même ligne. (#264) - fix(framework): un pheromone board corrompu de
framework/tools/(stigmergy.pyetstigmergy_hooks/scripts/stigmergy_hook.py) était remplacé par un board vide au dépôt suivant —load_boardrendait un board vide surJSONDecodeError/OSErrorsans le signaler, etdeposit_pheromone/save_boardécrasait ensuite le fichier corrompu avec un board ne contenant que le nouveau signal : une écriture tronquée (disque plein, deux hooks concurrents) détruisait tout l'historique stigmergique,deposit_pheromonerendant unePheromonecomme si de rien n'était.load_boardmet désormais le fichier corrompu de côté (<nom>.corrupt-<horodatage>) avant de rendre un board vide, et l'annonce sur stderr. La copiesrc/grimoire/tools/stigmergy.pya le même correctif depuis #270 ; celle-ci porte le correctif dansframework/(gelé, shrink-only) à diff net négatif sur les deux fichiers (garde compacté sur la même ligne que lereturn, plus deux signatures et un appel resserrés sur une ligne). (#265) - fix(cli):
grimoire.sh helpbloquait indéfiniment (timeout 30s) sous Git Bash Windows —find_project_root()remontait l'arborescence viadirnamejusqu'à/, mais sous Git Bash Windowsdirnamepeut cesser de progresser (ex. se stabiliser sur.) avant d'atteindre/, transformant la boucle en boucle infinie. La boucle s'arrête désormais dès quedirnamene progresse plus, en plus de la condition d'arrêt à/.TestGrimoireShRoutingrepasse de@requires_bash_posix_onlyà@requires_bash(couverte sous Windows) (#461). - fix(tests):
TestInstallSh::test_install_sh_has_shebanglevait unUnicodeDecodeErrorcp1252 sous Windows —INSTALL_SH.read_text()sansencoding=explicite décodait le fichier (légitimement UTF-8, avec des caractères accentués) selon la locale par défaut de Windows plutôt qu'en UTF-8 (même famille que #192).read_text(encoding="utf-8")corrige le test ;install.shlui-même n'a pas changé.TestInstallShrepasse de@requires_bash_posix_onlyà@requires_bash;requires_bash_posix_onlyn'ayant plus d'utilisateur (dernier hors-scope #231 traité), il est retiré (#462).
3.47.0 — Premier contact exécutable (wizard, cadrage, création de projet), hôtes déclarés, budget de session durci, Windows soutenu¶
- feat(cockpit): le wizard de setup (espace Piloter) exécute réellement le projet au lieu d'écrire une commande à copier-coller —
POST /api/setupappellegrimoire upen direct (cmd_up.run_up_pipeline, jamais un sous-processus), needs transmis (B3 rebranché sur B2,GET /api/needs), refus fail-closed avant toute écriture (archétype/backend/need inconnu, chemin non inscriptible), rapport d'exécution persistant (_grimoire/setup-run.json, doctor compris) affiché dans la fiche projet._select_cwd_projecthonore désormais un--project-rootexplicite même sur un dossier vierge (sans quoi aucune écriture n'était jamais possible sur le cas que l'issue décrit) ; le mode « copier-coller la commande » reste un repli explicite (#171). - feat(standard): le need
project-discoveryscaffolde réellement_grimoire/cadrage/par appel de code direct (cmd_up.py::_step_cadrage), plus par une simple suggestion textuelle deneeds_suggest.py. Sa complétude devient un contrôle du standard agentique (_verify_cadrage, dimensionartifacts) visible dansstandard verify/gate/score/audit:info/warningpar défaut,errorsur les phases gate (exigences, cahier des charges) uniquement quandproject-discoverya été explicitement choisi. Documentation dédiéedocs/cadrage.md, référencée depuis le README etdocs/cli-reference.md(#173). - feat(cockpit): « nouveau projet » depuis le portefeuille (espace Piloter, niveau Flotte) — un bouton ouvre le même formulaire que le wizard de setup (#171) sur un chemin choisi ;
POST /api/projects/create(cmd_cockpit.py) exécute le plan sur ce chemin (chaîneexecute_setup_plan/grimoire up, comme/api/setup), refuse un chemin hors des racines permises ou déjà un projet Grimoire, puis enregistre le projet dans le registre —grimoire up/initl'enrôlent comme n'importe quel autre. Le sélecteur de projets (le tableau Flotte) le montre aussitôt, sans redémarrer le cockpit. Point 1 (API réelle de lecture) déjà livré par #143 ; l'agrégation mémoire multi-projets (point 3) reste hors périmètre — toujours des lectures projet par projet (#172). - feat(hosts): déclarer les hôtes activés (
hosts.enableddansproject-context.yaml, schéma/validateur/Rust en parité), détection filesystem par défaut quand la clé est absente (claudeseul si rien n'est détecté),grimoire initécrit la clé avec le résultat de la détection.grimoire host sync/upn'émettent plus que les hôtes activés ; un--host <x>désactivé est un refus nommé sauf--force-host;grimoire host status --host allliste les fichiers orphelins d'un hôte désactivé,grimoire host sync --prune-disabledles retire (opt-in).grimoire doctorajoute INFO (orphelins) et WARN (hôte activé sans fichier émis). Alternative « canal plugin » Claude Code écartée par décision (#177). - fix(policies): quatre défauts de conception de la règle de budget de session, révélés le 2026-09-12 par un incident réel sur la Forge (
per_session: {max_writes: 800, max_tool_calls: 3000}sanstool_patterna fini par refuser tous les outils de la session, y compris l'édition du fichier de règles qui la portait). (1)max_writescomptait tout appelBashportant une commande, lectures comprises (cat,grep,find,git status...) ;classify_tooldistingue désormais unBashde lecture seule (grimoire.hosts.decisions.tool_facts.is_read_only_command), conservateur par construction. (2) Une règleper_sessionenblockn'atteint plus jamais un appel non-mutant ni une écriture ciblant_grimoire/standard/policies.yaml/_grimoire-output/.runs/session-*.json— exemption de réparation codée en dur, miroir Rust compris (rust/grimoire-policies-core). (3)per_session.subagents(shared/separate) nomme le partage de budget entre une session et ses sous-agents (le payload de hook de Claude Code ne les distingue pas) ;separateéchoue au chargement (GR-POL-003) plutôt que de simuler une isolation absente. (4)grimoire policies reset-session [--session-id]supprime l'état d'une session,grimoire policies statusavertit sous 10 % de plafond restant, et le message de refus d'un budget nomme désormais cette commande et le fichier de règles.grimoire doctorsignale (WARN) toute règleper_sessionsanstool_patternenverdict_on_match: block(#463). - fix(cli): sous Windows,
grimoire-init.sh reset/uninstall/quick-updaterestaient non prouvés -- les seize (puis vingt-deux) tests correspondants etaient satures depuis #256 sans jamais avoir tourne verts. La vraie cause n'etait pas les commandes elles-memes (elles repondent en <1s une fois invoquees) mais le harnais de test :subprocess.run(..., timeout=30)tue bien lebashdirect a l'expiration, puis redraine les tubes sans timeout -- si un petit-fils Windows (cp.exe,mkdir.exe...) tenait encore le tube stdout/stderr ouvert, ce second appel bloquait indefiniment le job jusqu'a son plafond._run()tue desormais l'arbre de processus complet (taskkill /T /F) avant de redrainer. Les vingt-deux tests des trois commandes tournent et passent souswindows-latest(matrice restauree, job bloquant conserve) ;grimoire.sh helpetinstall.shrestent hors scope avec leurs propres bugs Windows distincts (#461, #462) (#231).
3.46.1 — Correctifs trouvés en conditions réelles et par le triage¶
- feat(cli):
grimoire initetgrimoire upenrôlaient chaque projet dans le registre cockpit réel (~/.grimoire/cockpit/registry.json), même les jetables (/tmp, un scratchpad, une recette) — seule la variable d'environnement non documentéeGRIMOIRE_NO_COCKPITpouvait l'éviter, et rien ne la mentionnait à côté des options des deux commandes. Ajoute--no-cockpitàinitet àup(même effet que la variable, documentée au même endroit dans--help) (#305). - fix(cli):
grimoire upannonçaitrefresh: donealors quehost sync --dry-runtrouvait encore des dizaines de fichiers à écrire pour Claude Code, Copilot, Codex, Cursor et Gemini CLI juste après —refreshne régénère que le tier kit (_grimoire/kit/), pas les surfaces hôtes qui en découlent (chemin de code séparé,grimoire.hosts.emitters).upexécute désormais une étapehost_syncaprèsrefreshetstandardqui appelle la même synchronisation quegrimoire host sync --host all;host sync --dry-runexécuté juste aprèsupne trouve plus rien à écrire (#296). - fix(cli): un projet qui déclare déjà l'archétype
agentic-standardrestait en FAIL permanent aprèsgrimoire up— le profilstarter(choix par défaut sans--needs) ne couvre pas les acceptance criteria hard de la DNA de cet archétype (llm-provider-registry.yaml,compliance-declaration.md).uprésout désormais le needprovider-neutral(-> profilcontrolled) par défaut quand cet archétype est déjà déclaré ;knowledge-source-registry.yaml, écrit seulement à partir du profilorchestrated, rejoint les chemins « déclarés avant usage récurrent » quedoctorne réclame plus tant qu'aucune source n'est indexée (#295). - fix(cli):
grimoire doctor -o json .(l'option après le sous-commande, telle que documentée pardoctor --help) échouait avec « No such option: -o » — seule la forme globalegrimoire -o json doctor .fonctionnait.doctoraccepte désormais un--output/-olocal (même contrat que le-oglobal : mêmes contrôles, même structure JSON) (#293). - fix(policies):
tool_patterndes politiques temporelles n'était comparé qu'au nom nu de l'outil, si bien qu'un motif documenté commeBash(git push:*)ouBash(rm:*)ne matchait jamais —require_approval: truerépondaitallowdès le premier appel. Introduit une clé d'outil façon permissions Claude Code (Bash(<commande complète>),<Tool>(<file_path>)) contre laquelle le motif parenthésé est désormais comparé, rétro-compatible avec les motifs nus ("*","Bash") (#449). - fix(core): déclarer
source.assist(model,allow_lan) dans le schéma et le validateur, Python et Rust, pour quegrimoire check .accepte l'exemple documenté de l'assistant Source au lieu de refusersourcecomme clé inconnue (#451). - fix(cockpit): l'assistant Source distingue le chargement du modèle local d'un dépassement de délai —
GET /api/workspace/assistsonde/api/ps, déclenche lui-même le chargement (keep_alive, sans générer) et répond « chargement du modèle » ; le bouton Suggérer reste visible mais désactivé et se re-sonde toutes les 3 s au lieu d'échouer au premier clic (#450).
3.46.0 — Politiques temporelles et overrides partiels, coût et pass^k dans les gates, timeline et assistant cockpit, pont MCP à jour¶
-
feat(policies): politiques temporelles par session sur la médiation d'outils — budgets, approbation préalable, refroidissement (#439). Point 3 de l'audit de positionnement 2026-09-12 :
PolicyRulegagne quatre clés optionnelles et rétrocompatibles (tool_pattern,require_approval,per_session,cooldown_after, voir_grimoire/standard/policies.yaml), validées au chargement (GrimoirePolicyErrornommée sur clé inconnue). L'état de session (compteurs, approbations, horodatages — jamais de secret ni de contenu d'outil) vit dans_grimoire-output/.runs/session-<id>.json, écrit atomiquement, remis à zéro àSessionStart; un fichier absent ou corrompu redevient une session neuve. La décision pure (règle + état → verdict) est portée à l'identique en Python (grimoire.policies.temporal) et en Rust (rust/grimoire-policies-core,evaluate_temporal), testée en parité ; le hookPreToolUsereste sous +5 ms de surcoût mesuré.grimoire policies statusaffiche les compteurs et budgets restants de la session — le cockpit n'est pas dans ce lot. -
feat(hosts): un override d'agent peut désormais rester partiel (
extends: kit) et signale sa dérive au lieu de figer silencieusement une copie (#427). Migration réelle 3.38.0 → 3.44.2 : quatre overrides en copie intégrale n'avaient plus reçu une seule mise à niveau de leur agent depuis des mois,doctorétant 22/22. Trois changements : (1) tout override écrit par un chemin qui comprend le kit (cockpit,grimoire agent override convert) enregistrekit_source_hash:(empreinte tronquée du fichier kit au moment de l'écriture) ;doctoret le cockpit comparent cette empreinte à l'actuelle et signalent en WARN (jamais FAIL) une dérive, avec un résumé (sections de frontmatter ajoutées/retirées, delta du corps, ou champs figés pour un override partiel), et en INFO une empreinte inconnue (override antérieur à cette issue). (2)extends: kitdans le frontmatter d'un override ne redéfinit plus que les champs qu'il liste (model_affinity,context,skills,tools,use_when,dont_use_when,max_turns,description,tool_boundary) — le corps et le reste du frontmatter viennent du fichier kit de même nom, fusionnés danshosts/collect.py(dicts Python, avant tout appel au port Rust optionnel — parité inchangée sousGRIMOIRE_HOSTS_BACKEND=rust) ; unextends: kitsans agent kit de même nom refuse au chargement, nommant l'agent. Le cockpit (assigner un skill, éditer une clause) écrit désormais un override partiel dès qu'un agent kit du même nom existe, une copie intégrale sinon. (3)grimoire upliste, après avoir rafraîchi le palier kit, les overrides à revoir — sans jamais les toucher — etgrimoire agent override convert <nom> [--dry-run]convertit une copie intégrale en override partiel équivalent, refusant (lignes citées) quand le corps de la copie a divergé du kit plutôt que de fusionner du texte. -
feat(dispatch): coût par tâche résolue et pass^k, comptabilité continue et contrôle dans les gates (#442). Point 5 de l'audit de positionnement du 2026-09-12 : le kit dispatchait déjà des tâches en cascade sans jamais agréger en continu ce que ça coûte ni si ça marche de façon fiable — seules des campagnes d'évals manuelles (#308) répondaient à ces questions.
grimoire task dispatch/grimoire flow run --executor dispatchécrivent maintenant un événementdispatch.outcomepar cascade réellement tentée dans le journal de traces (classe, paliers tentés, coût total, verdict d'acceptance, résolu ou non — aucun contenu de prompt). Nouvelle commandegrimoire dispatch stats [--since 30d] [--json], sœur deproviders history: coût par tâche résolue, taux d'escalade, part d'inexécutable (par classe et par fournisseur), et pass^k sur les nœuds rejoués (une série est « toute au vert » seulement si toutes ses exécutions ont résolu la tâche). Les agrégations pures vivent dansrust/grimoire-traces-core/(extension du sixième port), avec parité de test sous les deux backends. Le standard gagne le contrôledispatch.cost_slo(patternprovider-cost-slo) :INFOfaute de données,WARNen cas de dépassement (coût ou pass^k),FAILuniquement si le projet déclaredispatch_cost_slo.enforce: true— jamais bloquant par défaut. Le cockpit n'est pas concerné par ce lot. -
feat(cockpit): timeline unifiée par tâche dans la vue de travail, l'export OTel de #322 devient une source lue plutôt qu'un mécanisme mort (#139). Audit de positionnement du 2026-09-12, point 2 :
TraceLedger.export_otel_jsonlproduit des spans GenAI conformes depuis #322, mais rien ne les consommait —/api/otelsert une pile d'événements différente (blueprint_telemetry,events.jsonl), sans rapport avec le TraceLedger.grimoire.missions.trace(déjà livré par #276) gagne une cinquième source,otel: si un export existe à l'emplacement conventionnel (<traces>/otel-export.jsonl), ses spans sont lus et corrélés pargrimoire.task_idpuis partraceIdpartagé avec les spans enfants — jamais par heuristique textuelle. Deux autres traces disparaissaient aussi en silence de la timeline avant ce correctif : les dispatchs d'agent (agent.dispatch,agent.miss) et tout futur fait du TraceLedger qui n'est ni un gate ni un appel d'outil — un repli générique les reprend désormais sous la sourcehooks. Côté cockpit : l'espace Exécuter (web/workspace/spaces/executer.js) ouvre la timeline d'une tâche depuis sa carte (bouton « Voir la timeline »), la filtre par source et par gravité, et détaille chaque ligne en accordéon ; l'espace Observer (observer.js) y renvoie depuis un span qui portegrimoire.task_id. Lecture seule (ADR-007) : aucune écriture, aucune reconstruction d'événement absent — une tâche sans trace montre « aucun événement » et nomme les sources lues. Tests :tests/unit/missions/test_trace.py(corrélation par identifiants, dispatch non perdu, otel présent/absent),tests/unit/test_workspace_api.py,tests/e2e/test_workspace_lot4_spaces.py(dispatch et transition refusée visibles, filtre par source). Docs :docs/cli-reference.md,docs/serve-blueprints.md,docs/audits/positionnement-2026-09-12.mdetframework/agentic-industry-reference.md(section 10) mis à jour avec la date de correction. -
feat(cockpit): suggestions de contenu par un petit modèle local (Ollama), toujours derrière l'IntelliSense déterministe de l'espace Source, jamais à sa place (#280). Voie 2 de #280, derrière la voie 1 (IntelliSense déterministe, PR #303) :
project-context.yaml: source.assist.model(vide par défaut, opt-in) plus la même sonde qu'grimoire providers audit(GET /api/tags) décident si le bouton Suggérer de l'éditeur Source apparaît — sinon l'interface ne montre rien et ne tente rien.GET /api/workspace/assist(sans coût) rend ce statut ;POST /api/workspace/assist(src/grimoire/tools/source_assist.py, projet d'accueil seulement) appelle réellement le modèle en local (http://127.0.0.1:11434, délai borné à 10 s), avec les identifiants du paquet de langage (agents, skills, workflows, patterns) injectés dans le prompt, et vérifie après coup tout identifiant cité par la réponse contre ce même paquet — marqué « inconnu » plutôt que corrigé à la place de l'utilisateur. Panneau d'aperçu dans l'éditeur (Ctrl+Maj+Espace, ou le bouton) avec Insérer/Ignorer ; l'insertion passe par le même chemin que la frappe clavier, la colorisation et les diagnostics se recalculent dessus. Aucun fournisseur distant, aucune clé, aucune écriture de fichier par la route. Garde de relecture : une URL Ollama résolue (OLLAMA_HOST) hors bouclage (127.0.0.1,::1,localhost) est refusée par défaut —source.assist.allow_lan: truel'autorise explicitement. -
feat(mcp): migrer le pont MCP vers la révision de protocole 2026-07-28 (#436). Le plancher
mcp>=1.10,<3laissait un résolveur retenir un SDK qui plafonne à la révision 2025-11-25 (pas deserver/discover, pas de mode sans état). Relevé àmcp>=2.0,<3: à partir de 2.0.0, le SDK négocie 2026-07-28 par défaut (server/discover, auto-dérivé des outils/prompts/ressources enregistrés) tout en servant encore, sur la même connexion, un hôte qui ne connaît que le handshakeinitialize(2025-06-18, 2025-11-25) —serve_dual_era_loopcôté SDK. Aucune ligne du pont (src/grimoire/mcp/server.py) n'a dû changer : il ne câblait déjà ni Roots, ni Sampling, ni Logging (les trois fonctionnalités que 2026-07-28 déprécie, retrait possible à partir de 2027-07-28), et ne garde aucun état entre deux appels d'outil en dehors des fichiers du projet ciblé. Nouveaux tests (tests/unit/mcp/test_protocol_revision.py) qui pilotent un vraiClientSessionsur des flux en mémoire : négociation 2026-07-28 parserver/discover, compatibilitéinitializeà 2025-06-18 et 2025-11-25, et absence d'état partagé entre deux connexions successives. Documentation (docs/mcp-integration.md) et carte de correspondance (framework/agentic-industry-reference.md, section 10) mises à jour. Hors périmètre : transport HTTP/SSE, authentification, exposition réseau distante — le pont reste stdio, en local. -
fix(yaml):
grimoire upgraderound-trippaitproject-context.yamlvia un chargeursafe(aucune métadonnée de commentaire) puis un dumper round-trip — tous les commentaires du fichier disparaissaient silencieusement à chaque migration v2→v3 (#430). -
fix(flows):
grimoire.runtime.kernel.create_instancetronquait silencieusementrecipe_id/blueprint_idaux 16 derniers caractères pour construirewfi_id/run_id(WFI-...) — deux blueprints partageant ce suffixe (par ex.tasklib-hardeningperdait déjà son premier caractère) pouvaient obtenir le mêmerun_idsur des kernels indépendants. L'identifiant complet est gardé tant qu'il tient dans une borne large (64 caractères) ; au-delà, il est raccourci et désambiguïsé par une empreinte de 8 hex desha256(recipe_id)plutôt qu'une simple coupe. Les runs déjà persistés sous l'ancien format restent lisibles parflow status/flow list(#446).
3.45.0 — Le gate de dispatch exécute l'acceptance, board du cockpit, sixième port Rust¶
-
fix(flows): le gate de
flow run --executor dispatchexécute l'acceptance structurée d'un node, pas seulement l'enveloppe (#428). Rejeu réel du 2026-09-11 (épic #307, lot 3) : un nœud V0 déclaré vert alors que sa vraie suitepytestne pouvait pas être collectée (dépendance absente) — le gate ne vérifiait que la conformité de l'enveloppe JSON de l'ouvrier au contrat de sortie, jamais le texte de l'acceptance.node.acceptanceaccepte désormais, en plus du texte libre (rétrocompatible), une forme structurée exécutable ({"run": "...", "expect_exit": 0, "cwd": ".", "timeout_s": 120, "expect_stdout_contains": "..."}) ou une evidence mécanique ({"path_exists": "..."},{"test": "..."}) — validée au chargement du blueprint (blueprint_loader.py), refus nommé sur une forme inconnue. Le gate (missions/dispatch.py) exécute ces commandes après la réponse de l'ouvrier et rend trois verdicts, jamais un quatrième : exécutée-verte (succès), exécutée-rouge (échec, suit la cascade normale : réessai/escalade), ou acceptance inexécutable (refus nommé — binaire absent,pytest5/4, erreur d'import dans la sortie — qui arrête la cascade net, jamais un succès).flow statuset le rapport de dispatch montrent par nœud le statut d'acceptance (exécutée/inexécutable/jugée) et la sortie tronquée à 2 Ko. La classe V0 exige une acceptance structurée : un nœud dont le texte seul le classerait V0 mais qui n'en déclare aucune est rétrogradé en V1 (cascade démarrant àmid, jamais fermé sur la seule enveloppe — marqué à relire), avec un avertissement nommé posé au chargement du blueprint et transmis à la fois au démarrage de la cascade et au rapport, pour queflow runetflow statuss'accordent sur la même classe. Un nœud V1 déclaré (vocabulaire de revue) garde le comportement actuel du kit, documenté sans être étendu. Aucun changement côté port Rust des flows (rust/grimoire-flows-core/) : seuleNodeContract.outputs(pins) y traverse la frontière PyO3, inchangée par ce correctif. -
feat(cockpit): le board de l'espace Exécuter montre le corps réel d'une tâche (description, garde-fous, dépendances) et les commandes d'intention restent gated par la preuve en multi-projet (#140). L'inspecteur de tâche affichait déjà les critères d'acceptation et les preuves attendues ; il gagne des blocs Description, Garde-fous et Dépendances, tirés du
MissionTaskdu ledger — ce quetask-board.yamln'a jamais su porter (ADR-005).kanban.htmlreste la vitrine statique et gagne un lien vers le board vivant. Aucune écriture nouvelle : les commandes de transition, le gate de preuve et la restriction au projet de lancement du cockpit existaient déjà ; ce lot les couvre par des tests dédiés (unitaires sur la route/api/workspace/tasks/<id>/<action>— succès, refus nommant l'artefact manquant, refus hors projet de lancement — et navigateur sur le critère d'acceptation à la lettre : une carte visée vers review sans evidence pack est refusée avec l'artefact nommé, et le board change quand on change de projet). -
feat(traces): sixième port Rust optionnel du cœur du système d'artefact émergent — les agrégations du journal de traces (
TraceLedger.agent_dispatch_counts/.agent_miss_counts/.oldest_started_at), la règle de fraîcheur (compute_agent_freshness, issue #396) et le déclencheur de propositions d'artefact (grimoire.proposals: nommage mécanique, résolution du porteur issue #402, décision de synchronisation seuil/refus issue #395/#394/#389) (issue #354).rust/grimoire-traces-core/, basculeGRIMOIRE_TRACES_BACKEND=python|rust|auto(lue indépendamment pargrimoire.traces.ledgeretgrimoire.proposals, qui ne s'importent pas l'un l'autre), jobs CIrust-traces / cargoetrust-traces / paritydans.github/workflows/rust-cores.yml, roue toujourspy3-none-any. Défaut trouvé par l'oracle Rust et corrigé dans cette même PR :grimoire.traces.ledger._parse_isoattrapaitValueErrormais pas laTypeErrornon rattrapée levée ensuite pardatetime.now(tz=UTC) - datetime.fromisoformat(...)sur un horodatage ISO-8601 naïf (sans décalage) — un journal JSONL édité à la main peut en porter un, etcompute_agent_freshnessrevendique explicitement ne jamais lever sur un journal arbitraire ; corrigé en traitant un horodatage naïf comme UTC des deux côtés (même correctif quegrimoire_traces_core, qui n'a jamais eu ce trou). Invariants de la doctrine rendus impossibles à violer par construction (clampés/exclus dans la fonction de décision elle-même, jamais par un appelant qui pourrait l'oublier) : seuil de proposition jamais sous 2, aucune proposition au premier non-choix, une proposition refusée ne revient que si son compte a doublé depuis le refus, la persona d'entrée est exclue de la recherche de porteur avant même de lire sonuse_when, le match de catégorie se fait par mot entier (limite de mot Unicode) et non par sous-chaîne. Corpus fixture de dix journaux JSONL réalistes (tests/fixtures/traces_ledgers/) et fuzz léger (200 enregistrements aléatoires) prouvant qu'aucune des fonctions d'agrégation/fraîcheur ne lève jamais, sous aucun backend, et qu'un champ de contenu libre (prompt/request) écrit à la main n'est jamais agrégé. -
fix(up):
identityne corrompt plus un scalaire commenté deproject-context.yaml(#426).cmd_setup._apply_project_contextréécrivait chaque ligneuser:connue avec une regex.+qui avalait tout le reste de la ligne — valeur et commentaire inline — comme si c'était « la valeur », puis rewrappait ce texte entier entre guillemets.skill_level: "expert" # beginner | intermediate | expertdevenaitskill_level: ""expert" # beginner | intermediate | expert"à chaquegrimoire up, y compris quand la valeur ne changeait pas — et cassait ensuitegrimoire doctor(GR002, YAML imparsable). Un nouveau_split_scalar_and_commentsépare correctement le scalaire (quoté ou non) du commentaire qui le suit avant toute lecture ou réécriture, et_apply_project_contextrecolle le commentaire original après la nouvelle valeur au lieu de le jeter.
3.44.2 — Correctif de régression : SessionStart sur un agent à skills attachés¶
- fix(hosts):
collect_agentsrésout ses propres skills quandknown_skillsn'est pas fourni —SessionStartet le porteur de proposition ne plantent plus sur un agent à skills attachés (#423).entry_persona_context(le hookSessionStart) et_category_carrier(le porteur par catégorie du déclencheur de propositions) appelaientcollect_agents(project_root)sans lui passer l'inventaire des skills du projet ; l'ensemble retombait alors sur un ensemble vide, ce qui faisait échouer fail-closed tout agent déclarant unskills:en frontmatter — la forme par défaut des archétypes depuis #377/#387. Tout projet avec un agent à skills attachés voyait sonSessionStartrépondre « en erreur » et sa persona d'entrée jamais injectée.collect_agentsrésout désormais lui-même l'inventaire viacollect_skills()quand aucun n'est fourni, pour qu'aucun appelant ne puisse retomber dans ce trou.
3.44.1 — CLI et hook plus rapides au démarrage, flow status corrigé sur abandon¶
- fix(flows): un abandon (ou un refus MAST) avant tout progrès n'affiche plus tous les nodes comme complétés dans
grimoire flow status(#417).FlowEngine.status()dérivaitcompleted_nodes/pending_nodesde la seule position decurrent_nodedans l'ordre topologique — sur un run ABORTED/REFUSED, cela rendaitcompleted_nodes == orderen entier même quand aucun node n'était réellement fait.completed_nodesreflète désormais lescompleted_stepsdu dernier checkpoint réel duRuntimeKernel; comportement identique sous les deux backends (GRIMOIRE_FLOWS_BACKEND=python|rust). - perf(cli): sous-commandes chargées à la demande —
grimoire --version313→89 ms,grimoire doctor .448→270 ms (#418).LazyTyperGroupremplace l'enregistrement direct des 20 modulescmd_*par un registre résolu à la demande ; aide identique au caractère près (--helpvérifié avant/après pour les 47 commandes). - perf(hosts):
grimoire.hosts.decisionsdécoupé par décision —grimoire-hook PreToolUse70→64 ms (#420). Suite de mesure de #418 : ce point d'entrée ne passe jamais par le CLI Typer, donc pas concerné par le lazy-loading ci-dessus. Cible des 50 ms non atteinte pourPreToolUse, assumé — le profil restant tient à trois postes incompressibles (dataclasses/inspect,standard_state/ruamel.yaml, moteur de politique) ; suivi en issue #419.
3.44.0 — Trois cœurs Rust optionnels de plus : hosts, flows, dispatch¶
grimoire.hosts.collect/.surfaceportés en Rust optionnel (#412). Lecture du frontmatter d'agent et garde de distinction, basculeGRIMOIRE_HOSTS_BACKEND=python|rust|auto. L'oracle Rust a révélé unValueErrornon rattrapé sur un chiffre Unicode non-ASCII dans_max_turns("²".isdigit()vrai,int()refuse) — corrigé côté Python. Un verbe d'outil hors périmètre n'est plus silencieusement ignoré : une note nommant les jetons rejetés apparaît désormais sous les deux backends.- La machine à états du moteur de flows portée en Rust optionnel (#413).
Contrat de sortie, node courant,
resume(),status(), transitions du kernel ; basculeGRIMOIRE_FLOWS_BACKEND=python|rust|auto. Divergence trouvée par l'oracle :REFUSEDmanquait aux statuts terminaux côté moteur de flows — unresume()sur un run refusé levait une erreur générique au lieu du message dédié. Corrigé, les deux backends s'accordent. - La cascade de dispatch et le routage de fournisseurs portés en Rust
optionnel (#415). Résolution de palier, rendu d'invocation, analyse de
la sortie d'un ouvrier délégué, classification de revue ; bascule
GRIMOIRE_DISPATCH_BACKEND=python|rust|auto. Deux défauts trouvés par l'oracle et corrigés côté Python : un coût JSONtrue/falseétait coercé en1.0/0.0(isinstance(True, int)vrai en Python) ; le JSON est désormais parsé strictement (RFC 8259), les jetonsNaN/Infinityqueserde_jsonrefuse font désormais échouer les deux côtés à l'identique. - Chacun des trois ports garde la roue publiée
py3-none-any— les crates Rust ne quittent jamaisrust/. VoirCHANGELOG.mdpour le détail complet.
3.43.1 — Le validateur, doctor et concierge alignés ; le gate Rust enfin requis¶
- Le validateur ne plante plus sur les champs énumérés (#409, #410).
Une liste ou une table sur un des neuf champs énumérés du schéma levait
TypeError: unhashable typeau lieu d'une erreur de validation propre ; cinq champs déclarés chaînes n'étaient en outre jamais type-vérifiés. Le cœur Rust servait déjà d'oracle correct ; parité stricte désormais testée entre les deux backends. doctorne confond plus un paquet npx/uvx/pipx/... avec un chemin (#393, #403).@playwright/mcp@0.0.80ressemblait à un chemin selon l'heuristique et déclenchait un fauxpath does not exist.- Le concierge n'est plus jamais retenu comme porteur d'un skill proposé (#402, #406). La persona d'entrée servait presque toujours de repli, donc presque toute proposition lui était attachée à tort — contraire à la doctrine (un skill s'attache à l'agent qui fait le travail).
rust-gatedevient l'unique check requis pour les cœurs Rust (#407, #408). Les anciens jobs, filtrés par chemin, pouvaient rester « attendus » indéfiniment sur une PR hors de leur périmètre.- Voir
CHANGELOG.mdpour le détail complet, dont le script de mesure des gains Rust (#354, #404).
3.43.0 — Le système observe ses propres angles morts¶
- Le concierge journalise ses non-choix (#389). Symétrique du choix
d'agent (#366) : quand aucun spécialiste ne correspond, ou qu'il se rabat
sur un généraliste,
grimoire agent-missjournalise catégorie, spécialité cherchée et agent de repli — jamais le contenu de la demande.grimoire registry dispatchesaffiche désormais les non-choix agrégés à côté des choix. - La répétition d'un non-choix propose un artefact (#395). Le
déclencheur lit ces non-choix et, passé un seuil configurable, écrit une
proposition
pending(agent ou skill selon la doctrine) — jamais de création sans acceptation explicite (grimoire proposals accept/reject), visible en CLI et dans le cockpit. - La fraîcheur des agents se signale, ne se retire jamais (#396, #398).
Un agent jamais dispatché depuis un seuil configurable est signalé par
grimoire doctor,grimoire registry dispatcheset le cockpit — jamais déprécié automatiquement. - Second port Rust, optionnel — le schéma et le validateur (#354, #392).
grimoire.core.schemaetgrimoire.core.validatorbasculent surGRIMOIRE_SCHEMA_BACKEND=python|rust|auto; le cœur Rust rejette des entrées que le validateur Python laissait passer sans erreur, sans roue publiée. - Un correctif : la clé
proposalsmanquait au schéma et au port Rust (#400), corrigé avant tout usage réel.
Releases précédentes¶
3.42.0 — Le modèle hybride d'agent : skills attachées, doctrine, cockpit qui les gère¶
- Doctrine de création d'artefact (#370) et contexte câblé sur le dispatch
(#378).
docs/artifact-doctrine.mdfixe quand un agent, un skill ou un prompt mérite d'exister ; le contexte qu'un agent déclare entre désormais dans le contrat de la tâche qu'il reçoit. - Cinq archétypes refaits en généraliste plus skills attachés —
platform-engineering,creative-studio,meta,stack,web-app/fix-loop/minimaletinfra-ops(#375, #380) — plus un nouvel agent de sécurité offensivesecurity-auditor(#357) et le retrait des manifestes d'équipe fantômes (#350). - Le cockpit gère les agents du projet. Une seule commande de service ouverte sur le projet courant (#356), une table des agents avec skills et usage réel, assignables depuis l'inspecteur (#382).
- Les skills se paient à l'usage, pas au tour de session (#377). Une skill déclarée par un agent se replie dans le fichier de cet agent au lieu d'être émise pour tout le projet — mesuré à 0 token payé hors activation contre 1480/tour avant. Le fichier émis ne charge plus que le contexte déclaré (#379), et le choix de l'agent d'entrée est désormais tracé (#366).
- Premier port Rust, optionnel —
grimoire.policies(#354). Un second cœur du moteur de règles, en Rust via PyO3, jamais publié sur PyPI et sans effet quand il n'est pas construit. - Neuf correctifs, dont les écritures du cockpit qui répondaient 404 dès
qu'un projet était sélectionné (#358) et
grimoire upqui pouvait rétrograder silencieusement le profil du standard (#348). VoirCHANGELOG.mdpour le détail complet.
Releases précédentes¶
3.41.0 — Le dispatch choisit sa preuve, le flow conduit un node à la fois¶
- Le dispatch choisit sa cascade selon ce qu'il faut prouver — épic
routage par vérifiabilité (#307). Chaque tâche dérive une classe de
vérifiabilité (V0/V1/V2) de son contrat, qui pilote la cascade de
grimoire task dispatch; le palier de départ s'ajuste à l'historique des verdicts, la relisibilité et les incertitudes déclarées affinent la décision, et la politique qui en résulte est émise aux hôtes avec l'état des fournisseurs dès leSessionStart. - Les fournisseurs se pilotent au coût. Registre étendu par palier de
coût,
grimoire providers statusetgrimoire providers auditpour sonder sans dépenser, choix de modèle qui croise reasoning et coût. - Le moteur de flow conduit un node à la fois (#204).
grimoire flow run|status|resume|abort, avec l'exécuteur par dispatch en cascade du routage par vérifiabilité branché dedans. - Garde-fous runtime et hôtes. Plafonds de tours et de budget par instance, garde des échanges pair-à-pair, claims à fichiers exclusifs, sous-agents bornés.
- Les écritures mémoire sont validées et la médiation d'outils vérifiée. Contrat MCP annoté, contenu externe marqué comme tel.
- Export de traces OTel GenAI conforme, évals avec pass^k.
Releases précédentes¶
3.40.0 — La tâche se souvient, l'éditeur comprend¶
- Le claim rappelle ce que la mémoire sait de la tâche. Historique,
tâches voisines avec leur cause d'arrêt, mémoire consolidée : en CLI
(
grimoire task recall), en MCP (task_recall), au SessionStart et dans l'inspecteur d'Exécuter. La consolidation n'écrit qu'à la clôture ou au blocage. - L'éditeur Source comprend le langage du kit. Colorisation, diagnostics (chemin mort, agent inconnu, clé de frontmatter inconnue, terme absent du glossaire, pattern ou workflow inconnu) et complétion, sans dépendance, adossés au doctor, au manifeste, au catalogue et au glossaire.
- Le bridge du standard converge avec la norme. Trente-six patterns rattachés au catalogue de la norme, cinq exigences obligatoires couvertes, plus aucun trou de N1 à N5.
- Moins de code mort. Onze accesseurs sans appelant retirés, trois branchés sur une vraie commande, un inventaire qui refuse le retour.
- Piloter distingue un kit aligné d'un kit installé.
Releases précédentes¶
3.39.1 — Le glossaire sur une installation nue¶
- La vue de travail s'ouvre sur
pip install grimoire-kitsans extra. Le glossaire importait PyYAML alors que la dépendance déclarée est ruamel ; les six espaces s'ouvraient avec une erreur sur une installation nue (#292, trouvé par le passage consommateur sur un projet réel). Chargeur du kit à la place, avec un test qui bloqueyaml. - Voir
CHANGELOG.mdpour le détail des autres changements de cette version.
Releases précédentes¶
3.39.0 — La vue de travail¶
- Une coque unique pour l'atelier et le cockpit. Six espaces de travail,
Piloter, Concevoir, Exécuter, Observer, Mémoire et Source, remplacent les
pages d'outil ; les anciennes redirigent vers l'espace correspondant
(
?legacy=1pour y revenir). Panneaux repliés, entrouverts ou épinglés, raccourcis 1 à 5, mode concentration, palette ⌘K qui atteint projets, workflows, tâches, fichiers et commandes du kit. - Des infobulles qui expliquent le système. Survol, Alt pour épingler, termes liés en bulles enfants, Échap ferme la pile ; une seule source, le glossaire du kit, dont la documentation est générée.
- Source. Les fichiers du projet par étage, kit généré, overrides et projections des hôtes ; éditer un fichier du kit crée l'override ; diff contre le catalogue de digests, provenance, historique. Une console qui n'exécute que les sous-commandes autorisées du kit, sans shell.
- Des données réelles ou rien. Le portefeuille lit les vrais champs, un état vide dit d'où viendra la donnée, la démo reste opt-in.
- Palette Encre, cinq surfaces, sombre et clair, contrastes mesurés par un test ; fontes Geist embarquées. Tests de navigateur Chromium en CI.
Releases précédentes¶
3.38.0 — Les agents connaissent leur tâche, les gardes parlent, la campagne enforced est mesurée¶
- Les agents lisent, réclament et clôturent leurs tâches. Outils MCP
task_list_ready,task_show,task_claim,task_update,task_contextsur le même service que le CLI ; la tâche courante se résout depuis le claim actif du Mission Ledger ;grimoire task trace <id>rend la timeline unifiée d'une tâche, refus de policy et gates rouges compris. - Les gardes n'échouent plus en silence. Une exception dans la politique
de hook rend
ask, un fichier de gates illisible ferme toutes les portes, un board stigmergique corrompu est mis de côté, un manifeste d'équipe cassé est nommé, le rendu des surfaces hôtes dit quand il échoue, et la CI ne porte plus d'étape qui ne peut pas échouer. - Six artefacts de plus pour la norme. Registres d'acceptation, de rétention, d'outils, d'incidents, de capacités et matrice risques-contrôles : plus aucune exigence obligatoire sans artefact de N1 à N5.
- Campagne enforced contre activated, pré-enregistrée. Vingt-quatre runs par bras : zéro régression dure sous enforcement, l'artefact de preuve garanti, ni complétion ni test supplémentaire, 39 % de tours en plus.
- Release plus honnête. L'étage TestPyPI qui n'a jamais tourné est retiré
au profit de
make wheel-check; chaque PR exécute la CI complète.
Releases précédentes¶
3.37.0 — Le bridge trace la norme, Windows compte, l'identité se déclare¶
- Le bridge du standard est tracé. La révision de la norme est épinglée et
grimoire standard upstreamdétecte quand elle avance ;traceability.yamlrelie chaque artefact et chaque famille de vérificateurs aux exigencesAG-*et contrôlesCTRL-*avec citation, etgrimoire standard traceabilityrend la matrice et les trous par niveau. Deux artefacts que la norme rend obligatoires sont livrés : le claim ledger (tous profils) et le registre des surfaces runtime (governed,production). Voir Intégration du standard. - La persona d'entrée se choisit par projet (
agents.entry) ; un projet qui porte déjà son orchestrateur déclare"". Voir Surfaces hôtes. grimoire setupécrit la source de vérité qu'il déclare, puis vérifie les miroirs contre le fichier relu.- Windows est bloquant en CI. Quarante-six outils ne meurent plus sur une console cp1252, le dernier rouge réel est corrigé, la jambe Windows des tests d'outils compte comme ubuntu.
- Le garde de release vérifie que chaque changement fusionné a son entrée, au bon endroit — le cas des trente-huit blocs égarés de la 3.36.0 ne peut plus se reproduire en silence.
Historique complet¶
Consultez le CHANGELOG complet sur GitHub.
Le format est basé sur Keep a Changelog et ce projet adhère au Semantic Versioning.