ArchitectureDev Studio — pilotage du développement

Dev Studio — pilotage du développement

Un seul cockpit, deux portes protégées. Dans l'Admin Panel, le bouton permanent du footer du panneau gauche ouvre /admin/platform/dev-studio. Son badge reprend le backlog en attente, avec les décisions ouvertes et les…

Un seul cockpit, deux portes protégées. Dans l'Admin Panel, le bouton permanent du footer du panneau gauche ouvre /admin/platform/dev-studio. Son badge reprend le backlog en attente, avec les décisions ouvertes et les runs actifs en récapitulatif. Les deux anciennes entrées du menu Plateforme ont disparu. Le cockpit admin utilise le centre et le panneau gauche du shell Admin, sans deuxième StudioShell, deuxième fond à pois ou double défilement. Les six sections remplacent le rail Admin dans ce contexte ; les Réglages et la documentation passent dans le menu « … » du centre. L'ancien Overview /admin redirige vers Dev Studio. La palette ⌘K reste utilisable sans champ Search dans le panneau gauche. Les ressources de design (Figma, Excalidraw, liens HTTPS) s'éditent directement dans ?s=design, avec une garde admin. Il n'existe plus de plafonds financiers de délégation dans Dev Studio. L'URL historique /admin/platform/dev-studio/settings redirige vers Design. /ops/dev reste la porte des collaborateurs mandatés via platform.dev.operate : même implémentation de cockpit, pas d'accès aux réglages réservés à l'administrateur.

RouteRépond à
/admin/platform/dev-studio?s=backlogQuels éléments restent prêts ou bloqués, et lesquels sont pris ?
/admin/platform/dev-studio?s=decisionsQuelles décisions attendent le propriétaire ?
/admin/platform/dev-studio?s=githubQuelles PR sont ouvertes et quels workflows ont tourné ?
/admin/platform/dev-studio?s=runsQuels agents tournent et quel est leur coût ?
/admin/platform/dev-studio?s=ciQuelles commandes et quels workflows gardent une PR ?
/admin/platform/dev-studio?s=designQuelles sources design sont partagées et éditables par un admin ?

Claude Code et les abonnements

Le lanceur du backlog propose Claude Code dans le navigateur (claude.ai/code), le deep link local vers la machine/Claude Desktop, la commande claude --cloud, ainsi que la commande claude a coller dans le terminal integre a Cursor. Il ouvre ou prepare la session ; il ne connait pas et ne stocke pas les identifiants Claude. Chaque surface utilise le compte Anthropic deja authentifie sur cette surface. Les abonnements Max ont leurs propres limites d'usage ; le Studio ne calcule pas de cout par run et n'applique pas de plafond en USD.

Frontiere de facturation : l'agent natif Cursor n'est pas Claude Code dans le terminal Cursor. Le lanceur passe par la CLI claude pour ne pas basculer involontairement sur le moteur Cursor. La variable locale ANTHROPIC_API_KEY peut faire basculer Claude Code vers la facturation API malgre un abonnement connecte : la connexion effective doit etre verifiee dans l'outil Claude, et non declaree connectee dans le panel.

L'ancienne fenetre de run suivi et les routes /api/ops/dev/{estimate,runs} ont ete retirees du cockpit. Les donnees historiques AgentRun et la lecture des PR et CI restent preservees pour le suivi, sans transformer le clic de lancement en dispatch API.

Deux sources, jointes une seule fois

Le manifeste (src/services/fleet/manifest.generated.json, ecrit par pnpm fleet:derive) sait ce qui est A FAIRE : il projette backlog/<pilier>/<id>-<slug>.md. La table AgentRun sait ce qui TOURNE. Aucune des deux ne connait l'autre, et c'est la page Flotte qui les rejoint, cote serveur, sur backlogRef (<pilier>/<id>).

Rien n'est lu sur le disque a l'execution. Ni backlog/, ni package.json, ni .github/workflows/, ni public/. La raison est celle qu'app-shell/0281 a payee sur le moniteur cron : un chemin de fichier construit au runtime est invisible au traceur de Next, donc les fichiers ne sont pas dans le bundle de la lambda. La lecture rend la bonne liste en local et une liste vide en production, ce qui est le mode d'echec le plus couteux qui soit : un ecran qui ne ment qu'une fois deploye.

D'ou deux artefacts derives, tous deux -merge dans .gitattributes :

ArtefactGenerateurSource
src/services/fleet/manifest.generated.jsonpnpm fleet:derivebacklog/, package.json, .github/workflows/
src/services/fleet/brand-assets.generated.tspnpm brand:derivepublic/**/*.{svg,png,jpg,webp,ico}

Les deux sont verifies au pre-push (fleet:derive:check, brand:derive:check) : un item ajoute au backlog sans regeneration bloque le push au lieu de disparaitre silencieusement de l'ecran.

Le moteur par defaut est Claude Code, et le modele plancher est Opus

Decision de gouvernance. La console n'est pas un selecteur de fournisseur : elle est centree sur Claude Code, et les autres moteurs n'existent que comme secours.

MoteurRoleQuand
CLAUDE_ACTIONprimaire, defautGitHub Actions execute le run et ouvre la PR
CLAUDE_LOCALprimairelien profond claude-cli://open, sur le poste de l'operateur
CURSORsecoursuniquement quand le quota Claude est epuise

L'ordre vit dans LAUNCH_ENGINES (src/services/fleet/launch.ts), avec DEFAULT_LAUNCH_ENGINE = "CLAUDE_ACTION". Le dialogue de lancement le rend dans cet ordre et etiquette Cursor « (secours) » : un operateur ne doit pas avoir a se souvenir de la politique, il doit la lire.

Le modele plancher est claude-opus-5, expose par FLEET_MODEL (src/services/fleet/prompt.ts) et ecrit dans le prompt remis a chaque run. La raison n'est pas le confort : un modele plus petit sur un item transverse coute plus en revue qu'il n'economise en tokens, et cette derive est invisible dans un diff. Le workflow .github/workflows/fleet-agent.yml doit passer --model claude-opus-5 dans ses claude_args, et cite FLEET_MODEL en commentaire comme la constante dont il est le miroir.

Le format de lien profond claude-cli://open n'accepte aujourd'hui que repo et q (cf. la doc des deep links) : il n'y a pas de parametre de modele a poser, donc le plancher passe par le PROMPT pour ce moteur-la. C'est une difference reelle entre les deux moteurs Claude, et elle est ecrite ici plutot que devinee.

Le cout se montre AVANT de lancer

GET /api/admin/fleet/estimate?backlogRef=&engine= rend { usd, basis, sample } : la mediane des runs termines de meme moteur et meme taille quand il y en a assez (basis: "history"), la grille par defaut sinon ("default"), et "unmeasurable" quand le moteur ne rapporte pas son cout. La taille n'est PAS un parametre de la requete : elle est lue dans le manifeste a partir de la reference, pour qu'un appelant ne puisse pas changer l'estimation en changeant sa saisie.

Afficher le cout apres le lancement en ferait un chiffre qui ne sert plus a decider. La fiche d'un run montre ensuite l'estime FACE au reel : c'est la seule facon de savoir si la grille par defaut est encore juste.

Le lancement, et ses cinq facons d'echouer

POST /api/admin/fleet/runs { backlogRef, engine } appelle launchAgentRun. Toute la decision est dans le service ; la route ne fait que traduire FleetLaunchError.code en statut HTTP (src/app/api/admin/fleet/runs/launch-status.ts) :

CodeStatutCe que ca veut dire
item-not-found404la reference ne designe aucun fichier de backlog
already-running409un run non terminal existe deja sur cet item
item-not-launchable422l'item est archive, done ou inbox
engine-not-configured422le jeton du moteur n'est pas pose
dispatch-failed502le moteur a ete joint et a refuse

Un code inconnu vaut 500, jamais 200 : repondre en succes ferait croire au panel qu'un run existe.

La route porte withAdminRoute(..., { action: "fleet.run.launch", resource: "AgentRun" }), donc chaque lancement ecrit sa ligne AdminAuditLog. Lancer un agent contre la production est une mutation, et elle se journalise comme telle.

Pour CLAUDE_LOCAL, la reponse porte un deepLink. Le client pose window.location.href dessus et affiche le lien en clair : un claude-cli:// que le systeme ne sait pas ouvrir echoue sans rien dire, et l'operateur se retrouverait avec une ligne QUEUED sans savoir pourquoi rien ne s'est passe.

La page Scripts ne lance rien

Deliberement. pnpm db:push derriere un bouton serait une ecriture de schema sur la production a un clic, et le panel a deja la surface qui le fait avec une confirmation tapee (/admin/platform/data-integrity). La page repond « quelle est la commande », pas « exécute-la pour moi ».

La bibliotheque est en lecture seule, et pour une raison

/admin/creative/library rassemble trois matieres : les StudioAsset recents toutes organisations confondues, le kit de marque derive de public/, et les liens GitHub vers les tokens et le pipeline Remotion.

Aucun verdict n'y est rendu. Approuver une creative, la rejeter, publier un drop : tout cela vit sur /admin/creative/qc. Deux surfaces de decision sur les memes lignes seraient deux boutons pour le meme etat, et c'est exactement le defaut que le Studio marchand a ete scinde pour eviter (cf. (dashboard)/CLAUDE.md, « Des resultats, pas des controles »).

Les vignettes passent par <img> et non next/image : les objets Blob sont des rendus finaux de tailles heterogenes, et re-encoder 48 vignettes a l'optimiseur couterait une invocation chacune pour une planche qu'on parcourt.

Lancer et éditer depuis /ops/dev (2026-09-26)

  • Lancer : chaque carte porte un split-button. Le bouton principal ouvre le dernier outil choisi sur le compte déjà connecté de l'opérateur ; la flèche ouvre le panneau (Claude Code web, local, cloud, Codex, Cursor), le modèle Claude (catalogue ai-models.ts), un contexte libre et une capture à coller. Liens et prompt : src/services/fleet/agent-links.ts, prompt en sept blocs fixes. claude.ai/code et Codex ne lisent pas de prompt dans l'URL : il est copié, puis la page s'ouvre. Le run suivi (GitHub Action, budget) reste LaunchRunDialog, dernière ligne du panneau.
  • Éditer : le crayon d'une carte ou d'une décision relit le fichier sur main et POST /api/ops/dev/items ouvre une PR draft sur backlog-edit/<pilier>-<id>-<horodatage> (src/services/fleet/item-edit.ts). Jamais de commit sur main. GITHUB_FLEET_TOKEN doit porter contents:write et pull_requests:write.

Quand tous les jobs échouent en quelques secondes (2026-10-02)

La section GitHub (src/services/fleet/live.ts) devinait « quota Actions épuisé, ça revient tout seul » sur la seule DURÉE des runs. Or la durée dit que les jobs n'ont rien compilé, pas pourquoi. Le 2 octobre l'annotation du check-run disait « recent account payments have failed or your spending limit needs to be increased » : la facturation GitHub, à la main du propriétaire, qui ne revient pas seule (platform-ops/2950, 3158).

  • La signature de durée (everyRunFailedInSeconds) ne conclut plus rien. Quand elle se présente, le service lit l'annotation du premier job du dernier run rouge (deux lectures de plus, jamais en état normal) et la classe (classifyBlackout) : billing si le message parle de paiement ou de plafond de dépenses, unknown sinon. Aucun des deux cas ne dit « revient seul ».
  • Le cockpit et @Atlas (getDevSection, section github, champ blackout) CITENT le message de GitHub, bornés à 400 caractères, avec le lien vers la facturation de l'organisation (billingUrl) quand la cause est billing.
  • Lire une annotation demande Checks: read au GITHUB_FLEET_TOKEN. Sans, la cause reste unknown et la bannière dit que le message n'a pas été livré : on n'invente jamais une cause.