Skip to content

Latest commit

 

History

History
1225 lines (1006 loc) · 129 KB

File metadata and controls

1225 lines (1006 loc) · 129 KB

BACKLOG — albert-code

Tickets de construction du bundle, ordonnés. À dérouler avec OpenCode. Convention : chaque ticket a un but, une définition de done (DoD) et un renvoi aux scénarios de TESTS.md. Un ticket issu d'un retour utilisateur cite son finding via <- AC-R### (registre FEEDBACK.md). 🔴 bloquant v1 · 🟠 important · 🟡 nice-to-have


EPIC 0 — Spike technique (dé-risquer la chaîne)

T0.1 🔴 Smoke test chaîne minimale ✅ validé

But : prouver que agent-vm + OpenCode + Albert (Mistral-Medium-3.5-128B) fonctionnent ensemble. Tâches :

  • Installer agent-vm (Lima) ; agent-vm setup.
  • Config OpenCode minimale avec provider Albert (@ai-sdk/openai-compatible, baseURL, {env:ALBERT_API_KEY}).
  • Dans la VM : opencode run "écris un hello world en python" → vérifier réponse + écriture fichier. DoD : un fichier est créé par l'agent dans la VM, via Albert, sans clé en clair. → TESTS.md S1 ✅.

T0.2 🔴 Valider une skill + le small_model ✅ validé

But : confirmer chargement skill local + bascule modèle. Tâches : placer react-dsfr dans ~/.config/opencode/skills/ ; demander une page DSFR ; configurer small_model = DeepSeek-V4-Flash et vérifier qu'il est utilisé pour les tâches légères. DoD : page DSFR générée conforme + small_model actif. → TESTS.md S2 ✅.


EPIC 1 — Config & runtime de référence

T1.1 🔴 opencode.json (provider + MCP + permissions) ✅ implémenté

But : config OpenCode canonique du bundle. Tâches :

  • provider.albert (@ai-sdk/openai-compatible, models Mistral-Medium-3.5-128B + DeepSeek-V4-Flash).
  • model / small_model.
  • mcp (réf ci-dessous).
  • permission : edit/bash raisonnables (allow dans la VM isolée, deny sur git push * hors VM à débattre), webfetch: allow.
  • Portée projet (non-destructif) : écrire ce fichier à la racine du projet cible, PAS dans ~/.config/opencode/. Le global perso de l'utilisateur (possiblement avec d'autres providers, ex. Scaleway) ne doit jamais être touché. OpenCode fusionne global+projet, projet prioritaire.

Config MCP de référence :

"mcp": {
  "data-gouv": { "type": "remote", "url": "https://mcp.data.gouv.fr/mcp", "enabled": true },
  "context7":  { "type": "remote", "url": "https://mcp.context7.com/mcp", "headers": { "Authorization": "Bearer {env:CONTEXT7_API_KEY}" }, "enabled": true },  // CLÉ API REQUISE
  "playwright": { "type": "local", "command": ["npx", "-y", "@playwright/mcp@latest"], "enabled": true },  // paquet à confirmer
  "chrome-devtools": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"], "enabled": true }  // paquet à confirmer
}
  • data.gouv : confirmé → https://mcp.data.gouv.fr/mcp (remote HTTP, lecture publique).
  • context7 : clé API requise (https://context7.com/plans) → stockée comme CONTEXT7_API_KEY dans ~/.zshenv. Rendre le MCP optionnel/skippable si pas de clé.
  • chrome-devtools : debug navigateur (DOM, console, réseau, perf). Déjà préinstallé dans la VM agent-vm (mais câblé pour Claude Code) → à déclarer côté OpenCode. Complémentaire de playwright (auto/tests).
  • playwright / chrome-devtools : paquets npx à valider au câblage. DoD : opencode démarre sans erreur de schéma, les 3 MCP sont listés et connectés (au moins data.gouv répond), Albert répond. → TESTS.md S3.

T1.2 🔴 .agent-vm.runtime.sh idempotent ✅ implémenté

But : bootstrap reproductible de l'environnement dans la VM. Tâches (idempotentes, tester l'état avant chaque action) :

  • Persister ALBERT_API_KEY (et baseURL) dans ~/.zshenv (piège auth shells non-interactifs). Idem CONTEXT7_API_KEY si fournie (optionnelle).
  • Cloner/git pull etalab-ia/skills dans ~/.config/opencode/skills/.
  • Installer les serveurs MCP requis (npx/binaire).
  • Vérifier OpenCode présent, sinon installer.
  • Non-destructif : détecter une config agent-vm/OpenCode déjà en place et ne rien écraser (écritures additives uniquement : clone skills, clés ~/.zshenv si absentes). DoD : 2 exécutions consécutives = la 2e est une suite de no-ops en quelques secondes ; skills à jour après git pull ; une config OpenCode perso préexistante (ex. provider Scaleway) reste intacte. → TESTS.md S4, S5, S12.

T1.3 🟠 Synchro skills au boot ✅ implémenté

But : garantir des skills toujours fraîches (réponse au « pas de maj auto »). DoD : ajouter une skill dans le repo distant → après reboot VM, elle apparaît dans OpenCode sans action manuelle. → TESTS.md S5.

T1.4 🟠 Ressources VM par défaut adaptées au code <- AC-R010 ✅ implémenté

But : les défauts d'agent-vm (1 CPU / 3 GiB / 10 GiB) sont trop justes pour un agent de code ; en usage réel on tourne à 4 CPU / 8 GiB / 30 GiB. agent-vm ne lit aucune variable d'env pour ses défauts (ressources uniquement via --cpus/--memory/--disk), et le disque se fige à la création du template (agent-vm setup) — il ne peut que grandir. Config proposée (surchargeable par env) : AC_VM_CPUS=4, AC_VM_MEMORY=8, AC_VM_DISK=32. Tâches :

  • Définir ces défauts (env-overridables) en tête d'install.sh.
  • check_base_vm() : agent-vm setup --disk ${AC_VM_DISK} (dimensionner le disque de base d'emblée).
  • « Prochaines étapes » + README : recommander le 1er lancement dimensionné agent-vm --cpus ${AC_VM_CPUS} --memory ${AC_VM_MEMORY} opencode (cpu/mémoire s'appliquent au run et persistent au clone du projet).
  • Garde-fou hôte : ne pas allouer plus que ~la moitié de la RAM/CPU de la machine (détecter sysctl/nproc, capper) — éviter 8 GiB sur un Mac 8 Go. DoD : une install fraîche produit une VM ≥ 4 CPU / 8 GiB / ≥30 GiB sans réglage manuel ; valeurs surchargeables par env ; pas de sur-allocation sur petite machine. → TESTS.md S20.

T1.5 🟡 context7 conditionnel selon présence de la clé <- AC-R011 ✅ implémenté

But : context7 est enabled: true en dur dans config/opencode.template.json ; sans CONTEXT7_API_KEY, le MCP échoue au démarrage dans la VM (401 / bearer vide) et s'affiche « cassé ». Répond à la note de T1.1 restée ouverte. Tâches : au scaffold (Phase B, pose de opencode.json), fixer context7.enabled selon la présence de CONTEXT7_API_KEY (env ou ~/.zshenv) — false (ou MCP retiré) si absente ; true si présente. Post-patch du fichier posé (sed/jq) ou template conditionnel. Documenter le comportement. DoD : install sans clé context7 → opencode.json posé a context7.enabled: false → aucun MCP en erreur dans la VM ; avec clé → enabled: true. → TESTS.md S21. Implémenté — la conditionnalité a évolué vers un opt-in plus strict que le ticket : les 4 MCP du template sont désormais enabled: false par défaut (config/opencode.template.json:24-48), et le scaffold n'écrit un MCP que si l'utilisateur le choisit ; la clé Context7 n'est demandée que si context7 est coché (scaffold_opencode_json, lib/phases.sh:784-792). Une personne sans clé répond simplement N à context7 → aucun MCP en erreur (objectif du ticket atteint).

T1.6 🟠 Scaffold : opencode.json existant sans provider albert <- AC-R012 ✅ absorbé par T7.7

Traité dans T7.7 (garde-fou générique run + setup) : l'avertissement au setup pour un opencode.json existant sans albert y est implémenté (scaffold_opencode_json, lib/phases.sh:729-733). Clôturé comme absorbé par T7.7 (DoD couverte : message explicite au setup + option de merge en T8.2). But : dans un repo ayant déjà un opencode.json, le scaffold le conserve (non-destructif) → le provider albert n'est jamais ajouté → Albert ne se connecte pas dans la VM, sans alerte (juste « conservé »). Footgun silencieux. Tâches : en Phase B, si ./opencode.json existe déjà, détecter s'il contient le provider albert ; sinon → avertir clairement (« opencode.json existant sans provider Albert → Albert non câblé ») et proposer/documenter le merge du bloc provider.albert + model/small_model (jq/sed) sans écraser le reste. Ne jamais écraser silencieusement. DoD : scaffold dans un repo avec opencode.json sans albert → message explicite (+ option de merge) ; avec albert déjà présent → info « rien à faire ». → TESTS.md S22.

T1.7 ✅ Auth GitHub de la VM : commit OK, mais push + PR impossibles depuis la bulle <- AC-R013 — résolu (validé 06/07/2026)

But : le README promet « l'agent pousse des PR depuis la VM », mais Albert Code ne configure dans la VM ni l'identité git (user.name/user.email), ni la clé SSH, ni de token gh → l'agent peut committer localement mais ni pusher ni ouvrir la PR. SSH = auth ≠ identité de commit.

⚠️ Confirmé en test réel (06/07/2026) — lors de l'ajout du modèle Qwen 3.6 (branche feat/add-qwen-3.6, commit b9eb1e9). Séquence observée dans la VM agent-vm :

  • git commit → OK (l'identité était présente sur ce poste).
  • git push → échec : ni auth HTTPS, ni host key SSH.
  • gh pr create → échec : gh non authentifié (pas de GH_TOKEN).
  • Fallback navigateur (chrome-devtools new_page sur l'URL pull/new/…) → échec : pas de navigateur ouvrable dans le sandbox.
  • Résultat : blocage total du push/PR côté VM. Contournement utilisé : push + gh pr create depuis l'hôte (où gh est authentifié). → à documenter comme procédure intérimaire tant que T1.7 n'est pas implémenté.

Tâches : au setup (ensure_vm_runtime / runtime VM), configurer l'identité git de la VM — reprendre le git config --global user.name/email de l'hôte s'il existe, sinon prompt (recommander l'email noreply GitHub, pas l'email perso) ; et régler l'auth push par l'une des voies (à trancher) : (a) injecter un GH_TOKEN fine-grained scopé dans l'env VM (jamais loggé, jamais commité), ou (b) injecter la clé SSH GitHub (cf. runtime.example.sh d'agent-vm) + url.insteadOf pour forcer SSH. Documenter aussi le fallback hôte (commit VM → push/PR hôte) pour les postes non configurés. ✅ Implémenté (06/07/2026, branche feat/github-auth-vm) — voie (a) retenue : GH_TOKEN fine-grained (SSH écarté car gh pr create exige un token, pas une clé). Câblage générique sans secret dans runtime/agent-vm.runtime.sh (setup_github_auth) : si GH_TOKEN présent → persiste le token, pose l'identité git globale (AC_GIT_USER_NAME/AC_GIT_USER_EMAIL), gh auth setup-git. Le secret reste dans ~/.agent-vm/runtime.sh (perso, hors dépôt). Doc utilisateur : README § « Push & PR depuis la VM ». Reste : test end-to-end en VM fraîche (DoD ci-dessous) + nettoyage de l'identité placeholder albert-code-user dans les .git/config locaux. DoD : dans une VM fraîche, l'agent fait un commit de la bonne identité + push + gh pr create sans config manuelle. → TESTS.md S23. ✅ Validé (06/07/2026) — dogfood réel : la PR #2 (le câblage lui-même) a été commitée, poussée et ouverte intégralement depuis la VM. Voir TESTS.md S23. Suite → T1.8.

T1.8 🟠 Intégrer l'auth GitHub à l'installeur (zéro config manuelle) <- T1.7 ✅ implémenté

But : T1.7 a livré le câblage (setup_github_auth dans le runtime), mais l'utilisateur doit encore créer un PAT et coller un bloc à la main dans ~/.agent-vm/runtime.sh (section README « Push & PR depuis la VM »). Sans cette étape, push/PR restent indisponibles (échec désormais explicite : « GH_TOKEN absent → voir README », plus silencieux). Objectif : rendre l'auth GitHub aussi transparente que les clés Albert, posées d'office par install.sh. Tâches :

  1. Prompt token à l'install (ensure_vm_runtime, Phase A) : proposer (optionnel, skippable) de saisir un GH_TOKEN, l'écrire dans le bloc géré # --- albert-code : clés VM --- aux côtés d'ALBERT_API_KEY (même pattern grep-guard + chmod 600, jamais loggé). Supprime la fragilité actuelle du 2ᵉ bloc manuel qui partage le marqueur de fin # --- /albert-code ---.
  2. Identité + garde-fou email : demander/dériver AC_GIT_USER_NAME + AC_GIT_USER_EMAIL, avec validation « doit finir en users.noreply.github.com » (aurait attrapé le gmail saisi le 06/07). Proposer de dériver le noreply depuis le compte gh de l'hôte si dispo.
  3. Gotcha de rotation : documenter (README) + garde-fou — un GH_TOKEN/email déjà persisté dans le ~/.zshenv de la VM n'est pas mis à jour par un changement côté hôte (grep-guard). Prévoir un chemin de mise à jour (réécrire la ligne ~/.zshenv de la VM, ou agent-vm rm documenté).
  4. Next-steps de l'install : mentionner l'auth GitHub dans « Prochaines étapes » (actuellement absente). DoD : sur un poste vierge, install.sh propose l'auth GitHub ; après acceptation, une VM fraîche pushe + ouvre une PR sans aucune édition manuelle de runtime.sh ; un email non-noreply est refusé avec un message clair. → TESTS.md S24. Implémenté (06/07/2026, branche feat/github-auth-installer) — sous-points 1 (prompt token dans Phase A), 2 (identité + garde-fou email noreply, 3 tentatives) et 4 (next-steps) faits, dans _github_auth (lib/phases.sh:476-587) ; token jamais loggé (vérifié par canari en dry-run). Gotcha de rotation (sous-point 3, resté ouvert) : désormais tracé par l'EPIC 10 (BACKLOG.md T10.1) — cf. cause racine scrutée le 2026-08-26. Validation S24 absorbée par T2-CH2.

T1.9 🟠 Rendre le baseURL Albert surchargeable <- AC-R051

Provenance : l'implémentation est attendue d'un contributeur externe, annoncée dans l'issue publique #36. Le dépôt documente le ticket pour donner une cible à la PR entrante : ne pas reprendre ce travail en parallèle. Être en contact avec le contributeur pour ne pas faire doublon.

Cadrage (communiqué au contributeur dans l'issue, partie de la DoD) :

  1. Un seul point de définition du défaut, en tête de fichier avec les autres variables surchargeables, pas un ${AC_ALBERT_BASE_URL:-…} répété à chaque usage.
  2. La valeur est résolue au setup et écrite littéralement dans l'opencode.json généré, jamais laissée sous forme {env:AC_ALBERT_BASE_URL}. Raison : OpenCode tourne dans la VM, et la liste des variables propagées vers cette VM est codée en dur dans lib/phases.sh (objet de T10.8, non traité) ; une nouvelle variable n'atteindrait pas la VM et le provider y verrait un baseURL vide. Le jour où T10.8 est fait, cette contrainte pourra être réexaminée.
  3. Non-régression de albert-code update. repair_stale_provider_albert et jq_albert_reconcile_program ne touchent en principe que provider.albert.models, .model et .small_model : un baseURL personnalisé doit survivre à un setup et à un update. À prouver par un test, pas à supposer.

Note de souveraineté : le défaut reste Albert, et le README devra dire ce que la surcharge implique, à savoir sortir de la chaîne souveraine (l'intermédiaire compatible OpenAI remplace alors la chaîne d'inférence souveraine prévue par le bundle). C'est défendable en test ou derrière un proxy d'entreprise, à condition que ce soit explicite.

But : une variable AC_ALBERT_BASE_URL, défaut https://albert.api.etalab.gouv.fr/v1, utilisée aux quatre emplacements (config/opencode.template.json, programme jq de merge du provider, repli par concaténation quand jq est absent, appel GET /v1/models du catalogue — cf. AC-R051), pour qu'un poste derrière un proxy d'entreprise ou une évaluation face à un intermédiaire compatible OpenAI n'aient plus à repatcher le dépôt après chaque setup.

Tâches :

  1. Exposer AC_ALBERT_BASE_URL (défaut https://albert.api.etalab.gouv.fr/v1), définie en un seul point en tête de fichier avec les autres variables surchargeables.
  2. Substituer l'URL codée en dur aux quatre emplacements de câblage par cette variable.
  3. Résoudre la valeur au setup et l'écrire littéralement dans l'opencode.json généré (jamais sous forme {env:AC_ALBERT_BASE_URL} — contrainte T10.8, cf. cadrage).
  4. Vérifier qu'un albert-code update (via repair_stale_provider_albert / jq_albert_reconcile_program) ne rétablit pas l'URL par défaut sur un projet à baseURL personnalisé (test, pas supposition).
  5. Documenter la variable dans le README, y compris ce qu'elle implique en termes de sortie de la chaîne souveraine.

DoD : AC_ALBERT_BASE_URL est honorée aux quatre emplacements, avec un défaut inchangé ; un setup avec la variable positionnée produit un opencode.json portant l'URL surchargée en clair et un appel de catalogue dirigé vers cette même URL ; un albert-code update sur ce projet ne rétablit pas l'URL par défaut ; le README documente la variable et ce qu'elle implique. → TESTS.md S68 (à créer par la PR entrante).


T2-CH2 🟠 Simplifier et fiabiliser l'auth GitHub du wizard <- AC-R036 ✅ implémenté

But : le bloc GitHub de phase_a est redondant et fragile : après la dérivation API réussie, il redemande quand même le nom et l'email (pré-remplis, donc inutiles). Et si l'utilisateur colle son token à la question o/N (cas réel bêta-testeur), le token est silencieusement ignoré. Tâches :

  1. #1 — Supprimer les 2 prompts quand la dérivation réussit : après curl 2xx + login + id, utiliser directement git_name = login et git_email = <id>+<login>@users.noreply.github.com. Afficher un seul récap ok "Compte GitHub : login <email>". Persister GH_TOKEN, AC_GIT_USER_NAME, AC_GIT_USER_EMAIL.
  2. #2 — Fallback uniquement si la dérivation échoue : curl non-2xx/réseau/parsing vide → prompts manuels nom/email avec validation noreply.
  3. #3 — Séparation visuelle : ajouter info "Étape suivante : colle ton PAT GitHub (scope repo)." avant le prompt_secret du token.
  4. #4 — Détection token collé à l'étape o/N : regex ^(ghp_|github_pat_|gho_|ghs_|ghr_|.{31,}$) → warn + retry. Token invalide (curl non-2xx) → warn + retry (max 3). Ne jamais persister un token non validé. DoD : dérivation OK → aucun prompt nom/email ; token collé à l'étape o/N → message clair + retry ; token invalide → message + retry ; limites à 3 tentatives. → TESTS.md S31, S40.

EPIC 2 — Profils & bootstrap (séparation des conventions)

T2.1 🔴 Structure profiles/ ✅ implémenté

But : isoler physiquement les conventions par contexte. Tâches : créer profiles/{beta.gouv,lasuite,iae}/AGENTS.md (+ set de skills + bouts d'opencode.json spécifiques). Reprendre etalab-ia/skills/templates/instructions/{beta.gouv,LaSuite}.md ; créer le profil iae (house style OpenGateLLM). Plus un choix autre = aucune convention imposée (ne copie aucun AGENTS.md de profil ; l'utilisateur fournit le sien s'il veut). DoD : 3 profils complets + option autre neutre, zéro convention partagée divergente entre eux (commits, package manager, design system). → TESTS.md S6.

T2.2 🔴 install.sh / bootstrap qui DEMANDE le contexte ✅ implémenté

But : aucun profil par défaut, aucun merge. Tâches : prompt « Quel contexte ? (1) beta.gouv (2) La Suite (3) IAE (4) Autre » → copie le bon AGENTS.md + installe le bon set de skills ; (4) Autre = ne copie rien (neutre). Refuser de continuer si rien n'est choisi. DoD : choisir beta.gouv → le projet ne contient AUCUNE convention IAE (commits gitmoji/uv absents), et inversement ; choisir Autre → aucun AGENTS.md de profil n'est posé. → TESTS.md S6, S7.

T2.3 🟠 Couche universelle commune ✅ implémenté

But : factoriser le socle (anglais code / FR UI, RGAA, ANSSI, RGPD, secrets, souveraineté) sans casser l'isolation. DoD : le socle est présent dans les 3 profils, les divergences restent dures par profil. → TESTS.md S6.

T2.4 🔴 Mode --dry-run + sandbox de test (testabilité) ✅ implémenté

But : pouvoir tester install.sh / runtime sur une machine déjà configurée sans rien modifier. Tâches :

  • install.sh et runtime/agent-vm.runtime.sh acceptent --dry-run : chaque action (write fichier, clone/pull skills, append ~/.zshenv, install outil) est AFFICHÉE préfixée [dry-run] mais PAS exécutée ; exit 0.
  • Toutes les écritures passent par UNE fonction unique (ex. apply()), qui en dry-run logge au lieu d'exécuter → aucun chemin ne peut « oublier » le dry-run.
  • Respecter des overrides d'env pour sandboxer les écritures : OPENCODE_CONFIG_DIR (défaut ~/.config/opencode) et un HOME configurable pour ~/.zshenv → tout peut être redirigé vers un dossier jetable.
  • --help documente --dry-run et ces variables. DoD : HOME=/tmp/ac-test ./install.sh --dry-run n'écrit AUCUN fichier (diff de /tmp/ac-test avant/après = vide), affiche le plan complet, exit 0. → TESTS S13.

EPIC 4 — Standards & CI

T-FIX-1 🟠 uninstall.sh laisse les clés + install non-idempotent ✅ implémenté

But : corriger le marqueur cassé (casse-sensible) et le retrait awk défectueux. Tâches :

  • Définir UNE constante marqueur unique (AC_MARKER) utilisée à l'écriture (install.sh) ET aux tests (install.sh idempotence, uninstall.sh retrait).
  • Réécrire le retrait dans uninstall.sh : grep -vE pour supprimer marqueur + lignes export ALBERT_API_KEY= / export CONTEXT7_API_KEY=. DoD : install.sh relancé n'ajoute pas un 2e en-tête (S4) ; uninstall.sh retire le bloc ET les 2 clés de ~/.agent-vm/runtime.sh (S14).

T-FIX-2 🟠 Posture sécurité clé/bash ✅ implémenté

But : durcir la posture sécurité (clé, exfiltration, permissions bash). Tâches :

  • Recommander/imposer une clé Albert DÉDIÉE par projet (révocable) → README + AGENTS.
  • Documenter le risque résiduel d'exfiltration (prompt-injection) dans README + AGENTS.
  • Corriger la deny-list git push : git push.*(--force|-f | --force-with-lease) (l'ancienne ne matchait pas ... main --force). DoD : README + AGENTS documentent clé dédiée + exfiltration ; config bash deny-list corrigée.

T-FIX-3 🟡 Quick wins ✅ implémenté

But : durcissements rapides. Tâches :

  • templates/.github/workflows/security.yml : pin trivy-action@0.28.0 (au lieu de @master).
  • chmod 600 sur ~/.zshenv et ~/.agent-vm/runtime.sh après création (contiennent une clé).
  • local val dans prompt_secret/prompt_input (lib/ui.sh).
  • Ne pas source agent-vm.sh en --dry-run (install.sh). DoD : bash -n OK ; dry-run ne source rien ; permissions 600 appliquées.

T-FIX-4 🟠 Test désinstallation ✅ implémenté

But : valider la désinstallation complète. DoD : S14 — après install puis uninstall, aucune clé ne subsiste dans ~/.agent-vm/runtime.sh ni ~/.zshenv, et le bloc marqueur a disparu.

Note numérotation : T-FIX-5 à T-FIX-10 ont été utilisés lors de la revue sécurité/robustesse (banner, skills sync, prompt_choice, clé VM 401, migrations — cf. git log), livrés puis retirés du backlog. Les tickets issus des retours utilisateurs reprennent donc à T-FIX-11 pour éviter toute collision.

T-FIX-11 🔴 agent-vm résolu immédiatement après install ✅ implémenté <- AC-R001, AC-R002

But : agent-vm est une fonction shell (sourcée) ; après ./install.sh, elle n'est pas chargée dans le terminal courant → « command not found », y compris après relance. Tâches : shim exécutable agent-vm posé dans un dossier déjà présent dans $PATH (source agent-vm.sh + dispatch) ; atténuer les messages « déjà sourcé » trompeurs ; vérif finale command -v agent-vm. DoD : agent-vm résout dans le même terminal juste après install, sans réouverture. → TESTS.md S15.

T-FIX-12 🟡 Retirer les chemins ~/Dev en dur <- AC-R003 ✅ implémenté

But : ~/Dev est une convention personnelle ; ne doit pas apparaître en dur dans le code, le README ni la doc. Tâches : défauts AGENT_VM_DIR/ALBERT_CODE_REPO → emplacement neutre (XDG / SELF_DIR), identiques install/uninstall ; placeholders neutres sans espace dans README/doc ; grep de contrôle 0 hit. DoD : grep -rE '\$HOME/Dev|~/Dev' (hors .git) ne renvoie rien ; --dry-run cohérent install/uninstall.

T-FIX-13 🟠 Hint de scaffold dynamique ($SELF_DIR) <- AC-R006 ✅ implémenté

But : le message de scaffold (phase_b, quand on est dans le dépôt) hardcode ~/albert-code/install.sh au lieu du chemin réel d'invocation → copier-coller cassé si le dépôt est cloné ailleurs. Régression de T-FIX-12. Tâches : remplacer par $SELF_DIR/install.sh ; commentaire d'usage en tête d'install.sh → placeholder neutre <chemin-du-dépôt>/install.sh ; vérifier qu'aucun message runtime ne hardcode un chemin de dépôt. DoD : le hint affiche le chemin réel quelle que soit la position du clone. → TESTS.md S16.

T-FIX-14 🔴 Onboarding VM de base (agent-vm setup) <- AC-R007 ✅ implémenté

But : après une install fraîche, agent-vm opencode échoue (Base VM not found) car la VM de base n'existe pas et agent-vm setup n'est ni lancé ni mentionné. Tâches : « Prochaines étapes » → lister agent-vm setup comme étape 1 (avant agent-vm opencode) ; en Phase A, détecter l'absence de VM de base et prévenir (option : proposer de lancer agent-vm setup avec confirmation — pas d'auto-run silencieux, c'est long). DoD : un nouvel utilisateur qui suit les instructions ne rencontre jamais Base VM not found. → TESTS.md S17.

T-FIX-15 🟡 Retirer le contournement OpenCode hors-VM <- AC-R009 ✅ implémenté

But : Albert Code s'utilise exclusivement via agent-vm (bulle isolée). L'installeur ne doit ni signaler l'absence d'OpenCode sur le PATH hôte, ni suggérer npm i -g opencode-ai (bypass de l'isolation). Tâches : dans install.sh (A.7), retirer le check host-opencode + les warns « absent du PATH » (×2) + l'info npm i -g opencode-ai. Optionnel : une seule ligne positive « OpenCode s'exécute dans la bulle agent-vm — rien à installer sur ton poste ». Vérifier qu'aucun message n'oriente vers une exécution d'OpenCode hors VM. DoD : grep -n 'opencode-ai\|absent du PATH' install.sh ne renvoie rien ; l'install ne mentionne plus d'OpenCode hôte.

T-FIX-16 🔴 opencode: command not found au run (PATH bash Lima) <- AC-R047 ✅ implémenté

But : albert-code run échoue avec /bin/bash: line 1: opencode: command not found alors qu'OpenCode EST installé dans la VM (~/.opencode/bin/opencode). Cause racine : Lima v2.2.0 (publiée le 21/07/2026, PR lima-vm/lima#5194) a introduit un champ user.shell et n'utilise plus le shell de connexion du guest pour limactl shell ; le défaut est /bin/bash. Le chsh posé par vendor/vm/agent-vm.setup.sh:81 est donc devenu inopérant, et bash non-login ne lit pas ~/.zshenv (PATH OpenCode + secrets). Distinct de T7.8 (binaire vraiment absent après clone périmé). Tâches :

  • phase_run : lancer via _vm run --tty zsh -l -c "opencode --auto" (login zsh = PATH + secrets chargés, TUI avec --tty). Ne plus appeler le verbe vendored opencode. Ne pas « réparer » le chsh : ignoré par Lima >= 2.2.0. (CORRIGÉ)
  • check_opencode : simple vérification de présence (message + chemin/version). Aucun symlink (ni ~/.local/bin ni ailleurs) : rendre opencode atteignable depuis bash le ferait démarrer sans clé Albert — panne plus opaque qu'un command not found. (CORRIGÉ)
  • Bloc marqué ensure_vm_runtime : le filet symlink ajouté en v1 est retiré. (CORRIGÉ) Règles : ne pas toucher vendor/vm/ ; bash 3.2 ; accents FR ; dry-run OK ; non-destructif. DoD : albert-code run ouvre le TUI OpenCode avec les clés chargées, plus de command not found ; aucun symlink opencode posé par le runtime. → TESTS.md S65. Implémenté — lancement zsh -l (lib/phases.sh:phase_run) ; check_opencode allégé (présence seule) et filet symlink retiré de ensure_vm_runtime.

T-FIX-17 🟡 Compatibilité Lima >= 2.2.0 du moteur de VM (autres verbes) — suivi amont

But : la régression Lima v2.2.0 (PR lima-vm/lima#5194 : user.shell, défaut /bin/bash) qui a cassé run (T-FIX-16) touche aussi les autres verbes du moteur de VM : claude, codex et vibe appellent encore le binaire nu via limactl shell … -- <cmd> (vendor/vm/agent-vm.sh:1132, 1199, 1233), donc sous /bin/bash sans utiliser le shell de connexion du guest. Périmètre : hors périmètre du bundle — albert-code n'utilise que OpenCode. Le correctif relève du dépôt amont sylvinus/agent-vm. Tâches (suivi) :

  • Vérifier le traitement amont chez sylvinus/agent-vm (attention aux verbes claude/codex/vibe face à limactl shell >= 2.2.0).
  • Ne rien coder dans le bundle tant que le correctif n'est pas d'actualité ici. Règles : ne pas toucher vendor/vm/ ; bash 3.2 ; accents FR. DoD : décision documentée dans docs/PLAN.md (suivi amont validé ou risque dûment écarté pour les 3 verbes non utilisés).

T4.1 🟠 Skill conventions-iae (dans etalab-ia/skills)

But : house style Python/Albert (uv, Ruff line-length=150/py312/ignores, pytest async unit+intég ≥80 %, Alembic upgrade+downgrade, Makefile/cli.py, PR DoD). PR sur etalab-ia/skills. DoD : skill chargée par OpenCode, déclenchée sur projet Python. → TESTS.md S9.

T4.2 🟠 Skill delivery-standards-beta (dans etalab-ia/skills)

But : scaffold projet conforme beta.gouv (linter + CI/CD + tests + Sentry + OpenAPI + README + /stats + DashLord + footer légal + /accessibilite) + checklist 28 standards. DoD : la skill génère la structure conforme. → TESTS.md S9.

T4.3 🟡 Templates CI templates/ ✅ implémenté

But : reprendre la chaîne sécu OpenGateLLM. Tâches : workflows réutilisables Semgrep + Trivy + CodeQL (HIGH=warn / CRITICAL=block) ; PULL_REQUEST_TEMPLATE.md (fusion DoD OpenGateLLM + checklist beta) ; script de conformité (footer légal + /accessibilite + scan secrets/URL prod + données fixtures). DoD : workflows valides, script de conformité exécutable. → TESTS.md S10.

T4.4 🟠 Test hermétique S15 rejouable en CI <- AC-R001 ✅ implémenté

But : rendre S15 automatisable (le dogfood manuel prouve mais ne se rejoue pas seul). Tâches :

  • lib/ui.sh install_shim : override SHIM_BIN_DIR (si défini et non vide → dossier du shim direct, sinon sonde /opt/homebrew/bin…$PATH inchangée) ; documenté à côté de HOME / OPENCODE_CONFIG_DIR.
  • tests/s15_shim.sh : sandbox jetable (HOME / SHIM_BIN_DIR / XDG_DATA_HOME sous mktemp -d), stub agent-vm.sh (agent-vm(){ echo "STUB OK $*"; }), PATH minimal ; asserter précondition command -v agent-vm introuvable → install_shimcommand -v agent-vm = $SHIM_BIN_DIR/agent-vm + exécution du stub ; vérifier non-pollution de /opt/homebrew/bin et du vrai $HOME ; cleanup trap EXIT. DoD : tests/s15_shim.sh exit 0 sans écrire hors de la sandbox ; intégrable en CI. → TESTS.md S15 (variante automatisée).

T4.5 🟠 Garde-fou CI anti-fuite chemin perso / username <- AC-R008 ✅ implémenté

But : empêcher qu'un chemin home absolu (/Users/<name>, /home/<name>) ou un username perso ne soit committé dans ce dépôt public. Fuite récurrente : défauts ~/Dev en code (T-FIX-12), puis chemin absolu dans une note de validation TESTS.md (attrapé au pré-vol du commit 7f84d9a). Tâches :

  • Ajouter un check (workflow CI du dépôt, ou script de conformité templates/) : git grep -nE '/Users/[^/ ]+|/home/[^/ ]+' sur les fichiers suivis → échec si hit. Tolérer les placeholders documentés (<chemin-du-dépôt>, $SELF_DIR, $HOME, ~/...) et exclure *.lock.
  • Documenter la règle dans AGENTS.md (« notes de validation : anonymiser les chemins absolus / username ; ne jamais coller de sortie brute contenant /Users/<toi> »).
  • Créer le scénario TESTS.md S18. DoD : un commit contenant /Users/<qqn>/… dans un fichier tracké fait échouer la CI ; les placeholders légitimes passent. → TESTS.md S18.

T4.6 🟡 Ancrer les scénarios de TESTS.md sur des repères stables ✅ implémenté

But : les scénarios de recette citent des numéros de ligne de lib/phases.sh. Le code bouge à chaque PR, les numéros ne suivent pas : les cinq références de S-ctx-2 et S-ctx-4 vers phase_b et ensure_vm_runtime pointaient toutes le mauvais endroit, sans que rien ne le signale. Un scénario qui pointe la mauvaise ligne ne prouve rien. Règle retenue : dans une étape de procédure, ancrer sur un repère qui ne bouge pas — nom de fonction, nom de branche conditionnelle, chaîne exacte affichée. Jamais un numéro de ligne. Dans une note **Validé le :**, qui est un constat figé à sa date, ne rien corriger : annoter que les numéros valaient à cette date. Tâches : convertir les références de procédure de S-ctx-2 et S-ctx-3 ; annoter la note datée de S-ctx-4 ; inscrire la règle dans AGENTS.md à côté de celle sur les chemins personnels. Hors périmètre : le garde-fou automatisé (un test qui vérifierait l'existence de chaque repère cité) est écarté pour l'instant : sur cinq scénarios concernés, il coûterait plus en faux positifs qu'il ne rapporterait. À rouvrir si la dérive réapparaît malgré la règle. Note : S61 cite « ligne 448 » dans sa section Pourquoi, mais c'est la citation d'un message d'erreur observé à l'époque, pas un pointeur de navigation : laissé tel quel volontairement. DoD : plus aucun numéro de ligne de lib/phases.sh dans une étape de procédure de TESTS.md ; les notes datées sont intactes et annotées ; la règle est écrite dans AGENTS.md.


EPIC 5 — Distribution

T5.1 🔴 README copier-coller (non-techs) ✅ implémenté

But : guide d'install pas-à-pas. Réutiliser l'UX de Produits/Albert Code/MVP/Open code + Ollama/spec.md. DoD : un non-tech installe et lance un premier projet sans aide. → TESTS.md S11.

T5.2 🟠 Test sur machine vierge + early adopters ✅ implémenté

But : valider hors du poste de développement habituel (machine vierge). DoD : install réussie par ≥1 early adopter externe. Implémenté — la bêta ouverte depuis le 01/07/2026 satisfait la DoD au-delà du cas unique demandé : plusieurs early adopters extérieurs au poste de développement d'origine ont installé et utilisé le bundle sur leurs propres machines, et leurs retours alimentent le registre FEEDBACK.md (AC-R036, AC-R038 à AC-R041, AC-R044). Le parcours d'installation qui en est issu est documenté dans le README.md. Deux réserves, suivies dans leurs propres tickets et qui ne rouvrent pas celui-ci : les installations validées par ce canal sont des installations macOS, le parcours Linux n'ayant été éprouvé qu'en interne (T5.4) ; et les régressions découvertes depuis vivent dans T7.8 et T-FIX-16.

T5.3 🟡 Publication etalab-ia/albert-code ✅ implémenté

DoD : repo public, LICENSE MIT, CI verte. Implémenté — le dépôt github.com/etalab-ia/albert-code est public (api.github.comprivate: false, rendu public le 01/07/2026), LICENSE est MIT, et la dernière run de CI est verte (conclusion: success, 2026-08-26).

T5.4 🟠 Écrire le PATH du shim dans le bon fichier de shell <- AC-R045 ✅ implémenté

But : install_shim écrit l'ajout de ~/.local/bin au PATH dans ~/.zshenv, invisible pour un utilisateur bash (cas nominal sur Linux). Détecter le shell ($SHELL) et écrire dans ~/.bashrc, ~/.zshenv ou ~/.profile selon le cas, sans jamais dupliquer une ligne déjà présente dans l'un d'eux. Le contournement est documenté dans le README depuis 2026-09-01 ; ce ticket le rend inutile. DoD : sur une machine Linux avec bash et aucun dossier writable dans le PATH, command -v albert-code répond après un nouveau shell, sans intervention manuelle. → scénario TESTS.md à créer. ✅ Implémenté — nouveau helper path_rc_file (lib/ui.sh) : zsh~/.zshenv (toute invocation, comportement historique conservé), bash+Linux → ~/.bashrc (terminaux interactifs non-login), bash+macOS → ~/.bash_profile (login), tout le reste → ~/.profile (repli POSIX), en reprenant la sonde uname -s déjà utilisée par lib/phases.sh. install_shim (lib/ui.sh) écrit désormais l'ajout au PATH dans path_rc_file, sonde les quatre fichiers candidats (~/.zshenv, ~/.bashrc, ~/.bash_profile, ~/.profile) pour ne jamais dupliquer une ligne déjà présente, et avertit explicitement le cas « tout le reste » (fish…) en donnant la ligne à poser soi-même. Le drapeau SHIM_PATH_ADDED devient parlant : print_path_hint (lib/ui.sh), appelé en fin d'installation (install.sh, bin/albert-code verbe install), nomme le fichier réellement modifié et l'action à faire (nouveau terminal ou source). Ajout au périmètre initial : symétrie de désinstallation — uninstall.sh (section 3bis) retire désormais la ligne d'ajout posée par le bundle (path_rc_file, confirm, jamais de suppression silencieuse, une provenance tierce conservée et signalée). → TESTS.md S67.

T5.5 🟠 Commande d'installation du README : emplacement libre et point d'entrée unique <- AC-R050

But : corriger deux défauts de la section d'installation du README.md. (a) La commande de clone impose l'emplacement du dépôt chez l'utilisateur, alors que rien dans le bundle n'en dépend : SELF_DIR est résolu dynamiquement depuis T-FIX-12 et T-FIX-13, précisément pour que le dépôt puisse vivre où son propriétaire le décide. (b) Le fichier expose deux points d'entrée contradictoires : ./install.sh dans la section d'installation, albert-code install dans le tableau des verbes plus bas. La seconde ne peut pas fonctionner depuis un clone frais, puisque le shim albert-code est posé par l'installation (bin/albert-code, verbe install). Tâches :

  • Retirer le chemin imposé de la commande de clone : cloner puis entrer dans le dossier, sans destination codée en dur.
  • Documenter ./bin/albert-code install comme commande de première installation : c'est la seule forme qui fonctionne avant la pose du shim, et elle aligne la section sur l'interface à 4 verbes. Une fois le shim posé, albert-code install reste valide pour un ré-amorçage.
  • Conserver install.sh comme amorçage de rétrocompatibilité, sans en faire le chemin documenté.
  • Passer en revue les autres occurrences de install.sh dans le README.md (dont l'exemple de surcharge AC_VM_* et la section de dépannage) et dans docs/PLAN.md, et les aligner. DoD : le README.md n'impose plus d'emplacement de clone ; la première installation y est documentée par une commande qui fonctionne réellement depuis un clone frais ; le fichier n'expose plus deux points d'entrée contradictoires. → TESTS.md S11 (à relire, le parcours copier-coller change).

EPIC 7 — Absorption d'agent-vm dans le bundle (vendoring)

T7.1 🔴 Vendoriser agent-vm dans vendor/vm/ ✅ implémenté

But : arrêter de cloner agent-vm depuis GitHub à chaque install. Copier les scripts au commit 6f20194 dans vendor/vm/ du repo albert-code. Plus de clone, plus de dépendance réseau pour le moteur d'isolation. DoD : vendor/vm/ contient agent-vm.sh, agent-vm.setup.sh, runtime.example.sh, LICENSE (MIT, attribution Sylvain Zimmer), VERSION. Le repo albert-code est auto-suffisant. bin/albert-code + install.sh : AGENT_VM_DIR pointe vers $SELF_DIR/vendor/vm, plus de AGENT_VM_REPO.

T7.2 🔴 Supprimer le shim et le nom « agent-vm » du PATH utilisateur ✅ implémenté

But : agent-vm n'est plus une commande utilisateur. Albert Code l'appelle en interne via _vm() helper après l'avoir sourcé. Plus de shim agent-vm sur le PATH, plus de sourçage dans ~/.zshrc. DoD : install_agent_vm() ne clone plus, ne pose plus de shim, ne modifie plus le rc. Vérifie seulement la présence de vendor/vm/agent-vm.sh. Nettoyage non-destructif : si un ancien shim ou sourçage traîne, propose de les retirer.

T7.3 🔴 VM de base OpenCode-only (--preinstall) ✅ implémenté

But : agent-vm setup installait tous les harnais (Claude, Codex, Vibe, OpenCode). Albert Code n'a besoin que d'OpenCode. Passage de --preinstall=node,gh,chromium,opencode. DoD : check_base_vm() et phase_run() appellent _vm setup --preinstall=node,gh,chromium,opencode. Migration : si une VM de base multi-harnais existe, l'utilisateur peut la garder ou la refaire.

T7.4 🔴 chrome-devtools au niveau MCP, pas global VM ✅ implémenté

But : agent-vm écrivait chrome-devtools dans le opencode.json global de la VM. Ce MCP doit être géré au niveau projet (opt-in au setup, avec flags headless). DoD : vendor/vm/agent-vm.setup.sh patché : le bloc OpenCode chrome-devtools est désactivé par défaut (INSTALL_OPENCODE_MCP=0). config/opencode.template.json et scaffold_opencode_json ajoutent --headless=true --isolated=true à la commande chrome-devtools.

T7.5 🟠 UI sans « agent-vm » pour l'utilisateur ✅ implémenté

But : l'utilisateur ne voit plus « agent-vm » dans les messages, juste des références à « la VM isolée » / « le moteur de VM d'Albert Code ». DoD : tous les messages utilisateur dans lib/phases.sh, lib/ui.sh, install.sh, README.md sont reformulés. Le nom « agent-vm » reste dans les commentaires de code, la LICENSE et vendor/vm/.

T7.6 🟠 Ne pas repasser les flags ressources au run si la VM projet tourne déjà <- AC-R037 ✅ implémenté

But : sur une VM projet déjà Running, chaque albert-code run affiche le prompt agent-vm « VM is currently running. It must be stopped to apply new resource settings. Stop the VM and apply changes? [y/N] » alors que rien ne change (ressources demandées = ressources courantes). Faux positif agaçant à chaque lancement.

Cause : phase_run passe toujours --cpus/--memory/--disk (lib/phases.sh:236), et le vendored ne compare jamais aux valeurs courantes : vendor/vm/agent-vm.sh:374 déclenche dès qu'un flag ressource est présent ET que la VM tourne (le commentaire « if ... changed » est faux). Le vendored est figé (pin 6f20194) → correction côté Albert Code uniquement.

⚠️ Post-mortem v1 (tentative du 21/07 — NE PAS refaire) : premier correctif = if _agent_vm_running "$(_agent_vm_name)"; then _vm opencode; else _vm --cpus … ; fi. Échec en run réel : le prompt revenait. Trace bash -x (/tmp/tr.txt) : le nom dérivé était bon (agent-vm-albert-code-b9ed5d09), mais _agent_vm_running renvoyait faux → branche else → flags → prompt. Cause : _agent_vm_running fait limactl list --format … | grep -q, et bin/albert-code tourne sous set -o pipefail (l.11). Quand grep -q matche la 1re ligne et ferme le pipe, limactl (encore en écriture de la 2e VM) prend un SIGPIPE, sort en 141, pipefail fait échouer tout le pipe → faux négatif intermittent (course, dépend de la charge). Preuve dans le trace : la MÊME fonction, même VM, renvoie vrai 100 lignes plus loin (agent-vm.sh:376) car appelée dans le sous-shell _vmset +o pipefail est actif (l.31). C'est exactement pourquoi _vm() enveloppe le vendored dans ( set +u +e +o pipefail; … ). Leçon : ne jamais appeler un helper vendored en … | grep -q directement sous pipefail.

Tâches (v2 — correctif retenu) :

  1. Dans phase_run() (lib/phases.sh), remplacer l'appel unique _vm --cpus … --memory … --disk … opencode par une détection sans pipe : capturer la liste des VM d'abord, puis test bash pur. Basculer de « déjà Running » vers « VM projet déjà présente » (les ressources sont figées à la création → inutile de les repasser quel que soit le statut ; évite aussi de dépendre du format Name Status).
  2. Ne PAS réutiliser _agent_vm_running (fragile sous pipefail, cf. post-mortem). Réutiliser _agent_vm_name (vendor/vm/agent-vm.sh:137) pour le nom (OK, pas de pipe fragile).
  3. Forme cible :
    local _vm_name _vm_list
    _vm_name="$(_agent_vm_name)"
    # Capture d'abord (aucun pipe → immunisé au SIGPIPE/pipefail, cf. post-mortem v1),
    # puis test en bash pur. `|| true` : une sortie limactl vide ne casse pas set -e.
    _vm_list="$(limactl list -q 2>/dev/null || true)"
    case $'\n'"$_vm_list"$'\n' in
      *$'\n'"$_vm_name"$'\n'*)
        info "VM déjà créée — rattachement sans re-réglage des ressources."
        apply "lancer la VM isolée" _vm opencode ;;
      *)
        apply "lancer la VM isolée" _vm --cpus "${EFF_CPUS}" --memory "${EFF_MEM}" --disk "${AC_VM_DISK}" opencode ;;
    esac
  4. Garder compute_effective_vm_resources appelé avant (message « Ressources hôte détectées ») : on ne fait que conditionner l'usage de EFF_*.

Règles :

  • Ne PAS toucher vendor/vm/ (vendored figé).
  • Aucun … | grep dans le chemin de décision : capture-first + case. C'est le cœur du fix.
  • Bash 3.2 compatible / set -euo pipefail : _agent_vm_name (${1:-$(pwd)}) sûr ; limactl … || true neutralise set -e ; case en bash pur, pas de pipe.
  • Ne pas canonicaliser pwd à la main (laisser _agent_vm_name faire).
  • Accents FR corrects, pas de tiret cadratin, non-destructif, dry-run OK.

DoD : VM projet présente (Running ou Stopped) → albert-code run n'affiche plus « must be stopped » et lance OpenCode directement (message « rattachement »). VM inexistante (1er run du projet) → flags passés, la VM projet est créée dimensionnée. Validation en run réel (le bug ne se voit pas en test isolé à froid : course SIGPIPE, cf. post-mortem). → TESTS.md S41 (VM running → plus de prompt, message rattachement) + S42 (VM inexistante → flags passés, création OK).

Extension (même racine SIGPIPE) : base_vm_exists() portait le même anti-pattern limactl list -q | grep -q '^agent-vm-base$', et il est appelé par phase_run/check_base_vm en amont → un faux négatif reproposait la création de la VM de base à chaque run (symptôme observé chez un bêta-testeur). Aligné sur le même correctif capture-first + case bash pur. → TESTS.md S43. Implémentéphase_run capture _vm_list puis case bash pur : VM présente → rattachement sans flags ; inexistante → flags (lib/phases.sh:237-248). base_vm_exists aligné capture-first + case (lib/phases.sh:278-286).

T7.7 🟠 Garde-fou : avertir quand un projet n'est pas câblé pour Albert (au run et au setup) <- AC-R040 (absorbe T1.6) ✅ implémenté

But : albert-code run dans un dossier sans opencode.json (jamais setup) — ou avec un opencode.json sans provider albert — ouvre OpenCode sur ses modèles par défaut, Albert absent de /models, sans aucune alerte. Footgun silencieux : 2 bêta-testeurs piégés (dont un power user), longue session de debug à chaque fois. Généralise T1.6 (fichier existant sans albert) au cas « pas de fichier du tout ».

Tâches :

  1. Garde-fou au run (phase_run, tout début) : si ./opencode.json absent ou ne contient pas "albert", avertir clairement (« projet non configuré pour Albert → fais albert-code setup ici ») et demander confirmation avant de lancer sans Albert (confirm → défaut non ; en dry-run/non-interactif, n'ouvre pas). Détection sur fichier (grep -q '"albert"' ./opencode.json — pas de pipe, pas de course SIGPIPE, cf. T7.6).
  2. Avertissement au setup (scaffold_opencode_json, branche fichier existant) : si ./opencode.json existe sans "albert", ne pas se contenter de « conservé » — signaler explicitement qu'Albert n'est pas câblé et pointer le bloc provider.albert à ajouter (ou renommer/supprimer + relancer). Couvre T1.6.

Règles : ne rien écraser (non-destructif) ; grep sur fichier uniquement (jamais limactl | grep) ; bash 3.2 / set -euo pipefail ; accents FR corrects ; dry-run : le garde-fou n'ouvre pas OpenCode sans config.

DoD : run dans un dossier sans opencode.json ou sans provider albert → avertissement + confirmation (pas de lancement silencieux) ; run dans un projet câblé → aucun changement. setup sur un opencode.json existant sans albert → message explicite. → TESTS.md S44 (garde-fou run) + S45 (avertissement setup, ex-T1.6/S22). Implémenté — garde-fou run en tête de phase_run (lib/phases.sh:204-211, détection sur fichier grep -q '"albert"') ; avertissement setup sur fichier existant sans albert (lib/phases.sh:729-733, généralisé en merge jq par T8.2).

T7.8 🔴 Détecter une VM projet clonée d'une base périmée et proposer un reset <- AC-R044 ✅ implémenté

But : la VM de base est reconstruite, mais la VM projet préexistante reste clonée de l'ancienne. Le moteur de VM le signale lui-même (« Existing VMs were not updated. Use --reset to re-clone them from the new base. » et « Warning: Base VM has been updated since this VM was cloned. »). Comme le runtime est lancé via limactl shell … zsh -l et que l'ancienne base n'a ni zsh ni opencode, on obtient zsh: command not found puis opencode: command not found : le runtime ne s'exécute jamais, donc pas de clé, pas de skills, pas d'OpenCode. Albert Code n'expose aucun --reset et n'en propose jamais.

Implémenté — helper _project_vm_from_stale_base (lib/phases.sh, à côté des autres helpers VM) : reprend la comparaison des fichiers de version du répertoire d'état du moteur (AGENT_VM_STATE_DIR, base contre VM projet) sans toucher à vendor/vm/. Règle en 4 cas : base absente → à jour/indéterminable (on n'affirme rien) ; VM projet absente → périmée ; contenus différents → périmée ; identiques → à jour. Garde-fou inséré dans phase_run, branche « VM déjà créée », avant le lancement : en cas de décalage, avertissement en français (cause + risque de command not found), rappel explicite de ce qui est perdu (sessions OpenCode, paquets installés dans la bulle, fichiers hors du dossier monté — le code monté depuis l'hôte est intact), puis proposition de recréation via confirm (défaut non) en _vm --reset run --tty zsh -l -c "opencode --auto" (options de VM avant le verbe). Sur refus, lancement normal conservé et rappel en une ligne que albert-code run pourra échouer et que la recréation reste possible. Non-destructif en dry-run : aucun --reset, action seulement annoncée. Arbitrage : le pré-vol dans la VM (vérifier zsh/opencode binaires avant le runtime) a été écarté : la détection par version attrape la cause en amont avant tout démarrage ; un pré-vol imposerait un aller-retour supplémentaire dans la VM à chaque run pour le même message actionnable ; le garde-fou de version d'OpenCode du bloc runtime (T8.3) couvre déjà le cas d'un binaire présent mais trop ancien. À rouvrir si un cas apparaît où les binaires manquent alors que les versions concordent. → TESTS.md S49.

Tâches : comparer la version de la base et celle de la VM projet (fichiers de version du répertoire d'état du moteur de VM), détecter le décalage, et proposer explicitement de recréer la VM projet. Vérifier aussi la présence de zsh et d'opencode dans la VM avant de lancer le runtime, avec un message actionnable plutôt qu'un command not found silencieux.

Règles : ne pas toucher vendor/vm/ (vendored figé) ; bash 3.2 / set -euo pipefail ; accents FR corrects ; pas de tiret cadratin ; non-destructif ; dry-run OK.

DoD : une VM projet clonée d'une base antérieure est détectée au run, et l'utilisateur se voit proposer la recréation. → TESTS.md S49. Source : Bêta-test juillet 2026, cause racine élucidée le 15/07 → FEEDBACK.md AC-R044.


EPIC 6 — Interface 3 verbes & simplification profils <- AC-R014, AC-R015, AC-R016, AC-R017

T6.1 🔴 Commande albert-code à 3 verbes <- AC-R014 ✅ implémenté

But : une commande unique albert-code avec 3 verbes : install (bootstrap poste), setup (scaffold projet), run (lancement VM) — au lieu de mémoriser install.sh + agent-vm setup + agent-vm opencode. Tâches :

  • Créer bin/albert-code : dispatcher en case "$1" in install|setup|run|--help).
  • Extraire phase_a / phase_b / phase_run de install.sh vers lib/phases.sh, sourçable par bin/albert-code ET par install.sh.
  • install.sh devient l'amorçage : joue phase_a PUIS pose le shim via install_shim "albert-code" "$SELF_DIR/bin/albert-code".
  • phase_run : reprend le bloc « Prochaines étapes » (créer la VM de base si absente, puis agent-vm --cpus "$EFF_CPUS" --memory "$EFF_MEM" --disk "$AC_VM_DISK" opencode).
  • Mettre à jour usage_install et README.md pour documenter les 3 verbes. DoD : albert-code install = Phase A ; albert-code setup = Phase B ; albert-code run = lance la VM ; albert-code --help documente tout. install.sh devient mince. → TESTS.md S25. Implémenté — dispatcher bin/albert-code (case "$verb" in install|setup|run, bin/albert-code:35,47-65) ; phase_a/phase_b/phase_run extraits dans lib/phases.sh, sourçés par bin/albert-code:25-26.

T6.2 🟡 Pédagogie agent-vm en phase A <- AC-R015 ✅ implémenté

But : avant d'installer Lima/agent-vm, afficher un encart en français simple expliquant ce qu'est cette VM et pourquoi c'est indispensable. Tâches :

  • Dans phase_a, avant le confirm Lima, afficher un encart title + info : « une bulle isolée (VM légère Lima) où l'assistant tourne sans accès à tes fichiers perso, clés SSH, cookies. Permet de le laisser tourner en autonomie. Installe : Lima, agent-vm, clé Albert révocable. »
  • Garder le confirm avant d'installer Lima. DoD : un non-dev comprend pourquoi on installe une VM avant le premier confirm. → TESTS.md S26. Implémenté — encart FR « À propos de l'isolation » (lib/phases.sh:45-56, title + info : bulle isolée, pas d'accès aux fichiers perso/clés SSH/cookies, autonomie) affiché avant le confirm Lima (lib/phases.sh:69).

T6.3 🔴 Supprimer les profils → un seul AGENTS.md par défaut <- AC-R016 ✅ implémenté

But : remplacer le choix de profil (beta.gouv / La Suite / IAE) par un unique templates/AGENTS.default.md avec sécurité, conventions de code et accessibilité — neutre, applicable à tout projet. Tâches :

  • Supprimer profiles/beta.gouv/, profiles/lasuite/, profiles/iae/.
  • Retirer le prompt_choice de contexte et le case profils dans phase_b.
  • Créer templates/AGENTS.default.md avec le contenu fourni (sécurité, plan mode, task management, self-improvement, bug fixing, code quality, git, accessibilité).
  • phase_b copie templates/AGENTS.default.md vers ./AGENTS.md via copy_template (n'écrase jamais un AGENTS.md existant).
  • Nettoyer les références aux profils dans README.md, AGENTS.md (repo), BACKLOG.md, TESTS.md (retirer S7, S6). DoD : plus de menu contexte ; un seul AGENTS.md par défaut ; profils physiquement supprimés du dépôt ; re-setup non-destructif (AGENTS.md conservé). → TESTS.md S27. Implémenté — dossier profiles/ absent du dépôt ; templates/AGENTS.default.md présent ; phase_b pose ce template via copy_template non-destructif (lib/phases.sh:155).

T6.4 🟠 Choix Y/N skills + MCP au setup (phase_b) <- AC-R017 ✅ implémenté

But : au lieu d'activer toutes les skills et MCP en aveugle, demander à l'utilisateur ce qu'il veut brancher. MCP (par projet, dans opencode.json) :

  • Passe les 4 MCP de config/opencode.template.json en enabled: false par défaut.
  • Dans phase_b, pour chaque MCP : confirm « Brancher le MCP (objectif : ) ? » → enabled: true seulement si oui. Génère le bloc MCP en bash (PAS de dépendance jq).
  • Objectifs : data.gouv = "accès aux données publiques (lecture)" ; context7 = "doc à jour des librairies (clé requise)" ; playwright = "piloter un navigateur / agir dans une page" ; chrome-devtools = "debug navigateur". Skills (choisies au setup, manifeste projet lu par le runtime) :
  • phase_b rafraîchit le cache etalab-ia/skills, énumère chaque skill (lit description du SKILL.md de chaque dossier) et demande confirm « Installer la skill (objectif : ) ? ».
  • Écrit la sélection dans ./.albert-code/skills.txt (une skill par ligne).
  • Modifie sync_skills du runtime : au boot, ne symlinke QUE les skills listées dans ./.albert-code/skills.txt du projet courant ; réconcilie le dossier global skills/ (retire les symlinks albert-code non sélectionnés, JAMAIS les skills perso). Si aucun manifeste → comportement actuel (toutes) pour rétrocompat. DoD : un « non » à un MCP/skill ne l'écrit pas ; re-setup conserve les choix ; skills perso jamais touchées par la réconciliation. → TESTS.md S28. Implémenté — MCP : 4 confirm dans scaffold_opencode_json (lib/phases.sh:745,752,758,763), bloc MCP généré en bash sans jq, enabled:true seulement si coché. Skills : confirm par skill (lib/phases.sh:944), sélection écrite dans .albert-code/skills.txt (lib/phases.sh:956) ; sync_skills lit ce manifeste et réconcilie les symlinks sans toucher aux skills perso (runtime/agent-vm.runtime.sh:168-232).

T6.5 🟡 Encart FR de transition avant wizard agent-vm <- AC-R018 ✅ implémenté

But : quand install.sh passe la main à agent-vm setup, le wizard natif (en anglais) s'affiche sans prévenir. Déroutant pour un public non-tech francophone. Tâches : dans check_base_vm(), JUSTE AVANT l'appel à agent-vm setup, afficher un encart info court : « Tu entres maintenant dans le wizard agent-vm (en anglais). C'est normal : agent-vm est l'outil d'isolation open source sur lequel s'appuie Albert Code. Valide les logiciels proposés par défaut (Python, Node, Docker, Chromium, gh, OpenCode…). » Idem dans phase_run() si création VM. DoD : l'encart FR s'affiche avant la sortie anglaise d'agent-vm, en dry-run comme en réel. → TESTS.md S30.

T6.6 🔴 Dérivation automatique email noreply GitHub <- AC-R019 ✅ implémenté

But : l'utilisateur non-tech ne connaît pas son email noreply ni comment le trouver. Débuter le prompt avec le vrai email (qui échoue la validation) = blocage. Tâches : après avoir collé le PAT, appeler l'API GitHub pour dériver l'email noreply :

  • curl -fsS -H "Authorization: Bearer <PAT>" https://api.github.com/user
  • Parser id et login avec grep/sed (pas de jq)
  • Pré-remplir le prompt avec "<id>+<login>@users.noreply.github.com"
  • Fallback : si l'appel API échoue, garder le prompt manuel avec aide FR : « Introuvable automatiquement. Tu le trouves sur GitHub > Paramètres > Emails, ou sur https://github.com/settings/emails. Il est de la forme <id>+<login>@users.noreply.github.com. »
  • Ne jamais afficher le PAT en clair. DoD : avec un PAT valide, fait Entrée et l'email est correct ; sans réseau, le message FR d'aide s'affiche. → TESTS.md S31.

T6.7 🟠 Investiguer image de base minimale / tolérante à l'échec <- AC-R020

But : agent-vm setup installe 4 harnais (Claude Code, OpenCode, Codex, Mistral Vibe). Un seul qui rate (429) = base non finalisée. Albert Code n'a besoin que d'OpenCode. Tâches : à investiguer avec Sylvain (upstream agent-vm) : (a) image de base minimale ne contenant que OpenCode, (b) mécanisme de tolérance à l'échec d'un installeur, (c) variable d'env pour sélectionner quels harnais installer. Documenter dans docs/PLAN.md. DoD : investigation terminée, décision documentée. Pas de changement de code dans albert-code.

T6.8 🟠 Fix commande albert-code : shim avant VM + migration ancien albert-code() <- AC-R021 ✅ implémenté

But : (a) install.sh pose le shim APRÈS la VM de base (fragile) ; échec VM = pas de shim = aucune commande. (b) ancien MVP écrivait albert-code() dans ~/.zshrc qui masque le nouveau shim. Tâches :

  • (a) Déplacer install_shim "albert-code" AVANT check_base_vm dans install.sh. Rendre la création VM non-fatale : en cas d'échec, warn + continue (pas exit). Le shim doit exister même si la VM échoue.
  • (b) Détecter un bloc albert-code() dans ~/.zshrc / ~/.bashrc / /.zshenv (.profile) et proposer de le retirer (confirm, non-destructif, avec grep -n pour localiser). Idem dans uninstall.sh DoD : install.sh pose le shim avant la VM ; la VM échouant ne bloque pas le reste ; albert-code fonctionne post-echec-VM. Ancienne fonction détectée et retirée si confirmée. → TESTS.md S32.

T6.9 🔴 Corriger le shim exécutable : exec au lieu de source+exec <- AC-R022 ✅ implémenté

But : le shim albert-code source le script (bin/albert-code) avec 2>/dev/null (avale les prompts interactifs) puis relance $name "$@" (double exécution possible). Résultat : albert-code setup figé (MCP/skills prompts invisibles). Tâches : dans install_shim, quand la source est un script exécutable (binaire) et non une fonction shell à sourcer, générer un shim minimal :

#!/usr/bin/env bash
exec "/chemin/absolu/bin/albert-code" "$@"

Préserve stdin/stdout/stderr. Pas de 2>/dev/null. Pas de double exécution. Option : ajouter un paramètre install_shim pour mode "exec" vs "source", ou détecter automatiquement (si source contient #!/usr/bin/env bash et est un script autonome). DoD : albert-code setup via le shim affiche bien les prompts MCP + skills et enregistre les choix. Pas de double exécution. bin/albert-code --help identique via shim ou direct. → TESTS.md S33.

T6.10 🟠 Hygiène dépôt : .gitignore par défaut dans AGENTS.default.md <- AC-R024 ✅ implémenté

But : sans .gitignore, l'agent commit node_modules/ (41 659 fichiers) + risque .env. Règle forte manquante dans le template. Tâches : dans templates/AGENTS.default.md, ajouter une section « Hygiène de dépôt » :

  • « Avant le premier commit, toujours créer ou vérifier un .gitignore adapté au langage (Node : node_modules, dist/build/.next, .env ; Python : __pycache__, .venv, .env) »
  • « Ne JAMAIS committer : dépendances installées, artefacts de build, fichiers volumineux, secrets/.env »
  • « Vérifier git status avant de committer » DoD : un agent qui scaffold un projet et commit inclut un .gitignore et PR sans node_modules. → TESTS.md S34.

T6.11 🟡 chrome-devtools MCP injecté par agent-vm <- AC-R023

But : chrome-devtools apparaît dans OpenCode (/mcp) même zéro MCP coché au setup, parce qu'agent-vm l'installe globalement dans la VM. Tâches : documenter dans README (section MCP) :

  • Préciser que chrome-devtools peut apparaître dans OpenCode même si non coché au setup : il est préinstallé par agent-vm (pas par Albert Code).
  • Documenter ce MCP dans la liste du README avec sa source (agent-vm). À investir avec Sylvain : le désactiver côté runtime quand non sélectionné, ou le documenter clairement. DoD : le README mentionne chrome-devtools comme venant d'agent-vm. Investigation documentée.

T6.12 🟠 install_shim idempotent : réécrire si contenu changé <- AC-R025 ✅ implémenté

But : install_shim fait « si le fichier shim existe → return 0 » → quand le contenu du shim évolue (ex. fix exec d'AC-R022), une réinstall ne repose PAS le nouveau shim. Il faut rm manuel. Tâches : dans install_shim (lib/ui.sh), avant de skip un shim existant, comparer les 3 premières lignes du fichier existant avec le shim_content attendu. Si différent : réécrire. Si identique : skip. DoD : après un changement de contenu du shim, ./install.sh réécrit le fichier (au lieu de « déjà présent »). → TESTS.md S36.

T6.12-p 🟠 Polish UX sortie setup <- AC-R027 AC-R028 AC-R029 AC-R030 ✅ implémenté

But : 4 retouches UX sur la sortie de albert-code setup, issues de dogfood.

Tâches :

  1. Afficher l'ASCII art Albert Code au début de Phase B (après le garde-fou PWD = SELF_DIR, avant title "Phase B …").
  2. Quand Context7 MCP est choisi, demander la clé API si absente (ni env ni ~/.zshenv) via prompt_secret, la persister via persist_zshenv. Si vide, avertir que le MCP sera en erreur.
  3. Réécrire print_next_steps() : seulement title "Prochaines étapes" + info "Lancer Albert Code : albert-code run" + une ligne NB sans flèche sur les skills. Retirer le bloc VM de base, opencode, « Parle en français », et le statut GitHub.
  4. Déplacer le statut GitHub (push/PR configuré ou non) JUSTE AVANT « ✓ Projet configuré. » dans phase_b().

DoD : les 4 retouches visibles en dry-run. L'ASCII art apparaît en Phase B. La clé Context7 est demandée quand on choisit le MCP sans clé préexistante. L'ordre des lignes en fin de Phase B est : statut GitHub → ✓ Projet configuré → Prochaines étapes → albert-code run → NB skills. → TESTS.md S37. Implémenté — ASCII art en tête de Phase B (lib/phases.sh:144) ; clé Context7 demandée quand context7 coché sans clé (lib/phases.sh:784-792) ; print_next_steps allégé (lib/phases.sh:991-996) ; ordre fin de Phase B statut GitHub → ✓ Projet configuré → panneau récap → next steps (lib/phases.sh:179-190).

T6.13 🟡 Bruit de debug « name= » dans sync_skills <- AC-R026 ✅ implémenté

But : au boot VM (runtime/agent-vm.runtime.sh, sync_skills), des lignes « name= » parasites apparaissent dans la sortie (ex. name=datagouv-apis). Bruit dû aux variables local name + name=$(basename …) en bash 3.2. Tâches : remplacer les déclarations local name puis name="$(basename …)" par local name="$(basename …)" (fusion local+assignation), et idem pour local ename, local resolved. Supprime la source probable du bruit. DoD : la sortie de sync_skills n'affiche que les messages _ok/_info/_warn, plus de lignes « name= ». → TESTS.md S36 (vérification sortie propre).

T6.14 🟡 Polish visuel du wizard <- AC-R031 AC-R032 AC-R033 AC-R034 ✅ implémenté

But : 4 retouches visuelles sur le wizard install/setup (feedback Leo Guillaume, Tech Lead Albert API). Gains de confiance utilisateur sans dépendance ajoutée — reste 100% bash, bash 3.2, non-destructif, dry-run OK.

Tâches :

  1. Nouvelle ASCII art (banner, lib/ui.sh) : remplacer par "Albert Code" en figlet slant (57 col, <=76 col max). Baseline "Coder avec l'IA souveraine de l'État, dans une bulle isolée." inchangée.
  2. Spinner (with_spinner dans lib/ui.sh) : helper with_spinner "message" cmd args... avec frames braille (⠋⠙⠹⠸⠴⠦⠧), dégradation non-TTY/dry-run. Appliquer UNIQUEMENT à : clone agent-vm, clone/refresh skills (jamais à agent-vm setup ni agent-vm ... opencode qui streament leur propre sortie).
  3. Compteur d'étapes : prefixer les 4 sous-étapes de phase_b par [1/4]..[4/4] : [1/4] AGENTS.md, [2/4] Connecteurs MCP, [3/4] Skills, [4/4] Runtime VM.
  4. Panneau récap : après "✓ Projet configuré.", afficher un panel aligné gauche (filet haut, titre, Projet/MCP/Skills/GitHub, filet bas). Utiliser variables globales AC_SELECTED_MCP / AC_SELECTED_SKILLS exposées par scaffold_opencode_json / scaffold_skills_selection. PAS d'encadré justifié à droite (piège UTF-8 width).

Règles :

  • Garder tous les accents français corrects (é è à ê ç …)
  • Pas de tiret cadratin
  • Bash 3.2 compatible
  • Dry-run : spinner dégradé (pas d'animation), récap affiché quand même

DoD : art <=76 col, spinner dégrade en non-TTY/dry-run, compteur [1/4]..[4/4] visible, récap affiche les bons choix. → TESTS.md S38. Implémenté — ASCII art figlet slant (banner, lib/ui.sh:231-240) ; spinner braille avec dégradation non-TTY/dry-run (with_spinner, lib/ui.sh:248-285) ; compteur [1/4]..[4/4] (lib/phases.sh:154,159,164,169) ; panneau récap (print_setup_summary, lib/phases.sh:962-989, utilisé à lib/phases.sh:188).

T6.15 🟠 Clé Context7 : plus jamais à l'install, seulement au setup si le MCP est choisi <- AC-R038 ✅ implémenté

But : albert-code install (phase A.4) demande la clé Context7 avant toute explication et avant que l'utilisateur ait choisi de brancher ce MCP (onboarding Adrien 21/07). La clé ne doit être demandée qu'au setup, après un Y à la question context7.

Tâches :

  1. Supprimer le bloc A.4 de phase_a() (lib/phases.sh:103-116) : plus aucun prompt ni mention Context7 à l'install. Ne plus appeler persist_zshenv "CONTEXT7_API_KEY" en phase A.
  2. Conserver le chemin setup existant (scaffold_opencode_json, lib/phases.sh:707-715) comme unique point de collecte : si Y à context7 et clé absente (env + ~/.zshenv), prompt_secret + persist_zshenv.
  3. Propager la clé saisie au setup vers la VM : appeler ensure_vm_runtime en fin de phase_b() (au moins quand une clé vient d'être persistée) pour régénérer le bloc de ~/.agent-vm/runtime.sh. Aujourd'hui ce bloc n'est écrit qu'en phase A : une clé saisie au setup n'arrive jamais dans la VM (runtime.sh garde export CONTEXT7_API_KEY='').
  4. ensure_vm_runtime est idempotent (remplacement du bloc marqué, lib/phases.sh:356-371) : vérifier en dry-run qu'un re-run en phase B ne duplique rien et n'écrase pas GH_TOKEN / identité git déjà posés.

DoD : albert-code install ne mentionne plus Context7. albert-code setup avec Y à context7 et sans clé demande la clé et la persiste (zshenv hôte + runtime.sh) ; au run suivant, echo $CONTEXT7_API_KEY dans la VM est non vide. N à context7 : aucune question de clé, ni à l'install ni au setup. → TESTS.md S-ctx-1, S-ctx-2, S-ctx-3, S-ctx-4.

Validé le : 2026-07-21 — dry-run Phase A sans mention Context7 (S-ctx-1). Code inspecté pour persistence zshenv + runtime.sh au setup (S-ctx-2). Réponse N → pas de prompt (S-ctx-3). Idempotence ensure_vm_runtime (S-ctx-4). bash -n lib/phases.sh OK.

T6.16 🟡 Une ligne d'explication avant chaque question d'option du setup <- AC-R039 ✅ implémenté

But : chaque option d'installation doit être compréhensible sans contexte préalable. Format cible : « Installer Context7 ? Context7 est un MCP qui permet de [...]. Y/n ».

Tâches :

  1. Dans scaffold_opencode_json (lib/phases.sh:672-688) : avant chaque confirm, une ligne info qui explique le connecteur (ce que l'agent saura faire en plus), puis un confirm court « Installer ? » :
    • data.gouv : « MCP qui permet à l'agent d'interroger les données publiques de data.gouv.fr (catalogue, datasets, API tabulaire), en lecture. »
    • context7 : « MCP qui donne à l'agent la documentation à jour des librairies et frameworks pendant qu'il code. Clé gratuite (https://context7.com/plans), demandée juste après si tu acceptes. »
    • playwright : « MCP qui permet à l'agent de piloter un navigateur headless dans la VM (ouvrir une page, cliquer, tester une UI). »
    • chrome-devtools : « MCP de debug navigateur : DOM, console, requêtes réseau, performance. »
  2. Vérifier que les skills du setup suivent le même pattern (une ligne d'objectif avant le Y/n) et harmoniser si besoin.

Règles : accents corrects, pas de tiret cadratin, bash 3.2, <=80 colonnes par ligne affichée.

DoD : en dry-run, chaque question MCP est précédée d'une ligne d'explication ; les questions sont de la forme « Installer ? ». → TESTS.md S-ctx-5.

Validé le : 2026-07-21 — dry-run setup affiche les 4 paires explication+confirm avec les libellés exacts du ticket. Skills déjà avec description inline dans le confirm. bash -n lib/phases.sh OK.

T6.17 🟠 Demander la clé Context7 au moment de l'acceptation du connecteur <- AC-R048 ✅ implémenté

But : scaffold_opencode_json pose les quatre questions MCP à la suite (data.gouv, Context7, Playwright, Chrome DevTools, lib/phases.sh:1079-1101), puis construit le contenu JSON. La saisie de la clé Context7 (prompt_secret, lib/phases.sh:1122) n'intervient que dans cette seconde phase, soit deux questions plus loin que l'acceptation qui la conditionne. La ligne d'explication affichée juste avant annonce pourtant « Clé gratuite (…), demandée juste après si tu acceptes » (lib/phases.sh:1086-1087) : la promesse est fausse en l'état. L'utilisateur a changé de contexte entre-temps et ne rattache plus la demande de clé au connecteur qu'il a accepté. Ce ticket complète T6.15 (clé déplacée de l'install vers le setup) : la clé est au bon verbe, pas encore au bon moment. Tâches : sortir la saisie de la clé de la boucle de construction du contenu JSON et la placer immédiatement après le confirm « Installer le connecteur Context7 ? » ; conserver la garde existante (ne rien demander si la clé est déjà présente en environnement ou dans le ~/.zshenv, cf. T6.15) ; conserver la sortie « Entrée pour passer » et l'avertissement émis quand aucune clé n'est fournie ; vérifier que le JSON produit est identique à celui d'avant le déplacement (la clé n'entre pas dans le fichier, seule la référence {env:CONTEXT7_API_KEY} y figure). Règles : bash 3.2 / set -euo pipefail ; accents FR corrects ; dry-run respecté ; non-destructif. DoD : en dry-run, la demande de clé Context7 suit immédiatement l'acceptation du connecteur et précède la question Playwright ; un setup qui refuse Context7 ne la demande jamais ; l'opencode.json produit est inchangé. → TESTS.md (étendre les scénarios S-ctx-* de T6.15). Implémenté — dans scaffold_opencode_json (lib/phases.sh), le bloc de demande de clé (prompt_secret + persist_zshenv, garde if [ -z "${CONTEXT7_API_KEY:-}" ] && ! file_contains …) a été déplacé inchangé de la phase de construction du JSON vers la branche du confirm "Installer le connecteur Context7 ?", juste après mcp_ctx7="true" (lignes ~1090-1098). La branche de construction du JSON en phase 2 se réduit à ses trois lignes de construction ("context7":… avec {env:CONTEXT7_API_KEY}), non touchées. Garde inchangée (aucune redemande si la clé est connue), JSON produit identique octet pour octet, propagation vers ~/.agent-vm/runtime.sh préservée (persistance avancée avant ensure_vm_runtime). → TESTS.md S-ctx-6.


EPIC 8 — Rafraîchir les fichiers projet figés au setup

Problème de fond : Au albert-code setup, les fichiers posés dans le projet (AGENTS.md, opencode.json, .agent-vm.runtime.sh) sont conservés s'ils existent — les évolutions du template ne redescendent jamais aux projets déjà configurés. Symptômes déjà vus : opencode.json conservé sans provider Albert (T1.6/T7.7), runtime projet ancien + OpenCode 1.2.9 dans la VM → opencode --auto inconnu → écran de help au lieu du TUI (AC-R041).

Fait de code clé : Le SEUL fichier rafraîchi à l'existant est le runtime USER ~/.agent-vm/runtime.sh : son bloc marqué ($AC_MARKER … $AC_MARKER_END) est réécrit par ensure_vm_runtime() à chaque phase A ET B. Toute logique évolutive doit passer par là.

T8.1 🟡 (doc) Convention zone gérée vs fichiers figés ✅ implémenté

But : Documenter dans ce ticket l'architecture de rafraîchissement du bundle, pour que tout contributeur sache où et comment ajouter une logique évolutive.

Architecture de rafraîchissement :

Fichier Rafraîchi ? Mécanisme Pourquoi
~/.agent-vm/runtime.sh bloc marqué ✅ Oui, à chaque phase A ET B ensure_vm_runtime() supprime le bloc $AC_MARKER … $AC_MARKER_END et le réécrit Clés API + GH_TOKEN doivent être à jour au boot VM
./opencode.json 🟡 Merge conditionnel (T8.2) Si jq présent → merge provider.albert dans existant, sinon avertissement Provider Albert doit pouvoir être ajouté sans écraser les MCP/permissions
./AGENTS.md 🟡 Zone gérée (T8.5) setup (B.1) et updatesync_agents_md() réécrit la zone $AC_MARKER_AGENTS … $AC_MARKER_AGENTS_END (insertion silencieuse en tête si absente, jamais de question) La zone porte les garanties du bundle (Sécurité, Git, Accessibilité, Hygiène) ; ## Expected Behavior et le contenu perso hors marqueurs sont préservés
./.agent-vm.runtime.sh (projet) ❌ Non (jamais écrasé) copy_template() → non-destructif pur Idem, fichier figé chez l'existant
vendor/vm/ (moteur VM) ❌ Non (vendored figé) Versionné dans le repo albert-code Mis à jour par git pull du dépôt albert-code
templates/ ❌ Non (versionnés) Versionné dans le repo Utilisés uniquement au premier setup

DoD : cette documentation est présente dans BACKLOG.md. Implémenté — le tableau « Architecture de rafraîchissement » figure ci-dessus dans ce ticket (DoD satisfaite). Décision de l'EPIC 10 (T10.2) : le runtime doit réécrire systématiquement la ligne dans le .zshenv de la VM (cf. EPIC 10). Mécanisme retenu (T10.2, lib/phases.sh ensure_vm_runtime) : le bloc marqué définit une fois le helper _ac_zsh_set VAR <valeur> (idempotent, portabilité POSIX, création ~/.zshenv + chmod 600, suppression de la ligne ancrée ^export VAR=) puis l'appelle par secret. Propriété d'auto-réparation : le bloc marqué est régénéré côté hôte à chaque install/setup/update ET le runtime rejoue _ac_zsh_set à chaque boot de VM → un jeton périmé présent dans la VM est réécrit automatiquement au run suivant, sans édition manuelle.

T8.2 🟠 opencode.json — réparer, pas seulement avertir (généralise T1.6/T7.7) ✅ implémenté

But : Dans scaffold_opencode_json(), branche « fichier existe sans albert » (ajoutée par T7.7) : si jq est présent, proposer (confirm) puis merger provider.albert + model + small_model du template dans l'opencode.json existant, sans toucher les autres clés (MCP, permissions, autres providers). Sauvegarder l'original (.bak). Si jq absent → garder l'avertissement T7.7 (fallback). Non-destructif.

DoD : opencode.json perso avec provider scaleway → après setup, provider albert ajouté ET scaleway préservé. → TESTS.md S46. Implémenté — merge jq dans scaffold_opencode_json : sauvegarde .bak puis jq '.provider.albert = {...} | .model = ... | .small_model = ...' (n'écrase pas les autres clés), fallback avertissement si jq absent (lib/phases.sh:699-733).

T8.3 🟠 Garde-fou version OpenCode dans le bloc marqué du runtime USER (absorbe AC-R041) ✅ implémenté

But : ensure_vm_runtime() écrit un garde-fou exécuté dans la VM au boot, à l'intérieur du bloc marqué ~/.agent-vm/runtime.sh (après les exports, avant $AC_MARKER_END). Logique : capture opencode --help, si --auto absent → upgrade auto.

DoD : VM avec opencode trop vieux → au boot, upgrade auto → run ouvre le TUI, plus de help. → TESTS.md S47 <- AC-R041. Implémenté — garde-fou OpenCode --auto écrit dans le bloc marqué ~/.agent-vm/runtime.sh par ensure_vm_runtime (lib/phases.sh:430-443).

T8.4 🟠 (OPTION) Verbe albert-code update pour rafraîchir un projet existant → promu en T9.3

But : Proposer un verbe simple pour rafraîchir un projet existant sans repasser tout le setup interactif. albert-code update (ou --update) exécute les logiques de rafraîchissement (T8.2 merge opencode.json + T8.3 garde-fou runtime) sans les questions MCP/skills.

Promu en 🟠 et détaillé dans T9.3 (EPIC 9) : c'est le seul canal possible pour distribuer un correctif de catalogue de modèles aux projets déjà configurés (cf. AC-R042). Voir BACKLOG.md EPIC 9, T9.3.

DoD : ticket documenté mais pas implémenté — T9.3 implémenté (voir EPIC 9) : phase_update (lib/phases.sh:257-292) répare opencode.json (T9.2) + régénère le bloc runtime (T8.3) sans questions MCP/skills. Voir note de promotion ci-dessus et T9.3.

T8.5 🔴 Propager les évolutions du bundle aux AGENTS.md existants ✅ implémenté

Problème : le tableau « Architecture de rafraîchissement » de T8.1 qualifie ./AGENTS.md de « jamais écrasé » : copy_template() est non destructif pur, car le projet peut en avoir personnalisé le contenu. Conséquence non traitée : toute évolution des règles portées par templates/AGENTS.default.md n'atteint aucun utilisateur déjà installé. Exemple concret et actuel : le trailer Co-Authored-By de T10.4, mergé le 27/08, ne sera vu par aucun projet scaffoldé avant cette date. Le futur hook de pré-commit de T10.6 non plus. Le bundle accumule donc des garanties que ses utilisateurs historiques n'ont pas.

Piste à instruire : transposer au ./AGENTS.md le motif de zone délimitée déjà éprouvé sur ~/.agent-vm/runtime.sh (marqueurs AC_MARKER / AC_MARKER_END, lib/ui.sh:32-33), et faire réécrire cette zone par le verbe albert-code update — dont c'est exactement le contrat (rafraîchir un projet existant sans repasser les questions MCP/skills).

Tâches :

  1. Migration. Les AGENTS.md existants n'ont aucun marqueur. Le premier update doit trancher : insérer le bloc et à quel endroit, demander confirmation, ou avertir et passer. Même classe de problème que T9.2 face aux catalogues périmés.
  2. Frontière. Décider ce qui entre dans la zone gérée (les garanties du bundle : règles de sécurité, conventions de commit, trailer Co-Authored-By de T10.4) et ce qui n'y entre pas (le contenu propre au projet). Une zone trop large écraserait des personnalisations légitimes.

Interaction : la règle ajoutée aujourd'hui dans templates/AGENTS.default.md vit sous ## Expected Behavior. Si la frontière exclut cette section de la zone gérée, aucune évolution des règles de comportement n'atteindra jamais un projet déjà scaffoldé — ce qui est exactement le problème que T8.5 existe pour résoudre. L'arbitrage de la frontière doit trancher ce cas explicitement.

DoD : après un albert-code update (ou un setup rejoué) sur un projet scaffoldé avant une évolution, les règles gérées par le bundle sont à jour et tout ce que l'utilisateur a écrit hors de la zone est intact. → scénario TESTS.md S69.

Implémenté — motif de zone délimitée transposé de ~/.agent-vm/runtime.sh à ./AGENTS.md (lib/ui.sh : marqueurs HTML AC_MARKER_AGENTS / AC_MARKER_AGENTS_END, invisibles au rendu Markdown — une ligne # aurait rendu un faux titre H1 ; templates/AGENTS.default.md : zone bornée par ces marqueurs). sync_agents_md (lib/phases.sh) est branché sur les deux canauxphase_b (étape B.1) et phase_update (étape 2) — sans question MCP/skills ni confirmation (contrat non interactif, T9.3) :

  • fichier absent → pose le template ;
  • marqueurs appariés_regenerate_agents_zone réécrit la zone entre marqueurs à partir du bloc géré du template courant, head/tail et les sections hors zone préservés, idempotent (diff avant écriture, AC_AGENTS_CHANGED) ;
  • sans marqueur (migration, projets antérieurs à T8.5) → insertion silencieuse de la zone gérée en tête via _migrate_agents_zone (contenu existant conservé en dessous), jamais de question — git diff fait la revue ;
  • marqueur orphelin (ouvrant sans fermant ou l'inverse) → aucune écriture, avertissement explicite. Frontière : la zone gérée = les quatre sections contiguës ## Sécurité (non négociable), ## Git & commits, ## Accessibilité & conformité, ## Hygiène de dépôt (les deux dernières promues de ### à ##, changement du fichier de référence de tous les projets). Est hors zone : l'en-tête, l'introduction et tout ## Expected Behavior (Plan Mode, Task Management, Self-Improvement Loop, Bug Fixing, Code Quality) — sections que les projets adaptent le plus et qui ne sont jamais écrasées. Position d'insertion (migration) : en tête, pour rendre les garanties du bundle immédiatement visibles. Règle 8 déplacée : le point 5 « Nommer ce qu'on détruit » est retiré de Plan Mode et posé dans la zone gérée, sous ## Sécurité (non négociable) — c'est une garantie du bundle, pas une consigne de planification (sans ce déplacement, elle serait retombée hors zone et n'aurait plus atteint les projets existants). Garde-fou newline conservé : $(...) avale les \n finaux de head/tail, un \n conditionnel est rendu pour ne pas coller head au marqueur ni marker à tail (idempotence). Réservation de plage abandonnée : les scénarios reçoivent leur numéro à l'écriture (→ S69). → TESTS.md S69.

EPIC 9 — Aligner le catalogue sur les id canoniques Albert <- AC-R042

Problème de fond : Le catalogue de modèles d'Albert API a churné pendant l'été 2026. Les trois identifiants embarqués dans le bundle (deepseek-ai/DeepSeek-V4-Flash, Qwen/Qwen3.6-27B, mistralai/Mistral-Medium-3.5-128B) sont morts en production (404 / 404 / 502). Le schéma OpenAPI d'Albert distingue formellement id (canonique, référençable) et aliases (commodité utilisateur non contractuelle). Doctrine : on ne code QUE l'id canonique renvoyé par GET /v1/models, jamais un alias.

Matrice de modèles autorisée (un seul modèle embarqué, DeepSeek par défaut) :

id canonique rôle contexte output
deepseek-v4-flash model (défaut) ET small_model 131072 65536

Un seul modèle embarqué. Décision du 2026-08-26 : le bundle ne livre que deepseek-v4-flash. Ajouter un autre modèle reste possible à la main dans l'opencode.json du projet.

Rappels : le contexte DeepSeek passe de 393216 à 131072 (correction, sinon OpenCode envoie des requêtes qui dépassent). Ne PAS ajouter d'autre modèle (pas de gemma, pas de ministral, pas de modèle de vision) : le bundle ne porte qu'un seul modèle de code.

T9.1 🔴 Aligner le repo sur le catalogue réel ✅ implémenté

But : remplacer partout les identifiants morts par les id canoniques en minuscules ; DeepSeek par défaut ; retirer Qwen/Qwen3.6-27B (le bundle ne livrant que deepseek-v4-flash) et toute mention de multimodal/vision (le bundle n'a plus de modèle de vision). Fichiers : config/opencode.template.json, lib/phases.sh (jq merge + fallback concaténation), README.md, AGENTS.md, TESTS.md (S48), docs/PLAN.md. Règles : ne coder QUE les id canoniques en minuscules ; DeepSeek model + small_model ; contexte DeepSeek = 131072 ; ne pas toucher vendor/vm/ ; ne pas modifier opencode.json racine (gitignoré, déjà corrigé) ; vérifier bash -n lib/phases.sh et jq . config/opencode.template.json. DoD : plus aucune occurrence de Qwen/Qwen3.6-27B, deepseek-ai/DeepSeek-V4-Flash ni mistralai/Mistral-Medium-3.5-128B dans le repo hors vendor/ ; un setup en dry-run produit l'unique id canonique deepseek-v4-flash (défaut). → TESTS.md S48.

T9.2 🟠 Étendre T8.2 : réparer aussi un provider.albert périmé ✅ implémenté

But : aujourd'hui scaffold_opencode_json ne merge que si le provider albert est ABSENT. Il faut aussi réparer quand il est présent mais que ses id ne sont plus dans le catalogue courant (c'est le cas de tous les projets déjà setupés des bêta-testeurs, cassés aujourd'hui). Tâches : appel GET /v1/models avec la clé déjà en main, comparaison des clés de provider.albert.models ; si l'appel réseau échoue, ne rien casser et se contenter d'un avertissement. DoD : un projet setupé avec l'ancien catalogue est réparé au setup suivant (ids remis à jour) ; sans réseau, avertissement et aucun écrasement. ✅ Implémentéscaffold_opencode_json (lib/phases.sh:905-911) : quand l'opencode.json existant contient "albert", si fetch_albert_catalog aboutit → repair_stale_provider_albert ; sinon repli « conservé ». fetch_albert_catalog (lib/phases.sh:759-788) interroge GET /v1/models, capture la réponse avant tout traitement (T7.6), replis jq absent / clé absente / injoignable / illisible = avertir sans rien écrire. repair_stale_provider_albert (lib/phases.sh:851-898) ne retire QUE les ids absents du catalogue et conserve les modèles ajoutés à la main (traitement distinct jq_albert_reconcile_program, lib/phases.sh:797-814, qui ne remplace pas tout provider.albert), remonte model/small_model sur le défaut s'ils pointaient un id retiré, dédoublonne la liste (bash 3.2), et retourne 0 en cas de succès (réparé ou non) en signalant la réparation via AC_REPAIR_APPLIED=0|1 — un code non nul est réservé aux vraies erreurs (catalogue vide), pour ne jamais couper une phase sous set -e. Le merge cible par défaut (jq_albert_merge_program, lib/phases.sh:740-751) reste la source de vérité du cas « provider absent ». → TESTS.md S54, S55, S56.

T9.3 🟠 Promouvoir T8.4 albert-code update (ex 🟡) ✅ implémenté

But : verbe non interactif qui rafraîchit opencode.json (T9.2) et le bloc marqué du runtime USER (T8.3) sans repasser les questions MCP/skills. C'est le seul canal possible pour distribuer un correctif de catalogue aux projets existants. ✅ Implémentéphase_update (lib/phases.sh:257-292) : applique fetch_albert_catalog + repair_stale_provider_albert sur ./opencode.json (confirmation avant écriture, succès toujours en retour 0 via AC_REPAIR_APPLIED), puis ensure_vm_runtime régénère le bloc marqué (embarque le garde-fou OpenCode T8.3) ; récap « mise à jour effectuée » / « rien à faire ». Dispatch dans bin/albert-code:36 (verbe update), bin/albert-code:67-68 (case → phase_update), message de fin d'install bin/albert-code:54-61 ; aide lib/ui.sh:467-468 (usage_albert_code) + lib/ui.sh:437 (usage_install) ; README.md:45-65 (« à 4 verbes ») ; AGENTS.md:76. Non interactif côté config (aucune question MCP/skills), --dry-run respecté. → TESTS.md S25, S57, S58.

T9.4 🟡 Garde-fou au run

But : avant de lancer le TUI, vérifier que le model configuré est green dans GET /health/models et proposer une bascule sinon, au lieu de laisser l'utilisateur sur un 502 opaque. (Documenté — PAS implémenté dans cette PR.)


EPIC 10 — Auth GitHub de l'agent <- AC-R043

Problème de fond : Albert Code ne peut plus pousser ni ouvrir de PR depuis la VM. Diagnostiqué le 2026-08-26, cause racine prouvée par comparaison d'empreintes de jetons :

  • le GH_TOKEN de l'hôte (~/.zshenv) est valide (api.github.com/user renvoie 200) ;
  • le GH_TOKEN présent dans le ~/.zshenv de la VM est un autre jeton, périmé, qui renvoie 401 (gh auth status : « Failed to log in to github.com using token »).

Deux gardes « ne rien faire si la variable existe déjà » se combinent :

  1. persist_zshenv (lib/phases.sh) sort avec « déjà présente » dès qu'une ligne export GH_TOKEN= existe sur l'hôte. Un jeton révoqué ou expiré ne peut donc pas être remplacé par le wizard.
  2. La ligne écrite dans le runtime (lib/phases.sh:413-414) est grep -q 'GH_TOKEN' ~/.zshenv || echo "export GH_TOKEN=..." >> ~/.zshenv, exécutée dans la VM. Une fois posée, elle n'est plus jamais rafraîchie.

Le shadowing rend le bug invisible : lib/phases.sh:415-416 exporte bien le jeton frais dans runtime.sh, mais zsh source ~/.zshenv à chaque invocation de shell. Le zsh -l du TUI ressource donc le .zshenv de la VM, dont l'export GH_TOKEN=<périmé> écrase la valeur fraîche héritée. C'est le piège agent-vm PR#16 retourné : .zshenv n'est plus le correctif, il est devenu la source de la panne.

Conséquence : sur les dernières PR, l'agent s'est arrêté avant le push et un humain a poussé à la main à chaque fois.

T10.1 🔴 Rafraîchir un secret existant, sur l'hôte et dans la VM <- AC-R043 ✅ implémenté

But : persist_zshenv doit pouvoir remplacer une valeur existante : détecter un GH_TOKEN présent mais invalide (curl api.github.com/user renvoie 401) et proposer « garder / remplacer ». Même logique pour la ligne posée dans le .zshenv de la VM : réécrire au lieu de sauter si la valeur diffère de celle du bloc marqué. Généraliser aux autres secrets (ALBERT_API_KEY, CONTEXT7_API_KEY), qui ont le même défaut. DoD : un jeton révoqué est remplacé de bout en bout par le wizard, sans édition manuelle d'aucun .zshenv, ni sur l'hôte ni dans la VM. → TESTS.md S50. ✅ Implémentépersist_zshenv (lib/phases.sh) : si la ligne ancrée ^export VAR= existe, trois cas pour GH_TOKEN via le nouveau _gh_token_valid (retour 0 = 2xx → silencieux « valide » ; 1 = 401 → warn « invalide (révoqué ou expiré) » + prompt garder/remplacer ; 2 = API injoignable → on n'affirme rien, comportement actuel) ; les autres secrets (ALBERT_API_KEY, CONTEXT7_API_KEY, identité) proposent toujours garder/remplacer via prompt_choice. Le remplacement passe par _zshenv_drop qui supprime la ligne ancrée (fichier temporaire, pas de sed -i) puis apply_append la nouvelle valeur. --dry-run : annonce le prompt et choisit « garder » (S59/S60 dry-run). Testé : replace ALBERT + replace GH_TOKEN 401 conservent les autres lignes ; code 200/500 silencieux. → TESTS.md S50 + S59.

Régression de périmètre (2026-09-09) : le remplacement de bout en bout promis par ce ticket n'était garanti que côté hôte et via le runtime perso ; le runtime de référence (persist_env_var dans runtime/agent-vm.runtime.sh) gardait la garde « déjà présente → ne rien faire », figeant les secrets dans les VM déjà provisionnées malgré une rotation. Corrigé par T10.11.

T10.2 🟠 Ne plus faire dépendre le push du .zshenv de la VM ✅ implémenté

But : soit le runtime réécrit systématiquement la ligne dans le .zshenv de la VM (source unique), soit le bundle cesse d'écrire dans ce fichier et passe le secret autrement. Trancher et documenter la décision dans T8.1. DoD : après rotation d'un jeton côté hôte, un run suffit à ce que la VM voie le nouveau jeton. → TESTS.md S51. ✅ Implémenté — décision : garder l'écriture dans le .zshenv de la VM comme source, mais la réécrire systématiquement à chaque boot (dropper l'écriture re-casserait la propagation du correctif agent-vm PR#16). ensure_vm_runtime (lib/phases.sh) définit une fois le helper _ac_zsh_set VAR valeur dans le bloc marqué (<<'AC_HELPER', émis littéralement, POSIX sh, pas de sed -i, pas de bas-de-casse 4) puis l'appelle par secret + identité git. Abstention : si l'hôte n'a pas de valeur, aucune ligne n'est émise (l'ancien export VAR='' est supprimé) → une valeur posée à la main dans la VM est conservée (S59). Idempotence : deux setup produisent exactement 1 marqueur, 1 définition de helper, 1 paire _ac_zsh_set+export par variable (S60). Propriété d'auto-réparation : bloc régénéré côté hôte à chaque install/setup/update ET _ac_zsh_set rejoué à chaque boot de VM → un jeton périmé est réécrit au run suivant. → TESTS.md S51, S59, S60. Décision documentée dans T8.1.

Régression de périmètre (2026-09-09) : ce correctif n'avait été appliqué qu'à la moitié du système d'écriture des secrets — le runtime perso ~/.agent-vm/runtime.sh produit par ensure_vm_runtime. L'autre chemin, persist_env_var dans runtime/agent-vm.runtime.sh (le runtime de référence copié dans chaque projet en .agent-vm.runtime.sh), gardait l'ancienne logique grep -qE "^export VAR=" && return (« déjà présente, inchangée ») avec 0 occurrence de _ac_zsh_set. Conséquence : les trois secrets (ALBERT_API_KEY, CONTEXT7_API_KEY, GH_TOKEN) restaient figés à vie dans une VM déjà provisionnée — exactement le symptôme que T10.1 devait éliminer. Corrigé par T10.11 (alignement de persist_env_var sur _ac_zsh_set dans le runtime de référence).

T10.3 🟠 Jeton dédié à l'agent, à permissions minimales

But : le bundle persiste aujourd'hui dans la VM le jeton personnel de l'utilisateur, souvent large. Trois raisons de changer : le moindre privilège (un agent n'a besoin que de pousser une branche et d'ouvrir une PR sur les dépôts du projet), la révocabilité (couper l'agent sans casser le compte de la personne), et la traçabilité (distinguer ce que fait l'agent de ce que fait la personne). Un incident de fuite de jeton personnel depuis runtime.sh a déjà eu lieu en juillet 2026 et a imposé une rotation. Tâches : cible = un jeton fine-grained dédié, limité aux dépôts nécessaires, avec les permissions minimales pour push et gh pr create. Documenter la marche à suivre dans le README et le demander explicitement au wizard comme un jeton dédié. Prendre en compte le cas du non-admin d'organisation, qui ne peut pas créer de fine-grained. DoD : le README décrit un jeton dédié à permissions minimales, et le wizard le réclame comme tel. → TESTS.md S52.

T10.4 🟡 Tracer le modèle dans les commits produits par l'agent ✅ implémenté

But : les commits produits par Albert Code ne portent aucune trace du modèle qui a généré le code. Ajouter à templates/AGENTS.default.md une consigne de trailer Co-Authored-By nommant le modèle Albert utilisé, pour retrouver a posteriori le code produit par un modèle donné (utile en cas de régression de qualité ou de retrait d'un modèle du catalogue, cas déjà vécu). Note : c'est une consigne de prompt, donc un garde-fou faible ; un contrôle côté forge serait la version robuste. DoD : un commit produit par Albert Code porte le trailer nommant le modèle. → TESTS.md S53. ✅ Implémenté — consigne ajoutée dans la section « Git & commits » (templates/AGENTS.default.md:57, trailer Co-Authored-By: Albert Code (<id-modèle>) <noreply@numerique.gouv.fr>, précédé d'une ligne vide). Source déclarative, non vérifiable : la version d'OpenCode embarquée n'expose aucun identifiant du modèle courant à l'agent (aucune variable d'env type OPENCODE_MODEL, aucune substitution dans le contenu d'AGENTS.md — seules {env:}/{file:} existent et uniquement dans opencode.json ; le contexte système injecte seulement cwd/root/git/plateforme/date). La consigne demande donc à l'agent de noter l'id canonique Albert du modèle tel qu'il le connaît ; l'EPIC 11 multipliera les rôles de modèles, mais ce garde-fou reste faible (à compléter par un hook de pré-commit en T10.5). → TESTS.md S53.

T10.5 🟡 Détection de secrets imposée techniquement, pas par consigne ✅ instruit

But : la détection de secrets avant commit repose sur une ligne de templates/AGENTS.default.md. Une consigne dans le contexte de l'agent peut être ignorée ou altérée : elle ne peut pas être l'unique garde-fou sur les secrets. Étudier la pose d'un hook de pré-commit au setup. DoD : ticket documenté, décision d'opportunité tranchée.

Décision (2026-08-27) — On implémente. Garde-fou local et contournable, assumé comme défense en profondeur, pas comme frontière de sécurité. Réponses A→E :

  • A. Où vit le hook → core.hooksPath vers un répertoire versionné (ex. .githooks/ du projet). .git/hooks/pre-commit n'est ni versionné ni partagé et disparaît à chaque clone : fragile et contraire à l'idempotence du bundle. hooksPath est posé une fois par le setup (git config core.hooksPath .githooks), partagé par versionnage, donc durable pour toute l'équipe du projet. La « contrainte » imposée à l'équipe est acceptée : une équipe scaffoldée adhère déjà aux conventions du bundle.
  • B. Quel détecteur → détection par nom de variable en contexte, pas par format du secret. gitleaks est écarté (binaire externe : coût d'installation dans la VM et chez l'utilisateur, épinglage de version, dépendance réseau) pour une cible prioritaire ALBERT_API_KEY dont le format n'est pas un motif reconnu (fait #2 : la push protection forge ne le voit pas non plus). Une regex par format (celle de conformity-check.sh) ne peut pas caractériser une clé sans forme connue. Détecter la ligne ALBERT_API_KEY=…, GH_TOKEN=…, CONTEXT7_API_KEY=… par leur nom dans le diff stagé (grep/awk, zéro dépendance) colle exactement au modèle de menace du ticket : un agent qui écrit un secret dans un fichier puis le committe. ⚠️ Justification exclusive : ce risque (secret écrit dans un fichier puis commité) est distinct des fuites de juillet/août 2026, passées par la sortie d'un outil et qu'aucun hook de pré-commit n'aurait attrapées.
  • C. Bloquer ou avertir → bloquer sur le nom suivi d'une valeur plausible, avertir sur les patterns flous. Le principe du ticket est respecté : un hook qui refuse à tort apprend à passer --no-verify par réflexe et désarme le garde-fou. Le nom seul n'est pas sans ambiguïté : lib/phases.sh et BACKLOG.md contiennent ALBERT_API_KEY= de façon parfaitement légitime, et un projet scaffoldé en aura autant dans son .env.example, son README ou sa configuration — refuser sur le nom seul bloquerait des commits sains et installerait le réflexe --no-verify que cette décision veut justement éviter. C'est donc le nom plus une valeur plausible qui est à faible bruit : exiger après le = une valeur d'au moins ~20 caractères qui ne soit pas un marqueur de remplacement (chaîne vide, xxx/…, <…>, $VAR, ${VAR}, valeurs documentaires ton-id/votre-clé/changeme). Sur ce couple nom+valeur : refus. Seul le nom, ou un motif de format ambigu (la regex de conformity-check) : simple avertissement pour ne jamais bloquer un commit légitime.
  • D. Un garde-fou contournable vaut-il le coup → oui, il garde sa valeur. --no-verify défait tout hook local et l'agent a un shell : il ne protège donc pas d'un agent décidé à passer outre. Mais il n'est pas disqualifié pour autant. Sa valeur est ailleurs : attraper l'erreur d'un agent de bonne foi (cas de très loin le plus fréquent — un secret posé par inadvertance dans un fichier), avant qu'il n'atteigne l'historique, et monter la friction pour le passage outre délibéré (un --no-verify devenu explicite et traçable). Garde-fou local = défense en profondeur, pas frontière de sécurité.
  • E. Quelle cible → les projets scaffoldés des utilisateurs. C'est là que vivent les secrets réels (les fixtures du bundle sont synthétiques, fait #3 du AGENTS.default.md). Le dépôt albert-code lui-même est déjà couvert par la push protection forge (fait #1) et ne devrait jamais contenir de secret utilisateur : il n'a donc pas besoin du hook. Ne pas faire les deux dans ce ticket : le travail sur le dépôt albert-code serait une piste distincte (et mineure).

Conséquence sur templates/AGENTS.default.md:8 : on comble, on ne retire pas. La phrase « gitleaks doit passer ; ne jamais contourner un hook (--no-verify interdit) » désigne un outil absent aujourd'hui (fait #3). Comme on implémente, on la garde mais on l'aligne : le garde-fou promis par la consigne doit devenir réel via T10.6 (la consigne sera reformulée pour nommer le mécanisme effectivement installé, et non un gitleaks hypothétique). T10.6 porte donc aussi l'obligation de rendre cette phrase vraie. Ne rien coder dans cette PR.

T10.6 🟠 Installer le hook de pré-commit au setup (détection par nom de variable)

But : donner corps à la décision T10.5 : poser le garde-fou de détection de secrets déclaré par templates/AGENTS.default.md:8 sur les projets scaffoldés des utilisateurs, par détection d'un secret au nom de la variable plus une valeur plausible en contexte (pas au format seul). Tâches :

  • Écrire .githooks/pre-commit (script bash, set -euo pipefail, compatible 3.2, idempotent) qui scrute le diff stagé (git diff --cached) et refuse si une ligne d'assignation d'un secret connu apparaît avec une valeur qui ressemble à un vrai secret : ALBERT_API_KEY=, GH_TOKEN=, CONTEXT7_API_KEY= (et variables d'identité) suivis d'une valeur d'au moins ~20 caractères. Ne jamais refuser sur le nom seul. Exclure explicitement : chaîne vide, marqueurs de remplacement (xxx/…, <…>, $VAR, ${VAR}) et valeurs manifestement documentaires (ton-id, votre-clé, changeme). Refus (exit ≠ 0) sur nom + valeur plausible ; simple avertissement sur les motifs de formats ambigus (regex de conformity-check.sh). Ne jamais bloquer sur un motif flou.
  • Poser le hook au setup : git config core.hooksPath .githooks, ajout additif et idempotent, sans écraser une config existante du projet.
  • Reformuler templates/AGENTS.default.md:8 pour nommer le mécanisme réellement installé (le hook de pré-commit du bundle), rendre la consigne vraie, et préciser le contournement --no-verify comme dernier recours explicite et assumé, pas un réflexe.
  • Tests dans TESTS.md : un commit contenant ALBERT_API_KEY=<vraie-clé-20+caractères> est refusé (exit ≠ 0) ; --no-verify le laisse passer ; un commit sain passe ; un ALBERT_API_KEY= sans valeur ou avec marqueur/placeholder (sk-xxx, $VAR, <clé>) passe ; le motif flou ne bloque pas. DoD : sur un projet scaffoldé, committer un fichier contenant ALBERT_API_KEY=<vraie-clé-plausible> est refusé par le hook (hors --no-verify) ; un commit sain passe (y compris un ALBERT_API_KEY= documentaire vide/placeholder) ; la consigne AGENTS.default.md est vraie (le garde-fou décrit est réellement installé au setup). → scénario TESTS.md à créer (S63). ← décision T10.5 (2026-08-27).

T10.7 🟠 Rétention et purge de la base de session OpenCode dans la VM

Problème : la base de session d'OpenCode dans la VM (~/.local/share/opencode/opencode.db) accumule les sorties d'outils. Tout secret qui a transité une seule fois dans le contexte de la session y persiste au repos et peut être re-résumé vers le modèle aux tours suivants. C'est un risque distinct de T10.5, observable lors de l'incident du 26/08 : le secret avait transité par la sortie d'un outil vers le contexte de l'agent, or un hook de pré-commit n'intercepte que ce qu'on écrit dans un fichier — la persistance en base de session, elle, n'est couverte par aucun garde-fou existant ni par la push protection forge (le secret ne passe jamais par git). But : instruire la rétention (durée de vie de la base, ce qui y est stocké : historiques de conversation, sorties d'outils, environnement) et la purge (rotation / nettoyage de la base en fin de session ou de VM) de cette base, et trancher la bonne politique pour le bundle. Tâches : cartographier la base (sqlite, tables, ce qui y persiste) ; décider la politique de rétention défendable pour un outil manipulé par des agents publics (durée de vie, purge au boot ou au reset de VM) ; documenter et implémenter la purge (script de nettoyage idempotent, exécuté au setup/run ou au démarrage de la VM). DoD : la politique de rétention de opencode.db est tranchée et documentée, et sa purge est implémentée (ou explicitement écartée avec justification) ; aucun secret connu ne persiste indéfiniment dans la base d'une session. → scénario TESTS.md à créer. ← incident 26/08.

T10.8 🟠 Permettre à l'utilisateur de déclarer des secrets supplémentaires

Problème (fait C) : la liste des variables propagées vers la VM est écrite en dur dans lib/phases.sh:473-497. Elle transporte exactement cinq variables (ALBERT_API_KEY, CONTEXT7_API_KEY, GH_TOKEN, AC_GIT_USER_NAME, AC_GIT_USER_EMAIL) ; il n'y a aucune boucle sur des secrets déclarés par l'utilisateur. Si l'utilisateur ajoute un provider tiers (cas de la répartition plan/build de T11.2), son secret n'atteint jamais la VM. Il doit poser la ligne à la main dans le ~/.zshenv de la VM. Ça marche — la règle d'abstention de T10.2 garantit qu'une valeur posée à la main n'est pas écrasée quand l'hôte n'a pas la variable (TESTS.md S59) — mais c'est une étape manuelle fragile, dont l'échec se manifeste par une erreur d'authentification opaque du fournisseur tiers.

But : un mécanisme déclaratif (liste de noms de variables dans une configuration du projet ou du bundle) qui étend le pont existant sans toucher au code à chaque nouveau fournisseur.

Tâches à esquisser :

  1. Choisir où la déclaration vit (configuration du projet ou du bundle).
  2. Ne transporter que des noms de variables, jamais de valeurs, dans un fichier versionné.
  3. Conserver la règle d'abstention : pas de valeur côté hôte → aucune ligne émise.
  4. Conserver l'idempotence du bloc marqué.

DoD : un secret déclaré par l'utilisateur atteint la VM par le même chemin que les cinq variables existantes, sans autre modification de lib/phases.sh.

T10.9 🔴 Divergences silencieuses entre les sources d'un secret

Problème : le bundle dispose de plusieurs sources de vérité pour un même secret et en choisit une sans le dire. Symptôme constant : l'utilisateur croit avoir changé son jeton, rien n'a changé, et aucun message ne l'en informe. C'est la même famille de panne que celle diagnostiquée à l'ouverture de l'EPIC 10 (deux .zshenv, une garde « ne rien faire si c'est déjà là »), déplacée d'un cran. Aucun test n'exerce le cas « mon jeton est valide et je veux le remplacer par un meilleur » : les scénarios existants ne jouent que « mon jeton est mort », jamais la migration que le produit prescrit pourtant à tous ses utilisateurs.

  • Facette A — l'environnement écrase le fichier sans avertir. ensure_vm_runtime (lib/phases.sh:393-395) lit ${GH_TOKEN:-} et ne consulte ~/.zshenv que si la variable d'environnement est vide. Un shell ouvert avant une modification du fichier porte donc l'ancienne valeur, et albert-code update écrit cette valeur périmée dans le bloc marqué. Constaté en conditions réelles le 27/08 : la VM a reçu l'ancien jeton et _ac_zsh_set a annoncé « inchangée » — exact et trompeur à la fois. La priorité à l'environnement est défendable (surcharge ponctuelle) ; le silence ne l'est pas. Correctif attendu : avertir quand les deux sources diffèrent, au lieu d'en choisir une au hasard du contexte.
  • Facette B — persist_zshenv jette la valeur saisie. Pour GH_TOKEN uniquement, la fonction sort immédiatement quand le jeton existant est valide (rc -eq 0ok « déjà présente (valide) » → return 0), sans jamais comparer à la nouvelle valeur. Le wizard demande un jeton, puis l'ignore sans le dire. Les autres secrets proposent toujours garder/remplacer ; seul GH_TOKEN a ce raccourci, introduit par T10.1 pour éviter de harceler l'utilisateur. Correctif attendu : la fonction dispose déjà de val (nouvelle) et existing (ancienne) → proposer le choix dès qu'elles diffèrent, quelle que soit la validité.
  • Facette C — le jeton de l'agent est imposé dans l'environnement de l'humain. GH_TOKEN est exporté dans le ~/.zshenv de l'hôte parce que le bundle l'y lit pour alimenter la VM. Mais une variable exportée est ambiante : elle capture tout ce qui lit GH_TOKEN sur la machine, à commencer par la CLI gh de l'utilisateur. Depuis que ce jeton est un fine-grained restreint à une organisation (T10.3), la CLI de l'humain hérite des restrictions d'un jeton qui ne lui était pas destiné — un gh api sur un dépôt hors périmètre renvoie un 404 trompeur (invisible, pas interdit). C'est contraire à l'objectif d'identité propre de l'agent porté par T10.3. Piste : vendor/vm/agent-vm.sh:477-482 pousse déjà ~/.agent-vm/env dans la VM à chaque démarrage — un canal conçu pour les secrets, qui n'exige aucun export dans le shell de l'humain.
  • Facette D — le transport git est un état partagé entre l'humain et l'agent. Le répertoire du projet est monté dans la VM, donc .git/config est le même fichier pour l'utilisateur sur son Mac et pour l'agent dans la bulle. Or les deux n'ont pas le même moyen d'authentification : l'humain dispose d'une clé SSH, l'agent d'un jeton en HTTPS. Un git remote set-url en SSH côté hôte casse donc immédiatement le push de l'agent (Permission denied (publickey), constaté le 27/08), et l'inverse est vrai. Contournement retenu : garder le remote partagé en HTTPS et déporter la préférence SSH dans le ~/.gitconfig de l'hôte (url."git@github.com:".insteadOf "https://github.com/"), fichier que la VM ne voit pas. Ça fonctionne mais reste une configuration manuelle non documentée, que rien ne rétablit après un reset de VM ou sur un nouveau poste. Le bundle devrait poser lui-même cette séparation de transport, au même titre qu'il pose l'identité git.

Tâches : traiter les trois facettes (A…C) et la facette D ; ajouter des scénarios TESTS.md (numéros attribués à l'écriture) couvrant explicitement le remplacement d'un secret valide par un autre secret valide, cas absent de la suite actuelle.

DoD : remplacer un jeton valide par un autre se fait par le wizard, de bout en bout jusqu'à la VM ; toute divergence entre environnement et fichier est signalée ; la CLI gh de l'utilisateur n'est plus contrainte par le jeton de l'agent ; le transport git de l'agent et celui de l'utilisateur sont indépendants, et modifier l'un ne casse pas l'autre.

← migration réelle T10.3 du 2026-08-27.

T10.10 🟠 Expliquer le jeton GitHub au moment de la saisie <- AC-R049

But : la saisie du jeton se fait aujourd'hui sur un libellé unique, « Colle ton PAT GitHub (scope repo ; Entrée pour abandonner) » (lib/phases.sh:696-698). Rien n'explique ce qu'est ce jeton, à quoi il sert, où le créer, ni quelles permissions lui donner. C'est l'étape la plus opaque du wizard pour un utilisateur qui n'a jamais créé de jeton. Aggravant : « scope repo » est le vocabulaire des jetons classiques, alors que la cible du bundle est un jeton fine-grained (T10.3). Le libellé oriente donc vers le mauvais type de jeton. Tâches :

  • Ajouter avant la saisie un encart court, au format posé par T6.16 (une ligne d'explication avant la question) : à quoi sert le jeton (permettre à l'agent de pousser une branche et d'ouvrir une PR depuis la VM isolée), où le créer, et la différence entre jeton classique et fine-grained en une phrase.
  • Corriger le libellé de saisie : ne plus nommer un scope de jeton classique si le bundle recommande un fine-grained.
  • Nommer les permissions minimales à cocher. Dépendance : le détail exact des permissions recommandées se cale sur T10.3 (jeton dédié à permissions minimales), qui n'est pas tranché. Le reste du ticket (nature du jeton, où le créer, classique contre fine-grained, correction du libellé trompeur) en est indépendant et peut être livré seul ; dans ce cas, formuler les permissions comme une recommandation provisoire plutôt que comme une liste arrêtée. Règles : sobriété du wizard (T6.14), quelques lignes et pas un mur de texte, 80 colonnes maximum par ligne affichée ; bash 3.2 ; accents FR corrects. DoD : en dry-run, la saisie du jeton est précédée d'un encart court disant à quoi il sert, où le créer et quel type choisir ; plus aucune mention de « scope repo » dès lors que le bundle recommande un fine-grained. → scénario TESTS.md à créer.

T10.11 🔴 Propager le correctif T10.1/T10.2 au runtime de référence (régression de périmètre)

← incident du 2026-09-09. Corrige la régression décrite dans l'annotation de T10.1 et T10.2 : le correctif n'avait atteint qu'un des deux chemins d'écriture des secrets.

Constat : deux mécanismes écrivent les secrets dans le ~/.zshenv de la VM.

  1. _ac_zsh_set dans lib/phases.sh (ensure_vm_runtime) : réécrit systématiquement la ligne à chaque boot, propriété d'auto-réparation. C'est le correctif T10.2.
  2. persist_env_var dans runtime/agent-vm.runtime.sh : gardait l'ancienne logique grep -qE "^export ${var}=" && return (« déjà présente dans ~/.zshenv (inchangée) »). Jamais corrigé, 0 occurrence de _ac_zsh_set.

Ce runtime de référence est copié dans chaque projet en .agent-vm.runtime.sh et s'exécute dans la VM à chaque boot. Il appelle persist_env_var pour ALBERT_API_KEY et CONTEXT7_API_KEY, et setup_github_auth l'appelle pour GH_TOKEN. Les trois secrets étaient donc figés à vie dans une VM déjà provisionnée, malgré une rotation — le symptôme exact que T10.1 devait éliminer.

Preuve empirique (2026-09-09, diagnostic par empreintes SHA256 tronquées, sans jamais exposer de valeur) : le ~/.zshenv hôte portait l'empreinte 507ef099 (/v1/models HTTP 200) ; le ~/.zshrc hôte, périmé, portait 216ee98e (/v1/models HTTP 401) ; la clé effective dans la VM portait 216ee98e (HTTP 401). Trois causes additionnées : (a) une seconde définition d'ALBERT_API_KEY, périmée, dans ~/.zshrc (zsh charge .zshenv puis .zshrc, la dernière gagne) ; (b) persist_env_var rendait la panne irréparable côté VM (redémarrer / albert-code run / albert-code update ne changeaient rien) ; (c) la rotation manuelle dans le runtime perso avait été faite sur la ligne export alors que c'est _ac_zsh_set qui alimente les VM — l'auto-réparation de T10.2 réécrivait donc la valeur périmée à chaque run par-dessus toute correction manuelle. Le diagnostic a coûté ~2 heures.

Tâches :

  1. Aligner persist_env_var (runtime/agent-vm.runtime.sh) sur le correctif T10.2 : réécriture systématique de la ligne ancrée ^export VAR=, à l'identique de _ac_zsh_set. Réutiliser le même comportement, pas une seconde implémentation (deux implémentations de la même chose = la régression). Conserver l'abstention : si l'hôte n'a pas de valeur, aucune ligne émise (une valeur posée à la main dans la VM est préservée). Appliquer aux trois secrets, GH_TOKEN compris via setup_github_auth. Idempotent, en bash (le runtime de référence a son shebang en bash), sans sed -i.
  2. Ajouter un garde-fou contre le shadowing (cause racine côté poste) : au setup, si ALBERT_API_KEY, CONTEXT7_API_KEY ou GH_TOKEN est défini dans plus d'un fichier de config de l'hôte (~/.zshenv, ~/.zshrc, ~/.zprofile, ~/.zlogin, ~/.profile, ~/.bashrc), émettre un warn nommant les fichiers concernés et rappelant que ~/.zshenv est la source unique attendue. Ne modifie rien, ne compare pas de valeur, n'affiche jamais de valeur ni d'empreinte : uniquement les chemins.
  3. Le runtime perso ~/.agent-vm/runtime.sh porte deux lignes par secret (_ac_zsh_set VAR '<val>' et export VAR='<val>'). Seule la première alimente les VM. Dériver les deux lignes d'une source unique dans le bloc marqué (fait par construction, cf. ensure_vm_runtime) et vérifier leur cohérence au run avec un warn nommant la divergence (jamais les valeurs) — une rotation faite à la main sur la mauvaise ligne est sinon une panne indétectable, l'auto-réparation réécrivant fidèlement la valeur périmée.
  4. Permissions du ~/.zshenv : dans persist_env_var (runtime de référence) et dans _ac_zsh_set (runtime perso), le chmod 600 était appliqué au fichier cible avant le mv ; le fichier final hérite donc des permissions du temporaire créé sous le umask courant (mesuré en VM : umask 0002 → le ~/.zshenv, qui contient les trois clés, retombe en 664 à chaque run). Corriger aux deux endroits : chmod 600 sur le fichier temporaire juste après sa création, et vérifier que le fichier final est bien en 600 après le mv. Le défaut préexiste dans _ac_zsh_set mergé sur main (il n'a pas été introduit par cette branche) ; ce ticket le répare dans la foulée.

DoD : depuis une VM dont le ~/.zshenv contient une valeur périmée pour un secret, un simple run fait voir à la VM la valeur fraîche de l'hôte, sans aucune édition manuelle ; le runtime de référence est idempotent (deux runs → exactement une définition par variable) ; toute divergence entre les deux lignes du runtime perso est signalée. → TESTS.md S66.


EPIC 11 — Catalogue de modèles adaptés au code (rôles plan / exécution)

Problème de fond : depuis l'EPIC 9, le bundle n'embarque plus qu'un seul modèle, deepseek-v4-flash, utilisé à la fois comme model et comme small_model. C'est un pis-aller assumé le temps de réparer le catalogue, pas une cible. DeepSeek Flash est un gros modèle : le faire porter aussi les tâches d'exécution répétitives (édition de fichiers, appels d'outils, boucles de correction) coûte des tokens et de la latence sans bénéfice de qualité.

En production, Albert API appliquera des limites de débit différenciées par modèle : plus le modèle est gros, plus le débit autorisé est faible. Un utilisateur qui met le même gros modèle partout épuisera donc son quota sur des tâches qui ne le justifient pas.

Le débit est appliqué par clé, côté Albert API : le bundle ne peut ni le contourner ni le lisser. Ses deux seuls leviers sont donc l'onboarding (quel modèle pour quel rôle) et la visibilité de la consommation.

T11.1 🟠 Doctrine de catalogue : quel modèle pour quel rôle

But : trancher, sur la base du catalogue réel renvoyé par GET /v1/models, quel modèle le bundle recommande pour le rôle « plan » (raisonnement, gros contexte, peu d'appels) et lequel pour le rôle « exécution » (édition, outils, gros volume d'appels). La matrice de l'EPIC 9 ne contient aujourd'hui qu'une seule ligne. Tâches : relever le catalogue courant (id canoniques, fenêtre de contexte, output max) ; évaluer au moins un candidat « petit modèle » pour l'exécution ; documenter le critère de choix retenu (contexte utile, coût en tokens, débit anticipé). DoD : la matrice de l'EPIC 9 nomme un modèle par rôle, avec son id canonique et la justification du choix.

T11.2 🟠 Rôles de modèles différenciés, proposés au setup

But : permettre, et proposer au setup, que la phase de plan (peu d'appels, gros contexte, raisonnement) et la phase d'exécution (beaucoup d'appels, édition de fichiers, outils) tournent sur deux modèles différents. Aujourd'hui config/opencode.template.json fixe un seul id pour tout. Levier tranché (2026-08-27) : la configuration de modèle par agent de l'opencode.json, pas small_model. Syntaxe officielle : "agent": { "plan": { "mode": "primary", "model": "<provider>/<id>" }, "build": { "mode": "primary", "model": "<provider>/<id>" } }. Un agent sans model explicite hérite du modèle global. Les agents plan et build ont des permissions d'outils différentes par défaut ; les valeurs exactes (la doc montre edit et bash à deny dans un exemple de configuration explicite, sans les affirmer comme défaut) sont à vérifier sur la version embarquée avant de s'appuyer dessus — le point est critique, une expérimentation à crédits limités (cf. plus bas) suppose que le mode plan ne puisse pas exécuter. small_model reste ce qu'il est : les tâches utilitaires internes (titres de session, résumés — vérification du ticket original confirmée : il n'est pas un levier d'exécution).

Tâches : embarquer les id retenus en T11.1 dans provider.albert.models ; câbler la répartition via le bloc agent (plan → modèle de raisonnement, build → modèle d'exécution) ; ajouter au setup une question unique, précédée de sa ligne d'explication (cf. T6.16), entre « répartition recommandée » et « un seul modèle partout » ; garder un défaut fonctionnel sans réseau.

Répartition cible (raison d'être), débits en ordre de grandeur anticipé, à confirmer :

Rôle Modèle visé Débit anticipé
agent.plan GLM (gros modèle de raisonnement) ~5 RPM
agent.build DeepSeek Flash, ou le nouveau Qwen ~50 RPM
small_model petit Qwen / Mistral (tâches utilitaires du build) ~200 RPM

Le débit est appliqué par clé côté fournisseur : le bundle ne peut ni le contourner ni le lisser, son seul levier est l'onboarding.

Point de vigilance (nuancé par le fait B) : le programme jq de réconciliation de T9.2 (jq_albert_reconcile_program) code en dur deepseek-v4-flash comme unique modèle de repli, et repair_stale_provider_albert remonte le modèle par défaut sur ce seul id. Mais ces deux programmes n'écrivent que dans provider.albert.models et .model (en racine) : un bloc agent et un provider tiers restent intacts à la réparation (déjà couvert par TESTS.md S46). Le risque résiduel ne porte donc que sur .model en racine, pas sur le bloc agent — lier les modèles au niveau agent est plus durable que via model en racine. À confirmer à l'implémentation pour s'assurer que la réparation d'un projet périmé n'écrase pas une répartition déjà en place.

Expérimentation prévue : une montée en charge réelle avec cinq alpha-testeurs équipés d'une clé tierce pour le rôle plan, le build restant sur Albert. La répartition peut en effet mobiliser deux fournisseurs distincts (ex. GLM chez un tiers pour le plan, Albert pour le build), ce qui suppose un second bloc provider et donc un second secret — renvoie à T10.8. DoD : le levier retenu est documenté dans le ticket avec sa source ; un setup en dry-run affiche la question et produit une configuration où le plan et l'exécution pointent deux id distincts quand la répartition est choisie ; un albert-code update ne casse pas un projet mono-modèle et ne perd pas le second rôle sur un projet réparti. → TESTS.md (nouveaux scénarios)

T11.3 🟡 Suivi de consommation par modèle

But : OpenCode affiche la consommation globale de la session, pas sa répartition par modèle. Dès qu'un projet jongle entre un modèle de plan et un modèle d'exécution (T11.2), cette agrégation ne dit plus où part le quota, ni si la répartition recommandée est effectivement suivie. Tâches : vérifier ce qu'OpenCode expose déjà dans son stockage de session ; à défaut, regarder ce que l'API Albert renvoie par clé et par modèle ; trancher entre une commande portée par le bundle et un simple renvoi vers un outil existant. DoD : ticket instruit, décision d'opportunité tranchée. Si une commande est retenue, elle est non interactive et n'affiche aucun secret.

T11.4 🟡 Limites de débit expliquées à l'onboarding

But : le débit est appliqué par Albert API sur la clé. Un utilisateur qui prend un 429 sans contexte l'attribuera au bundle. Documenter dans le README l'ordre de grandeur par modèle et la répartition recommandée (T11.1), et rendre explicite un 429 qui transite par le bundle. DoD : le README porte une section « quotas et répartition des modèles » ; un 429 rencontré au setup, à l'update ou au run est traduit en message actionnable et non en erreur brute.


EPIC 12 — Aide à la spécification pour les non-développeurs

Problème de fond : le bundle livre un agent de code à des utilisateurs dont une partie ne sont pas développeurs. Un non-développeur devant un TUI vide ne sait pas quoi demander : il décrit un besoin flou, l'agent code trop tôt, et le résultat n'est pas revoyable. La revue humaine avant toute mise en production, qui est le garde-fou de tout l'édifice, suppose que ce qui a été produit soit spécifié : sans spec écrite, il n'y a rien contre quoi relire.

Pattern éprouvé sur un projet antérieur : un agent « conception » qui interroge l'utilisateur jusqu'à obtenir une spec exploitable, un agent « développement » qui applique les règles techniques, et une commande de bascule de l'un à l'autre.

T12.1 🟠 Portage de l'agent de conception dans OpenCode

But : rapatrier la partie spec de ce pattern dans le bundle, sous une forme native OpenCode, pour qu'un utilisateur non-développeur arrive au code avec une spec écrite. Tâches : identifier le support OpenCode adapté (agent, commande ou skill) ; porter l'agent de conception ; produire un artefact de spec versionné dans le dépôt de l'utilisateur, pas seulement présent dans le contexte de la session ; décider s'il est embarqué par défaut ou proposé au setup au même titre que les skills (T6.4). DoD : depuis un projet vide, un utilisateur non-développeur obtient une spec écrite puis un premier squelette de code, sans avoir eu à rédiger le prompt initial. → TESTS.md (nouveau scénario)

T12.2 🟡 Rappel des règles d'usage au setup

But : les règles que le bundle applique déjà (revue humaine avant mise en production, jeton dédié à permissions minimales en T10.3, traçabilité du modèle dans les commits en T10.4, détection de secrets en T10.5) sont aujourd'hui soit implicites, soit enfouies dans templates/AGENTS.default.md. L'utilisateur qui installe ne les voit jamais. Les rappeler au setup, au moment où elles sont actionnables. Note : rappel court, pas mur de texte. La sobriété du wizard (T6.14) et la limite de 80 colonnes s'appliquent. DoD : le setup affiche un encart de rappel tenant en quelques lignes ; le README porte la version longue.