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###(registreFEEDBACK.md). 🔴 bloquant v1 · 🟠 important · 🟡 nice-to-have
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.mdS1 ✅.
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 ✅.
But : config OpenCode canonique du bundle. Tâches :
provider.albert(@ai-sdk/openai-compatible, modelsMistral-Medium-3.5-128B+DeepSeek-V4-Flash).model/small_model.mcp(réf ci-dessous).permission:edit/bashraisonnables (allow dans la VM isolée,denysurgit 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 :
- 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_KEYdans~/.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 :
opencodedé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.mdS3.
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). IdemCONTEXT7_API_KEYsi fournie (optionnelle). - Cloner/
git pulletalab-ia/skillsdans~/.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
~/.zshenvsi absentes). DoD : 2 exécutions consécutives = la 2e est une suite de no-ops en quelques secondes ; skills à jour aprèsgit pull; une config OpenCode perso préexistante (ex. provider Scaleway) reste intacte. →TESTS.mdS4, S5, S12.
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.
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 GiBsans réglage manuel ; valeurs surchargeables par env ; pas de sur-allocation sur petite machine. →TESTS.mdS20.
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).
Traité dans T7.7 (garde-fou générique run + setup) : l'avertissement au
setuppour unopencode.jsonexistant sansalberty est implémenté (scaffold_opencode_json,lib/phases.sh:729-733). Clôturé comme absorbé par T7.7 (DoD couverte : message explicite ausetup+ option de merge en T8.2). But : dans un repo ayant déjà unopencode.json, le scaffold le conserve (non-destructif) → le provideralbertn'est jamais ajouté → Albert ne se connecte pas dans la VM, sans alerte (juste « conservé »). Footgun silencieux. Tâches : en Phase B, si./opencode.jsonexiste déjà, détecter s'il contient le provideralbert; sinon → avertir clairement (« opencode.json existant sans provider Albert → Albert non câblé ») et proposer/documenter le merge du blocprovider.albert+model/small_model(jq/sed) sans écraser le reste. Ne jamais écraser silencieusement. DoD : scaffold dans un repo avecopencode.jsonsansalbert→ message explicite (+ option de merge) ; avecalbertdéjà présent → info « rien à faire ». →TESTS.mdS22.
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.
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 :ghnon authentifié (pas deGH_TOKEN).- Fallback navigateur (
chrome-devtools new_pagesur l'URLpull/new/…) → échec : pas de navigateur ouvrable dans le sandbox. - Résultat : blocage total du push/PR côté VM. Contournement utilisé : push +
gh pr createdepuis l'hôte (oùghest 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.
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 :
- Prompt token à l'install (
ensure_vm_runtime, Phase A) : proposer (optionnel, skippable) de saisir unGH_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 ---. - Identité + garde-fou email : demander/dériver
AC_GIT_USER_NAME+AC_GIT_USER_EMAIL, avec validation « doit finir enusers.noreply.github.com» (aurait attrapé le gmail saisi le 06/07). Proposer de dériver le noreply depuis le compteghde l'hôte si dispo. - Gotcha de rotation : documenter (README) + garde-fou — un
GH_TOKEN/email déjà persisté dans le~/.zshenvde 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~/.zshenvde la VM, ouagent-vm rmdocumenté). - Next-steps de l'install : mentionner l'auth GitHub dans « Prochaines étapes » (actuellement absente).
DoD : sur un poste vierge,
install.shpropose l'auth GitHub ; après acceptation, une VM fraîche pushe + ouvre une PR sans aucune édition manuelle deruntime.sh; un email non-noreply est refusé avec un message clair. →TESTS.mdS24. Implémenté (06/07/2026, branchefeat/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.mdT10.1) — cf. cause racine scrutée le 2026-08-26. Validation S24 absorbée par T2-CH2.
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) :
- 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.- La valeur est résolue au
setupet écrite littéralement dans l'opencode.jsongé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 danslib/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.- Non-régression de
albert-code update.repair_stale_provider_albertetjq_albert_reconcile_programne touchent en principe queprovider.albert.models,.modelet.small_model: un baseURL personnalisé doit survivre à unsetupet à unupdate. À 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 :
- Exposer
AC_ALBERT_BASE_URL(défauthttps://albert.api.etalab.gouv.fr/v1), définie en un seul point en tête de fichier avec les autres variables surchargeables. - Substituer l'URL codée en dur aux quatre emplacements de câblage par cette variable.
- Résoudre la valeur au
setupet l'écrire littéralement dans l'opencode.jsongénéré (jamais sous forme{env:AC_ALBERT_BASE_URL}— contrainte T10.8, cf. cadrage). - Vérifier qu'un
albert-code update(viarepair_stale_provider_albert/jq_albert_reconcile_program) ne rétablit pas l'URL par défaut sur un projet à baseURL personnalisé (test, pas supposition). - 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).
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 — Supprimer les 2 prompts quand la dérivation réussit : après curl 2xx + login + id, utiliser directement
git_name = loginetgit_email = <id>+<login>@users.noreply.github.com. Afficher un seul récapok "Compte GitHub : login <email>". Persister GH_TOKEN, AC_GIT_USER_NAME, AC_GIT_USER_EMAIL. - #2 — Fallback uniquement si la dérivation échoue : curl non-2xx/réseau/parsing vide → prompts manuels nom/email avec validation noreply.
- #3 — Séparation visuelle : ajouter
info "Étape suivante : colle ton PAT GitHub (scope repo)."avant le prompt_secret du token. - #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.mdS31, S40.
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.
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.
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.
But : pouvoir tester install.sh / runtime sur une machine déjà configurée sans rien modifier. Tâches :
install.shetruntime/agent-vm.runtime.shacceptent--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 unHOMEconfigurable pour~/.zshenv→ tout peut être redirigé vers un dossier jetable. --helpdocumente--dry-runet ces variables. DoD :HOME=/tmp/ac-test ./install.sh --dry-runn'écrit AUCUN fichier (diff de/tmp/ac-testavant/après = vide), affiche le plan complet, exit 0. → TESTS S13.
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 -vEpour supprimer marqueur + lignesexport 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).
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.
But : durcissements rapides. Tâches :
templates/.github/workflows/security.yml: pintrivy-action@0.28.0(au lieu de@master).chmod 600sur~/.zshenvet~/.agent-vm/runtime.shaprès création (contiennent une clé).local valdansprompt_secret/prompt_input(lib/ui.sh).- Ne pas
source agent-vm.shen--dry-run(install.sh). DoD : bash -n OK ; dry-run ne source rien ; permissions 600 appliquées.
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.
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.
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.
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.
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.
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.
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 vendoredopencode. 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/binni ailleurs) : rendre opencode atteignable depuis bash le ferait démarrer sans clé Albert — panne plus opaque qu'uncommand not found. (CORRIGÉ)- Bloc marqué
ensure_vm_runtime: le filet symlink ajouté en v1 est retiré. (CORRIGÉ) Règles : ne pas touchervendor/vm/; bash 3.2 ; accents FR ; dry-run OK ; non-destructif. DoD :albert-code runouvre le TUI OpenCode avec les clés chargées, plus decommand not found; aucun symlink opencode posé par le runtime. →TESTS.mdS65. Implémenté — lancementzsh -l(lib/phases.sh:phase_run) ;check_opencodeallégé (présence seule) et filet symlink retiré deensure_vm_runtime.
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 verbesclaude/codex/vibeface à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 dansdocs/PLAN.md(suivi amont validé ou risque dûment écarté pour les 3 verbes non utilisés).
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.
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.
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.
But : rendre S15 automatisable (le dogfood manuel prouve mais ne se rejoue pas seul). Tâches :
lib/ui.shinstall_shim: overrideSHIM_BIN_DIR(si défini et non vide → dossier du shim direct, sinon sonde/opt/homebrew/bin…$PATHinchangée) ; documenté à côté deHOME/OPENCODE_CONFIG_DIR.tests/s15_shim.sh: sandbox jetable (HOME/SHIM_BIN_DIR/XDG_DATA_HOMEsousmktemp -d), stubagent-vm.sh(agent-vm(){ echo "STUB OK $*"; }), PATH minimal ; asserter préconditioncommand -v agent-vmintrouvable →install_shim→command -v agent-vm=$SHIM_BIN_DIR/agent-vm+ exécution du stub ; vérifier non-pollution de/opt/homebrew/binet du vrai$HOME; cleanuptrap EXIT. DoD :tests/s15_shim.shexit 0 sans écrire hors de la sandbox ; intégrable en CI. →TESTS.mdS15 (variante automatisée).
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.mdS18. DoD : un commit contenant/Users/<qqn>/…dans un fichier tracké fait échouer la CI ; les placeholders légitimes passent. →TESTS.mdS18.
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.
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.
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.
DoD : repo public, LICENSE MIT, CI verte.
Implémenté — le dépôt github.com/etalab-ia/albert-code est public (api.github.com → private: false, rendu public le 01/07/2026), LICENSE est MIT, et la dernière run de CI est verte (conclusion: success, 2026-08-26).
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.
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 installcomme 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 installreste valide pour un ré-amorçage. - Conserver
install.shcomme amorçage de rétrocompatibilité, sans en faire le chemin documenté. - Passer en revue les autres occurrences de
install.shdans leREADME.md(dont l'exemple de surchargeAC_VM_*et la section de dépannage) et dansdocs/PLAN.md, et les aligner. DoD : leREADME.mdn'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.mdS11 (à relire, le parcours copier-coller change).
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.
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.
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.
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.
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. Tracebash -x(/tmp/tr.txt) : le nom dérivé était bon (agent-vm-albert-code-b9ed5d09), mais_agent_vm_runningrenvoyait faux → brancheelse→ flags → prompt. Cause :_agent_vm_runningfaitlimactl list --format … | grep -q, etbin/albert-codetourne sousset -o pipefail(l.11). Quandgrep -qmatche la 1re ligne et ferme le pipe,limactl(encore en écriture de la 2e VM) prend un SIGPIPE, sort en 141,pipefailfait é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_vmoùset +o pipefailest 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 -qdirectement souspipefail.
Tâches (v2 — correctif retenu) :
- Dans
phase_run()(lib/phases.sh), remplacer l'appel unique_vm --cpus … --memory … --disk … opencodepar 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 formatName Status). - Ne PAS réutiliser
_agent_vm_running(fragile souspipefail, cf. post-mortem). Réutiliser_agent_vm_name(vendor/vm/agent-vm.sh:137) pour le nom (OK, pas de pipe fragile). - 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
- Garder
compute_effective_vm_resourcesappelé avant (message « Ressources hôte détectées ») : on ne fait que conditionner l'usage deEFF_*.
Règles :
- Ne PAS toucher
vendor/vm/(vendored figé). - Aucun
… | grepdans 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 … || trueneutraliseset -e;caseen bash pur, pas de pipe. - Ne pas canonicaliser
pwdà la main (laisser_agent_vm_namefaire). - 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 :
- Garde-fou au
run(phase_run, tout début) : si./opencode.jsonabsent ou ne contient pas"albert", avertir clairement (« projet non configuré pour Albert → faisalbert-code setupici ») 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). - Avertissement au
setup(scaffold_opencode_json, branche fichier existant) : si./opencode.jsonexiste sans"albert", ne pas se contenter de « conservé » — signaler explicitement qu'Albert n'est pas câblé et pointer le blocprovider.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.
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 encase "$1" in install|setup|run|--help). - Extraire
phase_a/phase_b/phase_rundeinstall.shverslib/phases.sh, sourçable parbin/albert-codeET parinstall.sh. install.shdevient l'amorçage : jouephase_aPUIS pose le shim viainstall_shim "albert-code" "$SELF_DIR/bin/albert-code".phase_run: reprend le bloc « Prochaines étapes » (créer la VM de base si absente, puisagent-vm --cpus "$EFF_CPUS" --memory "$EFF_MEM" --disk "$AC_VM_DISK" opencode).- Mettre à jour
usage_installetREADME.mdpour documenter les 3 verbes. DoD :albert-code install= Phase A ;albert-code setup= Phase B ;albert-code run= lance la VM ;albert-code --helpdocumente tout. install.sh devient mince. →TESTS.mdS25. Implémenté — dispatcherbin/albert-code(case "$verb" in install|setup|run,bin/albert-code:35,47-65) ;phase_a/phase_b/phase_runextraits danslib/phases.sh, sourçés parbin/albert-code:25-26.
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 leconfirmLima, afficher un encarttitle+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
confirmavant d'installer Lima. DoD : un non-dev comprend pourquoi on installe une VM avant le premierconfirm. →TESTS.mdS26. 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 leconfirmLima (lib/phases.sh:69).
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_choicede contexte et lecaseprofils dansphase_b. - Créer
templates/AGENTS.default.mdavec le contenu fourni (sécurité, plan mode, task management, self-improvement, bug fixing, code quality, git, accessibilité). phase_bcopietemplates/AGENTS.default.mdvers./AGENTS.mdviacopy_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.mdS27. Implémenté — dossierprofiles/absent du dépôt ;templates/AGENTS.default.mdprésent ;phase_bpose ce template viacopy_templatenon-destructif (lib/phases.sh:155).
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.jsonenenabled: falsepar défaut. - Dans
phase_b, pour chaque MCP :confirm« Brancher le MCP (objectif : ) ? » →enabled: trueseulement 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_brafraîchit le cacheetalab-ia/skills, énumère chaque skill (litdescriptionduSKILL.mdde chaque dossier) et demandeconfirm« Installer la skill (objectif : ) ? ».- Écrit la sélection dans
./.albert-code/skills.txt(une skill par ligne). - Modifie
sync_skillsdu runtime : au boot, ne symlinke QUE les skills listées dans./.albert-code/skills.txtdu 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.mdS28. Implémenté — MCP : 4confirmdansscaffold_opencode_json(lib/phases.sh:745,752,758,763), bloc MCP généré en bash sans jq,enabled:trueseulement si coché. Skills :confirmpar skill (lib/phases.sh:944), sélection écrite dans.albert-code/skills.txt(lib/phases.sh:956) ;sync_skillslit ce manifeste et réconcilie les symlinks sans toucher aux skills perso (runtime/agent-vm.runtime.sh:168-232).
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.
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
idetloginavecgrep/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.mdS31.
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"AVANTcheck_base_vmdansinstall.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, avecgrep -npour localiser). Idem dansuninstall.shDoD :install.shpose le shim avant la VM ; la VM échouant ne bloque pas le reste ;albert-codefonctionne post-echec-VM. Ancienne fonction détectée et retirée si confirmée. →TESTS.mdS32.
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.
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
.gitignoreadapté 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 statusavant de committer » DoD : un agent qui scaffold un projet et commit inclut un.gitignoreet PR sans node_modules. →TESTS.mdS34.
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-devtoolspeut 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.
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.
But : 4 retouches UX sur la sortie de albert-code setup, issues de dogfood.
Tâches :
- Afficher l'ASCII art Albert Code au début de Phase B (après le garde-fou PWD = SELF_DIR, avant
title "Phase B …"). - Quand Context7 MCP est choisi, demander la clé API si absente (ni env ni ~/.zshenv) via
prompt_secret, la persister viapersist_zshenv. Si vide, avertir que le MCP sera en erreur. - Réécrire
print_next_steps(): seulementtitle "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. - 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).
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).
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 :
- 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. - Spinner (
with_spinnerdanslib/ui.sh) : helperwith_spinner "message" cmd args...avec frames braille (⠋⠙⠹⠸⠴⠦⠧), dégradation non-TTY/dry-run. Appliquer UNIQUEMENT à : clone agent-vm, clone/refresh skills (jamais àagent-vm setupniagent-vm ... opencodequi streament leur propre sortie). - 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. - 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_SKILLSexposées parscaffold_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 :
- Supprimer le bloc A.4 de
phase_a()(lib/phases.sh:103-116) : plus aucun prompt ni mention Context7 à l'install. Ne plus appelerpersist_zshenv "CONTEXT7_API_KEY"en phase A. - 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. - Propager la clé saisie au setup vers la VM : appeler
ensure_vm_runtimeen fin dephase_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.shgardeexport CONTEXT7_API_KEY=''). ensure_vm_runtimeest 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.
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 :
- Dans
scaffold_opencode_json(lib/phases.sh:672-688) : avant chaqueconfirm, une ligneinfoqui explique le connecteur (ce que l'agent saura faire en plus), puis unconfirmcourt « 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. »
- 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.
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.
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à.
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 update → sync_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.
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).
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.mdEPIC 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.
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 :
- Migration. Les
AGENTS.mdexistants n'ont aucun marqueur. Le premierupdatedoit 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. - 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-Byde 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.mdvit 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 canaux — phase_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_zoneréé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 difffait 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é dePlan Modeet 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\nfinaux de head/tail, un\nconditionnel 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.mdS69.
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.jsondu 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.
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.
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.
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.
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.)
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_TOKENde l'hôte (~/.zshenv) est valide (api.github.com/userrenvoie 200) ; - le
GH_TOKENprésent dans le~/.zshenvde 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 :
persist_zshenv(lib/phases.sh) sort avec « déjà présente » dès qu'une ligneexport GH_TOKEN=existe sur l'hôte. Un jeton révoqué ou expiré ne peut donc pas être remplacé par le wizard.- La ligne écrite dans le runtime (
lib/phases.sh:413-414) estgrep -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.
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_vardansruntime/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.
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.shproduit parensure_vm_runtime. L'autre chemin,persist_env_vardansruntime/agent-vm.runtime.sh(le runtime de référence copié dans chaque projet en.agent-vm.runtime.sh), gardait l'ancienne logiquegrep -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 depersist_env_varsur_ac_zsh_setdans le runtime de référence).
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.
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.
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.hooksPathvers un répertoire versionné (ex..githooks/du projet)..git/hooks/pre-commitn'est ni versionné ni partagé et disparaît à chaque clone : fragile et contraire à l'idempotence du bundle.hooksPathest 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_KEYdont 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 deconformity-check.sh) ne peut pas caractériser une clé sans forme connue. Détecter la ligneALBERT_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-verifypar réflexe et désarme le garde-fou. Le nom seul n'est pas sans ambiguïté :lib/phases.shetBACKLOG.mdcontiennentALBERT_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-verifyque 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 documentaireston-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-verifydé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-verifydevenu 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.
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 deconformity-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:8pour nommer le mécanisme réellement installé (le hook de pré-commit du bundle), rendre la consigne vraie, et préciser le contournement--no-verifycomme dernier recours explicite et assumé, pas un réflexe. - Tests dans
TESTS.md: un commit contenantALBERT_API_KEY=<vraie-clé-20+caractères>est refusé (exit ≠ 0) ;--no-verifyle laisse passer ; un commit sain passe ; unALBERT_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 contenantALBERT_API_KEY=<vraie-clé-plausible>est refusé par le hook (hors--no-verify) ; un commit sain passe (y compris unALBERT_API_KEY=documentaire vide/placeholder) ; la consigneAGENTS.default.mdest vraie (le garde-fou décrit est réellement installé au setup). → scénarioTESTS.mdà créer (S63). ← décision T10.5 (2026-08-27).
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.
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 :
- Choisir où la déclaration vit (configuration du projet ou du bundle).
- Ne transporter que des noms de variables, jamais de valeurs, dans un fichier versionné.
- Conserver la règle d'abstention : pas de valeur côté hôte → aucune ligne émise.
- 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.
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~/.zshenvque si la variable d'environnement est vide. Un shell ouvert avant une modification du fichier porte donc l'ancienne valeur, etalbert-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_seta 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_zshenvjette la valeur saisie. PourGH_TOKENuniquement, la fonction sort immédiatement quand le jeton existant est valide (rc -eq 0→ok« 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 ; seulGH_TOKENa ce raccourci, introduit par T10.1 pour éviter de harceler l'utilisateur. Correctif attendu : la fonction dispose déjà deval(nouvelle) etexisting(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_TOKENest exporté dans le~/.zshenvde 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 litGH_TOKENsur la machine, à commencer par la CLIghde 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é — ungh apisur 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-482pousse déjà~/.agent-vm/envdans 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/configest 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. Ungit remote set-urlen 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~/.gitconfigde 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.
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
scopede 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.
← 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.
_ac_zsh_setdanslib/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.persist_env_vardansruntime/agent-vm.runtime.sh: gardait l'ancienne logiquegrep -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 :
- 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_TOKENcompris viasetup_github_auth. Idempotent, en bash (le runtime de référence a son shebang en bash), sanssed -i. - Ajouter un garde-fou contre le shadowing (cause racine côté poste) : au
setup, si
ALBERT_API_KEY,CONTEXT7_API_KEYouGH_TOKENest défini dans plus d'un fichier de config de l'hôte (~/.zshenv,~/.zshrc,~/.zprofile,~/.zlogin,~/.profile,~/.bashrc), émettre unwarnnommant les fichiers concernés et rappelant que~/.zshenvest la source unique attendue. Ne modifie rien, ne compare pas de valeur, n'affiche jamais de valeur ni d'empreinte : uniquement les chemins. - Le runtime perso
~/.agent-vm/runtime.shporte deux lignes par secret (_ac_zsh_set VAR '<val>'etexport 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 unwarnnommant 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. - Permissions du
~/.zshenv: danspersist_env_var(runtime de référence) et dans_ac_zsh_set(runtime perso), lechmod 600était appliqué au fichier cible avant lemv; 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 600sur le fichier temporaire juste après sa création, et vérifier que le fichier final est bien en 600 après lemv. Le défaut préexiste dans_ac_zsh_setmergé 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.
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.
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.
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)
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.
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.
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.
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)
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.