feat(templates): refonte complète des templates façon Notion + vues/agents-skills
FlowDeck CI / lint (push) Failing after 1m32s
FlowDeck CI / test (push) Failing after 27m50s
FlowDeck CI / docker (push) Skipped

Templates (v7.71.x) :

- registre unifié \	emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler)

- sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document

- 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée

- récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates

- correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title

- tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8)

Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
This commit is contained in:
2026-10-10 18:52:19 -04:00
parent d0452e10dd
commit fc8548194a
36 changed files with 12290 additions and 137 deletions
+366
View File
@@ -0,0 +1,366 @@
# Agents & Skills pour Flowdeck — Phase 1 : Le Skill devient une page
| Champ | Valeur |
|---|---|
| Phase | **1 sur 5** — fondation de toute la solution |
| Document parent | `architecture-agents-skills-notion-flowdeck.md` v1.1 — en particulier §3.4 (les Skills dans Notion), §4.2 (écarts E1, E3, E4), §10.3–10.4, §13.1, §13.4–13.6 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | Prêt à implémenter |
| Prérequis | Flowdeck v7.69.8 ; migration courante = 38 |
| Migration créée | **39** |
| Dépendances | Aucune (première phase) |
| Débloque | Phases 2 (routeur de skills), 3 (skills des Custom Agents), 5 (Skills API) |
> **Résultat visible à la fin de cette phase.** N'importe quelle page Flowdeck peut devenir un skill ; une collection peut devenir une base de skills avec `Description` / `Files` / `Tags` ; la Library a un onglet *Skills* et un onglet *Discover* avec `Enable for me` ; les skills s'exécutent depuis le chat (`/`), le menu de sélection, le menu slash et le menu de bloc via un moteur unique (le Skill Runner) ; les skills intégrés d'AI Writing sont exposés comme skills ; chaque skill s'exporte en `SKILL.md` téléchargeable pour les agents locaux. Aucun skill existant n'est perdu : les enregistrements actuels et les 17 presets deviennent des pages.
---
## Table des matières
1. [Objectif et principe directeur](#1-objectif-et-principe-directeur)
2. [Périmètre](#2-périmètre)
3. [État d'entrée — ce qui existe en v7.69.8](#3-état-dentrée--ce-qui-existe-en-v7698)
4. [Modèle de données — migration 39](#4-modèle-de-données--migration-39)
5. [Tickets de travail](#5-tickets-de-travail)
6. [API livrée par la phase](#6-api-livrée-par-la-phase)
7. [Comportements détaillés](#7-comportements-détaillés)
8. [Plan de migration des données existantes](#8-plan-de-migration-des-données-existantes)
9. [Tests](#9-tests)
10. [Critères d'acceptation](#10-critères-dacceptation)
11. [Risques spécifiques et vigilance](#11-risques-spécifiques-et-vigilance)
12. [Ordre d'exécution](#12-ordre-dexécution)
13. [Définition de « terminé »](#13-définition-de--terminé-)
---
## 1. Objectif et principe directeur
Le principe (ADR-01 du document parent) : **la page est la source de vérité du skill ; la table `agent_skills` devient son index d'exécution.** Toute l'économie de la phase vient de ce que Flowdeck n'a pas à construire pour un skill ce qu'il a déjà pour une page : éditeur de blocs, ACL (partage, groupes, héritage), historique `page_versions`, corbeille, recherche FTS5 et sémantique, commentaires, Library.
Corollaires qui guident chaque ticket :
- **Marquer une page ne la déplace pas et ne la copie pas.** C'est l'insertion d'une ligne d'index. Démarquer supprime la ligne, la page ne bouge pas.
- **Un skill de base est une ligne de collection comme les autres** : son contenu est la page-ombre (`pages.collection_row_id`), ses métadonnées (`Description`, `Files`) sont des propriétés de la ligne.
- **Aucun nouvel éditeur, aucun nouveau modèle de permissions.** Si un comportement de skill ne peut pas s'exprimer avec les mécanismes de page/collection existants, c'est un signal d'arrêt et de redesign, pas une raison d'ajouter un sous-système.
## 2. Périmètre
### 2.1 Inclus
- Migration 39 (refonte `agent_skills`, `collections.is_skills_db`, `user_skill_enablements`, `skill_runs`, `skill_local_downloads`).
- Marquer/démarquer une page comme skill ; déplacer une page dans une base de skills.
- Créer une base de skills (gabarit) ; convertir une collection existante en base de skills (correspondance des propriétés).
- Bannière de skill sur la page (états, interrupteurs, téléchargement, badge de copie périmée).
- Library : onglet *Skills* et sous-vue *Discover*, `Enable for me`, désactivation pour soi.
- Skill Runner : chargement d'un skill (page + fichiers), construction de la consigne, exécution manuelle depuis 4 surfaces (chat `/`, sélection, slash, bloc), journalisation dans `skill_runs`.
- Skills intégrés : améliorer l'écriture, corriger, expliquer, reformater, traduire, résumer.
- Création assistée d'un skill par l'Agent (dialogue → création de la page).
- Export `SKILL.md` (+ fichiers partageables) et import `SKILL.md` ; format `flowdeck-skill` v1 conservé.
- Migration des skills existants et des 17 presets en pages.
### 2.2 Exclu (phases ultérieures)
- L'**usage automatique** des skills par l'Agent (routage sur la `Description`) → Phase 2. La `Description` est collectée et indexée dès cette phase, mais rien ne l'exploite encore automatiquement.
- L'usage des skills par les **Custom Agents** (accès accordés, runs autonomes) → Phase 3.
- La **comptabilité** d'usage détaillée par run (`agent_runs`, crédits) → Phases 2 et 4. Ici, `skill_runs` journalise avec les références disponibles (conversation, surface, utilisateur) ; la colonne `run_id` définitive arrive en Phase 2 — voir §4.3 pour la parade.
- L'écriture automatique dans les répertoires des agents locaux (utilitaire compagnon) → question ouverte Q4 du document parent ; cette phase livre le **téléchargement** du bundle et le suivi d'empreinte.
## 3. État d'entrée — ce qui existe en v7.69.8
| Élément | État | Fichier / mécanisme (architecture v7.69.8, §16.3) |
|---|---|---|
| Skills actuels | Prompts paramétrés + outils autorisés, table `agent_skills` | `app/services/skill_gallery.py` |
| Galerie | 17 presets installables | `skill_gallery.py`, routes `/api/agent/skills` |
| Format portable | `flowdeck-skill` v1, export/import JSON | idem + `/api/v2/skills/*` |
| Points d'entrée éditeur | Menu contextuel de bloc → *Skills ›* ; toolbar de sélection → *Skills* (§13.3) ; section Compétences du Menu + (gérer/parcourir) | `page_editor_scripts.js`, `agent_panel_*.js` |
| AI Writing | 6 actions headless (écrire, résumer, traduire, continuer, autocomplétion, propriétés) | `app/services/ai_writing.py` |
| Page-ombre de ligne | Chaque ligne de collection peut porter une page de contenu | `pages.collection_row_id` (v6.5) |
| Indexation sémantique | Embeddings maison, index incrémental, `resource_type` libre | `app/services/semantic_search.py` |
## 4. Modèle de données — migration 39
### 4.1 Schéma (repris du document parent §10.3–10.4, reséquencé)
```sql
-- Migration 39 — Phase 1
ALTER TABLE collections ADD COLUMN is_skills_db INTEGER NOT NULL DEFAULT 0;
-- Correspondance des propriétés dans collections.schema_json :
-- {"skill_properties": {"description": "<nom prop>", "files": "<nom prop>", "tags": "<nom prop>"}}
-- agent_skills est RECRÉÉE (patron : nouvelle table, copie, bascule, §8)
CREATE TABLE agent_skills_new (
id INTEGER PRIMARY KEY,
page_id INTEGER NOT NULL UNIQUE REFERENCES pages(id) ON DELETE CASCADE,
collection_id INTEGER REFERENCES collections(id),
name_cached TEXT NOT NULL,
description TEXT DEFAULT '',
files_json TEXT DEFAULT '[]',
tools_json TEXT DEFAULT '[]',
is_builtin INTEGER NOT NULL DEFAULT 0,
auto_use_default INTEGER NOT NULL DEFAULT 1,
editor_menu_default INTEGER NOT NULL DEFAULT 0,
owner_id INTEGER REFERENCES users(id),
status TEXT DEFAULT 'ready',
source TEXT DEFAULT 'page', -- 'page' | 'legacy' | 'import' | 'builtin'
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE user_skill_enablements (
user_id INTEGER NOT NULL REFERENCES users(id),
skill_id INTEGER NOT NULL REFERENCES agent_skills_new(id) ON DELETE CASCADE,
enabled INTEGER NOT NULL DEFAULT 1,
auto_use INTEGER, editor_menu INTEGER,
created_at TEXT NOT NULL,
PRIMARY KEY (user_id, skill_id)
);
CREATE TABLE skill_runs (
id INTEGER PRIMARY KEY,
skill_id INTEGER NOT NULL REFERENCES agent_skills_new(id),
run_id INTEGER, -- nullable en Phase 1, rattaché en Phase 2 (§4.3)
conversation_id INTEGER REFERENCES agent_conversations(id),
user_id INTEGER REFERENCES users(id),
surface TEXT NOT NULL, -- 'chat' | 'editor' | 'agent' | 'api'
invocation TEXT NOT NULL, -- 'manual' en Phase 1 ('auto' dès la Phase 2)
created_at TEXT NOT NULL
);
CREATE TABLE skill_local_downloads (
user_id INTEGER NOT NULL REFERENCES users(id),
skill_id INTEGER NOT NULL REFERENCES agent_skills_new(id) ON DELETE CASCADE,
target TEXT NOT NULL, -- 'claude-code'|'codex'|'cursor'|'gemini'|'grok'|'file'
page_version TEXT NOT NULL, -- empreinte au téléchargement
downloaded_at TEXT NOT NULL,
PRIMARY KEY (user_id, skill_id, target)
);
```
### 4.2 Règles du schéma
1. `page_id UNIQUE` : un skill = une page, sans exception. Le marquage est idempotent (un second marquage renvoie le skill existant).
2. `ON DELETE CASCADE` depuis `pages` : mettre la page à la corbeille **purge dure** ou la suppression effective retire le skill partout (menus, Library) — comportement attendu, testé (T-19).
3. Pour un skill de base, `description` et `files_json` sont des **caches relus** depuis les propriétés de la ligne à chaque sauvegarde de la ligne ou de sa page-ombre ; la ligne de collection fait foi.
4. Pour un skill autonome, `description`/`files_json` sont édités depuis la bannière et stockés ici.
5. `tools_json` hérite du modèle actuel : un skill peut **restreindre** les outils d'un run, jamais les élargir (intersection appliquée par le moteur, §7.4).
### 4.3 La parade `run_id`
`agent_runs` n'existe qu'en Phase 2. Pour ne pas bloquer la Phase 1 : `skill_runs.run_id` est nullable, `conversation_id` + horodatage portent la traçabilité immédiate ; la Phase 2 ajoute la contrainte de rattachement par une migration de données (et non de schéma). Ne pas créer de table `runs` temporaire.
### 4.4 Indexation
À la création/modification d'un skill : écrire ou mettre à jour les lignes `semantic_embeddings` avec `resource_type='skill'`, contenu = `name + description` (le titre seul ne suffit pas à l'appariement futur). Le scheduler d'indexation existant (300 s) est le filet ; l'écriture directe à la sauvegarde donne la fraîcheur nécessaire aux menus.
## 5. Tickets de travail
*Convention : chaque ticket est indépendamment testable ; « Fichiers » donne les points d'ancrage d'après l'architecture v7.69.8 — à confirmer contre le code réel au démarrage du ticket.*
### Socle données
**P1-T01 — Migration 39 et recréation d'`agent_skills`.**
Fichiers : `app/migrations.py` (`@register(39, ...)`), `app/db.py` si la baseline doit refléter le nouvel état pour les installations neuves.
Travail : créer le schéma du §4 ; la copie des données existantes est dans le même ticket que le script de migration de contenu (P1-T02), exécuté comme étape de la migration (données) — la migration reste une transaction unique.
Acceptation : sur une base de test v7.69.8 complète, `apply_migrations()` passe à la version 39 sans perte (comptages avant/après identiques pour les skills convertis, §8).
**P1-T02 — Migration de contenu : skills existants → pages.**
Travail : pour chaque ancien skill sans page : créer une page (workspace de l'utilisateur, privée par défaut, dans une base de skills privée créée à la demande — voir P1-T04), y écrire le prompt d'origine **à l'identique** comme corps de page, créer la ligne `agent_skills` avec `source='legacy'`, conserver `tools_json`. Les presets de la galerie ne sont **pas** matérialisés pour tous : ils le sont à l'installation (P1-T13).
Acceptation : chaque ancien skill reste invocable après migration (test de non-régression sur le jeu de skills de la base de test) ; aucun prompt modifié (comparaison de hash avant/après).
**P1-T03 — Service `skills_pages.py` : le cycle de vie page ↔ skill.**
Fichiers : nouveau `app/services/skills_pages.py` ; évolution de `skill_gallery.py` (qui devient un consommateur du service, pas le propriétaire du modèle).
Travail : `mark_as_skill(page_id, user)`, `unmark(page_id)`, `get_skill_by_page(page_id)`, `sync_skill_from_row(collection_page)` (recopie Description/Files depuis les propriétés), `sync_skill_from_page(page)` (titre, cache), suppression en cascade déjà portée par le schéma. Tous les appels vérifient l'ACL de la page via `PermissionManager` (marquer exige l'édition de la page).
Acceptation : les cinq opérations sont couvertes en tests unitaires, y compris les refus d'ACL.
### Bases de skills
**P1-T04 — Base de skills privée par défaut.**
Travail : à la première création d'un skill par un utilisateur sans base, créer sa collection `is_skills_db=1` privée (« Mes skills »), avec les propriétés `Description` (text), `Files` (files), `Tags` (multi_select) créées dans le schéma de la collection. Idempotent par utilisateur.
Acceptation : deux créations successives de skills par le même utilisateur n'ont créé qu'une base.
**P1-T05 — Gabarit « Skills » et conversion d'une collection.**
Fichiers : gabarits de bases (`db_templates`/seed existant), route `PUT /db/{id}/skills-db` (§6).
Travail : création directe d'une base de type Skills (mêmes propriétés qu'en T04 + vue par défaut triée par nom) ; dialogue de conversion : mapper des propriétés existantes vers `Description`/`Files`/`Tags` ou les créer ; option « activer les pages existantes comme skills pour moi » ; la conversion pose `is_skills_db=1` et crée les lignes `agent_skills` des lignes de la base (page-ombre requise : la créer si absente).
Acceptation : convertir la base de test « Playbooks » transforme chaque ligne en skill visible dans la Library ; la déconversion (`is_skills_db=0`) retire les lignes d'index sans toucher aux pages (règle du §18 du document parent).
**P1-T06 — Marquage depuis une page ordinaire.**
Fichiers : menu `•••` de page (gabarits de l'éditeur), route `POST /api/agent/pages/{id}/skill`.
Travail : entrée *Use as a skill* (case à cocher, état relu) ; si la page n'est dans aucune base de skills, le skill est autonome (description éditée via la bannière, T07) ; proposer en un clic « déplacer dans une base de skills » (réutilise le *Move to* existant + création de la ligne dans la base cible).
Acceptation : marquer, démarquer, re-marquer la même page donne un seul skill à chaque état stable.
### Expérience du skill
**P1-T07 — Bannière de skill.**
Fichiers : fragment de l'éditeur de page (au-dessus du corps, comme les bannières existantes), JS de l'éditeur.
Contenu : badge « Skill », base d'appartenance (lien), état d'activation pour l'utilisateur, interrupteurs *Use automatically* (défaut du skill) et *Add to text editor menu*, champ `Description` (skill autonome) ou lien vers la propriété (skill de base), bouton *Download for local agents*, badge orange « copie locale périmée » si une ligne `skill_local_downloads` de l'utilisateur est en retard d'empreinte.
Acceptation : la bannière n'apparaît que sur les pages-skills ; ses interrupteurs écrivent les bonnes colonnes (défaut du skill vs choix de l'utilisateur — ne pas confondre, §7.2).
**P1-T08 — Library : onglets Skills et Discover.**
Fichiers : `app/routers/library.py`, `app/templates/library.html`, `static/js/library.js`.
Travail : onglet *Skills* (skills créés + activés ; colonnes du §7.5 du parent : Nom, Base, Description, Use automatically, Menu éditeur, Propriétaire, Dernière utilisation depuis `skill_runs`) ; sous-vue *Discover* (skills lisibles non activés) avec bouton `Enable for me` par ligne ; recherche par nom/description ; les interrupteurs de l'onglet écrivent `user_skill_enablements`.
Acceptation : un skill partagé avec moi apparaît dans Discover, pas dans Skills ; après `Enable for me`, il bascule ; le désactiver le retire de mes menus sans affecter les autres utilisateurs (test multi-utilisateurs).
**P1-T09 — Le Skill Runner.**
Fichiers : nouveau `app/services/skill_runner.py`.
Travail : `load_skill(skill_id, user)` → {corps Markdown de la page (blocs éligibles sérialisés comme le fait le Context Builder), fichiers extraits (texte des fichiers `shareable`, plafonnés par budget), tools_json} ; `run(skill_id, input, surface, user, conversation_id)` → construit la consigne (page du skill en couche « consigne de tâche »), appelle le moteur existant en run contraint (outils = intersection), écrit `skill_runs`, renvoie la sortie structurée {texte, blocs, skill_name}. Mode LLM `offline` : sortie déterministe de test.
Acceptation : un run de skill produit une ligne `skill_runs` complète ; le nom du skill est présent dans la réponse rendue (préparation de l'invariant I5).
**P1-T10 — Invocation dans le chat : menu `/`.**
Fichiers : `agent_panel_*.js`, route `GET /api/agent/skills/menu`, compositeur du panneau.
Travail : taper `/` ouvre la liste (skills activés de l'utilisateur, recherche par nom) ; `/nom` filtre directement ; `+` lance la création assistée (T14) ; sélectionner un skill l'attache au prochain message (pastille retirable) et le run passe par le Runner (T09).
Acceptation : `/` sans skill activé affiche les intégrés + l'entrée de création ; un skill forcé par `/` n'est jamais remplacé par un autre (le routeur automatique n'existe pas encore, mais le contrat est posé).
**P1-T11 — Invocation dans l'éditeur : sélection, slash, bloc.**
Fichiers : `page_editor_scripts.js` (les trois points d'entrée existent — les rebrancher sur le Runner), registre des types de blocs.
Travail : déclarer `skill_eligible` par type de bloc (liste du document parent §13.4) ; menu de sélection : skills avec `editor_menu` effectif d'abord, intégrés ensuite, entrée *Manage Skills* ; menu de bloc → *Skills* (bloc + contenu imbriqué) ; slash `/skill` uniquement sur ligne non vide ; résultat en **aperçu diff** pour les transformations (accepter/refuser/réessayer), insertion sous le bloc pour les générations.
Acceptation : les trois gestes produisent le même résultat à entrée égale ; un bloc non éligible ou vide n'offre pas de skills.
**P1-T12 — Skills intégrés sur AI Writing.**
Fichiers : `app/services/ai_writing.py`, seed des skills intégrés.
Travail : créer les 6 pages-skills intégrées (`is_builtin=1`, `source='builtin'`, workspace système ou seed par utilisateur — décision : seed **par workspace**, page système non éditable, désactivable par utilisateur via les enablements) : Améliorer l'écriture, Corriger, Expliquer, Reformater, Traduire, Résumer — chacune appelle le service AI Writing existant via le Runner plutôt que de dupliquer les prompts ; l'autocomplétion inline reste sur le service direct (latence).
Acceptation : le menu de sélection d'un nouvel utilisateur affiche exactement les 6 intégrés, dans l'ordre ; désactiver « Corriger » le retire pour cet utilisateur seul.
### Portabilité
**P1-T13 — Galerie rebranchée sur les pages.**
Travail : installer un preset = créer la page-skill correspondante dans la base privée de l'utilisateur (corps = le contenu du preset, `Description` rédigée pour un futur appariement — relire les 17 descriptions pendant ce ticket) + enablement activé. La galerie existante devient une vue de presets-pages ; l'ancien format d'installation est abandonné après migration (T02).
Acceptation : réinstaller les 17 presets sur un compte de test donne 17 pages éditables ; modifier la page modifie le comportement du skill au run suivant.
**P1-T14 — Création assistée par l'Agent.**
Travail : depuis le chat (`/` → `+` ou « crée un skill qui… »), un run guidé de l'Agent personnel pose les questions minimales (travail, entrées, règles, exemples, sortie) puis appelle un outil interne de création de page-skill dans la base privée ; l'utilisateur est amené sur la page créée pour relire.
Acceptation : le parcours complet produit un skill invocable sans quitter le chat autrement que pour la relecture.
**P1-T15 — Export `SKILL.md` et suivi de copie locale.**
Fichiers : `app/services/skill_export.py` (nouveau), route `GET /api/agent/skills/{id}/download`.
Travail : sérialisation Markdown (frontmatter `name`/`description` + corps de page) ; bundle ZIP = `SKILL.md` + fichiers `shareable=1` ; écriture d'une ligne `skill_local_downloads` (target `file` par défaut, cible nommée si choisie) avec l'empreinte = hash du contenu sérialisé + `updated_at` de la page ; l'empreinte courante est recalculée à la lecture pour le badge (T07).
Acceptation : le `SKILL.md` exporté est relu par le test d'import (T16) sans perte de nom/description/corps.
**P1-T16 — Import `SKILL.md` et compatibilité v1.**
Travail : `POST /api/agent/skills/import` accepte un `SKILL.md` (frontmatter analysé) ou un bundle, crée une page-skill (`source='import'`) dans la base privée, range les fichiers joints en propriété `Files` ; le format `flowdeck-skill` v1 reste accepté et produit le même résultat (page + ligne d'index, `tools_json` conservé).
Acceptation : les trois sources (page existante, `SKILL.md` externe, JSON v1) convergent vers le même objet skill.
### Finitions
**P1-T17 — API v2 des skills alignée.**
Travail : `GET /api/v2/skills` et `/{id}` exposent `page_id`, `description`, `collection_id` ; négociation de contenu : `Accept: text/markdown` renvoie la sérialisation `SKILL.md` ; le JSON v1 reste le défaut (compatibilité des clients existants).
Acceptation : les tests API v2 actuels passent inchangés ; un test nouveau couvre la variante Markdown.
**P1-T18 — Documentation in-app et aide.**
Travail : entrée d'aide du produit (section Skills : page, base, Library, Discover, agents locaux), textes de la bannière et des dialogues en français, noms d'objets en anglais conformément au document parent.
Acceptation : revue des libellés (pas de « compétence » imposé dans l'UI là où le produit dit *Skill*).
## 6. API livrée par la phase
| Route | Ticket |
|---|---|
| `POST /api/agent/pages/{id}/skill` · `DELETE …` | T06 |
| `PUT /db/{id}/skills-db` (conversion) | T05 |
| `GET /api/agent/skills/menu` | T10 |
| `PUT /api/agent/skills/{id}/enablement` | T08 |
| `POST /api/agent/skills/{id}/run-chat` · `POST …/run-editor` | T09–T11 |
| `GET /api/agent/skills/{id}/download` | T15 |
| `POST /api/agent/skills/import` | T16 |
| `GET /api/v2/skills` · `GET /api/v2/skills/{id}` (étendus) | T17 |
Toutes les routes internes suivent les conventions existantes : cookie-auth, CSRF complet, contrôle d'ACL dans le routeur, erreurs au format du produit.
## 7. Comportements détaillés
### 7.1 Qui peut faire quoi
| Action | Droit requis |
|---|---|
| Marquer une page | Éditer la page |
| Éditer un skill | Éditer sa page (rien de plus, rien de moins) |
| Activer un skill pour soi | Lire sa page |
| Exécuter un skill | Lire sa page + être activé (ou être son créateur) |
| Convertir une base | Admin/owner de la collection |
| Désactiver le skill d'un autre | Impossible : la désactivation est toujours « pour soi » ; retirer pour tous = supprimer la page ou la démarquer par un éditeur |
### 7.2 Défaut du skill vs choix de l'utilisateur
Deux niveaux, à ne jamais écraser l'un par l'autre : les colonnes `*_default` d'`agent_skills` (posées par le propriétaire) et les surcharges nullable de `user_skill_enablements` (posées par chaque utilisateur). Résolution : surcharge utilisateur si non NULL, sinon défaut du skill. La bannière édite le **défaut** (si l'utilisateur peut éditer la page) ; l'onglet Library édite la **surcharge**.
### 7.3 Contenu éligible d'un bloc de skill
Le corps du skill est la page entière sérialisée, mais l'**entrée** d'un run éditeur respecte l'éligibilité (§13.4 du parent) : types listés seulement, contenu imbriqué inclus, blocs vides ignorés. Le Runner reçoit l'entrée déjà filtrée — il ne connaît pas l'éditeur.
### 7.4 Contrainte d'outils
`outils_du_run = outils_autorisés_par_la_politique ∩ (outils_du_skill si tools_json non vide, sinon tous)`. Un skill ne peut ni ajouter un outil MCP, ni lever une approbation. Test dédié obligatoire (T-19).
### 7.5 Le skill supprimé en cours de route
Si la page part à la corbeille entre l'affichage du menu et le run : le Runner répond par une erreur propre « skill indisponible », le menu se rafraîchit, aucun run fantôme dans `skill_runs`.
## 8. Plan de migration des données existantes
1. **Sauvegarde** : snapshot SQLite par le service de backup existant avant le déploiement (procédure habituelle des migrations risquées).
2. **Inventaire** : compter les skills existants par utilisateur et par type ; photographier les hashes des prompts.
3. **Migration 39** : schéma + T02 en une transaction ; en cas d'échec, rien n'est appliqué (patron des migrations Flowdeck).
4. **Vérification post-migration** (script jetable, pas de code livré) : comptages avant/après, hashes des corps de pages générées = hashes des prompts d'origine, échantillon de 5 skills invoqués en mode offline.
5. **Bascule d'UI** : les anciens écrans de gestion des skills (galerie enregistrements) sont retirés dans le même déploiement ; pas de période de double modèle.
## 9. Tests
*Pyramide existante : pytest (TestClient, base temporaire), Playwright pour les gestes réels. Tous les tests de skill doivent passer en mode LLM `offline`.*
| # | Niveau | Cas |
|---|---|---|
| T-01 | Unitaire | Marquer/démarquer : idempotence, unicité `page_id`, refus sans droit d'édition |
| T-02 | Intégration | Migration 39 sur fixture v7.69.8 : comptages, hashes, skills legacy invocables |
| T-03 | Intégration | Conversion d'une collection : propriétés mappées ou créées, lignes → skills, déconversion propre |
| T-04 | Intégration | Skill de base : modifier la propriété `Description` de la ligne met à jour le cache du skill |
| T-05 | Intégration | Discover/Enable : visibilité par ACL, activation personnelle, désactivation sans effet sur autrui |
| T-06 | Intégration | Runner : ligne `skill_runs` écrite ; nom du skill dans la sortie ; intersection d'outils respectée |
| T-07 | Intégration | Un skill dont `tools_json` tente d'ajouter un outil hors politique : l'outil n'est pas disponible au run |
| T-08 | Intégration | Export `SKILL.md` → import : nom, description, corps et fichiers conservés |
| T-09 | Intégration | Badge de copie périmée : téléchargement, puis édition de la page → badge présent ; re-téléchargement → badge absent |
| T-10 | E2E | Sélectionner un texte → menu Skills → Corriger → diff → accepter : le texte est remplacé, l'historique de page contient la version |
| T-11 | E2E | Chat : `/` → choisir un skill → pastille visible → réponse produite |
| T-12 | E2E | Supprimer la page d'un skill affiché dans un menu ouvert : erreur propre au run, pas d'écran cassé |
| T-13 | Non-régression | Les tests existants des routes skills (cookie et v2) passent ; le format v1 s'importe toujours |
## 10. Critères d'acceptation
- [ ] Sur une installation migrée, l'inventaire des skills avant/après est identique (noms, prompts) et chaque skill est devenu une page éditable.
- [ ] Un playbook existant (page ordinaire) devient un skill invocable en moins de cinq gestes, sans copier-coller.
- [ ] Une équipe partage une base de skills ; un membre la découvre dans *Discover*, l'active pour lui, et l'invoque depuis l'éditeur.
- [ ] Les quatre surfaces d'invocation (chat `/`, sélection, slash, bloc) passent par le même Runner (vérifiable : elles écrivent le même format de `skill_runs`).
- [ ] Un skill exporté en `SKILL.md` se réimporte sans perte ; le badge signale une copie locale périmée après édition de la page.
- [ ] Aucun run de skill ne peut utiliser un outil que la politique ou le skill ne lui donne pas.
- [ ] Le mode LLM `offline` exécute tous les parcours de test de la phase.
## 11. Risques spécifiques et vigilance
| Risque | Vigilance |
|---|---|
| La recréation d'`agent_skills` casse les clés étrangères existantes (références depuis d'autres tables) | Inventaire des FK entrantes **avant** T01 ; la recréation suit le patron SQLite (désactiver les FK pendant la copie, `foreign_key_check` après) |
| Les pages générées par la migration polluent les *Recents* et la recherche des utilisateurs | Marquer les pages migrées avec leur date d'origine ; ne pas toucher `recents` ; la recherche les indexe comme des pages normales (c'est voulu), mais le titre est préfixé du nom du skill d'origine, sans décor |
| Deux bases privées créées en concurrence (double premier skill) | Contrainte d'unicité applicative + création dans la même transaction que le premier skill (T04) |
| Les descriptions des 17 presets, écrites pour des humains, sont trop vagues pour le futur routeur | T13 impose leur relecture dès cette phase (le routeur de la Phase 2 en dépend) |
| Confondre défaut du skill et choix utilisateur dans l'UI | §7.2 + libellés distincts dans la bannière (« pour tous ») et la Library (« pour moi ») |
## 12. Ordre d'exécution
```text
T01 ── T02 ── T03 ──┬── T04 ── T05 ── T06 ── T07
├── T09 ──┬── T10
│ ├── T11
│ └── T12
├── T08
├── T13 ── T14
└── T15 ── T16 ── T17
T18 en fil continu ; tests au fur et à mesure, jamais en bloc final.
```
Le chemin critique est T01 → T03 → T09 : tant que le Runner n'existe pas, aucune surface ne peut être rebranchée.
## 13. Définition de « terminé »
La phase est terminée quand : les critères du §10 passent sur une base migrée depuis la v7.69.8 **et** sur une installation neuve ; la suite de tests existante est verte ; la documentation in-app (T18) est livrée ; le document parent est annoté (version 1.2 : « Phase 1 livrée », avec les écarts éventuels entre plan et réalisation consignés plutôt que tus).
---
*Document de phase 1/5 — voir le document parent pour l'analyse fonctionnelle Notion et l'architecture d'ensemble.*
@@ -0,0 +1,305 @@
# Agents & Skills pour Flowdeck — Phase 2 : L'Agent choisit, l'Agent personnel se personnalise
| Champ | Valeur |
|---|---|
| Phase | **2 sur 5** — intelligence d'usage de l'Agent personnel |
| Document parent | `architecture-agents-skills-notion-flowdeck.md` v1.1 — en particulier §3.2 (l'Agent personnel), §10.2 et §10.5, §12.1–12.4, §13.2–13.3 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | Prêt à implémenter **après la Phase 1** |
| Prérequis | Phase 1 livrée (skills = pages, descriptions indexées, Skill Runner, enablements) |
| Migration créée | **40** |
| Débloque | Phase 3 (les Custom Agents héritent du contexte de run, du routeur et du journal de run), Phase 4 (le journal d'usage s'appuie sur `agent_runs`) |
> **Résultat visible à la fin de cette phase.** L'Agent personnel a une identité et une **page d'instructions** personnelle (« Mon Flowdeck AI »), commutable ; dans le chat, le sélecteur **All sources** choisit ce que l'Agent peut consulter, le sélecteur de modèle propose **Auto** ; l'Agent **applique tout seul un skill pertinent** quand la demande correspond à sa description, le nomme dans sa réponse et le liste dans le panneau des sources ; les conversations s'épinglent et se titrent automatiquement ; et **chaque exécution devient un objet `agent_runs` relisible** — modèle réellement utilisé, sources lues, skills appliqués, étapes, coût en tokens.
---
## Table des matières
1. [Objectif](#1-objectif)
2. [Périmètre](#2-périmètre)
3. [État d'entrée](#3-état-dentrée)
4. [Modèle de données — migration 40](#4-modèle-de-données--migration-40)
5. [Tickets de travail](#5-tickets-de-travail)
6. [API livrée par la phase](#6-api-livrée-par-la-phase)
7. [Comportements détaillés](#7-comportements-détaillés)
8. [Tests](#8-tests)
9. [Critères d'acceptation](#9-critères-dacceptation)
10. [Risques spécifiques et vigilance](#10-risques-spécifiques-et-vigilance)
11. [Ordre d'exécution](#11-ordre-dexécution)
12. [Définition de « terminé »](#12-définition-de--terminé-)
---
## 1. Objectif
Deux manques séparent l'Agent personnel actuel de son modèle Notion, et cette phase les comble :
1. **Il ne choisit rien.** Ni le skill pertinent (l'utilisateur doit le nommer), ni le modèle adapté (le modèle est celui de la configuration), ni ses sources au-delà des mentions. La phase livre le **Skill Router** (déterministe d'abord, arbitrage LLM en zone grise), le **routage de modèle `Auto`** et le sélecteur **All sources** persisté par conversation.
2. **Il n'a pas de mémoire de comportement durable et éditable.** Les instructions vivent dans des champs, pas dans une page que l'utilisateur peut écrire, versionner et commuter comme ses autres documents. La phase livre la **page d'instructions personnelle** et son assemblage dans le prompt du run.
Le troisième livrable est invisible mais structurel : **`agent_runs`**. Sans un objet « run » explicite, ni l'Activity des Custom Agents (Phase 3), ni la comptabilité (Phase 4), ni le débogage des appariements de skills ne sont possibles.
## 2. Périmètre
### 2.1 Inclus
- Migration 40 : `agent_personal_settings`, `agents.instruction_page_id`, `agent_runs`, `agent_actions.run_id`, colonnes de conversation (`pinned`, `title_auto`, `sources_json`).
- Personnalisation de l'Agent personnel : nom, avatar/accessoires, mode d'affichage du chat (barre latérale / flottant).
- Page(s) d'instructions personnelle(s) : création (« Mon Flowdeck AI »), choix d'une page existante, modèles de départ, commutation de la page active ; résolution des instructions dans le prompt du run (ordre du document parent §12.3).
- Modification des instructions « par l'Agent » sur demande de l'utilisateur — en proposition diff, jamais en écriture silencieuse.
- Contexte de run typé (`RunContext`, §12.1 du parent) refactoré dans le moteur : surface, acteur, régime (`user` en cette phase), instructions, sources, modèle demandé, budget.
- Sélecteur **All sources** dans le compositeur : workspace, connecteurs actifs, serveurs MCP ; état persisté par conversation ; bouton `@` existant conservé.
- Sélecteur de modèle dans le panneau : `Auto` + modèles configurés ; affichage du modèle **réellement utilisé** dans la réponse ; signalement des modèles « web seulement » (sources workspace décochées pour le run).
- **Skill Router** : appariement description ↔ demande (recherche hybride existante, seuils, arbitrage LLM optionnel), au plus 2 skills automatiques, jamais d'écartement d'un skill forcé, journal de décision.
- Nomination du skill utilisé dans la réponse et **panneau des sources** du run (pages, connecteurs, MCP, skills).
- Conversations : épinglage, titrage automatique (premier échange), historique cherchable dans l'onglet *Chat*.
- Étapes nommées du run dans le flux SSE (`step`, `skill_used` — extension du protocole existant).
- Actions suggérées contextuelles au-dessus du compositeur (amorces de prompt selon la page courante et les blocs sélectionnés).
### 2.2 Exclu
- Les **crédits** et limites tarifaires (Phase 4) : en cette phase, `agent_runs` comptabilise les **tokens** ; le champ `credits` existe mais reste à 0 et n'est jamais affiché comme un coût.
- Les modèles **autorisés par surface** et l'activation admin des premium (Phase 4) : le sélecteur montre les modèles de la configuration LLM existante.
- Les runs **autonomes** (déclencheurs, régime `allowlist`) → Phase 3. `agent_runs.surface` accepte dès maintenant les valeurs futures, sans les produire.
- Les fichiers de conversation (dépôt, production) → Phase 4. `sources_json` ne contient pas encore d'entrées fichiers.
## 3. État d'entrée
| Élément (v7.69.8 + Phase 1) | Usage dans cette phase |
|---|---|
| Moteur ReAct, flux SSE (`reasoning`, `action`, `notice`, `final`, `error`) | Reçoit le `RunContext` ; émet `step` et `skill_used` en plus |
| Context Builder (snapshot Markdown filtré ACL, mentions résolues) | Devient « v2 » : ajoute page/blocs courants, page d'instructions, sources choisies, skills résolus |
| Mémoire par conversation (`agent_memory`) | Couche 3 de l'assemblage d'instructions (§7.1), inchangée |
| Client LLM 23 fournisseurs + précédence de config en 4 niveaux | Le routeur `Auto` choisit **parmi** les modèles de cette configuration |
| Recherche hybride (FTS5 + cosinus, RRF k=60) | Le Skill Router la réutilise avec `resource_type='skill'` |
| Skills = pages, descriptions indexées, enablements | Entrées du routeur |
| Skill Runner (Phase 1) | Exécute les skills forcés **et** résolus par le routeur (même chemin d'exécution) |
## 4. Modèle de données — migration 40
```sql
-- Migration 40 — Phase 2
CREATE TABLE agent_personal_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
display_name TEXT,
avatar_json TEXT DEFAULT '{}',
instruction_page_ids TEXT DEFAULT '[]',
active_instruction_page_id INTEGER REFERENCES pages(id),
chat_mode TEXT DEFAULT 'sidebar', -- 'sidebar' | 'floating'
default_model TEXT -- NULL = Auto
);
ALTER TABLE agents ADD COLUMN instruction_page_id INTEGER REFERENCES pages(id);
-- (les autres colonnes d'agents — kind, web_access, délégation — arrivent en Phase 3)
CREATE TABLE agent_runs (
id INTEGER PRIMARY KEY,
agent_id INTEGER REFERENCES agents(id), -- NULL en cette phase (Agent personnel)
user_id INTEGER REFERENCES users(id),
conversation_id INTEGER REFERENCES agent_conversations(id),
parent_run_id INTEGER REFERENCES agent_runs(id), -- posé dès maintenant, utilisé en Phase 3
surface TEXT NOT NULL, -- 'chat' | 'editor' (+ valeurs futures admises)
trigger_id INTEGER, -- NULL en cette phase (Phase 3 rattachera agent_triggers)
trigger_payload_json TEXT DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'running',
-- 'running'|'waiting_approval'|'done'|'failed'|'cancelled'|'budget_exceeded'
provider TEXT, model TEXT, -- modèle RÉELLEMENT utilisé
tokens_in INTEGER DEFAULT 0, tokens_out INTEGER DEFAULT 0,
credits REAL DEFAULT 0, -- toujours 0 en cette phase (Phase 4)
skills_json TEXT DEFAULT '[]', -- [{skill_id, name, invocation}]
sources_json TEXT DEFAULT '[]', -- sources réellement lues (panneau des sources)
router_decision_json TEXT DEFAULT '{}', -- candidats, scores, seuils, arbitre (§7.3)
error TEXT,
started_at TEXT NOT NULL, finished_at TEXT
);
ALTER TABLE agent_actions ADD COLUMN run_id INTEGER REFERENCES agent_runs(id);
ALTER TABLE agent_conversations ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
ALTER TABLE agent_conversations ADD COLUMN title_auto INTEGER NOT NULL DEFAULT 0;
ALTER TABLE agent_conversations ADD COLUMN sources_json TEXT DEFAULT '{}';
-- Rattachement de l'existant : créer rétroactivement un agent_runs 'done' par
-- conversation ayant des agent_actions ? NON — décision : les runs commencent
-- à la livraison de la phase ; l'historique ancien reste lisible comme avant.
```
Règles :
1. **Un run par exécution du moteur**, créé avant le premier appel LLM, terminé dans tous les cas (succès, erreur, annulation, budget) — un run qui reste `running` après un redémarrage du processus est soldé en `failed` (« interrompu ») au démarrage par le lifespan.
2. `skill_runs.run_id` (Phase 1) devient renseigné systématiquement ; les lignes de Phase 1 sans run restent valides.
3. `sources_json` du **run** (ce qui a été lu) est distinct de `sources_json` de la **conversation** (ce qui est autorisé à être lu) — la collision de nom est assumée et documentée partout où les deux apparaissent.
## 5. Tickets de travail
### Le run comme objet
**P2-T01 — Migration 40 et modèle de run.**
Fichiers : `app/migrations.py`, nouveau `app/services/agent_runs.py` (création, transitions d'état, solde au démarrage).
Acceptation : un run de chat crée exactement une ligne ; un arrêt du processus en plein run laisse une ligne soldée `failed/interrompu` après redémarrage, jamais un zombie `running`.
**P2-T02 — `RunContext` dans le moteur.**
Fichiers : `app/services/agent_engine.py`, `app/services/context_builder.py` (v2).
Travail : introduire le dataclass du document parent §12.1 ; tous les points d'entrée existants (panneau, éditeur via le Runner, API v2 synchrone) construisent un `RunContext` ; le régime est `user` pour tous en cette phase ; les écritures de journal (actions) portent `run_id`.
Acceptation : les tests existants du moteur passent sans modification de leurs attentes fonctionnelles ; chaque `agent_actions` nouveau a un `run_id` non nul.
**P2-T03 — Étapes nommées en SSE.**
Travail : événements `step` {key, label, status} émis aux frontières réelles (contexte, routage, chaque famille d'outils, génération) et `skill_used` {skill_id, name, invocation} ; le panneau les rend comme une liste cochée ; repli : un client qui ignore ces événements affiche le flux actuel inchangé.
Acceptation : pendant un run long, l'utilisateur voit progresser les étapes ; aucune étape « décorative » (une étape affichée = un travail réel du moteur).
### Personnalisation et instructions
**P2-T04 — Réglages personnels et identité de l'Agent.**
Fichiers : routes `GET/PUT /api/agent/personal-settings`, panneau (en-tête, menu de personnalisation).
Travail : nom d'affichage, avatar (accessoires en `avatar_json`), mode d'affichage persisté, modèle par défaut personnel ; valeurs par défaut = comportement actuel pour les utilisateurs sans ligne de réglages.
Acceptation : la personnalisation d'un utilisateur ne fuit jamais chez un autre (test multi-utilisateurs sur la même instance).
**P2-T05 — Page d'instructions personnelle.**
Fichiers : route `POST /api/agent/personal-settings/instructions-page`, sélecteur de page existant.
Travail : à la première demande, créer la page privée « Mon Flowdeck AI » (gabarit : ton souhaité, ce que l'Agent doit retenir — rôle, habitudes —, pages/canaux à consulter d'abord) ; permettre de choisir une page existante ou un modèle, d'en maintenir plusieurs (`instruction_page_ids`) et de commuter la page active ; rappel dans l'interface : quiconque édite cette page modifie le comportement de l'Agent — pour partager un exemple, dupliquer la page.
Acceptation : modifier la page active change la réponse au run suivant (test en mode offline avec consigne détectable) ; une page active devenue illisible (ACL retirée) est ignorée avec un `notice`, pas un échec.
**P2-T06 — Assemblage des instructions du run.**
Fichiers : `context_builder.py` v2.
Travail : ordre des couches du document parent §12.3 (système produit → instructions de la page active → mémoire → skills résolus → contexte de travail) ; chaque couche journalisée (présente/absente, taille) dans le run ; la mémoire reste désactivable par conversation (comportement actuel conservé).
Acceptation : un test d'assemblage vérifie l'ordre et l'absence de doublon quand la page d'instructions est aussi mentionnée par `@` dans la demande.
**P2-T07 — L'Agent propose de mettre à jour ses instructions.**
Travail : quand l'utilisateur dit « retiens que… », l'Agent propose une modification de la page active rendue en **diff** dans le panneau ; l'application passe par le chemin d'édition ordinaire des pages (donc versionnée) ; refus = rien n'est écrit.
Acceptation : aucune écriture d'instructions sans validation explicite (test négatif : une instruction « retiens » glissée dans un contenu lu par l'Agent ne produit aucune proposition).
### Contexte et modèle
**P2-T08 — Sélecteur All sources.**
Fichiers : compositeur du panneau, `PUT /api/agent/conversations/{id}/sources`.
Travail : popover à cases — Workspace (toujours présent), chaque connecteur actif de l'utilisateur, chaque serveur MCP connecté (rubrique dédiée) ; l'état est stocké dans `agent_conversations.sources_json` et **appliqué** par le Context Builder (une source décochée n'est ni cherchée ni lue) ; les mentions `@` explicites restent possibles mais limitées aux sources cochées (le préciser dans l'interface au moment du choix).
Acceptation : décocher Gmail rend ses résultats absents d'un run qui les aurait eus cochés (test avec connecteur de test).
**P2-T09 — Routage de modèle `Auto`.**
Fichiers : `llm_config.py` (extension), moteur, sélecteur du panneau.
Travail : valeur `auto` acceptée partout où un modèle se choisit ; routeur léger : classification de la demande (longueur, multi-étapes détectées, recherche web requise, présence de fichiers — heuristiques déterministes documentées dans le code) → choix parmi les modèles configurés selon une table de profils (rapide / équilibré / fort) déclarée dans la configuration du workspace ; en mode `offline`, le routeur choisit toujours le profil déterministe de test ; le modèle choisi est écrit dans le run et affiché sous la réponse (« Modèle : X ») ; choix manuel = jamais de routage.
Acceptation : deux demandes types (courte factuelle / longue multi-étapes) choisissent deux profils différents sur la configuration de test ; le modèle affiché est celui du run, pas celui demandé.
**P2-T10 — Modèles « web seulement ».**
Travail : un modèle peut être marqué dans la configuration comme ne recevant pas le contexte workspace ; le sélectionner décoche visuellement les sources workspace/connecteurs pour le run et l'annonce avant l'envoi (pas de surprise après coup).
Acceptation : impossible d'envoyer un run « web seulement » avec une mention `@page` active sans avertissement explicite.
### Le Skill Router
**P2-T11 — Service `skill_router.py`.**
Fichiers : nouveau `app/services/skill_router.py`.
Travail : entrée (texte de la demande + contexte minimal) → candidats par recherche hybride sur `resource_type='skill'` (top 8) restreints aux skills activés de l'utilisateur (enablements de Phase 1) avec `auto_use` effectif vrai et `description` non vide → scores RRF → décision du document parent §13.2 (seuil haut : appliquer ; zone grise : arbitrage ; seuil bas : rien) ; seuils en réglages de workspace avec des défauts prudents ; sortie = liste ordonnée (0 à 2 skills) + décision complète sérialisée.
Acceptation : sur le jeu d'essai du §8 (30 demandes étiquetées), précision et rappel mesurés et consignés ; les seuils par défaut sont ceux qui passent le jeu d'essai, pas des valeurs devinées.
**P2-T12 — Arbitrage LLM en zone grise.**
Travail : quand les scores tombent en zone grise, un appel court au modèle du run (prompt fermé : descriptions des candidats, choix ou « aucun ») tranche ; l'arbitrage est **interdit en mode offline** (la branche déterministe décide seule, par le seuil) ; le résultat et sa justification courte rejoignent `router_decision_json`.
Acceptation : désactiver l'arbitrage (réglage) ramène le routeur à un comportement 100 % déterministe et testable.
**P2-T13 — Intégration du routeur au run et au Runner.**
Travail : le moteur appelle le routeur après l'assemblage du contexte (couche 4) ; les skills retenus sont exécutés par le Skill Runner de la Phase 1 (même chemin que les skills forcés, `invocation='auto'`) ; un skill **forcé** (`/nom`, menu) court-circuite le routeur ; les intégrés ne sont jamais choisis automatiquement (sauf Traduire/Résumer si leur description correspond — décision explicite : non en cette phase, ils restent manuels) ; la réponse nomme le skill utilisé, le panneau des sources le liste (T14).
Acceptation : UC-03 du document parent passe de bout en bout ; un mauvais appariement est explicable depuis `router_decision_json` seul.
### Conversations et panneau
**P2-T14 — Panneau des sources du run.**
Travail : panneau latéral du chat listant, pour le dernier run ou un run sélectionné : pages lues, lignes de bases, connecteurs interrogés, serveurs MCP appelés, skills appliqués (nom + lien vers la page du skill) ; données lues depuis `agent_runs.sources_json` + `skills_json` alimentés par le Context Builder et le registre d'outils (chaque lecture d'outil s'y déclare).
Acceptation : chaque source affichée a réellement été lue pendant le run (test : une source simplement disponible mais non lue n'apparaît pas).
**P2-T15 — Épinglage, titrage, historique.**
Travail : `PATCH /api/agent/conversations/{id}` (épingler, renommer) ; titre automatique après le premier échange complet (génération par le service de titrage léger — même patron que le titre des notes de réunion : un appel borné ou, en offline, une troncature propre de la demande) ; liste des conversations dans l'onglet *Chat* : épinglées en tête, recherche par titre ; un titre renommé à la main n'est plus jamais écrasé (`title_auto` repéré).
Acceptation : renommer puis continuer la conversation conserve le titre manuel.
**P2-T16 — Actions suggérées contextuelles.**
Travail : rangée au-dessus du compositeur, calculée depuis la page courante (type de page, blocs sélectionnés) : 2 à 4 amorces (extraire les actions, raccourcir, résumer la sélection…) ; cliquer pré-remplit le compositeur — ne lance **pas** le run.
Acceptation : aucune suggestion ne déclenche d'appel LLM à l'affichage.
## 6. API livrée par la phase
| Route | Ticket |
|---|---|
| `GET/PUT /api/agent/personal-settings` | T04 |
| `POST /api/agent/personal-settings/instructions-page` | T05 |
| `PATCH /api/agent/conversations/{id}` | T15 |
| `PUT /api/agent/conversations/{id}/sources` | T08 |
| `GET /api/agent/conversations/{id}/runs` · `GET /api/agent/runs/{id}` | T01/T14 |
| (SSE) événements `step`, `skill_used` | T03 |
## 7. Comportements détaillés
### 7.1 L'ordre des couches d'instructions (rappel normatif)
Système produit → page d'instructions active → mémoire de conversation → skills du run → contexte de travail. En cas de contradiction entre la page d'instructions et une consigne du skill : le **skill gagne pour sa tâche** (il est plus spécifique et plus récent dans le prompt) ; en cas de contradiction avec le système produit : le système gagne toujours. Ces deux règles sont écrites dans les tests d'assemblage (T06).
### 7.2 Ce que « Auto » ne doit jamais faire
Ne jamais choisir un modèle non configuré ; ne jamais changer de modèle **en cours de run** ; ne jamais choisir un modèle premium payant à l'insu de l'utilisateur (les premium n'existent pas encore comme catégorie — Phase 4 — mais dès cette phase, si la configuration distingue des modèles « coûteux », `Auto` les évite sauf tâche classée longue/forte, et l'affiche).
### 7.3 Le journal de décision du routeur
`router_decision_json` contient : les candidats (skill, score), les seuils en vigueur, la branche prise (`forced` / `high` / `grey-llm` / `grey-deterministic` / `none`), l'arbitre éventuel et sa réponse brute tronquée. C'est un outil de débogage produit, pas une télémétrie : il est visible depuis le panneau des sources (section repliable « Pourquoi ce skill ? »).
### 7.4 Un skill automatique peut être refusé après coup
Sous chaque réponse ayant utilisé un skill automatique : action « Ce skill n'était pas pertinent » → enregistre un contre-exemple (demande + skill écarté) dans le journal, et propose de désactiver `Use automatically` pour ce skill. Les contre-exemples alimentent le jeu d'essai (§8) — c'est la matière de l'optimisation de la Phase 5.
## 8. Tests
| # | Niveau | Cas |
|---|---|---|
| T-01 | Intégration | Run complet → une ligne `agent_runs` correcte (statut, modèle réel, tokens > 0 en mode test, skills) |
| T-02 | Intégration | Processus tué en plein run (simulation) → run soldé au redémarrage |
| T-03 | Unitaire | Assemblage d'instructions : ordre des couches, déduplication, page illisible ignorée avec notice |
| T-04 | Intégration | Page d'instructions modifiée → comportement du run suivant modifié (mode offline, consigne détectable) |
| T-05 | Intégration | Proposition de mise à jour des instructions : refus = aucune écriture ; acceptation = nouvelle version de page |
| T-06 | Intégration | All sources : source décochée absente des lectures du run |
| T-07 | Unitaire | Routeur `Auto` : profils choisis sur demandes types ; jamais de changement en cours de run |
| T-08 | Unitaire | Skill Router sur jeu d'essai de 30 demandes (10 doivent matcher un skill précis, 10 aucun, 10 zone grise) : seuils validés, décision journalisée complète |
| T-09 | Intégration | Skill forcé `/nom` : le routeur ne s'exécute pas, `invocation='manual'` dans `skill_runs` et `skills_json` |
| T-10 | Intégration | Skill automatique : réponse nomme le skill ; panneau des sources le liste ; `skill_runs.invocation='auto'` |
| T-11 | Intégration | Contre-exemple « pas pertinent » enregistré et retrouvable |
| T-12 | E2E | Épingler une conversation, la renommer, la retrouver par la recherche de l'onglet Chat |
| T-13 | E2E | Changer la page d'instructions active depuis la personnalisation ; le changement est visible au run suivant |
| T-14 | Non-régression | Tous les tests du moteur, du panneau et de l'API v2 agent de la v7.69.8 + Phase 1 passent |
**Jeu d'essai du routeur (à constituer dans T11, livré comme fixture)** : 30 demandes réelles rédigées à partir des descriptions des 17 presets migrés et des 6 intégrés, étiquetées par un humain. C'est un actif permanent du projet, pas un artefact de phase.
## 9. Critères d'acceptation
- [ ] UC-01 et UC-03 du document parent passent de bout en bout (tâche multi-sources ; skill automatique nommé et sourcé).
- [ ] Un utilisateur personnalise son Agent (nom, page d'instructions) et constate le changement de comportement au run suivant.
- [ ] Chaque run de la phase est relisible : modèle réel, sources lues, skills et décision du routeur sont dans `agent_runs` et affichables.
- [ ] Le routeur est entièrement déterministe en mode offline et passe le jeu d'essai aux seuils livrés.
- [ ] Aucun skill automatique ne peut être un skill non activé pour l'utilisateur, sans description, ou un intégré (en cette phase).
- [ ] Le panneau des sources ne montre que des sources réellement lues.
## 10. Risques spécifiques et vigilance
| Risque | Vigilance |
|---|---|
| Le routeur applique un mauvais skill en silence et dégrade la confiance | Skill toujours nommé, bouton « pas pertinent », seuils prudents, arbitrage journalisé ; communiquer le comportement dans l'aide in-app |
| La page d'instructions devient un fourre-tout qui contredit les skills | Règle de précédence du §7.1 ; gabarit de page qui sépare comportement général et notes ; l'assemblage tronque la page à un budget explicite et le signale |
| `Auto` choisit un modèle lent/cher pour des demandes triviales | Profils testés (T-07) ; le modèle réel est affiché, donc le comportement est observable et corrigeable |
| `agent_runs` grossit vite (un run par message) | Index sur `conversation_id` et `started_at` ; la rétention et l'archivage sont traités en Phase 5 — mais dès cette phase, ne stocker aucun contenu volumineux dans le run (les sources sont des références, pas des copies) |
| Collision de noms `sources_json` (conversation vs run) | §4 règle 3 ; nommer les champs différemment dans les API publiques (`allowed_sources` pour la conversation, `sources` pour le run) |
## 11. Ordre d'exécution
```text
T01 ── T02 ──┬── T03
├── T06 (après T05)
├── T08
└── T09 ── T10
T04 ── T05 ── T07
T11 ── T12 ── T13 ── T14
T15, T16 en parallèle, après T02.
```
Chemin critique : T01 → T02 → T13 (le routeur intégré au moteur) — T11 peut démarrer dès la Phase 1 livrée, en service isolé.
## 12. Définition de « terminé »
Critères du §9 verts ; jeu d'essai du routeur versionné dans les tests ; aide in-app mise à jour (personnalisation, All sources, Auto, skills automatiques) ; document parent annoté en 1.2 (« Phase 2 livrée ») avec les seuils du routeur réellement retenus.
---
*Document de phase 2/5 — dépend de la Phase 1 ; prépare les Phases 3 et 4.*
@@ -0,0 +1,314 @@
# Agents & Skills pour Flowdeck — Phase 3 : Les Custom Agents deviennent autonomes
| Champ | Valeur |
|---|---|
| Phase | **3 sur 5** — le passage de l'assistant à l'équipe d'agents |
| Document parent | `architecture-agents-skills-notion-flowdeck.md` v1.1 — en particulier §3.3 (les Custom Agents), §10.2 et §10.5, §12.1, §12.6–12.8, §16.1–16.2 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | Prêt à implémenter **après les Phases 1 et 2** |
| Prérequis | Phase 1 (skills = pages), Phase 2 (`agent_runs`, `RunContext`, routeur) |
| Migration créée | **41** |
| Débloque | Phase 4 (les déclencheurs de messagerie et les écritures externes s'appuient sur ce dispatcher et ce régime de permissions) |
> **Résultat visible à la fin de cette phase.** Chaque Custom Agent a sa **page en trois onglets — Chat, Activity, Settings** ; il se déclenche sur **horaire, événements du workspace** (ligne ajoutée/modifiée/retirée, commentaire, note de réunion terminée) **et mentions** `[[fdagent:…]]`, avec filtres ; il n'accède qu'aux **ressources explicitement accordées** (régime `allowlist`) ; il se **partage en trois niveaux**, se **duplique** selon des règles explicites, son historique de configuration se **restaure** ; il peut **déléguer** à d'autres agents ; il se crée aussi en **décrivant le travail** à l'IA, qui propose un brouillon à relire.
---
## Table des matières
1. [Objectif](#1-objectif)
2. [Périmètre](#2-périmètre)
3. [État d'entrée](#3-état-dentrée)
4. [Modèle de données — migration 41](#4-modèle-de-données--migration-41)
5. [Tickets de travail](#5-tickets-de-travail)
6. [API livrée par la phase](#6-api-livrée-par-la-phase)
7. [Comportements détaillés](#7-comportements-détaillés)
8. [Tests](#8-tests)
9. [Critères d'acceptation](#9-critères-dacceptation)
10. [Risques spécifiques et vigilance](#10-risques-spécifiques-et-vigilance)
11. [Ordre d'exécution](#11-ordre-dexécution)
12. [Définition de « terminé »](#12-définition-de--terminé-)
---
## 1. Objectif
Flowdeck a déjà des « agents personnalisés » (instructions, outils autorisés, déclencheurs planifiés, trigger API). Ce qui leur manque pour être des Custom Agents au sens de Notion tient en quatre blocs, qui forment cette phase :
1. **Un cycle de vie complet** : page dédiée en 3 onglets, partage gradué, versions de configuration, duplication, intégration dans les pages, modèles de galerie, création assistée.
2. **Des déclencheurs événementiels** branchés sur le bus `fire_event()` des automatisations — l'infrastructure existe, le câblage est le travail.
3. **Le régime de permissions `allowlist`** : en autonome, l'agent ne voit que ses accès accordés, jusqu'aux résultats de recherche. C'est le ticket de sécurité central de toute la solution.
4. **La délégation** entre agents, comme outil borné et comptabilisé.
## 2. Périmètre
### 2.1 Inclus
- Migration 41 : `agents.kind/web_access/credit_limit_monthly/delegation_json`, `agent_permissions`, `agent_versions`, extension événementielle d'`agent_triggers`.
- Page agent SSR `/agents/{id}` : onglets Chat / Activity / Settings (formulaire complet : Instructions par page liée, Triggers, Tools and access, Modèle, Avancé).
- Régime `allowlist` appliqué dans le Context Builder et le Tool Registry pour les runs autonomes (filtre des ressources, y compris résultats de recherche et d'index sémantique).
- Dispatcher de déclencheurs : abonnement unique à `fire_event()`, correspondance type/ressource/filtres, déduplication, file bornée, plafonds, marque d'acteur anti-boucle.
- Déclencheurs livrés : horaire (existant, migré), `collection.row_added`, `collection.property_updated`, `collection.row_removed`, `page.comment_added`, `meeting.summarized`, mention d'agent. Filtres : propriété (opérateurs des automatisations), vue de base, mots-clés.
- Mentions d'agents `[[fdagent:ID]]` dans les pages, propriétés de base et commentaires ; sélecteur de mention étendu.
- Partage à 3 niveaux (`full` / `edit` / `interact`) pour personnes et groupes ; section *Agents* de la sidebar alimentée par les partages ; recherche incluant les agents accessibles.
- Historique de configuration (`agent_versions`) : snapshot à chaque sauvegarde des Settings, restauration.
- Duplication avec les règles de reprise du document parent §3.3.
- Bloc `agent_embed` : intégrer le chat d'un agent dans une page, sans transfert d'accès ni lecture implicite de la page hôte.
- Délégation : outil `delegate_to_agent`, runs enfants (`parent_run_id`), plafonds de profondeur et de budget (en tokens à ce stade).
- Création assistée : description → brouillon {instructions, déclencheurs, accès proposés} → relecture → enregistrement inactif par défaut.
- Galerie de modèles de Custom Agents (les 6 modèles de l'Annexe C du parent).
- Onglet Activity complet : filtres, détail d'un run (étapes, outils, sources, skills, erreurs, coût en tokens), *Re-run* avec la même entrée.
- Onglet Insights **minimal** : compteurs de runs (statut, période), modèles utilisés, tokens — l'export CSV et les vues avancées sont en Phase 5.
### 2.2 Exclu
- Déclencheurs et actions de **messagerie externe** (message/réaction/mention dans Slack, Discord, Telegram, Teams), courriel et calendrier → Phase 4. Le dispatcher accepte dès maintenant tout `event_name` : la Phase 4 n'ajoutera que des émetteurs et des types de ressources.
- Écritures externes et leurs approbations fines → Phase 4 (le régime d'approbation existant couvre les écritures internes dès cette phase).
- Budgets en **crédits** → Phase 4 ; `credit_limit_monthly` est créé et affiché, son application tarifaire attend le journal d'usage (le plafond **tokens** du moteur, existant, borne déjà les runs).
- Restriction « qui peut créer des agents » au niveau workspace (`who_can_create_agents`) → créée en données en Phase 4 avec les réglages IA du workspace ; en cette phase, la création suit les droits actuels des agents.
## 3. État d'entrée
| Élément | État |
|---|---|
| `agents` (instructions, `scope_json`, `approval_mode`, modèle), conversations, triggers horaires + scheduler 60 s | Existants (v7.69.8) |
| Bus d'événements des automatisations : `fire_event()`, événements `page.*`, `collection.*`, `form.submitted`, `meeting.summarized` ; action `agent_trigger` | Existant (§19.1 de l'architecture) — le dispatcher s'y abonne, il ne le remplace pas |
| `agent_policies` / `agent_approvals`, gates d'outils, HTTP 428 sur le destructif | Existants — étendus au régime `allowlist` |
| `agent_runs`, `RunContext`, Skill Router, panneau des sources | Livrés en Phase 2 |
| Skills = pages + accès par ACL de page | Livrés en Phase 1 — un skill « accordé » à un agent = sa page dans ses accès |
| Jetons wiki `[[fdpage:ID]]`, sélecteur de mentions | Existants — étendus à `[[fdagent:ID]]` |
## 4. Modèle de données — migration 41
```sql
-- Migration 41 — Phase 3
ALTER TABLE agents ADD COLUMN kind TEXT NOT NULL DEFAULT 'custom';
-- toutes les lignes existantes sont des Custom Agents ; l'Agent personnel
-- n'est pas une ligne d'agents (Phase 2).
ALTER TABLE agents ADD COLUMN web_access INTEGER NOT NULL DEFAULT 0;
ALTER TABLE agents ADD COLUMN credit_limit_monthly INTEGER; -- appliqué en Phase 4
ALTER TABLE agents ADD COLUMN delegation_json TEXT DEFAULT '[]'; -- agent_ids délégables
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
-- 'active' | 'suspended' (suspension = aucun nouveau run autonome)
CREATE TABLE agent_permissions (
id INTEGER PRIMARY KEY,
agent_id INTEGER NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id),
group_id INTEGER REFERENCES user_groups(id),
level TEXT NOT NULL CHECK (level IN ('full','edit','interact')),
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
CHECK ((user_id IS NULL) <> (group_id IS NULL))
);
CREATE TABLE agent_versions (
id INTEGER PRIMARY KEY,
agent_id INTEGER NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
snapshot_json TEXT NOT NULL,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
note TEXT
);
ALTER TABLE agent_triggers ADD COLUMN trigger_kind TEXT NOT NULL DEFAULT 'schedule';
ALTER TABLE agent_triggers ADD COLUMN event_name TEXT;
ALTER TABLE agent_triggers ADD COLUMN resource_type TEXT;
ALTER TABLE agent_triggers ADD COLUMN resource_id TEXT;
ALTER TABLE agent_triggers ADD COLUMN filter_json TEXT DEFAULT '{}';
ALTER TABLE agent_triggers ADD COLUMN show_typing INTEGER DEFAULT 1; -- utilisé en Phase 4
ALTER TABLE agent_runs ADD COLUMN trigger_id INTEGER REFERENCES agent_triggers(id);
-- Les triggers horaires existants migrent tels quels (trigger_kind='schedule').
-- agent_runs.agent_id (Phase 2, nullable) reçoit désormais les Custom Agents.
```
Le snapshot d'`agent_versions` contient : instructions (texte **et** `instruction_page_id`), modèle et politique d'approbation, `scope_json`, accès (liste des ressources accordées, §7.2), déclencheurs (copie complète), `web_access`, `delegation_json`. Les accès accordés vivent dans une table de liaison simple créée ici aussi :
```sql
CREATE TABLE agent_access (
agent_id INTEGER NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
resource_type TEXT NOT NULL, -- 'page' | 'collection' | 'skill' | 'agent' | 'workspace_shared'
resource_id TEXT NOT NULL, -- id, ou '*' pour workspace_shared
access TEXT NOT NULL DEFAULT 'read', -- 'read' | 'edit'
PRIMARY KEY (agent_id, resource_type, resource_id)
);
```
## 5. Tickets de travail
### Régime de permissions (à faire en premier — tout le reste en dépend)
**P3-T01 — Migration 41 et modèle d'accès.**
Fichiers : `app/migrations.py`, nouveau `app/services/agent_access.py`.
Travail : schéma du §4 ; service de résolution : `can_access(agent, resource, mode)` (lecture/écriture) combinant `agent_access`, l'ACL du propriétaire (`PermissionManager`) et les politiques du workspace — l'ordre d'évaluation est celui du document parent §16.1, et le service rend aussi le **motif** d'un refus (journalisable).
Acceptation : la matrice du §7.1 est couverte par des tests unitaires exhaustifs (chaque cellule).
**P3-T02 — Le filtre `allowlist` dans le contexte et les outils.**
Fichiers : `context_builder.py`, `tool_registry.py`, `semantic_search.py` (point de filtrage).
Travail : quand `RunContext.permission_regime == 'allowlist'` : le Context Builder ne charge que les ressources accordées ; chaque outil de lecture vérifie la ressource cible ; la recherche (FTS et sémantique) reçoit un filtre de ressources **avant** le classement des résultats — pas un masquage après coup (un titre hors accès ne doit pas transiter par le prompt ni par les journaux visibles) ; les outils d'écriture exigent `access='edit'` sur la cible en plus des gates existants.
Acceptation : test de fuite dédié — un agent sans accès à une page secrète ne peut ni la lire, ni la trouver par recherche, ni apprendre son titre par un résultat, ni l'atteindre par une relation depuis une page accordée (les relations sortantes vers du non-accordé sont tronquées et signalées dans le run, pas suivies).
### Page agent et cycle de vie
**P3-T03 — Page agent et onglet Settings.**
Fichiers : nouveau routeur SSR `/agents/{id}`, gabarits, `PUT /api/agent/agents/{id}/settings`.
Travail : les trois onglets ; Settings en sections (Instructions — sélecteur de page liée + aperçu du cache texte ; Triggers — cartes par déclencheur avec filtres et, pour les horaires, l'aperçu du prochain passage ; Tools and access — sélecteurs de pages/collections avec niveau read/edit, skills accordés, agents délégables, interrupteur web ; Modèle ; Avancé — politique d'approbation, plafond d'itérations) ; chaque sauvegarde écrit `agent_versions` et une ligne d'audit.
Acceptation : un agent se configure entièrement depuis cette page, sans passer par les anciens écrans ; l'ancien CRUD d'agents du panneau redirige vers elle.
**P3-T04 — Onglet Chat et onglet Activity.**
Travail : Chat = le panneau de conversation lié à l'agent (composants de la Phase 2, identité de l'agent) ; Activity = table des `agent_runs` de l'agent (déclencheur libellé, statut, durée, modèle, tokens, skills), détail dépliable (étapes, lectures/écritures avec liens d'annulation existants, erreur en clair), filtres statut/déclencheur/période, bouton *Re-run* créant un run lié (`trigger_payload_json.rerun_of`).
Acceptation : un run en échec se débugue et se relance depuis Activity sans quitter la page.
**P3-T05 — Partage à trois niveaux.**
Fichiers : `agent_permissions`, dialogue de partage (patron des partages existants), sidebar, recherche.
Travail : appliquer la matrice du §7.4 ; la section *Agents* de la sidebar liste les agents accessibles triés par niveau ; la recherche globale inclut les agents (résultat typé, ouvrant la page agent) ; un utilisateur sans aucun accès ne voit pas l'agent, y compris dans les listes de délégation des autres agents.
Acceptation : tests par niveau (un `interact` ne peut ni modifier les Settings ni voir Activity au-delà de ses propres runs — décision : Activity est réservé à `full`, les runs **manuels** d'un `interact` lui restent visibles dans son Chat).
**P3-T06 — Versions, restauration, duplication.**
Travail : liste des versions (auteur, date, note auto-générée résumant les sections changées) ; restauration = nouvelle version créée depuis le snapshot choisi (on ne réécrit pas l'histoire) ; duplication selon les règles du parent §3.3 : copie **privée**, nom suffixé, modèle et instructions repris (page d'instructions **copiée**), ressources et déclencheurs filtrés par les accès **du duplicateur** (les ressources inaccessibles sont exclues et listées dans un rapport de duplication affiché), connexions d'outils, limites et historique **non** repris.
Acceptation : le rapport de duplication d'un agent contenant une ressource inaccessible énumère exactement ce qui a été écarté.
**P3-T07 — Bloc `agent_embed` et mentions d'agents.**
Fichiers : registre des types de blocs, rendu SSR du bloc, sélecteur de mentions de l'éditeur et des commentaires, tokens.
Travail : coller le lien d'un agent dans une page propose *Embed* → bloc `agent_embed` {agent_id} rendant le chat de l'agent (composant du panneau, mode embarqué) ; états explicites : pas d'accès à l'agent / agent suspendu / agent supprimé ; le bloc ne donne au modèle **aucune** lecture de la page hôte ; jeton `[[fdagent:ID]]` résolu au rendu (nom de l'agent), reconnu dans les pages, les propriétés texte des bases et les commentaires ; la sauvegarde d'un contenu contenant le jeton émet l'événement de mention (T09).
Acceptation : un lecteur sans accès à l'agent voit l'état « pas d'accès », jamais le chat ; l'agent embarqué ne peut pas répondre sur la page hôte sans y avoir été accordé.
### Déclencheurs
**P3-T08 — Dispatcher de déclencheurs.**
Fichiers : nouveau `app/services/agent_triggers_dispatch.py`.
Travail : abonnement unique à `fire_event()` ; correspondance (event_name, resource, filtres avec les opérateurs des automatisations) ; création du run via le même chemin que les runs manuels (mais régime `allowlist` et conversation journalisée dédiée à l'agent) ; déduplication par empreinte (trigger + identifiant d'événement, fenêtre glissante) ; file d'attente par workspace avec plafond de concurrence partagé avec les runs interactifs (priorité à l'interactif) ; plafond horaire par agent (réglage, défaut documenté) → au plafond, les événements suivants produisent des runs `budget_exceeded` visibles, pas des pertes silencieuses ; suspension d'agent (`status='suspended'`) = dispatcher muet pour cet agent.
Acceptation : déclencher 50 fois le même événement en rafale produit le nombre de runs attendu après déduplication, tous visibles dans Activity.
**P3-T09 — Les déclencheurs du workspace.**
Travail : brancher les émetteurs : lignes de collections (ajout/mise à jour de propriété/retrait — émis par les services de collections aux mêmes points que les événements d'automations), commentaires de page, `meeting.summarized` (déjà émis), mentions (T07) ; filtres propriété/vue/mots-clés évalués contre le payload ; l'acteur d'une écriture faite par un run autonome est marqué `agent:<id>` et, **par défaut**, les événements dont l'acteur est l'agent lui-même ne redéclenchent pas cet agent (anti-boucle), sauf si le déclencheur l'autorise explicitement (case « se redéclencher », déconseillée et signalée).
Acceptation : UC-06 de bout en bout ; le scénario de boucle (agent qui écrit dans la base qu'il surveille) s'arrête de lui-même et le signale.
**P3-T10 — Déclencheurs horaires : continuité.**
Travail : les horaires existants basculent sur le dispatcher (même file, mêmes plafonds) sans changer leur sémantique ; l'aperçu du prochain passage est calculé par le même code que le scheduler.
Acceptation : un agent planifié de la v7.69.8 continue de tourner après migration, à la même heure, avec ses runs désormais dans Activity.
### Délégation et création
**P3-T11 — Outil `delegate_to_agent`.**
Fichiers : registre d'outils, nouveau `app/services/agent_delegation.py`.
Travail : visibilité conditionnée à `delegation_json` non vide ; le run enfant est créé avec le régime et les accès **de l'enfant**, le contexte transmis = tâche rédigée + artefacts attachés explicitement ; profondeur max 3 par défaut (plafonnée à 5), budget tokens prélevé sur l'enveloppe du parent ; le résultat de l'enfant (réponse + statut) revient comme résultat d'outil ; échec de l'enfant = résultat d'échec motivé, le parent décide.
Acceptation : UC-07 ; un enfant ne peut pas lire une ressource du parent qui ne lui est pas accordée (test de fuite croisée) ; une délégation circulaire A→B→A est stoppée par la profondeur et journalisée.
**P3-T12 — Création assistée par l'IA et galerie d'agents.**
Travail : `/agents/new` en trois cartes (document parent §7.3) ; la création assistée est un run spécial de l'Agent personnel dont la sortie structurée est un brouillon {nom, instructions proposées (texte), déclencheurs proposés, accès proposés (ressources retrouvées par recherche, à cocher)} présenté en écran de relecture — **rien n'est enregistré ni activé avant validation**, et l'agent créé naît sans déclencheur actif ; la galerie propose les 6 modèles de l'Annexe C du parent, chacun étant un brouillon pré-rempli passant par le même écran de relecture.
Acceptation : décrire « trie les tickets de la base X » produit un brouillon dont les accès proposés sont exactement la base X (ou rien si X est introuvable/illisible — jamais un accès deviné).
**P3-T13 — Insights minimal et gouvernance de création.**
Travail : onglet Insights de la page agent : runs par statut et par période, modèles utilisés, tokens consommés, taux d'échec ; visible au niveau `full` ; la création d'agents continue de suivre les droits existants (la restriction workspace arrive en Phase 4, la donnée est préparée : ne pas coder de second mécanisme).
Acceptation : les compteurs d'Insights recoupent exactement `agent_runs` sur la même période.
## 6. API livrée par la phase
| Route | Ticket |
|---|---|
| `GET /agents/{id}` (SSR, 3 onglets) | T03/T04 |
| `PUT /api/agent/agents/{id}/settings` | T03 |
| `GET /api/agent/agents/{id}/activity` | T04 |
| `POST /api/agent/agents/{id}/runs/{run_id}/rerun` | T04 |
| `POST /api/agent/agents/{id}/duplicate` (+ rapport) | T06 |
| `GET /api/agent/agents/{id}/versions` · `POST …/versions/{vid}/restore` | T06 |
| `PUT /api/agent/agents/{id}/permissions` | T05 |
| `POST /api/agent/agents/generate` (brouillon) | T12 |
| `GET /api/agent/agents/{id}/insights` | T13 |
| `POST /api/v2/agents/{id}/trigger` (existant, renvoie `run_id`) | T08 |
## 7. Comportements détaillés
### 7.1 Matrice des régimes (normatif, extrait du parent §16.1)
| Contrôle | Run manuel (Chat, trigger API par un humain) | Run autonome (déclencheur, horaire, mention) |
|---|---|---|
| Identité | L'utilisateur | L'agent |
| Lecture | ACL de l'utilisateur | `agent_access` ∩ ACL du propriétaire |
| Écriture | ACL utilisateur + gates existants | Idem + `agent_access` en `edit` + politique d'approbation de l'agent |
| Recherche | Filtrée ACL utilisateur | Filtrée ACL **et** accès accordés, avant classement |
| Skills utilisables | Ceux de l'utilisateur | Uniquement les skills accordés à l'agent |
| Délégation | Vers les agents délégables de l'agent utilisé | Idem, profondeur comptée depuis le run racine |
### 7.2 Ce qu'« accorder » veut dire
`agent_access` est la **seule** source des accès d'un agent autonome. Lier une page dans les instructions n'accorde rien (les liens d'instructions vers du non-accordé rendent un jeton « sans accès » au moment de l'assemblage du contexte, comme dans la duplication). La ligne spéciale `workspace_shared` donne accès à ce qui est partagé avec tout le workspace **au moment du run** (évalué dynamiquement, pas photographié).
### 7.3 Le run autonome a une conversation
Chaque agent a une conversation journalisée permanente pour ses runs autonomes (une par agent, ou une par déclencheur si le créateur préfère — défaut : une par agent) : c'est là que *Re-run*, les approbations en attente et le débogage trouvent leur matière. Elle n'est visible qu'aux niveaux `full`/`edit`.
### 7.4 Matrice de partage
| Action | `full` | `edit` | `interact` | aucun accès |
|---|---|---|---|---|
| Discuter / lancer un run manuel | ✓ | ✓ | ✓ | ✗ |
| Voir les Settings (lecture) | ✓ | ✓ | ✓ | ✗ |
| Modifier les Settings | ✓ | ✓ | ✗ | ✗ |
| Partager / changer les niveaux | ✓ | ✗ | ✗ | ✗ |
| Activity complète / Insights | ✓ | ✗ | (ses runs manuels) | ✗ |
| Dupliquer | ✓ | ✓ | ✓ | ✗ |
| Supprimer / suspendre | ✓ | ✗ | ✗ | ✗ |
Exception héritée du modèle Notion : un utilisateur sans accès peut **déclencher indirectement** un agent (par exemple en ajoutant une ligne à une base surveillée) et voir ses sorties là où elles sont publiées — sans jamais voir l'agent lui-même.
### 7.5 Approbations en autonome
Le régime existant s'applique tel quel : une écriture couverte par `require_approval` suspend le run (`waiting_approval`, webhook) ; les approbations se décident depuis Activity ; expiration du run en attente selon le délai configuré (défaut proposé au parent : 72 h).
## 8. Tests
| # | Niveau | Cas |
|---|---|---|
| T-01 | Unitaire | Matrice complète de `can_access` (régimes × niveaux × modes) |
| T-02 | Intégration | **Fuite** : recherche, lecture directe, relation, mention — aucune voie ne laisse passer une ressource non accordée (le test central de la phase) |
| T-03 | Intégration | UC-06 : ligne ajoutée avec filtre propriété → un run, bonnes lectures, écriture dans le périmètre accordé |
| T-04 | Intégration | Filtre non satisfait → aucun run ; événement dupliqué → un seul run |
| T-05 | Intégration | Anti-boucle : l'agent écrit dans la base surveillée → pas de second run ; case « se redéclencher » → run fils compté et plafonné |
| T-06 | Intégration | Plafond horaire atteint → runs `budget_exceeded` visibles, aucun événement perdu silencieusement |
| T-07 | Intégration | `meeting.summarized` déclenche l'agent de suivi avec le résumé en payload |
| T-08 | Intégration | Partage : chaque ligne de la matrice §7.4 testée par niveau |
| T-09 | Intégration | Restauration de version : la configuration revient, l'historique garde la trace de la restauration |
| T-10 | Intégration | Duplication avec ressource inaccessible : rapport exact, copie privée, aucun accès fantôme |
| T-11 | Intégration | Délégation : périmètre de l'enfant respecté, profondeur plafonnée, coût agrégé au parent |
| T-12 | Intégration | Création assistée : brouillon non enregistré avant validation ; accès jamais devinés |
| T-13 | E2E | Configurer un agent de zéro (Settings), le tester dans Chat, l'activer sur déclencheur, lire son run dans Activity |
| T-14 | E2E | Embed dans une page : chat fonctionnel pour un utilisateur partagé, état « pas d'accès » pour un autre |
| T-15 | Non-régression | Les déclencheurs horaires et le trigger API v2 existants fonctionnent après bascule sur le dispatcher |
## 9. Critères d'acceptation
- [ ] UC-05, UC-06 et UC-07 passent de bout en bout.
- [ ] Le test de fuite (T-02) est vert sur les quatre voies d'accès à une ressource.
- [ ] Un agent événementiel tourne une semaine en autonomie sur l'instance de test avec un Activity propre (aucun run fantôme, aucune boucle).
- [ ] Un agent se partage, se duplique et se restaure conformément aux matrices du §7.
- [ ] Aucun accès n'est accordé par un lien d'instructions, une mention ou un embed — seulement par `agent_access`.
## 10. Risques spécifiques et vigilance
| Risque | Vigilance |
|---|---|
| Le filtre `allowlist` appliqué **après** classement laisse fuiter titres et extraits | T02 teste les voies une à une ; le filtrage se fait dans la requête d'index, pas dans le rendu |
| Le dispatcher double les automatisations existantes (une automation `agent_trigger` + un déclencheur d'agent sur le même événement = deux runs) | Comportement documenté et détecté : les Settings signalent les automatisations existantes qui lancent déjà cet agent sur le même événement |
| La file autonome affame l'interactif sur le mono-processus | Priorité stricte à l'interactif dans la file partagée ; plafond de concurrence réglable ; les runs autonomes sont interruptibles proprement |
| La duplication « perd » des accès sans le dire | Le rapport de duplication est un livrable du ticket, pas un log : affiché, téléchargeable |
| Les créateurs accordent « tout le workspace partagé » par facilité | L'interface propose par défaut un périmètre vide et nomme le risque ; l'Insights montre ce que l'agent a réellement lu (écart visible entre accordé et utilisé) |
## 11. Ordre d'exécution
```text
T01 ── T02 ──┬── T03 ── T04
├── T05 ── T06
└── T08 ── T09 ── T10
T07 après T03 ; T11 après T02 et T08 ; T12 après T03 ; T13 en dernier.
```
Chemin critique : T01 → T02 → T08 → T09. Tant que le test de fuite (T02) n'est pas vert, **aucun** déclencheur événementiel ne doit être activable en production — c'est la porte de la phase.
## 12. Définition de « terminé »
Critères du §9 verts, dont le test de fuite ; une semaine de fonctionnement autonome propre sur l'instance de test ; aide in-app (page agent, déclencheurs, accès) ; document parent annoté (« Phase 3 livrée ») avec les valeurs retenues (plafonds, profondeur de délégation).
---
*Document de phase 3/5 — dépend des Phases 1 et 2 ; prépare la Phase 4.*
@@ -0,0 +1,304 @@
# Agents & Skills pour Flowdeck — Phase 4 : Agir dehors, travailler sur fichiers, compter
| Champ | Valeur |
|---|---|
| Phase | **4 sur 5** — la parité complète (dernière phase de fonctionnalité) |
| Document parent | `architecture-agents-skills-notion-flowdeck.md` v1.1 — en particulier §3.2 et §3.5–3.6, §10.6–10.7, §14, §15, §16.3–16.5 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | Prêt à implémenter **après la Phase 3** |
| Prérequis | Phase 3 (dispatcher, régime `allowlist`, approbations en autonome) |
| Migrations créées | **42** (modèles & usage) et **43** (fichiers des runs) |
| Débloque | Phase 5 (optimisations sur données réelles d'usage et de coûts) |
> **Résultat visible à la fin de cette phase.** L'Agent agit dans le monde extérieur, sous confirmation : il cherche et poste dans **Slack** (nouveau connecteur), rédige/envoie/archive dans **Gmail**, **trouve un créneau** entre participants avec des suggestions classées et une grille, puis réserve ; il **gère l'Inbox** Flowdeck par le chat ; les Custom Agents gagnent des déclencheurs messagerie/courriel/calendrier. Dans le chat, on **dépose des fichiers** (PDF, CSV, XLSX, DOCX, PPTX, ZIP) que l'Agent lit, analyse avec du code sandboxé, transforme en pages ou bases, et il **rend des fichiers téléchargeables**. Et tout est **compté** : modèles autorisés par surface, modèles premium activés par l'admin, allocation d'usage, crédits, limites par membre et par agent, tableaux d'usage.
---
## Table des matières
1. [Objectif](#1-objectif)
2. [Périmètre](#2-périmètre)
3. [État d'entrée](#3-état-dentrée)
4. [Modèle de données — migrations 42 et 43](#4-modèle-de-données--migrations-42-et-43)
5. [Tickets de travail](#5-tickets-de-travail)
6. [API livrée par la phase](#6-api-livrée-par-la-phase)
7. [Comportements détaillés](#7-comportements-détaillés)
8. [Tests](#8-tests)
9. [Critères d'acceptation](#9-critères-dacceptation)
10. [Risques spécifiques et vigilance](#10-risques-spécifiques-et-vigilance)
11. [Ordre d'exécution](#11-ordre-dexécution)
12. [Définition de « terminé »](#12-définition-de--terminé-)
---
## 1. Objectif
Trois chantiers indépendants mais livrés ensemble parce qu'ils partagent la même exigence — **ne jamais agir ou dépenser dans l'ombre** :
1. **Les actions externes** (écarts E8 du parent) : chaque écriture hors Flowdeck est un outil de classe `EXTERNAL_WRITE`, confirmé ou couvert par une politique explicite, et journalisé comme les écritures internes.
2. **L'espace fichiers** (écart E9 + E10) : l'Agent cesse d'être limité au texte du workspace ; il ingère, calcule et produit, en assemblant importers, sandbox Workers et exports existants.
3. **La comptabilité** (écart E13) : les modèles deviennent gouvernés (autorisés par surface, premium activés) et chaque run a un coût calculé depuis un journal — c'est ce qui rend les deux premiers chantiers soutenables en production.
## 2. Périmètre
### 2.1 Inclus
- Contrat « agent-ready » pour les connecteurs (sources / outils de lecture / outils d'écriture / déclencheurs / identité) appliqué à tous les connecteurs livrés dans la phase.
- Connecteur **Slack** natif : connexion workspace (admin) + connexion de compte (utilisateur) ; outils de l'Agent personnel (identité *utilisateur*) et des Custom Agents (canaux accordés) ; déclencheurs message/réaction/mention.
- Google/MS365 : second consentement pour les **scopes d'écriture** ; outils Gmail (brouillon, envoi, archive, étiquettes, corbeille, désabonnement) ; outils calendrier (trouver un créneau + grille, créer, préparer, déplacer si organisateur) ; dépôt d'un fichier produit dans Drive/Files.
- Outils **Inbox** : lire/résumer, regrouper, marquer, archiver (dont en masse avec confirmation et compte exact) ; bornes documentées (pas de décision d'approbation, pas de création de notification, pas de réglages).
- Promotion des connecteurs Discord/Telegram/Teams au contrat agent-ready (canaux comme sources, cibles d'envoi, déclencheurs).
- Déclencheurs Phase 4 dans le dispatcher de la Phase 3 : `message.posted`, `message.reaction`, `message.mention` (par connecteur), `mail.received`, `calendar.event_created/updated/cancelled`.
- Classe d'outils `EXTERNAL_WRITE` dans la gouvernance : approbation par défaut, politiques pré-approuvées bornées (outil × cible) pour les Custom Agents, envoi de courriel toujours confirmé pour l'Agent personnel.
- Espace fichiers : dépôt dans le chat (trombone + glisser-déposer), `agent_run_files`, extraction par les importers, profil tabulaire, outil `run_code_on_data` sur la sandbox Workers avec montage borné, `produce_file` (XLSX, PDF, DOCX, PPTX), `create_collection_from_file`, **tableau interactif de résultats** dans le chat, rétention et purge.
- Migration 42 : `workspace_ai_settings` (modèles autorisés par surface, premium, modèle par défaut, limites, qui peut créer des agents, accès web par défaut), `ai_usage_ledger` ; service `usage_meter` ; tarification par modèle en réglages ; allocation d'usage (fenêtre calculée depuis le journal) ; limites membre et agent appliquées au routage de modèle ; tableaux d'usage (admin) et coût par run dans Activity ; webhooks de seuil de crédits.
- Application de `credit_limit_monthly` (Phase 3, colonne créée) et de `who_can_create_agents`.
### 2.2 Exclu
- Le **serveur MCP** Flowdeck (chantier séparé déjà documenté côté Flowdeck) et le SDK d'agents externe → Phase 5 (préparation seulement).
- Les connecteurs métier nouveaux au-delà de Slack (CRM, billetterie…) : le contrat agent-ready et le client MCP les couvrent sans code dédié.
- La refacturation réelle : les crédits sont une unité de compte interne (question ouverte Q1 du parent : quotas de gouvernance ou reflet de coûts fournisseurs — le journal livré ici sert les deux, seuls les taux changent).
## 3. État d'entrée
| Élément | État |
|---|---|
| Connecteurs natifs (gitea, github, web, google, ms365) + personnels (custom, discord, telegram, mcp) ; OAuth Google/MS365 **lecture seule**, refresh auto, secrets Fernet | Existants (§16.4 de l'architecture) |
| `connectors.py` (fetch gardé SSRF), `oauth_connectors.py`, client MCP complet | Existants |
| Importers (PDF, DOCX, CSV/XLSX, ZIP via pipeline) ; exports (XLSX, PDF, DOCX…) | Existants (§21) |
| Workers sandboxés (AST, pas de réseau/filesystem, timeout 30 s, budget journalier) | Existants (§19.2) |
| Service de notifications + Inbox sidebar | Existant (§19.4) |
| `agent_approvals`, webhooks `agent.*`, dispatcher + régime `allowlist`, `agent_runs` avec `credits` à 0 | Livrés Phases 2–3 |
| `llm_config` (23 fournisseurs, précédence 4 niveaux) | Existant — les taux par modèle s'y ajoutent |
## 4. Modèle de données — migrations 42 et 43
```sql
-- Migration 42 — Phase 4 (gouvernance modèles & usage)
CREATE TABLE workspace_ai_settings (
workspace_id INTEGER PRIMARY KEY REFERENCES workspaces(id),
allowed_models_personal_json TEXT DEFAULT '[]', -- [] = tous les modèles configurés
allowed_models_custom_json TEXT DEFAULT '[]',
premium_models_json TEXT DEFAULT '[]', -- activés par l'admin
default_model TEXT,
member_credit_limit INTEGER, -- crédits / membre / mois ; NULL = illimité
allowance_json TEXT DEFAULT '{}', -- {window_hours, volume} de l'allocation incluse
who_can_create_agents TEXT DEFAULT 'everyone', -- 'everyone' | 'admins'
web_access_default INTEGER NOT NULL DEFAULT 0,
updated_by INTEGER REFERENCES users(id), updated_at TEXT
);
CREATE TABLE ai_usage_ledger (
id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
run_id INTEGER REFERENCES agent_runs(id),
user_id INTEGER REFERENCES users(id),
agent_id INTEGER REFERENCES agents(id),
provider TEXT, model TEXT,
tokens_in INTEGER DEFAULT 0, tokens_out INTEGER DEFAULT 0,
credits REAL NOT NULL DEFAULT 0,
bucket TEXT NOT NULL DEFAULT 'allowance', -- 'allowance' | 'premium'
created_at TEXT NOT NULL
);
-- Index : (workspace_id, created_at), (user_id, created_at), (agent_id, created_at).
-- Taux par modèle : table de correspondance dans les réglages (JSON versionné),
-- {"<provider>/<model>": {"in_per_1k": x, "out_per_1k": y, "premium": bool}}
-- le mode offline a un taux de 0 par construction.
-- Migration 43 — Phase 4 (espace fichiers)
CREATE TABLE agent_run_files (
id INTEGER PRIMARY KEY,
run_id INTEGER REFERENCES agent_runs(id),
conversation_id INTEGER REFERENCES agent_conversations(id),
direction TEXT NOT NULL, -- 'input' | 'output'
name TEXT NOT NULL, mime TEXT, size INTEGER,
storage_path TEXT NOT NULL, -- data/uploads/agent-files/<conversation_id>/…
created_at TEXT NOT NULL,
expires_at TEXT -- NULL = livrable conservé par l'utilisateur
);
```
Règles : le ledger est **append-only** (aucune correction d'écriture — un ajustement est une ligne négative motivée, créée par un admin) ; les agrégats (allocation consommée, crédits du mois) sont **toujours calculés** depuis le ledger ; `agent_run_files` ne stocke jamais le contenu en base.
## 5. Tickets de travail
### Le contrat connecteur et Slack
**P4-T01 — Contrat agent-ready et registre des capacités.**
Fichiers : `app/services/connectors.py` (extension), registre dans `tool_registry.py`.
Travail : formaliser la déclaration de capacités du parent §14.1 (sources, read_tools, write_tools, triggers, identité) ; le sélecteur *All sources* (Phase 2) et le Tool Registry consomment cette déclaration au lieu de listes codées en dur ; un connecteur sans déclaration reste utilisable en lecture comme aujourd'hui (compatibilité).
Acceptation : brancher un connecteur de test déclaratif suffit à le faire apparaître dans All sources et dans les outils, sans autre code.
**P4-T02 — Connecteur Slack natif : connexions.**
Fichiers : catalogue des connecteurs natifs, flux OAuth (patron des natifs existants).
Travail : connexion **workspace** par un admin (préalable aux agents) stockée comme les autres connecteurs workspace ; connexion **compte** par utilisateur pour l'Agent personnel (jeton utilisateur, Fernet) ; écran de statut distinguant les deux niveaux ; révocation indépendante.
Acceptation : sans connexion workspace, les déclencheurs Slack d'agents sont refusés proprement ; sans connexion compte, l'Agent personnel signale le manque au lieu d'échouer en cours de run.
**P4-T03 — Outils Slack.**
Travail : lecture (chercher des personnes ; lire canaux publics/privés et fils **dans la visibilité du jeton utilisé** ; lire les fichiers partagés récents) et écritures `EXTERNAL_WRITE` (poster, répondre en fil, éditer **ses propres** messages, réagir) ; identité *utilisateur* pour l'Agent personnel (les messages portent son nom), identité *agent* pour les Custom Agents limitée aux canaux présents dans `agent_access` (extension de `resource_type` à `'channel'`) ; l'outil d'édition refuse tout message non émis par la même identité.
Acceptation : test de borne — avec un jeton limité à un canal, aucune lecture ni écriture hors de ce canal n'est possible, y compris par recherche.
**P4-T04 — Déclencheurs Slack et messageries existantes.**
Travail : émission vers le bus — Slack : `message.posted` (filtres mots-clés, inclusion des fils), `message.reaction`, `message.mention` ; indicateur d'activité dans le canal pendant le run (`show_typing` du déclencheur) ; promotion de Discord/Telegram/Teams au même contrat : leurs événements existants alimentent les mêmes `event_name`, leurs canaux deviennent des ressources accordables et des cibles de `channel_post`.
Acceptation : UC-05 (rapport horaire posté) et un triage sur mention fonctionnent sur Slack **et** sur un connecteur de messagerie préexistant, sans code de dispatcher différent.
### Écritures Google / Microsoft 365 et Inbox
**P4-T05 — Second consentement d'écriture et outils Gmail.**
Fichiers : `app/services/oauth_connectors.py`.
Travail : demande de scopes d'écriture séparée et révocable indépendamment (l'état « lecture seule » reste le défaut et un état stable) ; outils : `mail_draft`, `mail_send`, `mail_archive`, `mail_label`, `mail_trash` (+ désabonnement d'expéditeur) sur Gmail et, en parité de contrat, Outlook/Graph ; l'envoi est **toujours** confirmé pour l'Agent personnel (carte d'approbation montrant destinataires, objet, corps complet) ; pour les Custom Agents, l'envoi exige une politique pré-approuvée explicite par boîte.
Acceptation : aucun envoi ne part sans que le contenu exact envoyé soit celui qui a été approuvé (hash du contenu dans la demande d'approbation, revérifié à l'exécution).
**P4-T06 — Outils calendrier d'agent.**
Travail : `calendar_find_time` (croise les disponibilités des participants sur les calendriers connectés, rend des suggestions **classées** avec les motifs du classement — chevauchements évités, préférences horaires — et la matière de la **grille interactive** rendue dans le panneau : créneaux × participants, sélection en un clic) ; `calendar_create_event` (organisateur = l'utilisateur ou l'agenda accordé) ; `calendar_prep` (assemble événement, participants, pages liées, dernière note de réunion) ; `calendar_move/cancel` bornés aux événements dont l'acteur est organisateur ; en multi-calendriers : calendrier par défaut, sinon question à l'utilisateur.
Acceptation : UC-09 (calendrier) passe ; déplacer un événement d'un tiers est refusé avec le motif, pas tenté.
**P4-T07 — Outils Inbox.**
Fichiers : service de notifications existant (ajout d'une API de service consommable par les outils, pas d'accès direct aux tables depuis le registre).
Travail : `inbox_read` (résumé structuré : nouveau, à répondre, peut être effacé), `inbox_group` (type / statut / projet / page), `inbox_mark`, `inbox_archive` (masse comprise) ; confirmation obligatoire au-delà d'un seuil d'éléments (défaut proposé : 10) avec **compte exact** affiché ; bornes : les notifications d'approbation/demande d'accès sont signalées mais jamais décidées ; aucun outil de création de notification ni de modification des préférences.
Acceptation : « archive tout ce qui est lu » sur une Inbox de test de 250 éléments archive exactement les éléments lus, après une confirmation affichant « 250 » — ni 249 ni 251 (test de comptage sur fixture).
**P4-T08 — Classe `EXTERNAL_WRITE` dans la gouvernance.**
Fichiers : `app/services/agent_policies.py`, `agent_approvals`.
Travail : quatrième classe d'outils ; par défaut approbation par action (le run passe en `waiting_approval`, carte dans le panneau **ou** dans Activity pour les runs autonomes) ; politiques pré-approuvées pour les Custom Agents : tuples (agent, outil, cible) accordés par un niveau `full`, révocables, expirant par défaut (durée réglable, défaut proposé : 90 jours) et affichés dans les Settings de l'agent ; toute écriture externe exécutée écrit dans `agent_actions` avec, quand le tiers le permet, l'action inverse (supprimer le message…) sinon la mention « non annulable ».
Acceptation : il n'existe aucun chemin d'exécution d'un outil `EXTERNAL_WRITE` qui contourne soit l'approbation, soit une politique en vigueur (test par énumération des points d'entrée du registre).
### Espace fichiers
**P4-T09 — Dépôt et lecture de fichiers de conversation.**
Fichiers : routes de conversation (téléversement), nouveau `app/services/agent_files.py`, importers.
Travail : dépôt par trombone et glisser-déposer ; validation (liste blanche de types alignée sur les importers, taille `AGENT_FILE_MAX_MB`) ; stockage sous `data/uploads/agent-files/` ; outil `read_uploaded_file` : extraction texte (PDF/DOCX/PPTX), profil tabulaire (CSV/XLSX : colonnes, types devinés, aperçu), inventaire ZIP (pas d'extraction récursive, plafond décompressé) ; le LLM ne reçoit que résumés et aperçus, jamais le binaire.
Acceptation : déposer le CSV d'UC-08 donne à l'Agent un profil fidèle (noms et types de colonnes, nombre de lignes exact).
**P4-T10 — Calcul sandboxé et production de fichiers.**
Travail : `run_code_on_data` sur le moteur des Workers avec l'unique écart documenté (montage en lecture des fichiers de la conversation, répertoire de sortie en écriture, quotas renforcés) ; `produce_file` pour XLSX/PDF/DOCX/PPTX avec les bibliothèques d'export existantes ; cartes téléchargeables dans le message (`GET …/files/{id}/download` avec contrôle d'ACL de la conversation) ; `create_collection_from_file` via le pipeline d'import (déduplication comprise).
Acceptation : UC-08 de bout en bout ; un code qui tente un accès réseau ou hors du montage est bloqué par la sandbox, et le run continue avec l'échec comme résultat d'outil.
**P4-T11 — Tableau interactif de résultats dans le chat.**
Fichiers : panneau, outil `query_collection` formalisé (Annexe B du parent).
Travail : quand un résultat d'outil est tabulaire (lignes × colonnes typées), le panneau le rend en tableau en lecture (tri client, ouverture de la ligne source) ; le tableau est un artefact du message, pas une collection ; plafond de lignes affichées avec mention du total.
Acceptation : une question « quelles tâches en retard ? » rend le tableau sans ouvrir la base, et chaque ligne ouvre la bonne tâche.
**P4-T12 — Rétention des fichiers.**
Travail : `expires_at` posé au dépôt (défaut `AGENT_FILE_RETENTION_DAYS`, 30 j) sauf pour les livrables que l'utilisateur épingle/conserve ; purge ajoutée au `trash_purge_scheduler` existant ; la suppression d'une conversation supprime ses fichiers et sa mémoire (règle du parent §16.6).
Acceptation : après purge simulée, les métadonnées de run subsistent (nom, taille) mais aucun fichier expiré n'est téléchargeable.
### Comptabilité et gouvernance des modèles
**P4-T13 — `usage_meter` et journal d'usage.**
Fichiers : nouveau `app/services/usage_meter.py`, migration 42, extension de `llm_config` (taux par modèle).
Travail : à la fin de chaque run (et, pour les runs longs, par paliers), écrire la ligne de ledger depuis `agent_runs` (tokens réels remontés par le client LLM) : bucket `allowance` si le modèle est inclus, `premium` sinon ; le taux zéro du mode offline est un cas de test permanent ; webhooks `agent.credits.threshold` aux seuils 50/80/100 % des limites concernées.
Acceptation : la somme du ledger recoupe les tokens des `agent_runs` sur toute période de test ; aucun run terminé sans ligne (ou ligne à zéro explicitement offline).
**P4-T14 — Modèles gouvernés : autorisés par surface, premium, replis.**
Travail : le routeur `Auto` (Phase 2) et les sélecteurs ne voient que les modèles **autorisés pour la surface** ; les premium exigent l'activation admin et puisent dans les crédits ; un modèle désactivé disparaît (grisé pendant la transition) et les agents qui l'utilisaient basculent au prochain run sur défaut/`Auto` (comportement annoncé dans les Settings de l'agent, pas silencieux pour son propriétaire : notification) ; à limite de crédits atteinte (membre ou agent), les modèles inclus continuent et les premium s'arrêtent avec un état explicite dans le run.
Acceptation : retirer un modèle utilisé par un agent ne casse aucun run suivant et laisse une trace visible pour le propriétaire.
**P4-T15 — Tableaux d'usage et réglages IA du workspace.**
Fichiers : page de réglages (section IA & Agents du parent §7.6).
Travail : réglages de `workspace_ai_settings` (deux listes de modèles, premium, défaut, limites, `who_can_create_agents` **appliqué**, accès web par défaut) ; tableaux : usage par membre, par agent, par modèle (période, bucket, crédits), export CSV ; le coût (tokens + crédits) rejoint l'onglet Activity des agents et le détail d'un run du panneau.
Acceptation : un admin répond en moins d'une minute, depuis l'interface, à « qui a dépensé quoi, sur quel modèle, cette semaine ? ».
## 6. API livrée par la phase
| Route | Ticket |
|---|---|
| `POST /api/agent/conversations/{id}/files` · `GET …/files/{fid}/download` | T09/T10 |
| `GET/PUT /api/settings/ai` (réglages IA du workspace, admin) | T15 |
| `GET /api/settings/ai/usage` (+ `?format=csv`) | T15 |
| `POST /api/agent/connectors/slack/connect` (workspace et compte) | T02 |
| Outils (pas des routes) : Slack, Gmail, calendrier, Inbox, fichiers | T03/T05/T06/T07/T09/T10 |
| Webhooks : `agent.credits.threshold` ; champs `credits` dans `agent.run.*` | T13 |
## 7. Comportements détaillés
### 7.1 Confirmation : ce que l'utilisateur voit
Une demande d'approbation montre **l'action exacte** : pour un courriel, destinataires/objet/corps ; pour un message de canal, le canal et le texte ; pour une archive en masse, le compte et le critère. Approuver exécute **ce** contenu (hash revérifié pour le courriel) ; toute modification du contenu par l'Agent après coup exige une nouvelle approbation. Un refus laisse le run se terminer proprement avec le motif « refusé » dans la réponse.
### 7.2 Allocation, crédits, limites : les trois compteurs en pratique
```text
Run démarre → modèle résolu (Phase 2/4)
├─ modèle inclus → le run puise dans l'ALLOCATION (fenêtre glissante du workspace)
│ allocation épuisée → le run est refusé proprement (« allocation épuisée,
│ renouvellement à … »), sauf si des crédits peuvent prendre le relais (réglage)
└─ modèle premium → le run puise dans les CRÉDITS
limite membre ou limite agent atteinte → premium refusé, repli annoncé
sur un modèle inclus (jamais d'arrêt sec quand un repli existe)
Fin du run → ligne de ledger (toujours), seuils vérifiés → webhooks éventuels
```
### 7.3 Identités externes — le tableau à ne jamais violer
| Contexte | Jeton utilisé | Signature visible chez le tiers |
|---|---|---|
| Agent personnel → Slack/Gmail | Celui de l'utilisateur | L'utilisateur |
| Custom Agent → canaux accordés | Celui de l'app/du connecteur workspace | L'agent (nommé) |
| Custom Agent → Gmail | Uniquement via politique pré-approuvée par boîte | La boîte accordée |
| Tout agent → calendrier | Organisateur = l'utilisateur ou l'agenda accordé | L'organisateur réel |
### 7.4 Ce que les fichiers ne sont pas
Un fichier déposé est une **donnée**, jamais une consigne : son contenu est délimité comme les autres contenus lus (§16.4 du parent). Un tableur contenant « envoie ce fichier à… » ne déclenche rien d'autre qu'un éventuel résultat d'outil refusé par la gouvernance.
## 8. Tests
| # | Niveau | Cas |
|---|---|---|
| T-01 | Intégration | Contrat connecteur : un connecteur de test déclaratif apparaît dans All sources et fournit ses outils |
| T-02 | Intégration | Slack borné : lecture/écriture hors canal accordé impossibles (jeton restreint) |
| T-03 | Intégration | Envoi Gmail : contenu approuvé = contenu envoyé (hash) ; refus = rien n'est parti |
| T-04 | Intégration | `calendar_find_time` sur disponibilités de fixture : classement explicable, grille cohérente ; déplacement d'un événement tiers refusé |
| T-05 | Intégration | Inbox : archive en masse au compte exact après confirmation ; notifications d'approbation jamais décidées |
| T-06 | Intégration | Énumération des points d'entrée `EXTERNAL_WRITE` : aucun ne contourne approbation/politique |
| T-07 | Intégration | UC-08 complet en mode offline (extraction et production déterministes, le calcul par un « faux » code sandboxé inclus) |
| T-08 | Intégration | Sandbox : tentative réseau et tentative de sortie du montage bloquées |
| T-09 | Intégration | Ledger : somme par période = somme des runs ; run offline = ligne à 0 ; allocation épuisée = refus propre |
| T-10 | Intégration | Limite membre atteinte : premium refusé avec repli annoncé, modèles inclus toujours utilisables |
| T-11 | Intégration | Modèle retiré : l'agent bascule, le propriétaire est notifié, Activity le montre |
| T-12 | E2E | Déposer un CSV dans le chat, obtenir le tableau de résultats et le XLSX produit téléchargeable |
| T-13 | E2E | Trouver un créneau à trois et réserver depuis la grille |
| T-14 | Non-régression | Les connecteurs en lecture seule fonctionnent inchangés pour les utilisateurs sans consentement d'écriture |
## 9. Critères d'acceptation
- [ ] UC-08, UC-09 et UC-10 passent de bout en bout.
- [ ] Aucune écriture externe sans approbation ou politique pré-approuvée en vigueur (T-06).
- [ ] Aucun run sans ligne au ledger ; les tableaux d'usage recoupent les runs.
- [ ] Un Custom Agent déclenché par une mention dans un canal accordé répond dans ce canal et nulle part ailleurs.
- [ ] La parité fonctionnelle du document parent §3 est atteinte : la Phase 5 n'a plus de fonctionnalité à ajouter, seulement à durcir.
## 10. Risques spécifiques et vigilance
| Risque | Vigilance |
|---|---|
| Consentements OAuth d'écriture trop larges demandés d'un bloc | Le second consentement est séparé, nommé par domaine (courriel **ou** calendrier), et la lecture seule reste un état complet et digne |
| Un envoi approuvé puis modifié par une reprise de run | Hash du contenu dans l'approbation (T05) ; un run repris après refus repart d'une demande nouvelle |
| Explosion des coûts par un déclencheur de messagerie bavard | Les plafonds de la Phase 3 s'appliquent, plus les budgets crédits désormais réels ; l'indicateur d'activité (`show_typing`) est désactivable et les filtres mots-clés sont proposés par défaut |
| Le montage fichiers de la sandbox devient une brèche | C'est l'unique écart au modèle Workers : revue de sécurité dédiée avant T10, chemins canonisés et vérifiés sous le répertoire de la conversation, quotas bas par défaut |
| L'allocation « fenêtre glissante » mal comprise crée des refus surprenants | L'état courant de l'allocation est visible par l'utilisateur dans ses réglages, avec l'heure de renouvellement calculée depuis le ledger — jamais un message « réessayez plus tard » sans date |
| Slack change ses API/scopes | Le connecteur isole les appels derrière son service ; les tests d'intégration Slack utilisent un double d'API, les tests réels sont manuels et listés dans la grille de test |
## 11. Ordre d'exécution
```text
T01 ──┬── T02 ── T03 ── T04
├── T05 ──┐
├── T06 ├── T08 (gouvernance, avant toute mise en service des écritures)
└── T07 ──┘
T09 ── T10 ── T11 ; T12 après T10
T13 ── T14 ── T15
Les trois chantiers (externe / fichiers / comptabilité) sont parallélisables,
mais T08 et T13 sont les portes : pas d'écriture externe en production sans
gouvernance complète, pas de premium sans comptage.
```
## 12. Définition de « terminé »
Critères du §9 verts ; revue de sécurité passée sur `EXTERNAL_WRITE` et sur le montage fichiers ; deux semaines de fonctionnement avec le ledger en observation (écarts ledger/runs à zéro) ; document parent annoté (« Phase 4 livrée — parité atteinte ») ; aide in-app complète pour les nouveaux réglages.
---
*Document de phase 4/5 — dépend des Phases 1 à 3 ; la Phase 5 ne fait que durcir et étendre.*
@@ -0,0 +1,158 @@
# Agents & Skills pour Flowdeck — Phase 5 : Durcissement et extensions
| Champ | Valeur |
|---|---|
| Phase | **5 sur 5** — optionnelle, fondée sur les données réelles des Phases 1 à 4 |
| Document parent | `architecture-agents-skills-notion-flowdeck.md` v1.1 — en particulier §16.4 (injection de prompt), §17 (exigences non fonctionnelles), §20 Phase 5 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | À planifier après retour d'usage des Phases 1 à 4 — contrairement aux phases précédentes, son périmètre exact se **confirme avec les données**, il ne se devine pas |
| Prérequis | Phases 1 à 4 en production avec du trafic réel |
| Migration créée | **Aucune planifiée** (toute migration nécessaire serait numérotée à partir de 44, au moment du besoin) |
| Nature | Durcissement (sécurité, coûts, fiabilité), mesure avancée, extensions publiques |
> **Résultat visible à la fin de cette phase.** La solution complète tourne avec des preuves : le routeur de skills s'est amélioré sur les contre-exemples réels ; les *Insights* couvrent le workspace entier avec export à l'échelle ; une campagne de tests d'injection de prompt a été menée et ses résultats traités ; les runs et journaux ont une politique de rétention et d'archivage appliquée ; la Skills API publique permet de **publier une base de skills vers des agents externes** ; et les deux chantiers voisins — **serveur MCP Flowdeck** et **SDK d'agents** — sont soit lancés avec leur cadrage prêt, soit explicitement écartés, sur données plutôt que sur impressions.
---
## Table des matières
1. [Pourquoi cette phase est différente](#1-pourquoi-cette-phase-est-différente)
2. [Conditions d'entrée](#2-conditions-dentrée)
3. [Chantier A — Améliorer le routeur de skills sur données réelles](#3-chantier-a--améliorer-le-routeur-de-skills-sur-données-réelles)
4. [Chantier B — Insights à l'échelle et administration avancée](#4-chantier-b--insights-à-léchelle-et-administration-avancée)
5. [Chantier C — Sécurité offensive : campagne d'injection de prompt](#5-chantier-c--sécurité-offensive--campagne-dinjection-de-prompt)
6. [Chantier D — Rétention, archivage et performance des journaux](#6-chantier-d--rétention-archivage-et-performance-des-journaux)
7. [Chantier E — Skills API publique et publication externe](#7-chantier-e--skills-api-publique-et-publication-externe)
8. [Chantier F — Préparer (ou écarter) le serveur MCP et le SDK d'agents](#8-chantier-f--préparer-ou-écarter-le-serveur-mcp-et-le-sdk-dagents)
9. [Chantier G — Qualité des agents : évaluation continue](#9-chantier-g--qualité-des-agents--évaluation-continue)
10. [Tests transverses de la phase](#10-tests-transverses-de-la-phase)
11. [Critères de sortie](#11-critères-de-sortie)
12. [Ce que cette phase ne doit pas devenir](#12-ce-que-cette-phase-ne-doit-pas-devenir)
---
## 1. Pourquoi cette phase est différente
Les Phases 1 à 4 construisent des mécanismes ; leurs documents peuvent donc être écrits avant le code. La Phase 5, elle, **consomme les retours de ces mécanismes** : taux d'erreur du routeur, distribution réelle des coûts, tentatives d'injection observées, volume des journaux. Écrire aujourd'hui ses seuils ou ses priorités internes serait de la fausse précision.
Ce document fixe donc : les **conditions d'entrée** (quelles données doivent exister), les **chantiers** avec leur méthode et leurs critères de sortie, et les **décisions à prendre sur données**. Chaque chantier est indépendant : ils peuvent être menés dans n'importe quel ordre, partiellement, ou pas du tout — sans remettre en cause la complétude des Phases 1 à 4.
## 2. Conditions d'entrée
Avant d'ouvrir un chantier, réunir (une page de constat suffit) :
- Le nombre de runs par surface (chat, éditeur, déclencheurs, API) sur au moins 4 semaines, et leur taux d'échec.
- Les décisions du Skill Router : distribution des branches (`forced`, `high`, `grey`, `none`), nombre de contre-exemples « skill pas pertinent » collectés (mécanisme livré en Phase 2).
- La distribution des coûts : crédits par agent, par membre, par modèle ; part des runs autonomes.
- Les incidents : approbations expirées, boucles stoppées, plafonds atteints, écritures externes refusées.
- Le volume des tables de journaux (`agent_runs`, `ai_usage_ledger`, `agent_actions`, `skill_runs`) et les temps de requête des écrans Activity/Insights.
Chaque constat débouche sur un classement des chantiers A à G par bénéfice attendu — c'est ce classement, pas ce document, qui fixe l'ordre réel.
## 3. Chantier A — Améliorer le routeur de skills sur données réelles
**Méthode.**
1. Étendre le jeu d'essai de la Phase 2 (30 demandes) avec les cas réels : demandes ayant reçu un « pas pertinent », demandes où l'utilisateur a forcé un skill que le routeur n'avait pas retenu (signal faible d'un rappel insuffisant), demandes relancées juste après un skill automatique (signal faible d'une mauvaise application).
2. Rejouer le routeur hors ligne sur ce corpus élargi ; ajuster les seuils et, si le corpus le justifie, enrichir l'arbitrage (par exemple fournir à l'arbitre les **exemples** du skill, pas seulement sa description).
3. Traiter la cause avant le symptôme : une description qui attire de mauvais appariements se réécrit — fournir dans la Library un signalement « ce skill est souvent écarté » visible par son propriétaire, avec ses demandes fautives.
4. Toute modification des seuils par défaut est livrée avec le rapport du corpus (précision/rappel avant/après), comme un changement de modèle se livre avec son évaluation.
**Critères de sortie.** Le corpus dépasse 150 demandes étiquetées et fait partie des tests permanents ; les taux d'erreur mesurés ont baissé par rapport au constat d'entrée, ou l'analyse montre qu'ils sont dominés par des descriptions à réécrire (et la liste de ces descriptions a été traitée).
## 4. Chantier B — Insights à l'échelle et administration avancée
**Travaux.**
- Vue workspace des Insights : tous les agents (runs, statuts, modèles, crédits, taux d'échec), avec la règle d'accès déjà posée — un admin doit être ajouté à un agent pour voir le détail de **ses conversations** ; les agrégats anonymisés de contenu, eux, sont visibles sans cet ajout.
- Export CSV à l'échelle : conversations et runs sur une période, avec un plafond documenté par export et un export asynchrone (job, comme les imports) au-delà ; format stable, colonnes versionnées.
- Alertes d'administration : agent en échec répété, agent dont le coût dévie fortement de sa moyenne, skill partagé massivement édité, modèle devenu indisponible chez le fournisseur.
- Comparaison de modèles sur les données réelles : pour un même agent, qualité perçue (feedbacks), coût et latence par modèle — l'écran qui permet de choisir un modèle **pour une raison**, pas par habitude.
**Critères de sortie.** Un admin répond depuis l'interface, sans SQL, aux questions du constat d'entrée (qui coûte, qui échoue, quoi optimiser) ; les exports sont utilisés par au moins un processus réel (revue mensuelle) plutôt que seulement testés.
## 5. Chantier C — Sécurité offensive : campagne d'injection de prompt
**Méthode.** Constituer une suite d'attaques reproductibles (fixtures, exécutées en CI sur le mode offline quand c'est possible, et contre un fournisseur de test sinon) couvrant les trois surfaces du document parent §16.4 :
| Surface | Scénarios types |
|---|---|
| Contenus lus (pages, courriels, messages, fichiers, résultats web/MCP) | Instruction cachée demandant une écriture, une exfiltration vers un outil externe, ou la désactivation d'une approbation |
| Skills et instructions partagés | Skill qui tente d'élargir ses outils, de se rendre obligatoire, de faire taire la nomination du skill dans la réponse |
| Descriptions de skills | Description écrite pour capter des demandes hors sujet (détournement du routeur) |
| Délégations | Un sous-agent qui tente de faire exécuter par le parent une action hors de son propre périmètre |
| Données tabulaires / fichiers | Instructions dans les cellules, les noms de fichiers, les métadonnées |
**Règle de lecture des résultats.** La défense attendue n'est pas « le modèle refuse poliment » — c'est **la gouvernance refuse mécaniquement** : l'outil n'existe pas, l'accès n'est pas accordé, l'approbation n'a pas été donnée. Un scénario où le modèle « a failli » mais où le contrôle a tenu est un succès du système et doit être consigné comme tel ; un scénario où seul le modèle a tenu (aucun contrôle mécanique derrière) est une **faille à corriger** par un contrôle, pas par un meilleur prompt.
**Critères de sortie.** La suite tourne en CI ; chaque scénario est classé (contrôle mécanique / comportement modèle) ; les scénarios reposant sur le seul comportement ont donné lieu à un ticket de contrôle ou à une acceptation de risque explicite et datée.
## 6. Chantier D — Rétention, archivage et performance des journaux
**Travaux.**
- Politique de rétention par type de journal, réglable par workspace : runs interactifs, runs autonomes, ledger (le plus long — c'est une pièce comptable), actions et leurs snapshots d'annulation, fichiers de runs (déjà en Phase 4).
- Archivage : extraction périodique des journaux expirés vers un fichier du volume de sauvegarde avant suppression ; la suppression d'un run archivé laisse une tombe (identifiant, dates, agent, statut) pour garder les références des exports.
- Performance : vérifier sur les volumes réels les index posés aux Phases 2 et 4 ; ajouter les index que les requêtes d'Activity/Insights réclament (et seulement ceux-là) ; solder automatiquement les runs orphelins dans tous les chemins de redémarrage (le solde au démarrage existe depuis la Phase 2 — l'étendre aux états `waiting_approval` expirés).
- Limites de taille des champs JSON de journal (`sources_json`, `trigger_payload_json`) : tronquer par conception (références, pas de contenus), avec un test qui échoue si un payload dépasse le plafond.
**Critères de sortie.** Sur les volumes du constat d'entrée projetés à 12 mois, Activity et Insights répondent dans les cibles du document parent §17 ; la politique de rétention est appliquée par un job observable (compte rendu de purge), pas par une promesse.
## 7. Chantier E — Skills API publique et publication externe
**Travaux.**
- Enrichir `/api/v2/skills` : lister les skills d'une base de skills donnée (avec jeton Bearer scopé et ACL de la base), lire la sérialisation `SKILL.md` et le bundle complet par skill, et recevoir un **webhook** `skill.updated` permettant aux consommateurs externes de resynchroniser (le pendant serveur du badge de copie périmée de la Phase 1).
- Page de documentation d'API correspondante dans l'OpenAPI v2 existant, avec le format `SKILL.md` décrit comme le format d'échange externe de référence.
- Option de « publication » d'une base : un réglage de la base qui la rend listable par l'API avec un jeton dédié (lecture seule), sans ouvrir l'API générale du workspace.
**Critères de sortie.** Un agent externe de démonstration (par exemple un agent local) consomme une base de skills Flowdeck par l'API et se resynchronise sur un `skill.updated` sans intervention manuelle.
## 8. Chantier F — Préparer (ou écarter) le serveur MCP et le SDK d'agents
Ces deux sujets sont des chantiers **voisins déjà identifiés** côté Flowdeck (conception d'un serveur MCP officiel dans `FLOWDECK_MCP_SERVER_GUIDE.md`, non implémentée). Cette phase ne les réalise pas ; elle produit la **décision cadrée** :
- Serveur MCP : les prérequis produits par les Phases 1 à 4 sont réunis (API v2 agents/skills complète, jetons scopés, journal d'audit) ; le cadrage consiste à lister les outils que le serveur exposerait (lecture/écriture de pages et bases, skills, runs d'agents) et leur correspondance exacte avec l'API v2, puis à décider au vu de l'usage réel des agents externes.
- SDK d'agents : la question est de savoir si des applications externes doivent pouvoir **continuer une conversation** d'agent en streaming ; l'API v2 actuelle est synchrone (SSE tamponné) ; le cadrage évalue le besoin sur les intégrations réelles avant tout développement.
- Dans les deux cas, « écarter pour l'instant, avec la raison et la date de réexamen » est une sortie valide de la phase — l'important est que la décision soit prise sur les données du §2, pas laissée en suspens.
**Critères de sortie.** Une note de décision par sujet, rangée avec les documents de la solution, avec périmètre, prérequis vérifiés et date de réexamen.
## 9. Chantier G — Qualité des agents : évaluation continue
**Travaux.**
- Pour les Custom Agents les plus utilisés : constituer des **jeux d'évaluation** (entrées réelles anonymisées + sorties attendues ou critères de jugement), rejoués à chaque changement d'instructions, de modèle ou de version — le pendant des tests pour des objets configurés par des non-développeurs.
- Brancher ces évaluations sur l'écran de restauration de version (Phase 3) : avant de restaurer, on peut évaluer l'ancienne configuration sur le jeu de l'agent.
- Feedback 👍/👎 (existant) agrégé par agent, par skill et par modèle dans les Insights — en fermant la boucle ouverte dès la Phase 2 (les feedbacks existent, leur agrégation utile attendait les volumes).
**Critères de sortie.** Les trois agents les plus utilisés du workspace ont un jeu d'évaluation versionné ; un changement de modèle sur l'un d'eux a été décidé avec son rapport d'évaluation.
## 10. Tests transverses de la phase
| # | Cas |
|---|---|
| T-01 | La suite d'injection (chantier C) tourne en CI et classe chaque scénario |
| T-02 | Le corpus du routeur élargi (chantier A) est une fixture permanente ; les seuils livrés le passent |
| T-03 | Rétention : la purge archive avant de supprimer, et les tombes gardent les références des exports |
| T-04 | Skills API : un jeton scopé à une base ne peut pas lister une autre base |
| T-05 | Évaluation d'agent (chantier G) : rejouer un jeu donne le même verdict sur la même configuration (déterminisme en mode offline) |
## 11. Critères de sortie
- [ ] Le constat d'entrée (§2) existe et chaque chantier mené y répond explicitement.
- [ ] Au moins les chantiers **C** (sécurité) et **D** (rétention/performance) sont menés : ce sont les deux seuls qui relèvent de la dette plutôt que de l'opportunité.
- [ ] Chaque chantier non mené a une raison datée, pas un oubli.
- [ ] Le document parent passe en version finale de la série (« Phases 1 à 5 closes ») avec un bilan honnête : ce qui a été livré, ce qui a été écarté, ce qui reste ouvert.
## 12. Ce que cette phase ne doit pas devenir
- **Pas un fourre-tout de fonctionnalités ajournées.** Si une fonctionnalité visible manque, elle appartient à un correctif des Phases 1 à 4, pas à cette phase.
- **Pas d'optimisation sans mesure.** Aucun seuil, aucun cache, aucun index sans le constat qui le demande.
- **Pas de machine learning maison pour le routeur.** L'amélioration passe par le corpus, les seuils et les descriptions — les actifs que l'équipe peut relire et corriger.
- **Pas de seconde plateforme d'administration.** Tout ce que cette phase ajoute vit dans les écrans existants (Library, page agent, réglages IA).
---
*Document de phase 5/5 — le dernier de la série. La solution est complète dès la Phase 4 ; cette phase la rend durable.*
@@ -0,0 +1,933 @@
# Architecture & instructions d'implémentation — Gestion et interface des Templates (modèle Notion) pour Flowdeck
| Champ | Valeur |
|---|---|
| Version | 1.0 |
| Date | 10 octobre 2026 |
| Auteur | Spark, pour Bruno — **à remettre à Hermes comme cahier des charges d'implémentation** |
| Statut | Proposition d'architecture et plan d'implémentation — à valider contre le code réel de Flowdeck (voir le ticket T0-1 d'audit) |
| Produit cible | Flowdeck **v7.69.8** (monolithe FastAPI / SSR Jinja2 + htmx + Alpine.js CSP, SQLite WAL, API publique v2) — d'après `ARCHITECTURE.md` fourni par Bruno le 9 octobre 2026 |
| Références fonctionnelles | Les templates de Notion d'après son centre d'aide officiel : templates de base de données, templates récurrents, templates d'équipe / galerie (liens en fin de document, consultés le 10 octobre 2026) |
| Référence visuelle | **1 capture de l'état actuel fournie par Bruno le 10 octobre 2026** : page vide « Untitled » de Flowdeck, rangée « Get started with », bouton-pilule **Templates** sous le curseur — conservée dans `templates-notion-reference/capture-etat-actuel-bouton-templates.png` |
| Documents compagnons | `architecture-meeting-notion-flowdeck.md` (v1.2), `architecture-agents-skills-notion-flowdeck.md` (v1.1), `architecture-vues-notion-flowdeck.md` (v1.0) — même méthode, même ancrage v7.69.8 |
> **Historique des versions**
>
> - **1.0 — 10 octobre 2026** : création. Point de départ double, formulé par Bruno : (1) il n'existe dans Flowdeck **aucune gestion ni interface digne de ce nom pour les templates** — le seul point d'entrée visible est la pilule « Templates » de la rangée *Get started with* d'une page vide ; (2) **ce bouton ne fonctionne pas**. Le document traite donc les deux : un **diagnostic encadré** du bouton cassé (phase 0, cause à établir par la preuve, pas par hypothèse) et la **conception complète** d'un système de templates unifié — registre, gestionnaire, sélecteur, éditeur de template, templates de base de données avec défaut et récurrence, variables dynamiques — ancré sur les trois tables de templates existantes de la v7.69.8. Migrations proposées **48 et 49**, reséquencées après les 39 à 43 (Agents & Skills) et les 44 à 47 (Vues) déjà proposées par les documents compagnons (voir §9.1 — le moteur de migrations n'applique que les versions supérieures à la version courante, actuellement **38**).
> **Avertissement méthodologique — à lire avant tout**
>
> L'architecture interne de Notion est propriétaire et n'est pas publique. Ce document ne prétend donc pas décrire « comment Notion est construit en interne ».
>
> Il contient trois choses distinctes, clairement séparées :
>
> 1. **Une analyse fonctionnelle** (section 3) de ce que font les templates dans Notion, établie à partir de sa documentation publique officielle consultée le 10 octobre 2026. C'est le *comportement observable* du produit, résumé avec mes mots.
> 2. **Un état des lieux Flowdeck** (section 4) tiré du document d'architecture v7.69.8 fourni par Bruno et de sa capture du 10 octobre 2026 : ce qui existe déjà, ce qui existe partiellement, ce qui manque, et ce qui est **constaté cassé**. La cause du bouton inopérant n'y est **pas** affirmée : elle fait l'objet d'un protocole de diagnostic en phase 0 (§4.3 et §15), parce qu'aucun élément disponible ne permet de la trancher à distance.
> 3. **Une architecture cible originale** (sections 5 à 20) pour porter Flowdeck à parité fonctionnelle avec Notion sur les templates. Les choix techniques, le modèle de données, les API et les maquettes sont des propositions qui respectent les invariants de Flowdeck (monolithe, SQLite, SQL brut, zéro bundler, libs vendorisées, CSP stricte sans `unsafe-eval`, un seul processus), pas une reproduction de l'existant Notion.
>
> **Note de vocabulaire.** *Template* est gardé en anglais, comme dans l'UI actuelle de Flowdeck (pilule « Templates ») et dans les noms de tables (`page_templates`, `database_templates`). On distingue quatre **genres** de templates, que Notion sépare et que Flowdeck mélange aujourd'hui : **template de page** (contenu d'une page libre), **template de ligne** (propre à une base : propriétés pré-remplies + corps de la page de ligne), **template de base** (une base entière prête à créer : schéma, vues, lignes d'exemple), **template de blocs** (un groupe de blocs à insérer dans la page courante). *Gestionnaire* désigne l'écran de gestion des templates (§7.2) ; *sélecteur* (picker), le panneau de choix et d'application d'un template (§7.1) ; *instanciation*, l'opération qui crée un objet réel à partir d'un template.
---
## 0. Brief pour Hermes — à lire en premier
**Mission.** Construire dans Flowdeck la gestion et l'interface des templates, au niveau d'expérience de Notion, et réparer le bouton « Templates » de la page vide. L'ordre des phases du §15 fait foi : ne pas commencer par l'interface complète avant le diagnostic (phase 0) et le registre unifié (phase 1).
**Ce qui est attendu, dans l'ordre :**
1. **Diagnostiquer avant de réparer** (phase 0). Reproduire le clic mort de la pilule « Templates » dans un test Playwright qui échoue, identifier la cause racine avec des preuves (console, réseau, code du handler), la consigner, puis seulement corriger. Aucune « réparation » par contournement (masquer la pilule, la remplacer par un lien mort ailleurs) n'est acceptée.
2. **Unifier avant d'ajouter** (phase 1). Les trois tables existantes (`database_templates`, `page_templates`, `page_global_templates`) restent la source des contenus historiques ; le nouveau **registre `templates`** (§9) les catalogue toutes derrière un seul service, `TemplateService` (§8). Toute nouvelle surface (UI, API v2, agent, automatisations, récurrence) passe par ce service — jamais par un accès direct à une des trois tables.
3. **Une seule implémentation de l'instanciation.** L'outil agent existant `apply_template`, l'UI humaine, l'API v2 et le scheduler de récurrence doivent tous appeler la même fonction d'instanciation transactionnelle (§8.3). C'est l'invariant déjà appliqué à l'API publique agent de Flowdeck (« une seule implémentation, jamais re-développée », §16.2 de l'architecture v7.69.8) ; l'étendre aux templates.
4. **Respecter les invariants Flowdeck** : SQLite + SQL brut sans ORM, migrations versionnées une par une et transactionnelles, Alpine **build CSP** (tout composant Alpine doit être enregistré via `Alpine.data` dans `base.html` — un gestionnaire inline non enregistré est une cause classique de clic silencieusement mort, voir H3 en §4.3), aucun bundler, icônes via le global Jinja `fd_icon`, cache-busting `?v=VERSION`, navigation partielle maison (`fdNavigate`) plutôt que des rechargements complets.
5. **Chaque clic produit un effet visible.** Critère d'acceptation transversal, né du bug actuel : tout point d'entrée « Templates » doit ouvrir le sélecteur, ou afficher un état vide explicite avec une action de création — **jamais un no-op silencieux**, y compris hors ligne, sans permission, ou avec un catalogue vide.
**Définition de « terminé »** (par phase) : tickets de la phase livrés, tests du §17 verts (dont le test de régression du clic), migrations appliquées sur une base v7.69.8 de test sans perte des templates existants, et parcours manuel de vérification du §17.4 effectué.
**Hors mandat** : ne pas refondre l'éditeur de blocs, ne pas toucher aux templates Jinja de `app/templates/` (le mot « templates » y désigne les gabarits HTML du serveur — voir le piège de vocabulaire en §4.4), ne pas implémenter de marketplace publique.
---
## Table des matières
1. [Résumé exécutif](#1-résumé-exécutif)
2. [Périmètre, personas et cas d'usage](#2-périmètre-personas-et-cas-dusage)
3. [Analyse fonctionnelle : les templates dans Notion](#3-analyse-fonctionnelle--les-templates-dans-notion)
4. [État des lieux Flowdeck v7.69.8, capture de Bruno et analyse d'écart](#4-état-des-lieux-flowdeck-v7698-capture-de-bruno-et-analyse-décart)
5. [Principes directeurs](#5-principes-directeurs)
6. [Vue d'ensemble du système (C4)](#6-vue-densemble-du-système-c4)
7. [Interface : sélecteur, gestionnaire, menus de base et éditeur de template](#7-interface--sélecteur-gestionnaire-menus-de-base-et-éditeur-de-template)
8. [Back-end : `TemplateService`, instanciation, variables dynamiques](#8-back-end--templateservice-instanciation-variables-dynamiques)
9. [Modèle de données et migrations 48 à 49](#9-modèle-de-données-et-migrations-48-à-49)
10. [API et événements](#10-api-et-événements)
11. [Templates de base de données : défaut, récurrence, bases liées](#11-templates-de-base-de-données--défaut-récurrence-bases-liées)
12. [Galerie système et templates fournis](#12-galerie-système-et-templates-fournis)
13. [Intégrations : agent, automatisations, réunions, recherche](#13-intégrations--agent-automatisations-réunions-recherche)
14. [Sécurité, permissions et vie privée](#14-sécurité-permissions-et-vie-privée)
15. [Plan d'implémentation par phases et tickets](#15-plan-dimplémentation-par-phases-et-tickets)
16. [Exigences non fonctionnelles, résilience, déploiement](#16-exigences-non-fonctionnelles-résilience-déploiement)
17. [Tests et critères d'acceptation](#17-tests-et-critères-dacceptation)
18. [Décisions d'architecture (ADR — résumé)](#18-décisions-darchitecture-adr--résumé)
19. [Risques et mitigations](#19-risques-et-mitigations)
20. [Questions ouvertes pour Bruno](#20-questions-ouvertes-pour-bruno)
- [Annexe A — Correspondance Notion → Flowdeck](#annexe-a--correspondance-notion--flowdeck)
- [Annexe B — Format de manifeste `flowdeck-template` v1](#annexe-b--format-de-manifeste-flowdeck-template-v1)
- [Annexe C — Glossaire](#annexe-c--glossaire)
- [Sources](#sources)
---
## 1. Résumé exécutif
Dans Notion, un template n'est pas un fichier modèle caché dans un menu : c'est un **objet de première classe, visible et gérable**. Une base possède ses propres templates, créés et édités comme des pages, listés dans le menu du bouton *New*, qu'on peut dupliquer, définir comme défaut, ou rendre récurrents. Une page vide propose des templates au démarrage, la barre latérale a une entrée *Templates* qui ouvre une galerie, et une page publiée peut être dupliquée comme template par d'autres. Le système est unifié : créer, gérer, appliquer et automatiser sont quatre vues du même objet.
**Le constat Flowdeck est presque inverse.** D'après l'architecture v7.69.8 (§10.2, §5.3), les *données* de templates existent — trois tables, un seed de bases prêtes, des lignes récurrentes, un outil agent `apply_template`, un module d'export/import `templates_io` dans l'API v2 — mais il n'y a **aucune surface de gestion** : pas d'écran qui liste les templates, pas d'endroit où les créer, les renommer, les dupliquer, choisir un défaut ou régler une récurrence autrement que par le code ou la base. Et le seul point d'entrée visible par l'utilisateur, la pilule « Templates » de la rangée *Get started with* d'une page vide (capture de Bruno, §4.2), **ne fonctionne pas** — Bruno le constate, la capture le montre figé au milieu d'entrées d'une autre nature (*Ask AI*, *AI meeting notes*, *Form*…).
**Les sept écarts structurants** (détaillés en section 4) :
| # | Écart | Nature du travail |
|---|---|---|
| **E1** | **Point d'entrée unique et cassé** : la pilule *Templates* de la page vide est le seul accès visible, et son clic est sans effet | **Diagnostic prouvé puis réparation** (phase 0), puis multiplication des points d'entrée légitimes (sélecteur depuis page vide, menu *New* des bases, menu de page, gestionnaire) |
| **E2** | **Trois silos de données sans catalogue commun** : `database_templates`, `page_templates`, `page_global_templates` (+ presets de réunion et canevas Design System à la marge), aucun registre, aucune recherche transverse | **Registre unifié `templates`** (migration 48) qui catalogue les trois silos sans les détruire, backfill idempotent (§9) |
| **E3** | **Aucune interface de gestion** : pas de liste, pas de CRUD, pas de portée (perso / workspace / teamspace), pas d'archivage, pas de compteur d'usage | **Gestionnaire de templates** (§7.2) : page dédiée `/templates` + onglet *Templates* dans la Library, sur les patrons UI existants |
| **E4** | **Aucun éditeur de template** : le contenu d'un template ne s'édite pas « comme une page » ; les propriétés par défaut d'un template de ligne ne se règlent pas dans l'UI de la base | **Édition par page-support** (§7.4, §9.3) : le contenu du template vit dans une page Flowdeck ordinaire mais masquée, éditée avec l'éditeur existant, bandeau de mode template |
| **E5** | **Instanciation fragmentée** : l'outil agent `apply_template` existe, le seed crée des bases, les lignes récurrentes ont leur mécanique — trois chemins qui ne partagent ni validation, ni journal, ni gestion d'erreur | **`TemplateService.instantiate()` unique** (§8.3), transactionnel, idempotent, journalisé dans `template_runs`, consommé par l'UI, l'API v2, l'agent et le scheduler |
| **E6** | **Valeurs dynamiques non systématisées** : Notion résout `@today`, `@now`, `@me` dans les titres et propriétés des templates ; Flowdeck a les briques (tokens de date `[[fddate:…]]`, fonctions `now`/`today` du moteur de formules) mais pas de contrat de variables pour les templates | **Contrat de variables** (§8.4) : variables système résolues à l'instanciation + variables déclarées demandées à l'utilisateur dans le sélecteur, validation par `validate_property_value()` existant |
| **E7** | **Défaut et récurrence invisibles** : un template de ligne peut être récurrent « éventuellement » (v7.69.8) mais rien ne permet de le voir, le régler ou l'arrêter ; aucun concept de template par défaut d'une base exposé dans le menu *New* | **Défaut + récurrence de première classe** (§11) : menu *New ▾* complet, éditeur RRULE réutilisant `recurrence.py`, scheduler et déduplication sur le patron des rappels |
**Recommandation principale.** Ne pas « réparer un bouton » : construire le système dont ce bouton n'est que la porte. D'abord le diagnostic (la cause du clic mort conditionne la confiance dans tout le reste de l'UI), puis le registre et le service d'instanciation, puis seulement les surfaces — sélecteur, gestionnaire, éditeur — dans cet ordre. Le plan en 5 phases (§15) rend le système **utilisable dès la phase 1** (la pilule réparée ouvre un sélecteur minimal qui applique réellement un template) et complet en phase 4.
---
## 2. Périmètre, personas et cas d'usage
### 2.1 Dans le périmètre
**Les quatre genres de templates, unifiés** (§5, principe P1) :
- **Template de page** : titre, icône, couverture et blocs d'une page libre ; proposé sur page vide, depuis le menu *New page*, et applicable à une page existante (ajout ou remplacement de contenu, avec confirmation).
- **Template de ligne** : propre à une collection ; propriétés par défaut + corps de la page-ombre de la ligne ; géré depuis la base (menu *New ▾*) et depuis le gestionnaire ; peut être **par défaut** et/ou **récurrent**.
- **Template de base** : crée une collection complète (schéma, propriétés, vues, dashboards simples, lignes d'exemple optionnelles) ; proposé à la création d'une base et dans la galerie.
- **Template de blocs** : un ensemble de blocs inséré à la position du curseur dans la page courante (successeur unifié des `page_global_templates` et, à terme, des canevas Design System — §13.4).
**La gestion** : créer (depuis zéro, depuis une page/ligne/base existante, par duplication), renommer, éditer le contenu et les réglages, changer la portée, définir/retirer le défaut, régler/arrêter la récurrence, dupliquer, archiver/restaurer, supprimer, exporter/importer, voir l'usage (nombre d'instanciations, dernière utilisation).
**L'application** : sélecteur avec recherche, aperçu et saisie des variables ; instanciation atomique avec retour visuel (navigation vers l'objet créé, ou insertion des blocs) ; journal des instanciations ; échecs explicites et récupérables.
**Les intégrations existantes** : outil agent `apply_template` rebranché sur le service ; action d'automatisation « créer depuis un template » ; presets de réunion enregistrés comme templates ; recherche Ctrl+K.
### 2.2 Hors périmètre
- Marketplace publique, templates payants, partage hors instance (le « dupliquer comme template » d'une page **publiée sur la même instance** est en phase 5 optionnelle, §15).
- Refonte de l'éditeur de blocs ou du moteur de vues (les documents compagnons couvrent les vues ; celui-ci consomme l'existant).
- Templates d'e-mails, de sites publiés ou de formulaires publics (objets distincts, déjà couverts par `sites` / `form_config_json`).
- Migration des gabarits HTML Jinja (`app/templates/`) — voir §4.4.
### 2.3 Personas et cas d'usage directeurs
| Persona | Cas d'usage directeur |
|---|---|
| **Bruno, seul sur son instance** | « Je crée une page vide, je clique *Templates*, je choisis *Revue hebdo*, la page se remplit. La semaine suivante, la ligne *Revue hebdo* apparaît toute seule dans ma base de tâches, datée du jour. » |
| **Bruno, gestion de projets dans Flowdeck** | « Ma base *Projets* a un template de ligne *Spécification* : propriétés pré-remplies (statut, priorité) et corps structuré. *New* l'applique par défaut ; *New ▾* me laisse choisir *Bug* ou *Spécification*. » |
| **Membre d'un teamspace** | « Les templates du teamspace sont visibles par les membres, invisibles dehors (404 comme les pages privées), et seuls les éditeurs de la base peuvent modifier son template par défaut. » |
| **L'agent Flowdeck** | « Quand on me demande de créer une note de réunion, j'utilise `apply_template` — le même service que l'UI — et l'action apparaît dans le journal, annulable comme mes autres actions. » |
---
## 3. Analyse fonctionnelle : les templates dans Notion
Synthèse, avec mes mots, de la documentation publique de Notion consultée le 10 octobre 2026 (liens en fin de document). Seuls les comportements qui fondent une décision de conception Flowdeck sont retenus.
### 3.1 Templates de base de données — le cœur du système
- Un template de base est **propre à une base** : il n'existe que pour elle et s'applique à ses nouvelles lignes. Chaque base peut en avoir plusieurs (par type de réunion, par type de tâche…).
- Il combine deux choses : des **valeurs de propriétés pré-remplies** et un **corps de page** structuré (titres, sections, checklists, blocs liés…) qui devient le contenu de la page de la ligne créée.
- **Création et gestion** se font depuis le menu déroulant accolé au bouton *New* de la base : *+ New template* pour créer ; chaque template listé a un menu *•••* avec **Edit**, **Duplicate**, **Set as default**, **Repeat…**, **Delete**.
- **Éditer** un template ouvre son contenu **comme une page**, dans l'éditeur ordinaire ; les modifications valent pour les créations futures, jamais rétroactivement pour les lignes existantes.
- Le template **par défaut** s'applique quand on clique *New* sans ouvrir le menu. Sans défaut, *New* crée une ligne vide.
- Un template peut être appliqué **après coup** à une page de base existante créée sans template.
- Les titres et propriétés acceptent des **mentions dynamiques** — date du jour, instant courant, utilisateur courant — résolues au moment de la création de la ligne, pas à la définition du template.
### 3.2 Templates récurrents
- Tout template de base peut être rendu **récurrent** depuis son menu (*Repeat*) : quotidien, hebdomadaire, mensuel, annuel, ou fréquence personnalisée (intervalle, jours de semaine, fin par date ou nombre d'occurrences).
- À l'échéance, une **nouvelle ligne** est créée depuis le template, avec les valeurs dynamiques résolues à cette date. On arrête la récurrence en la désactivant dans le même menu ou en supprimant le template ; les lignes déjà créées ne sont jamais touchées.
### 3.3 Templates de page et galerie
- À la création d'une page, Notion propose des **templates de démarrage** (page vide, ou partir d'un template de la galerie) ; une entrée *Templates* dans la barre latérale ouvre la **galerie** de l'espace de travail, prolongée par la galerie communautaire.
- Une page **publiée** peut autoriser sa **duplication comme template** : les visiteurs obtiennent un bouton *Duplicate* qui copie la page (ou le système de pages/bases) dans leur espace.
### 3.4 Boutons et insertion de blocs
- Notion a fondu l'ancien « template button » dans ses **boutons** génériques : un clic peut insérer des blocs, créer des pages ou modifier des entrées. Pour Flowdeck, l'équivalent fonctionnel existe déjà sous deux formes — le bloc `button` de l'éditeur (déclenche une automatisation) et la propriété `button` des bases — ce qui classe ce besoin en **intégration** (§13.2), pas en nouveau mécanisme de templates.
### 3.5 Ce que Notion ne fait pas (et que ce document ne demande pas)
- Pas de variables nommées à saisir à l'application (seules les mentions dynamiques existent) : les **variables déclarées** du §8.4 sont donc un **choix Flowdeck délibéré**, marqué comme tel — elles répondent au besoin de Bruno de paramétrer (projet, personne, échéance) sans multiplier les templates quasi identiques.
- Pas d'aperçu « diff » avant application : le sélecteur Flowdeck propose une prévisualisation du contenu résolu (§7.1), sans prétendre que Notion la fournit.
---
## 4. État des lieux Flowdeck v7.69.8, capture de Bruno et analyse d'écart
### 4.1 Ce qui existe déjà (d'après l'architecture v7.69.8)
| Brique | État décrit en v7.69.8 | Référence |
|---|---|---|
| Templates de bases prêtes | `database_templates` — bases prêtes à créer, alimentées par un seed `db_templates.py` | §10.2 |
| Templates de lignes | `page_templates` — lignes de base, « éventuellement récurrentes » | §10.2, §5.3 |
| Templates de blocs | `page_global_templates` — ensembles de blocs réutilisables | §10.2, §5.3 |
| Route legacy | `page-templates` dans le package `board/` | §4 |
| Export / import | Module `templates_io` dans l'API publique v2 (`app/routers/api_v2/`) | §3, §4 |
| Outil agent | `apply_template` parmi les 26 outils statiques du `ToolRegistry` ; chaque mutateur renvoie un snapshot d'undo | §16.3 |
| Presets de réunion | Instructions de résumé prédéfinies (auto, standup, team, sales, 1:1, interview) et modèles de notes par type de réunion prévus par le document Meetings | §18.1, doc. compagnon |
| Canevas Design System | `block_templates.py`, insertion de blocs depuis le menu + de l'assistant (phase 3 du hub, v7.53) | §16.4 |
| Briques de récurrence | `recurrence.py` (sous-ensemble RRULE : daily/weekly/monthly, interval, COUNT, UNTIL, BYDAY, fuseaux) ; rappels par scan périodique 60 s dédupliqué par journal | §19.3 |
| Briques de valeurs dynamiques | Tokens de mentions de date `[[fddate:YYYY-MM-DD[Thh:mm]]]` résolus au rendu ; fonctions `now` / `today` du moteur de formules | §14.1, §11.3 |
| Validation des propriétés | `validate_property_value()` par type + contraintes `validation_json` | §11.2 |
| Patrons d'interface réutilisables | Library à onglets avec colonnes configurables et side peek ; galerie de skills à presets installables (17 presets, format portable) ; menus contextuels de page façon Notion | §15.2, §16.3, §13.3 |
Conclusion de l'inventaire : **le contenu et les moteurs existent ; le catalogue, la gestion et les surfaces manquent.** C'est un travail d'unification et d'exposition, pas de création ex nihilo — exactement comme pour les vues (document compagnon).
### 4.2 Lecture de la capture de Bruno (10 octobre 2026)
Ce que la capture montre, élément par élément — c'est l'**état observable** du point d'entrée actuel :
- Une page libre « Untitled » ouverte dans l'éditeur (fil d'Ariane *Home / Untitled*), **vide** : le pied indique « 1 blocks » et « Saved offline ».
- Au centre-bas de la page, la rangée **« Get started with »** : pilules *Ask AI*, *AI meeting notes*, *Form*, un bouton **« ••• »**, et la pilule **Templates** (icône de page + libellé), sur laquelle le curseur en forme de main est positionné — Bruno est donc en train de cliquer dessus, ou vient de le faire.
- Un **menu déroulant est ouvert au-dessus**, listant *Table, Board, List, Timeline, Calendar, Gallery, Import* : c'est le menu de **création de base** (les types de vues + l'import), pas un menu de templates. Deux lectures possibles, à trancher par le diagnostic (§4.3) : soit ce menu appartient à une autre pilule de la rangée et il masque/perturbe la zone de clic de *Templates*, soit le clic sur *Templates* ouvre… ce menu-là, c'est-à-dire le mauvais.
- La **barre latérale** (panneau Home) ne contient **aucune entrée Templates** : sections *test, Manage Workspaces, Meetings, Recents, Favorites, Agents, Teamspaces, Shared, Published, Library, My Tasks, Trash, Help*. Dans Notion, l'entrée *Templates* de la barre latérale est un accès de premier niveau à la galerie ; ici, la Library est le seul endroit où un onglet Templates pourrait exister — il n'y en a pas (§15.2 : Recents, AI Meeting Notes, Favorites, Shared, Private, Published, Workspace, Repository).
- Aucun état « catalogue vide », aucun panneau, aucune notification visible : si des templates existent en base, rien ne les rend atteignables depuis cette page.
**Constat de Bruno, pris comme fait : le clic sur « Templates » ne fonctionne pas.** La capture corrobore au minimum ceci : même dans l'hypothèse où le clic déclencherait quelque chose, l'écran n'offre ni retour visible, ni liste, ni état vide — ce qui viole le critère transversal du brief (§0, point 5).
### 4.3 Diagnostic du bouton cassé — hypothèses à trancher par la preuve
Aucune cause n'est affirmée ici. Le tableau donne à Hermes les hypothèses **ordonnées par probabilité compte tenu des invariants connus de Flowdeck**, la vérification qui tranche chacune, et la correction attendue si elle se confirme. Le ticket T0-2 (§15) impose de toutes les passer, dans l'ordre, et de consigner le résultat.
| # | Hypothèse | Pourquoi elle est plausible chez Flowdeck | Vérification qui tranche | Correction si confirmée |
|---|---|---|---|---|
| H1 | **Aucun handler effectif** : la pilule est rendue (fragment de la rangée *Get started with*) mais son gestionnaire JS est absent, renommé ou jamais lié | La rangée mélange des actions de natures différentes (IA, réunion, formulaire, base) ; une pilule ajoutée « pour plus tard » sans câblage est le scénario le plus simple | Chercher la chaîne `Templates` / l'id de la pilule dans `static/js/page_editor_scripts.js` et les fragments `_page_editor_*.html` ; poser un point d'arrêt console sur le clic | Câbler le handler sur le sélecteur (§7.1) — pas sur un comportement ad hoc |
| H2 | **Composant Alpine non enregistré** : le clic passe par Alpine, mais le composant n'est pas déclaré via `Alpine.data` dans `base.html` | Invariant documenté de la build CSP : les enregistrements `Alpine.data` de `base.html` sont **obligatoires** ; un composant inline échoue **silencieusement** (pas d'erreur visible) | Console : avertissements Alpine ; comparer avec un composant voisin fonctionnel (*Ask AI*) ; vérifier la présence de l'enregistrement | Enregistrer le composant ; ajouter le test de régression T0-3 |
| H3 | **Endpoint absent ou en erreur** : le handler appelle une route templates inexistante (404) ou en échec (500), et l'erreur est avalée sans retour UI | Il n'existe dans l'architecture aucune route de gestion des templates côté UI (seuls `page-templates` legacy sous `/board` et `templates_io` en API v2) | Onglet réseau au clic : URL appelée, statut ; logs serveur corrélés | Créer la route du §10 ; **et** rendre toute erreur visible (toast + état d'erreur du sélecteur) |
| H4 | **Interception par le menu de création de base** : le menu *Table…Import* visible sur la capture recouvre la zone ou capte l'événement ; le clic atteint le mauvais élément | Le menu est ouvert exactement au-dessus de la pilule sur la capture ; les menus contextuels globaux (`_ctx_menu`) ont leur propre gestion de z-index et de fermeture | Inspecter l'élément au point de clic (`document.elementFromPoint`) avec le menu ouvert/fermé ; tester le clic menu fermé | Corriger la pile des menus (fermeture au clic extérieur, `pointer-events`) ; le sélecteur de templates doit être un panneau distinct, pas un menu partageant cette pile |
| H5 | **JS périmé servi par le service worker** : l'état « Saved offline » et les caches PWA peuvent servir une version de `page_editor_scripts.js` sans le handler | PWA : statique en cache-first, bump de cache à chaque release ; un décalage version HTML/JS produit des comportements « bouton mort » typiques | Comparer la version servie et la version au dépôt ; tester en navigation privée / cache désactivé | Bump de cache ; vérifier la cohérence `?v=VERSION` du fragment et du JS |
| H6 | **Abandon silencieux faute de contexte** : le handler exige un workspace/une collection que la page libre « Untitled » n'a pas, et sort sans rien afficher | La page est une page libre (pas une ligne de base) ; un handler pensé pour les templates de lignes n'aurait rien à appliquer ici | Lire le code du handler : conditions de sortie ; journaliser temporairement les sorties précoces | Le sélecteur doit s'ouvrir **quel que soit le contexte** ; le contexte filtre les genres proposés (page, blocs), il ne bloque jamais l'ouverture |
**Règle de sortie de diagnostic** : la phase 0 ne se termine que lorsque (a) le test Playwright de reproduction passe de rouge à vert pour la bonne raison (la cause consignée), et (b) le clic produit l'ouverture du sélecteur minimal de la phase 1 — ou, si la phase 1 n'est pas encore livrée, d'un panneau d'état explicite « catalogue » (même vide). Un bouton qui « ne fait plus d'erreur en console » mais n'ouvre rien n'est **pas** réparé.
### 4.4 Piège de vocabulaire à éviter pendant l'implémentation
`app/templates/` contient les **gabarits HTML Jinja** de Flowdeck (~35 fichiers) ; les routes SSR les rendent. Le présent document parle des **templates métier** (objets réutilisables par l'utilisateur). Toute nouvelle surface doit éviter la confusion : fichiers front nommés `templates_manager.html`, ` _template_picker.html`, JS `templates_manager.js` / `template_picker.js`, routes `/templates` et `/api/templates…` — jamais `template.html` seul, jamais de variable Python nommée `template` sans suffixe métier (`fd_template`, `tpl`) dans les modules partagés.
### 4.5 Les sept écarts, en détail
- **E1 — Point d'entrée unique et cassé.** Voir §4.2 et §4.3. Au-delà du bug : une fonctionnalité dont l'unique porte est une pilule contextuelle de page vide est **indécouvrable** (rien dans la sidebar, rien dans la Library, rien dans les bases). Notion multiplie les portes cohérentes : sidebar, page vide, menu *New* des bases.
- **E2 — Trois silos sans catalogue.** Impossible aujourd'hui de répondre à « quels templates ai-je ? » sans interroger trois tables aux sémantiques différentes, ni de chercher un template par son nom tous genres confondus. Le registre (§9) est la réponse ; la recherche du sélecteur et de Ctrl+K s'appuie dessus.
- **E3 — Aucune gestion.** Créer un template de ligne récurrent, aujourd'hui, suppose le seed ou la base de données. Il manque : liste, création guidée, renommage, duplication, portée, archivage, suppression, compteurs d'usage — tout ce que la galerie de skills possède déjà pour les skills (§16.3 de l'architecture), ce qui donne le patron à suivre.
- **E4 — Aucun éditeur de template.** Dans Notion, éditer un template = éditer une page. Flowdeck a un excellent éditeur de pages ; il faut un moyen d'y brancher le *contenu* d'un template sans créer une vraie page visible dans l'arbre, la recherche et les récents — d'où la page-support masquée (§9.3), qui réutilise aussi versions, temps réel et blocs synchronisés.
- **E5 — Instanciation fragmentée.** Trois chemins (seed, outil agent, récurrence des lignes) = trois comportements d'erreur et aucune journalisation commune. Le service unique (§8) factorise validation des propriétés, résolution des variables, création transactionnelle et journal `template_runs`.
- **E6 — Variables non systématisées.** Les mentions dynamiques existent comme tokens de rendu, les formules savent `now()`/`today()`, mais rien ne définit ce qui est résolu **à l'instanciation** (et figé dans l'objet créé) vs **au rendu** (et vivant). Le contrat du §8.4 tranche : un template fige ses valeurs à la création de l'objet — sinon une note de réunion « du 3 octobre » changerait de date en la rouvrant.
- **E7 — Défaut et récurrence invisibles.** Le menu *New* d'une base Flowdeck ne montre ni le défaut ni les templates disponibles ; la récurrence des `page_templates` n'a ni écran de réglage, ni prochain passage visible, ni bouton d'arrêt. §11 spécifie les trois surfaces manquantes.
---
## 5. Principes directeurs
- **P1 — Un objet, quatre genres, un registre.** Tout template, quel que soit son genre, est une ligne du registre `templates` avec un `kind`. Les surfaces ne connaissent que le registre ; les différences de genre vivent dans le manifeste (§9.2) et dans le service.
- **P2 — Le contenu reste dans les modèles existants.** Le corps d'un template de page/ligne est un contenu de page Flowdeck (blocs JSON / markdown legacy, `content_format`) porté par une page-support ; le schéma d'un template de base est un `schema_json` de collection ; les valeurs de propriétés sont un `property_values_json`. Aucun nouveau format de contenu.
- **P3 — Figer à l'instanciation.** Variables, dates dynamiques et valeurs par défaut sont résolues **au moment de créer l'objet**, dans la transaction d'instanciation. L'objet créé est ensuite un objet ordinaire, indépendant du template (le modifier ou supprimer le template ne l'affecte jamais).
- **P4 — Une instanciation, quatre appelants.** UI, API v2, agent, automatisations/récurrence : même fonction, même validation, même journal, mêmes erreurs (§8.3).
- **P5 — Aucun clic sans effet visible.** Ouverture, état vide, erreur explicite ou progression : jamais de no-op (§0 point 5, §4.3 règle de sortie).
- **P6 — Les permissions du template et de la cible se composent.** Lire le template + écrire la cible ; un template ne donne jamais accès à un contenu que son lecteur ne pourrait pas voir, et sa page-support n'apparaît dans aucune surface de navigation (§14).
- **P7 — Réutiliser les patrons d'UI existants.** Gestionnaire = patron Library (onglets, colonnes, side peek) ; galerie système = patron galerie de skills ; éditeur RRULE = patron des récurrences de propriétés ; sélecteur = patron des menus contextuels de l'éditeur. Aucun nouveau langage visuel.
- **P8 — L'historique ne casse pas.** Backfill idempotent des trois tables legacy vers le registre ; les objets créés avant la refonte restent valides ; les routes legacy `page-templates` et `templates_io` continuent de fonctionner (adaptateurs sur le service).
---
## 6. Vue d'ensemble du système (C4)
```mermaid
graph TD
subgraph Surfaces["Surfaces (SSR + JS existants étendus)"]
PICKER["Sélecteur de templates<br/>(page vide, menu de page, New ▾ de base)"]
MANAGER["Gestionnaire /templates<br/>+ onglet Library"]
TEDITOR["Éditeur de template<br/>(éditeur de page + bandeau)"]
DBMENU["Menu New ▾ d'une base<br/>(DBInstance)"]
end
subgraph Core["Noyau (nouveau)"]
REG["Registre templates<br/>(migration 48)"]
SVC["TemplateService<br/>preview / instantiate / duplicate<br/>create_from_* / resolve_variables"]
RUNS["template_runs<br/>(journal + dédup récurrence)"]
end
subgraph Existing["Existant Flowdeck réutilisé"]
HOLDER["Pages-support masquées<br/>(pages, search_excluded)"]
LEGACY["database_templates<br/>page_templates<br/>page_global_templates"]
VAL["validate_property_value()"]
REC["recurrence.py + scheduler 60 s"]
AGENT["Outil agent apply_template"]
AUTO["Automatisations (action create_from_template)"]
end
PICKER --> SVC
MANAGER --> REG
TEDITOR --> HOLDER
DBMENU --> PICKER
SVC --> REG
SVC --> HOLDER
SVC --> LEGACY
SVC --> VAL
SVC --> RUNS
REC --> SVC
AGENT --> SVC
AUTO --> SVC
```
**Flux directeur (application depuis une page vide)** : clic *Templates* → le sélecteur interroge le registre (portées visibles, genre `page`) → l'utilisateur choisit, l'aperçu résout les variables système et demande les variables déclarées → `POST /api/templates/{id}/instantiate` → `TemplateService` crée/met à jour la page dans une transaction, journalise dans `template_runs`, renvoie l'URL → le front navigue (ou reste, pour les blocs) avec un toast « Créé depuis le template X — Annuler » (l'annulation supprime l'objet créé, sur le patron des snapshots d'undo de l'agent).
---
## 7. Interface : sélecteur, gestionnaire, menus de base et éditeur de template
Quatre surfaces, toutes sur les patrons existants (P7). Les maquettes ASCII donnent la **disposition** attendue ; le style suit le Design System et les composants actuels (pilules de la rangée *Get started with*, menus contextuels de l'éditeur, tableaux de la Library).
### 7.1 Le sélecteur de templates (picker)
**Déclencheurs** (tous ouvrent le même composant, filtré par le contexte) :
| Contexte | Déclencheur | Filtre initial |
|---|---|---|
| Page vide | Pilule **Templates** de la rangée *Get started with* (**le bouton à réparer**) | `kind=page` + `kind=blocks` |
| Page quelconque | Menu *•••* de la page → *Appliquer un template…* ; et slash command `/template` dans l'éditeur | `kind=page` (mode ajout/remplacement) + `kind=blocks` (insertion au curseur) |
| Base (pleine page ou inline) | Bouton *New ▾* (§7.3) → nom d'un template, ou *Tous les templates…* | `kind=row`, `target_collection_id` = la base |
| Création de base | Menu de création de base → *Partir d'un template* | `kind=database` |
| Palette Ctrl+K | Action « Appliquer un template… » | selon la page courante |
**Disposition** (panneau modal centré, ou side peek sur écran étroit ; recherche autofocus, navigation clavier ↑/↓/Entrée/Échap) :
```text
┌─ Appliquer un template ──────────────────────────────────────── ✕ ─┐
│ 🔍 Rechercher un template… │
│ [ Tous ] [ Pages ] [ Lignes ] [ Bases ] [ Blocs ] ☐ Portée: ▾ │
├──────────────────────────────┬─────────────────────────────────────┤
│ RÉCENTS │ 📄 Revue hebdo │
│ 📄 Revue hebdo ★ │ Template de page · Workspace │
│ 🐞 Rapport de bug │ ───────────────────────────────── │
│ FAVORIS │ APERÇU (valeurs résolues) │
│ 📄 Note de réunion 1:1 ★ │ # Revue du {{date}} │
│ MON WORKSPACE │ ## Fait / En cours / Blocages │
│ 📄 Spécification produit │ ☐ Actions de la semaine │
│ 🗂 Base Projets (base) │ ───────────────────────────────── │
│ SYSTÈME │ VARIABLES │
│ 📄 Journal quotidien │ Semaine : [ 2026-W42 ] │
│ 📄 Compte rendu (système) │ Projet : [ Flowdeck ▾ ] │
│ │ │
│ + Nouveau template │ [ Annuler ] [ Utiliser ] │
├──────────────────────────────┴─────────────────────────────────────┤
│ Entrée : appliquer · ★ : favori · ••• : éditer / dupliquer / … │
└────────────────────────────────────────────────────────────────────┘
```
**Comportements obligatoires :**
- **Aperçu résolu** à droite : le contenu du template avec les variables système déjà résolues pour *maintenant* (dates, utilisateur) et les variables déclarées éditables inline ; les propriétés par défaut d'un template de ligne s'affichent en chips sous l'aperçu.
- **État vide par section** : si aucun template n'existe pour le genre filtré, la section *Système* reste proposée (galerie §12) et un bouton *+ Créer un template* est visible. Le panneau **s'ouvre toujours** (P5).
- **État d'erreur** : si l'API échoue, le panneau affiche l'erreur et un bouton *Réessayer* — c'est l'antidote direct de H3.
- Actions secondaires par ligne (menu *•••*, selon permissions) : *Éditer*, *Dupliquer*, *Renommer*, *Définir comme défaut* (lignes), *Répéter…* (lignes), *Archiver*, *Exporter*.
- Après application : toast avec *Annuler* (10 s) ; pour un template de page appliqué à la page courante vide, pas de navigation (la page se remplit en place, temps réel synchronisé) ; pour une création (ligne, base, nouvelle page), navigation vers l'objet créé via `fdNavigate`.
- **Mobile** (≤768 px) : le sélecteur devient la feuille plein écran « Insert block » déjà utilisée par l'éditeur, aperçu en second onglet.
### 7.2 Le gestionnaire de templates
**Accès** : nouvelle page **`/templates`** (SSR, `templates_manager.html` + `templates_manager.js`), atteinte par (a) une entrée **Templates** dans la sidebar, panneau Home, sous *Library* ; (b) un **onglet Templates** dans la Library (même données, patron Library : colonnes configurables par *Show columns*, sélection multiple, side peek d'aperçu) ; (c) le lien *Gérer les templates…* du sélecteur et du menu *New ▾* des bases.
```text
┌─ Templates ───────────────────────────────────── [ + Nouveau ▾ ] ─┐
│ 🔍 Rechercher… Genre: [Tous ▾] Portée: [Toutes ▾] │
│ Base: [Toutes ▾] ☐ Archivés Tri: [Dernière utilisation ▾]│
├────────────────────────────────────────────────────────────────────┤
│ Icône Nom Genre Portée Base Défaut/Répète │
│ 📄 Revue hebdo Page Workspace — — │
│ 🐞 Rapport de bug Ligne Workspace Support ★ Défaut │
│ 📅 Tâche hebdo Ligne Workspace Tâches ↻ Lun. 9 h │
│ 🗂 Projets (starter) Base Système — — │
│ ▦ Sommaire + actions Blocs Personnel — — │
├────────────────────────────────────────────────────────────────────┤
│ Ligne sélectionnée → aperçu en side peek : contenu, propriétés │
│ par défaut, variables, usage (42 utilisations, dernière le …), │
│ [ Utiliser ] [ Éditer ] [ Dupliquer ] [ Archiver ] [ Exporter ] │
└────────────────────────────────────────────────────────────────────┘
```
**Menu *+ Nouveau*** : *Template de page vierge*, *Template de ligne pour une base…* (choix de la base), *Template de base vierge*, *Template de blocs (depuis la sélection)*, *Depuis une page existante…*, *Depuis une ligne existante…*, *Importer un template (.json)*.
**Règles** : les templates **système** (§12) sont listés avec un badge, non éditables directement (*Dupliquer pour modifier*) ; l'archivage retire le template des sélecteurs sans le supprimer (restauration depuis le filtre *Archivés*) ; la suppression est un soft-delete aligné sur la corbeille unifiée (`deleted_at`, §15.4 de l'architecture) ; les compteurs d'usage viennent de `template_runs` (§9.4), jamais d'un compteur dénormalisé à maintenir à la main — une vue SQL d'agrégation suffit.
### 7.3 Le menu *New ▾* d'une base
Dans `DBInstance` (base pleine page, inline et bloc `database`) :
```text
[ New ] [ ▾ ]
┌──────────────────────────────┐
│ + Nouvelle ligne vide │
│ ──────────────────────────── │
│ 🐞 Rapport de bug ★ │ ★ = défaut (appliqué par [New])
│ 📄 Spécification │
│ 📅 Tâche hebdo ↻ Lun. │
│ ──────────────────────────── │
│ + Nouveau template… │
│ Gérer les templates… │
└──────────────────────────────┘
Sur chaque template, au survol : [ ••• ] → Éditer / Dupliquer /
Définir comme défaut / Répéter… / Supprimer
```
- Le bouton principal *New* applique le **défaut** s'il existe, sinon crée une ligne vide (comportement actuel préservé).
- Le même menu, ouvert depuis une **base liée** (`collection_data_sources`), liste les templates de la base **source** (§11.3).
- Dans la vue *Board/Calendar/Gallery/List*, le *New* d'une colonne/d'un jour pré-remplit en plus la propriété de regroupement ou de date — qui **prime** sur la valeur par défaut du template en cas de conflit, et le sélecteur l'affiche (« Date : imposée par la vue »).
### 7.4 L'éditeur de template
Éditer le contenu d'un template de page ou de ligne ouvre la **page-support** (§9.3) dans l'éditeur ordinaire, avec trois différences, et trois seulement :
```text
┌────────────────────────────────────────────────────────────────────┐
│ 🧩 Vous modifiez le template « Rapport de bug » — les changements │
│ s'appliquent aux prochaines créations. [ Terminé ] │
├────────────────────────────────────────────────────────────────────┤
│ 🐞 Rapport de {{titre}} │
│ ## Reproduction … (blocs éditables, éditeur inchangé) │
│ │
│ ── Panneau latéral « Réglages du template » ── │
│ Nom / Icône / Description / Portée : Workspace │
│ Propriétés par défaut (ligne) : Statut=[À trier] Priorité=[P2] │
│ Variables : {{titre}} texte requis · {{date}} = aujourd'hui │
│ ☐ Template par défaut de la base ↻ Répéter : [ Lun. 9 h ▾ ] │
└────────────────────────────────────────────────────────────────────┘
```
- **Bandeau de mode** permanent (composant du fragment `_page_editor_content.html`, conditionné par le contexte `template_id`), bouton *Terminé* qui ramène au gestionnaire ou à la base d'origine.
- **Panneau de réglages** (tiroir droit, même patron que les réglages de page) : métadonnées, portée, propriétés par défaut éditées avec les **éditeurs de cellule existants** de `DBInstance` (pour les lignes), variables (§8.4), défaut et récurrence (§11).
- La page-support est **exclue** de l'arbre, de la recherche (`search_excluded=1`), des récents et de la Library ; son URL directe renvoie un non-membre de la portée vers 404 (patron teamspace privé).
- Pour un template de **base**, l'édition ouvre un assistant en 3 étapes (schéma et propriétés — sur l'éditeur de schéma existant ; vues par défaut ; lignes d'exemple optionnelles), pas l'éditeur de page.
- Pour un template de **blocs**, l'édition ouvre la page-support en mode « blocs seuls » (pas de titre de page éditable).
### 7.5 La rangée *Get started with*, corrigée
La pilule *Templates* reste à sa place (c'est un bon réflexe produit, aligné sur Notion) mais son contrat change : **clic → sélecteur §7.1 filtré page/blocs, toujours**. Si la rangée affiche aussi le menu de création de base (*Table…Import* de la capture), les deux piles de menus doivent être mutuellement exclusives (ouvrir l'un ferme l'autre — antidote H4). La pilule reçoit un état de chargement bref si le registre met plus de 300 ms à répondre, jamais un silence.
---
## 8. Back-end : `TemplateService`, instanciation, variables dynamiques
### 8.1 Module et responsabilités
Nouveau service `app/services/templates.py` (logique métier, accès données — patron de l'architecture : les routeurs authentifient et rendent, les services portent la logique), routeurs :
- `app/routers/templates.py` — pages SSR (`/templates`, édition) et API interne cookie (`/api/templates…`, §10.1) ;
- extension de `app/routers/api_v2/templates_io.py` — l'import/export existant devient le transport du format de manifeste (§10.2, annexe B) ;
- le package `collections/` ne gagne qu'un endpoint mince pour le menu *New ▾* (liste des templates d'une base), qui délègue au service.
### 8.2 Fonctions du service (contrat)
```python
class TemplateService:
def list_templates(actor, workspace_id, *, kind=None, scope=None,
target_collection_id=None, query=None,
include_archived=False) -> list[TemplateDTO]
def get_template(actor, template_id) -> TemplateDTO # 404 si hors portée (P6)
def create_template(actor, spec) -> TemplateDTO # vierge ou depuis source
def create_from_page(actor, page_id, spec) -> TemplateDTO
def create_from_row(actor, collection_id, row_id, spec) -> TemplateDTO
def create_from_collection(actor, collection_id, spec) -> TemplateDTO
def update_template(actor, template_id, patch) -> TemplateDTO
def duplicate_template(actor, template_id, spec) -> TemplateDTO
def set_default(actor, template_id) -> None # exclusif par base (§11.1)
def set_recurrence(actor, template_id, rrule | None) -> None # lignes uniquement
def archive_template(actor, template_id) / restore / delete # soft-delete
def preview(actor, template_id, variables, context) -> PreviewDTO
def instantiate(actor, template_id, variables, context, *,
idempotency_key=None, run_source) -> InstantiateResult
def run_due_recurrences(now) -> list[InstantiateResult] # appelé par le scheduler
```
`context` porte la cible : `{target: {type: current_page|new_page|row|collection|insertion}, page_id?, collection_id?, parent_id?, view_hints?: {group_property, date_property}}` — les `view_hints` matérialisent la règle du §7.3 (la vue prime sur le défaut).
### 8.3 Instanciation — algorithme unique (P4)
1. **Charger et autoriser** : template visible par l'acteur (P6) ; cible inscriptible par l'acteur ; sinon erreur RFC 7807 explicite.
2. **Résoudre le manifeste** (§9.2) depuis la source du template (page-support ou silo legacy via adaptateur §9.5) ; vérifier `manifest.version`.
3. **Résoudre les variables** (§8.4) : système d'abord, déclarées ensuite (valeurs fournies ou défauts) ; toute variable requise manquante → erreur de validation listant les champs, **avant** toute écriture.
4. **Valider les propriétés par défaut** avec `validate_property_value()` contre le schéma **actuel** de la base cible ; une propriété disparue ou renommée depuis la création du template produit un **avertissement** dans le résultat (et dans l'aperçu), pas un échec — le reste s'applique (règle de dégradation §16).
5. **Créer dans une transaction** : page / ligne (+ sa page-ombre) / base (+ vues et lignes d'exemple) / insertion de blocs, avec les valeurs figées (P3). Les blocs copiés reçoivent de nouveaux identifiants ; les blocs synchronisés sont **copiés en blocs ordinaires** (jamais référencés — sinon éditer la copie modifierait la source synchronisée partout) ; les mentions de pages restent des tokens `[[fdpage:…]]` si la cible est lisible par l'acteur, sinon dégradées en texte simple.
6. **Journaliser** dans `template_runs` (§9.4) : source (`ui`, `api`, `agent`, `automation`, `recurrence`), acteur, cible créée, statut, avertissements, clé d'idempotence.
7. **Émettre** l'événement `template.applied` (webhooks sortants, catalogue existant) et, pour une ligne, les événements `collection.*` ordinaires de la création — les automatisations existantes se déclenchent donc normalement.
8. **Renvoi** : `{created_type, created_id, url, warnings[]}`. L'undo du toast (§7.1) supprime l'objet créé (soft-delete) et marque le run `undone`.
L'idempotence suit le patron API v2 existant (en-tête `Idempotency-Key`, table `idempotency_keys`, réponse rejouée) — indispensable pour le scheduler et les retries réseau.
### 8.4 Variables et valeurs dynamiques — le contrat
Deux familles, résolues à l'instanciation (P3), jamais au rendu :
**Variables système** (syntaxe dans les titres, valeurs de propriétés et textes de blocs) :
| Token | Résolution | Équivalent existant |
|---|---|---|
| `{{today}}` / `{{now}}` | Date / horodatage de l'instanciation, fuseau de l'utilisateur | `today()` / `now()` des formules |
| `{{me}}` | Utilisateur courant (propriétés `person`, texte « @Prénom ») | mentions de personnes |
| `{{date:+7d}}`, `{{date:-1w}}`, `{{date:+1mo}}` | Date décalée (jour/semaine/mois), format de sortie selon le type de la propriété cible | `dateAdd` des formules |
| `{{workspace}}`, `{{collection}}` | Nom du workspace / de la base cible | — |
| `{{title}}` | Titre saisi à la création (pour une ligne : le titre demandé dans le sélecteur) | — |
**Variables déclarées** (définies dans le panneau de réglages du template, stockées dans `templates.variables_json`, §9.2) : `{name, label, type: text|number|date|person|select, required, default?, options?, prompt?}`. Le sélecteur les rend en formulaire (§7.1) ; une variable déclarée s'utilise avec la même syntaxe `{{nom}}` dans le titre, les propriétés par défaut (par référence, pas par texte, pour les types non textuels) et les blocs.
**Frontière avec les tokens existants** : les tokens de **rendu** (`[[fdpage:…]]`, `[[fddate:…]]`) restent vivants dans l'objet créé ; les tokens `{{…}}` de **template** n'y survivent jamais — un `{{…}}` non résolu à l'instanciation est un échec de validation, pas un texte copié. Cette règle doit être testée (§17) car c'est la confusion la plus probable à l'implémentation.
---
## 9. Modèle de données et migrations 48 à 49
### 9.1 Séquencement des migrations
La version courante décrite par la v7.69.8 est **38**. Les documents compagnons proposent déjà : **39 à 43** (Agents & Skills), **44 à 47** (Vues). Les templates prennent donc **48** (registre et réglages) et **49** (journal des runs et récurrences), dans l'ordre des phases du §15 : le registre (48) en phase 1, le journal (49) dès la phase 1 aussi — il est le socle des compteurs et de la déduplication — mais sa partie récurrence n'est consommée qu'en phase 4 ; les deux migrations sont donc écrites en phase 1, dans cet ordre, pour ne jamais rééditer une migration inférieure (règle déjà établie par le document Agents & Skills v1.1). Comme toujours : `@register(version, name)`, une migration = une transaction, backfill séparé et idempotent (le backfill du §9.5 est une fonction du service appelée au boot si le registre est vide, pas du DDL).
### 9.2 Table `templates` — le registre (migration 48)
```sql
CREATE TABLE templates (
id TEXT PRIMARY KEY, -- tpl_<uuid court>
workspace_id INTEGER NOT NULL REFERENCES workspaces(id),
teamspace_id INTEGER REFERENCES teamspaces(id),
kind TEXT NOT NULL CHECK (kind IN ('page','row','database','blocks')),
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
icon TEXT,
cover_url TEXT,
category TEXT NOT NULL DEFAULT '', -- libre : Réunion, Projet, Perso…
scope TEXT NOT NULL DEFAULT 'workspace'
CHECK (scope IN ('personal','workspace','teamspace','system')),
owner_id INTEGER REFERENCES users(id), -- requis si scope='personal'
target_collection_id TEXT, -- requis si kind='row' ; la base propriétaire
content_page_id INTEGER REFERENCES pages(id), -- page-support (§9.3), kinds page/row/blocks
source_kind TEXT NOT NULL DEFAULT 'native'
CHECK (source_kind IN ('native','legacy_database','legacy_page','legacy_global')),
source_id TEXT, -- id dans le silo legacy, si source_kind != 'native'
manifest_json TEXT NOT NULL DEFAULT '{}', -- schémas/valeurs par genre (§9.2.1)
variables_json TEXT NOT NULL DEFAULT '[]', -- variables déclarées (§8.4)
is_default INTEGER NOT NULL DEFAULT 0, -- kind='row' uniquement, exclusif par base
is_system INTEGER NOT NULL DEFAULT 0, -- galerie fournie (§12), non éditable
position INTEGER NOT NULL DEFAULT 0, -- ordre dans les menus
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
archived_at TEXT,
deleted_at TEXT, -- soft-delete, corbeille unifiée
sync_version INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_templates_ws_kind ON templates(workspace_id, kind) WHERE deleted_at IS NULL;
CREATE INDEX idx_templates_target ON templates(target_collection_id) WHERE kind='row';
CREATE UNIQUE INDEX uq_templates_default
ON templates(target_collection_id) WHERE is_default=1 AND deleted_at IS NULL AND archived_at IS NULL;
```
#### 9.2.1 Contenu de `manifest_json`, par genre
Le manifeste décrit **tout ce qui n'est pas le corps de blocs** (le corps vit dans la page-support, P2) :
```jsonc
// kind = 'row'
{
"format": "flowdeck-template", "version": 1,
"property_defaults": { "Status": "À trier", "Priority": "P2",
"DueDate": "{{date:+7d}}", "Assignee": ["{{me}}"] },
"title_template": "Bug — {{titre}}"
}
// kind = 'page'
{ "format": "flowdeck-template", "version": 1,
"title_template": "Revue du {{today}}", "full_width": false, "font_small": false }
// kind = 'database'
{ "format": "flowdeck-template", "version": 1,
"collection": { "name": "Projets", "icon": "🗂",
"schema_json": { /* schéma déclaratif existant */ },
"properties": [ /* définitions collection_properties */ ],
"views": [ { "view_type": "board", "config_json": { } } ],
"is_task": true, "task_mapping": { } },
"seed_rows": [ { "template_ref": "tpl_…", "variables": { } } ], // lignes d'exemple, optionnel
"child_pages": [ { "title": "Guide", "content_page_id": 0 } ] } // pages compagnes, optionnel
// kind = 'blocks'
{ "format": "flowdeck-template", "version": 1, "insert_mode": "at_cursor" }
```
### 9.3 La page-support — éditer un template comme une page (E4)
Chaque template natif de genre `page`, `row` ou `blocks` possède une **page-support** : une ligne ordinaire de `pages`, créée par le service, qui porte son contenu (blocs/markdown, `content_format`, icône, couverture). Quatre garde-fous la rendent invisible et sûre :
1. `pages.search_excluded = 1` (drapeau existant) et exclusion explicite de l'arbre sidebar, des récents, de la Library et de la FTS (les requêtes de ces surfaces filtrent déjà `search_excluded` pour la recherche ; le ticket T3-2 ajoute le filtre aux listes) ;
2. elle n'a **pas** de `parent_id` de navigation (racine technique du workspace), son accès passe **uniquement** par le registre : la route d'édition vérifie la visibilité du template (P6) avant de servir l'éditeur, l'URL directe de la page renvoie 404 aux non-ayants droit ;
3. ses **versions** (`page_versions`) donnent gratuitement l'historique du template ; la restauration d'une version = nouvelle version du template ;
4. à la suppression du template, la page-support suit le même soft-delete (et la même restauration).
Pour `kind='database'`, pas de page-support unique : le schéma vit dans `manifest_json` ; les éventuelles pages compagnes ont chacune leur page-support référencée dans le manifeste.
*Alternative écartée* : stocker les blocs directement dans `manifest_json`. Elle aurait imposé un éditeur de blocs dédié ou une sérialisation parallèle — contraire à P2/P7 — et perdu versions, temps réel et blocs synchronisés.
### 9.4 Tables `template_runs` et récurrences (migration 49)
```sql
CREATE TABLE template_recurrences (
template_id TEXT PRIMARY KEY REFERENCES templates(id),
rrule TEXT NOT NULL, -- sous-ensemble RRULE de recurrence.py
timezone TEXT NOT NULL, -- zoneinfo de l'utilisateur propriétaire
enabled INTEGER NOT NULL DEFAULT 1,
next_run_at TEXT, -- UTC, recalculé après chaque passage
last_run_at TEXT,
end_count INTEGER, -- miroir COUNT/UNTIL pour l'affichage
updated_by INTEGER REFERENCES users(id),
updated_at TEXT NOT NULL
);
CREATE TABLE template_runs (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL REFERENCES templates(id),
run_source TEXT NOT NULL CHECK (run_source IN
('ui','api','agent','automation','recurrence')),
actor_id INTEGER REFERENCES users(id), -- NULL = scheduler
occurrence_key TEXT, -- récurrence : '<template_id>@<occurrence ISO>'
created_type TEXT, -- page | row | collection | insertion
created_id TEXT,
status TEXT NOT NULL CHECK (status IN ('ok','error','undone')),
warnings_json TEXT NOT NULL DEFAULT '[]',
error TEXT,
idempotency_key TEXT,
created_at TEXT NOT NULL
);
CREATE UNIQUE INDEX uq_template_runs_occurrence
ON template_runs(occurrence_key) WHERE occurrence_key IS NOT NULL;
CREATE UNIQUE INDEX uq_template_runs_idem
ON template_runs(idempotency_key) WHERE idempotency_key IS NOT NULL;
```
`occurrence_key` unique = la déduplication des récurrences (même patron que `reminder_log` pour les rappels) ; `template_runs` sert aussi les compteurs du gestionnaire (§7.2) et l'audit.
### 9.5 Backfill des silos legacy (P8)
Fonction `TemplateService.backfill_registry()`, idempotente (clé : `source_kind + source_id`), exécutée au premier boot après la migration 48 et rejouable depuis l'admin :
| Source legacy | Ligne de registre créée |
|---|---|
| `database_templates` (seed `db_templates.py`) | `kind='database'`, `scope='system'` si issue du seed, `source_kind='legacy_database'`, manifeste construit depuis la définition du seed |
| `page_templates` (par base, récurrentes ou non) | `kind='row'`, `target_collection_id` de la base propriétaire, `source_kind='legacy_page'` ; la récurrence existante est transcrite dans `template_recurrences` (RRULE) et `next_run_at` recalculé |
| `page_global_templates` | `kind='blocks'`, `source_kind='legacy_global'`, page-support créée par copie des blocs |
| Presets de réunion (document Meetings) | `kind='page'`, `category='Réunion'`, `scope='system'` — simple enregistrement, le pipeline de réunion reste propriétaire de son exécution (§13.3) |
Tant qu'un adaptateur legacy n'a pas été remplacé par le natif (phase 2), `instantiate()` lit le contenu **depuis le silo d'origine** pour les lignes `source_kind != 'native'` : le registre catalogue, il ne déplace pas. La convergence des contenus vers le natif (pages-supports) se fait à la première **édition** du template via le nouvel éditeur, jamais par une migration silencieuse.
---
## 10. API et événements
### 10.1 API interne (cookie, consommée par le front)
```text
GET /api/templates?kind=&scope=&target_collection_id=&q=&include_archived=
GET /api/templates/{id} → DTO + aperçu du contenu (blocs résumés)
POST /api/templates → créer (vierge ou {from_page_id|from_row|from_collection})
POST /api/templates/{id}/duplicate
PATCH /api/templates/{id} → nom, description, icône, catégorie, portée, position
DELETE /api/templates/{id} → soft-delete (corbeille)
POST /api/templates/{id}/archive | /restore
PUT /api/templates/{id}/property-defaults → (row) manifeste des valeurs par défaut
PUT /api/templates/{id}/variables → variables déclarées
POST /api/templates/{id}/set-default → (row) défaut exclusif de la base
PUT /api/templates/{id}/recurrence → {rrule, timezone, enabled} ou null
POST /api/templates/{id}/preview → {variables, context} → contenu résolu, sans écrire
POST /api/templates/{id}/instantiate → {variables, context, Idempotency-Key} → §8.3 pas 8
GET /api/templates/{id}/runs → journal (gestionnaire)
GET /api/collections/{id}/templates → menu New ▾ (mince, délègue au service)
```
Erreurs au format des API internes existantes ; l'aperçu et l'instanciation renvoient les `warnings[]` (propriétés disparues, mentions dégradées) en plus du résultat.
### 10.2 API publique v2 (Bearer, parité avec l'interne)
Même contrat sous `/api/v2/templates…` (scopes `read`/`write`), construit sur `templates_io` existant : liste/détail en `read` ; instantiate/duplicate en `write` ; **export** `GET /api/v2/templates/{id}/export` et **import** `POST /api/v2/templates/import` au format de l'annexe B (RFC 7807, idempotence, audit `api_audit_log`, rate limiting par jeton — tout le contrat §4.1 de l'architecture s'applique sans exception). L'outil agent et les automatisations n'utilisent **pas** HTTP : ils appellent le service en processus (P4).
### 10.3 Événements
Nouveaux événements au catalogue des webhooks sortants : `template.created`, `template.updated`, `template.applied`, `template.deleted`, `template.recurrence_fired`, `template.recurrence_failed`. Le bus d'automatisations reçoit `template.applied` via `fire_event()` (patron `form.submitted`, `meeting.summarized`).
---
## 11. Templates de base de données : défaut, récurrence, bases liées
### 11.1 Template par défaut
- Au plus **un** défaut actif par base (index unique partiel §9.2). `set_default` est transactionnel : il retire l'ancien défaut et pose le nouveau dans la même transaction.
- Le défaut s'applique : au bouton *New* principal, à la création par l'agent sans template précisé **uniquement si** l'utilisateur l'a demandé ainsi (l'agent ne doit pas changer de comportement silencieusement — par défaut l'outil garde son comportement actuel : pas de template sauf demande), et aux créations par automatisation `create_page` sur la base **si** l'étape coche « appliquer le template par défaut » (défaut : non coché). Ces deux règles évitent le piège classique du défaut qui se met à créer du contenu partout.
- Retirer le défaut : menu *•••* du template → *Retirer le défaut*, ou réglage dans le panneau du template.
### 11.2 Récurrence
- Réglage depuis le menu *•••* du template (base ou gestionnaire) → **éditeur RRULE** reprenant les contrôles des récurrences de propriétés existantes (fréquence, intervalle, jours, fin par date/nombre), stocké dans `template_recurrences`, fuseau de l'utilisateur qui règle.
- Exécution : le scheduler mutualisé (60 s, celui des rappels et des agents planifiés) appelle `run_due_recurrences()` : pour chaque récurrence due et activée, calcul de l'occurrence par `recurrence.py`, instanciation avec `run_source='recurrence'` et `occurrence_key` dédupliqué, recalcul de `next_run_at`. Un échec journalise `template.recurrence_failed`, notifie le propriétaire (notification in-app, patron rappels) et **ne boucle pas** : le prochain passage est la prochaine occurrence, pas un retry immédiat.
- Le gestionnaire et le menu de base affichent l'état (« ↻ Lun. 9 h — prochain : 12 oct. ») et permettent pause/reprise/suppression de la récurrence sans toucher au template.
- Une récurrence dont la base cible est archivée/supprimée se désactive elle-même (et le journal le dit).
### 11.3 Bases liées et vues
Les templates appartiennent à la base **source** ; une base liée les expose en lecture/application (création dans la source), jamais en édition depuis la liée. Les `view_hints` (§8.2) couvrent Board (propriété de regroupement), Calendar (propriété de date), Gallery/List (aucun hint) ; en vue Table sans hint, défaut et valeurs du template s'appliquent tels quels.
---
## 12. Galerie système et templates fournis
Sur le patron de la galerie de skills (presets installables, §16.3 de l'architecture) :
- Les `database_templates` du seed actuel deviennent les premiers templates `scope='system'`, `kind='database'`, **sans changement de contenu**.
- Nouveaux presets `kind='page'` fournis (contenus courts, en français comme l'UI) : *Note de réunion* (alignée sur les presets du document Meetings), *Revue hebdo*, *Spécification produit*, *Rapport de bug* (en `kind='row'` de démonstration sur une base créée par le preset *Projets*), *Journal quotidien* (avec `{{today}}` dans le titre), *Décision (ADR)*, *1:1* — la liste exacte est un livrable du ticket T4-1, validée par Bruno.
- Un template système n'est jamais édité en place : *Dupliquer pour modifier* crée une copie `scope='personal'` (ou workspace selon le rôle). « Installer » un preset de page = le dupliquer dans la portée de l'utilisateur ; les presets de base s'utilisent directement (l'instanciation crée une base neuve, elle ne modifie pas le preset).
- Les mises à jour de presets entre versions de Flowdeck ne touchent que les lignes `is_system=1` jamais dupliquées.
---
## 13. Intégrations : agent, automatisations, réunions, recherche
### 13.1 Agent Flowdeck
L'outil `apply_template` du `ToolRegistry` est **rebranché** sur `TemplateService.instantiate()` (même validation, même journal `template_runs` avec `run_source='agent'`, en plus du journal `agent_actions` et de son snapshot d'undo existants). Ajouter un outil de lecture `list_templates` (lecture seule, filtré par les permissions de l'utilisateur appelant — l'agent n'agit jamais au-delà, §6.1 de l'architecture) pour que l'agent puisse proposer le bon template au lieu de l'exiger nommé. `AgentPolicies.check_tool()` s'applique aux deux, comme à tout outil.
### 13.2 Automatisations
Nouveau type d'action de step : **`create_from_template`** (9e type, après les 8 existants §19.1 de l'architecture) : paramètres `{template_id, variables figées ou mappées depuis le trigger, cible}`. Elle hérite de tout le moteur (conditions, délais, journal de run). Le bloc `button` de l'éditeur et la propriété `button` des bases peuvent la déclencher via leurs automatisations existantes — c'est la réponse Flowdeck au « template button » de Notion (§3.4), sans nouveau mécanisme.
### 13.3 Réunions
Les presets de notes de réunion existants (standup, 1:1…) sont **enregistrés** au registre (§9.5) pour être découvrables dans le sélecteur et le gestionnaire, mais le pipeline *AI Meeting Notes* (bloc `meeting`, `run_processing()`) reste le propriétaire de leur exécution : le template de réunion, appliqué depuis le sélecteur, crée la page avec le bloc `meeting` pré-configuré du bon preset — pas de second moteur de résumé.
### 13.4 Design System et canevas
Les canevas `design_system` (`block_templates.py`) sont des templates de blocs spécialisés (mise en page). Phase 5 : les enregistrer au registre (`kind='blocks'`, `category='Design System'`) pour les rendre trouvables dans le sélecteur, en gardant leur service d'insertion actuel comme adaptateur — même patron que les silos legacy.
### 13.5 Recherche et palette
Ctrl+K : les templates visibles par l'utilisateur deviennent cherchables par nom (requête sur le registre, pas d'index FTS dédié nécessaire à cette échelle), avec l'action *Appliquer* / *Ouvrir dans le gestionnaire*. La recherche sémantique/Ask AI n'indexe **pas** les pages-supports (cohérent avec leur exclusion, §9.3).
---
## 14. Sécurité, permissions et vie privée
- **Visibilité par portée** : `personal` → propriétaire seul ; `workspace` → membres du workspace ; `teamspace` → membres du teamspace (hors teamspace privé : 404, patron existant) ; `system` → tous les utilisateurs de l'instance, lecture seule.
- **Écriture** : créer/éditer un template `workspace` exige le rôle éditeur du workspace ; pour un `kind='row'`, éditer le template (et surtout changer le défaut ou la récurrence) exige le droit d'édition **de la base cible** — pas seulement du workspace.
- **Application** : double contrôle (lire le template, écrire la cible), §8.3 pas 1. Un template ne sert jamais de cheval de Troie : les propriétés par défaut qui référencent des personnes/pages inaccessibles à l'applicateur sont dégradées en avertissements (§8.3 pas 4-5).
- **Page-support** : jamais exposée par les API de navigation/recherche/Library ; les endpoints de l'éditeur vérifient l'appartenance au registre avant de servir son contenu (ticket T3-2, test dédié §17).
- **Contenu importé** : l'import de manifeste (annexe B) valide le schéma, borne la taille et la profondeur (sous-pages/blocs imbriqués), et neutralise les blocs à risque comme le fait l'import existant (SSRF sur `embed`/`bookmark` : gardien `url_fetch` existant). Un template importé arrive en `scope='personal'`, jamais `system`.
- **Audit** : créations/modifications via l'API interne et v2 suivent les journaux existants (`api_audit_log` en v2) ; `template_runs` couvre les instanciations, y compris celles du scheduler (acteur système explicite).
---
## 15. Plan d'implémentation par phases et tickets
Ordre contraignant : **0 diagnostic → 1 registre + sélecteur minimal → 2 gestion → 3 éditeur → 4 galerie/récurrences/intégrations**, phase 5 optionnelle. Chaque ticket est livrable et testable séparément ; les critères d'acceptation sont cumulatifs avec §17.
### Phase 0 — Diagnostic du bouton cassé (prérequis, petit)
| Ticket | Contenu | Acceptation |
|---|---|---|
| T0-1 | **Audit de l'existant réel** : inventorier dans le code les handlers de la rangée *Get started with*, les routes `page-templates` (board) et `templates_io` (v2), et les schémas exacts des trois tables legacy (colonnes réelles, via `docs/DATA_MODEL.md` et la base) ; consigner les écarts avec §4.1 | Note d'audit jointe au dépôt ; tout écart avec ce document est signalé avant la phase 1 |
| T0-2 | **Diagnostic H1→H6** (§4.3) dans l'ordre, avec preuves (console, réseau, `elementFromPoint`, comparaison de version SW) | Cause racine consignée par écrit ; si aucune hypothèse ne se confirme, escalade à Bruno avec les traces — pas de correction spéculative |
| T0-3 | **Test de reproduction Playwright** : ouvrir une page vide, cliquer la pilule *Templates*, attendre un panneau visible ; le test échoue avant correctif, passe après | Test rouge→vert pour la cause du T0-2 ; intégré à la suite E2E existante |
| T0-4 | **Correctif minimal de la cause** + garde-fou P5 : en attendant la phase 1, le clic ouvre un panneau d'état du catalogue (même vide, avec *Créer un template* désactivé proprement si la création n'existe pas encore — jamais un no-op) | Parcours manuel §17.4, étape 1 |
### Phase 1 — Registre unifié, service d'instanciation, sélecteur minimal (MVP utilisable)
| Ticket | Contenu | Acceptation |
|---|---|---|
| T1-1 | Migrations **48** (`templates`) et **49** (`template_recurrences`, `template_runs`) — §9 | Migrations appliquées sur une copie de base v7.69.8 ; idempotentes ; rollback documenté |
| T1-2 | `TemplateService` : `list/get/create/update/duplicate/archive`, adaptateurs legacy (§9.5), backfill idempotent rejouable | Backfill sur données du seed : registre complet, zéro doublon au second passage |
| T1-3 | `instantiate()` transactionnel pour `page` et `row` (§8.3), résolution des variables système (§8.4), validation des propriétés, journal `template_runs`, idempotence | Tests unitaires §17.2 ; un échec en cours de création ne laisse aucun objet partiel |
| T1-4 | API interne §10.1 (liste, détail, preview, instantiate) ; API v2 : liste/détail/instantiate | Contrat testé ; erreurs RFC 7807 en v2 |
| T1-5 | **Sélecteur minimal** (§7.1, sans l'aperçu riche : liste + recherche + variables déclarées en formulaire simple) branché sur : pilule *Templates* de page vide (**remplace le panneau d'état du T0-4**), menu *•••* de page, *New ▾* de base en lecture | Le parcours directeur de Bruno (§2.3, persona 1) fonctionne de bout en bout |
| T1-6 | Rebranchement de l'outil agent `apply_template` sur le service + outil `list_templates` (§13.1) | Un run agent créant depuis un template apparaît dans `template_runs` et dans `agent_actions` |
### Phase 2 — Gestionnaire et gestion par base
| Ticket | Contenu | Acceptation |
|---|---|---|
| T2-1 | Page `/templates` (§7.2) : liste filtrable (genre, portée, base, archivés), recherche, tri, sélection, side peek d'aperçu, compteurs depuis `template_runs` | Toutes les opérations de gestion (§2.1) faisables sans SQL ni API manuelle |
| T2-2 | Onglet **Templates** dans la Library (mêmes données, patron Library) + entrée **Templates** dans la sidebar (panneau Home, sous *Library*, respectant `sidebar_config`) | L'onglet et la page affichent des données identiques |
| T2-3 | Création depuis le gestionnaire : vierge (page/row/blocks), *depuis une page/ligne/base existante* (`create_from_*`), import JSON (annexe B) | Un template créé depuis une page existante reproduit fidèlement titre/icône/blocs à l'instanciation |
| T2-4 | Menu *New ▾* complet (§7.3) : défaut marqué, *•••* par template, *+ Nouveau template…* (crée et ouvre l'éditeur phase 3 en mode minimal : nom + propriétés par défaut), *Gérer les templates…* filtré sur la base | Le défaut s'applique par *New* ; l'exclusivité du défaut tient en concurrence (deux réglages simultanés) |
| T2-5 | Portées et partage : réglage de portée dans le gestionnaire, contrôles §14 (dont 404 hors teamspace privé) | Matrice de tests §17.3 verte |
### Phase 3 — Éditeur de template complet
| Ticket | Contenu | Acceptation |
|---|---|---|
| T3-1 | Page-support à la création des templates natifs (§9.3) + route d'édition avec bandeau de mode et bouton *Terminé* (§7.4) | Éditer un template = éditer une page ; historique des versions visible et restaurable |
| T3-2 | Masquage complet de la page-support : arbre, récents, Library, recherche, Ask AI, backlinks ; 404 direct hors ayants droit | Tests dédiés §17.3 ; aucune fuite du contenu d'un template personnel via la recherche d'un autre utilisateur |
| T3-3 | Panneau de réglages : métadonnées, portée, **propriétés par défaut** avec les éditeurs de cellule de `DBInstance` (row), **variables déclarées** (CRUD + aperçu de résolution), défaut | L'aperçu du sélecteur (T3-5) reflète exactement ce qui sera créé |
| T3-4 | Assistant d'édition `kind='database'` (schéma, vues, lignes d'exemple) ; édition `kind='blocks'` en mode blocs seuls | Un template de base édité crée une base conforme au schéma affiché |
| T3-5 | **Aperçu résolu** dans le sélecteur (§7.1) : contenu résolu, chips de propriétés, avertissements ; modes *ajouter / remplacer* sur page existante avec confirmation explicite pour *remplacer* | Aucun `{{token}}` non résolu ne peut atteindre l'objet créé (§8.4) |
| T3-6 | Application après coup à une ligne existante (§3.1) : depuis la page de ligne, *Appliquer un template…* (propriétés : ne remplit que les vides par défaut, case « écraser » explicite ; corps : ajouté à la fin) | Comportement par défaut non destructif, testé |
### Phase 4 — Récurrences, galerie système, automatisations
| Ticket | Contenu | Acceptation |
|---|---|---|
| T4-1 | Galerie système (§12) : presets convertis + nouveaux presets validés par Bruno, badges, *Dupliquer pour modifier* | Un nouvel utilisateur trouve ≥ 5 templates utilisables dès l'installation |
| T4-2 | Récurrences complètes (§11.2) : éditeur RRULE, affichage « prochain passage », pause/reprise, branchement scheduler + déduplication + notification d'échec | Une tâche hebdo de test se crée à l'heure dite, une seule fois, même si le scheduler redémarre entre-temps |
| T4-3 | Action d'automatisation `create_from_template` (§13.2) + option « template par défaut » des actions `create_page` (§11.1) | Un bouton de page et un bouton de base déclenchent chacun une création depuis template, journalisée |
| T4-4 | Enregistrement des presets de réunion au registre (§13.3) ; export/import v2 complets (§10.2) ; événements webhooks §10.3 | Export d'un template → import sur une autre instance → instanciation identique |
### Phase 5 — Extensions (optionnelle)
| Chantier | Contenu |
|---|---|
| A | *Dupliquer comme template* sur les pages publiées de l'instance (réglage dans *Share → Publish*, bouton *Duplicate* sur la page publique, copie vers le workspace du visiteur connecté) |
| B | Canevas Design System enregistrés au registre (§13.4) |
| C | Templates de dashboard (une mise en page `collection_dashboards` comme contenu de template de base) — à coordonner avec le document Vues (E4 Dashboard) |
| D | Suggestions de templates par l'agent (« cette page ressemble à un format récurrent, l'enregistrer comme template ? ») — proposition uniquement, jamais de création silencieuse |
---
## 16. Exigences non fonctionnelles, résilience, déploiement
- **Performance** : liste du registre filtrée < 100 ms à 1 000 templates (index §9.2) ; le sélecteur ne charge jamais les corps de blocs en liste (aperçu à la demande, un template à la fois) ; instanciation d'un template de page < 500 ms hors pièces jointes.
- **Dégradation** (§8.3 pas 4) : schéma de base ayant dérivé depuis la création du template → appliquer ce qui reste valide + avertissements nommés ; jamais d'échec total pour une propriété renommée, jamais d'application silencieusement partielle sans avertissement.
- **Hors ligne** : le sélecteur en mode offline affiche les métadonnées mises en cache mais **désactive** l'instanciation avec un message explicite (les écritures restent à la file de sync existante seulement pour les objets ordinaires ; une instanciation n'est pas rejouable telle quelle hors ligne). Le bouton ne doit en aucun cas redevenir « mort » hors ligne — c'est un cas de test (§17).
- **Fichiers et médias** : un template référençant des uploads (images, pièces jointes) les **copie** à l'instanciation (nouveaux fichiers), sur le patron de la duplication de page existante ; jamais de référence partagée dont la suppression du template casserait les objets créés.
- **Déploiement** : aucun nouveau service, aucune dépendance ; migrations dans le flux de boot existant ; le backfill est journalisé (nombre de lignes par `source_kind`) et son échec ne bloque pas le démarrage (le registre reste vide, les silos legacy continuent de servir leurs anciennes surfaces — dégradation, pas panne).
---
## 17. Tests et critères d'acceptation
### 17.1 Régression du bug d'origine
- Le test Playwright du T0-3 reste dans la suite : page vide → clic *Templates* → sélecteur visible. Variantes : catalogue vide, hors ligne, utilisateur sans aucun template personnel. **Dans tous les cas, un panneau visible.**
### 17.2 Tests unitaires / d'intégration du service
- Résolution des variables : système (dates au fuseau de l'utilisateur), déclarées (requise manquante → erreur avant écriture ; défaut appliqué), offsets de dates, `{{me}}` dans une propriété `person`.
- Aucun token `{{…}}` survivant dans un objet créé (assertion sur titre, propriétés et blocs).
- Propriétés par défaut : valide, renommée (avertissement), type changé (avertissement), relation vers cible inaccessible (dégradée).
- Transaction : panne simulée au milieu de la création d'une base depuis template → ni base ni lignes orphelines ; `template_runs` en `error`.
- Idempotence : double `instantiate` même clé → un seul objet, réponse rejouée.
- Défaut : exclusivité sous concurrence ; *New* applique le défaut ; bases liées : lecture des templates de la source.
- Récurrence : occurrence calculée au bon fuseau ; redémarrage du scheduler entre deux passages → pas de doublon (`occurrence_key`).
### 17.3 Tests de permissions
Matrice portée × rôle (propriétaire / membre workspace / membre teamspace / extérieur / agent au nom de chacun) sur : voir dans le sélecteur, éditer, changer le défaut et la récurrence, appliquer, accéder à l'URL directe de la page-support, chercher le contenu d'un template personnel d'autrui.
### 17.4 Parcours manuel de vérification (à exécuter par Hermes, résultat consigné)
1. Page vide → *Templates* → le sélecteur s'ouvre (le bug d'origine, refermé).
2. Créer un template de page depuis la page courante ; l'appliquer à une nouvelle page ; vérifier titre daté résolu et blocs.
3. Dans une base de tâches : créer deux templates de ligne, en définir un comme défaut, vérifier *New* vs *New ▾*.
4. Rendre un template récurrent (quotidien) ; forcer le passage du scheduler ; vérifier **une** ligne créée, datée du jour.
5. Ouvrir le gestionnaire `/templates` et l'onglet Library : mêmes données, compteurs à jour.
6. Demander à l'agent de créer une note depuis un template nommé ; vérifier `template_runs` (`run_source='agent'`) et l'undo.
---
## 18. Décisions d'architecture (ADR — résumé)
| # | Décision | Alternative écartée | Motif |
|---|---|---|---|
| ADR-T1 | **Registre `templates` fédérant les silos**, contenus conservés à leur place puis convergés à l'édition | Fusion immédiate des trois tables en une seule | Zéro perte, livrable en phase 1, conforme à P8 |
| ADR-T2 | **Contenu des templates de page/ligne dans une page-support masquée** | Blocs sérialisés dans `manifest_json` | Réutilise éditeur, versions, temps réel ; aucun éditeur parallèle (P2, P7) |
| ADR-T3 | **Instanciation unique dans `TemplateService`**, HTTP seulement pour les surfaces externes | Logique par surface (UI, agent, scheduler) | L'invariant « une seule implémentation » déjà appliqué à l'API agent de Flowdeck |
| ADR-T4 | **Variables `{{…}}` figées à l'instanciation**, distinctes des tokens de rendu `[[…]]` | Résolution au rendu | Un objet créé depuis un template doit être stable dans le temps (P3) |
| ADR-T5 | **Récurrence portée par le template** (table dédiée + scheduler 60 s + dédup par `occurrence_key`) | Une automatisation générée par récurrence | Le réglage « Repeat » de Notion est une propriété du template ; le patron rappels prouve le mécanisme dans Flowdeck |
| ADR-T6 | **Gestionnaire = page `/templates` + onglet Library**, pas un écran Settings | Réglages dans Settings | Les templates sont des objets de travail quotidiens, pas de la configuration ; Library est le patron « toutes mes ressources » |
| ADR-T7 | **Le défaut ne s'applique pas implicitement à l'agent ni aux automatisations existantes** (opt-in explicite, §11.1) | Défaut global à toute création | Éviter qu'un réglage d'UI change silencieusement le comportement de systèmes déjà en production chez Bruno |
---
## 19. Risques et mitigations
| Risque | Mitigation |
|---|---|
| La cause du bouton cassé est plus profonde (fragment partagé par d'autres pilules de la rangée) | Phase 0 avant tout ; le test T0-3 couvre la rangée entière, pas seulement *Templates* |
| Fuite de contenu via la page-support (recherche, backlinks, partage) | Garde-fous §9.3 + matrice §17.3 ; revue spécifique du ticket T3-2 |
| Dérive des schémas de bases rendant les templates de ligne partiellement inapplicables | Règle de dégradation §16 + avertissements dans l'aperçu et le résultat ; le gestionnaire signale les templates « à revoir » (propriété par défaut introuvable) |
| Doublons de récurrence après redémarrage ou double instance | `occurrence_key` unique + scheduler mono-processus existant |
| Confusion des deux sens du mot « template » (Jinja vs métier) chez les contributeurs | Convention de nommage §4.4, à ajouter à `AGENTS.md` du dépôt Flowdeck |
| Le périmètre gonfle vers un marketplace | Hors périmètre §2.2 ; la phase 5 est explicitement optionnelle et fermée |
---
## 20. Questions ouvertes pour Bruno
1. **Presets système** : la liste de départ du §12 (7 presets) te convient-elle, ou veux-tu tes propres formats dès la phase 4 (par ex. tes formats de notes Flowdeck/ObsiGate) ?
2. **Portée par défaut d'un nouveau template** : `personal` (sûr, mais invisible aux autres membres) ou `workspace` (pratique en équipe) ? Proposition du document : `personal`, promotion explicite — à confirmer vu que tu es souvent seul sur l'instance.
3. **Application sur page non vide** : le mode par défaut du sélecteur doit-il être *ajouter à la fin* (proposition, non destructif) ou demander à chaque fois ?
4. **Récurrence et défaut des bases de tâches liées à My Tasks** : un template récurrent sur une base de tâches doit-il hériter du mapping `is_task` (assigné/statut/échéance) automatiquement ? Proposition : oui, via les `view_hints` et le mapping existant, sans réglage supplémentaire.
5. **Nom de la route du gestionnaire** : `/templates` est proposé ; si une route legacy `page-templates` sous `/board` doit rester l'URL canonique pour compatibilité de liens existants, le signaler avant la phase 2.
---
## Annexe A — Correspondance Notion → Flowdeck
| Notion | Flowdeck (cible) |
|---|---|
| Template de base (propre à la base) | Template `kind='row'` + `target_collection_id` (§9.2) |
| Menu *New ▾* : *+ New template*, *•••* (Edit / Duplicate / Set as default / Repeat / Delete) | Menu *New ▾* §7.3, mêmes actions, mêmes libellés |
| Édition du template comme une page | Page-support + bandeau de mode (§7.4, §9.3) |
| Template par défaut | `is_default` exclusif par base (§11.1) |
| *Repeat* (daily/week/month/year/custom) | `template_recurrences` RRULE + scheduler (§11.2) |
| Mentions dynamiques `@today/@now/@me` dans le titre | Variables système `{{today}}/{{now}}/{{me}}` (§8.4) |
| Galerie *Templates* de la sidebar | Gestionnaire `/templates` + onglet Library (§7.2) + galerie système (§12) |
| Page publiée « Allow duplicate as template » | Phase 5, chantier A (§15) |
| Boutons (ex-template button) | Bloc/propriété `button` + action d'automatisation `create_from_template` (§13.2) |
| Appliquer un template à une page existante | Sélecteur en mode ajout/remplacement (§7.1) ; lignes : T3-6 |
## Annexe B — Format de manifeste `flowdeck-template` v1
Transport d'export/import (API v2 §10.2) — un objet JSON autonome :
```jsonc
{
"format": "flowdeck-template",
"version": 1,
"kind": "row",
"name": "Rapport de bug",
"description": "Structure standard d'un rapport de bug",
"icon": "🐞",
"category": "Support",
"target": { "match_by": "collection_name", "collection_name": "Support" },
// à l'import, la base cible est résolue par nom dans le workspace choisi,
// jamais par id externe ; si absente, l'import crée le template « orphelin »
// en scope personnel et le signale.
"variables": [ { "name": "titre", "type": "text", "required": true } ],
"manifest": { "property_defaults": { "Status": "À trier" },
"title_template": "Bug — {{titre}}" },
"content": { "content_format": "blocks", "blocks": [ /* blocs de la page-support */ ] },
"recurrence": { "rrule": "FREQ=WEEKLY;BYDAY=MO", "timezone": "America/Toronto" } // optionnel
}
```
Règles : `version` supérieure non reconnue → refus explicite ; les `content_page_id` et identifiants internes ne voyagent jamais ; les blocs synchronisés sont exportés **matérialisés** (contenu copié).
## Annexe C — Glossaire
- **Template** : objet réutilisable du registre, de genre page, ligne, base ou blocs.
- **Page-support** : page technique masquée portant le contenu d'un template (§9.3).
- **Instanciation** : création d'un objet réel depuis un template, par `TemplateService.instantiate()` (§8.3).
- **Sélecteur** : panneau de choix/aperçu/application d'un template (§7.1).
- **Gestionnaire** : écran de gestion des templates, page `/templates` et onglet Library (§7.2).
- **Variable système / déclarée** : valeur résolue automatiquement / demandée à l'utilisateur, à l'instanciation (§8.4).
- **Run** : une instanciation journalisée dans `template_runs` (§9.4).
## Sources
Analyse fonctionnelle de Notion (section 3), documentation publique officielle consultée le 10 octobre 2026 :
- *Using database templates* — https://www.notion.com/help/guides/using-database-templates
- *Using database templates to help cement your team's process* — https://www.notion.com/help/guides/using-database-templates-for-teams
- *Automate work with repeating database templates* — https://www.notion.com/help/guides/automate-work-repeating-database-templates
- *Getting started with templates for your team* — https://www.notion.com/help/guides/getting-started-with-templates-for-your-team
État Flowdeck : `ARCHITECTURE.md` v7.69.8 fourni par Bruno (§§ cités dans le texte) et capture de l'état actuel fournie le 10 octobre 2026 (`templates-notion-reference/capture-etat-actuel-bouton-templates.png`).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
# Flowdeck Agents & Skills — Phase 1 : découpage en tickets prêts à développer
_Dérivé du document d'architecture v1.0 (`architecture-agents-skills-notion-flowdeck.md`, §20 Phase 1). Chaque ticket cite la section source. Estimations relatives (S/M/L), pas des engagements. Périmètre strict de la Phase 1 : « le skill devient une page » (écarts E1, E3, E4 côté modèle). Le routeur automatique (§13.2), `agent_runs` (migration 40) et la personnalisation de l'Agent personnel sont en Phase 2 et ne sont **pas** dans ce découpage._
## Critère de sortie de la phase (§20)
UC-02 (skill manuel dans le chat : `/` → choisir un skill → résultat au format du skill) et UC-04 (skill dans l'éditeur, sur sélection ou bloc) fonctionnent entièrement sur des pages ; un playbook existant devient un skill sans copier-coller.
## Avant de coder : 1 spike de décision
- **SPIKE-A — Skills intégrés et AI Writing** (§23 Q3) : les prompts des 6 actions d'`ai_writing.py` deviennent-ils littéralement les pages des skills intégrés (éditables par l'admin), le service restant seulement pour l'autocomplétion inline (latence) ? C'est la voie proposée par le document pour les quatre skills d'édition. _Sortie : une page de décision qui conditionne A1-13, pas du code._
## Modèle de données — migration 41 (§10.3, §10.4)
- **A1-1 (M)** Migration 41 — skills (§10.3) : `collections.is_skills_db` + correspondance des propriétés dans `collections.schema_json` (`Description` / `Files` / `Tags`) ; recréation `agent_skills` → `agent_skills_v2` par copie (`page_id` unique, `name_cached`, `description`, `files_json`, `tools_json`, `is_builtin`, `auto_use_default`, `editor_menu_default`, `status`, `source`). _Acceptation : migration transactionnelle ; aucun drapeau `is_skill` sur `pages` — être un skill = avoir une ligne `agent_skills` (décision §10.3 n°1)._
- **A1-2 (S)** Activation par utilisateur (§10.4) : table `user_skill_enablements` (`enabled`, `auto_use` et `editor_menu` en NULL = suit le réglage du skill). _Acceptation : désactiver pour soi ne modifie ni la page ni les réglages des autres utilisateurs._
- **A1-3 (M)** Migration du contenu existant (§10.3 n°3, §22) : chaque skill `agent_skills` sans page reçoit une page générée conservant le prompt d'origine à l'identique (`source='legacy'`) ; les 17 presets de la galerie deviennent des pages dans la base de skills privée de l'utilisateur à l'installation. _Acceptation : vérification par comptage et échantillon avant bascule (§22, risque n°1) ; aucun skill ne survit hors du modèle page._
- **A1-4 (S)** Indexation (§10.3 n°4) : titre + `description` indexés dans `semantic_embeddings` avec `resource_type='skill'` par le moteur d'indexation incrémental existant. _Acceptation : créer/modifier un skill met à jour son index sans réindexation globale — c'est l'index que lira le routeur en Phase 2._
## Page, base et bannière (§7.5, §13.1)
- **A1-5 (M)** Marquer / démarquer une page (§7.5, §13.1) : entrée `•••` → *Use as a skill* ; marquer = insérer la ligne `agent_skills`, décocher = la supprimer, le contenu de la page ne bouge jamais. Créateur activé automatiquement (`Enable for me` implicite).
- **A1-6 (M)** Bannière de skill (§7.5, maquette §7A.2) : nom, base, état activé pour moi, interrupteurs *Use automatically* / *Add to text editor menu*, bouton *Download for local agents*, rappel qu'un skill partagé en édition est mutable par ses éditeurs pour tous. Aucun nouvel éditeur : on écrit un skill comme une page.
- **A1-7 (M)** Base de skills (§7.5) : conversion d'une collection ordinaire via le dialogue de correspondance `Description` / `Files` / `Tags` (créer ou mapper), case « activer les pages existantes comme skills pour moi » ; gabarit *Database → Skills* à la création ; badge « Skills » sur la collection. Dans une base, le skill est la page-ombre de la ligne (`pages.collection_row_id`), `Description`/`Files` relues depuis `property_values_json`.
## Library (§7.4)
- **A1-8 (M)** Onglet Skills de la Library : table des skills activés/créés (Nom, Base, Description tronquée, *Use automatically*, *Menu éditeur*, Propriétaire, Dernière utilisation), actions *New skill*, ouvrir, désactiver pour soi ; recherche/filtres par nom, description, base, propriétaire, `Tags`.
- **A1-9 (S)** Sous-vue Discover (§7.4, §13.5) : skills accessibles en lecture mais non activés ; bouton **Enable for me** par ligne, confirmation immédiate. `Discover` = accès lecture sans ligne `user_skill_enablements` active ; le partage reste celui de la page/base, aucun nouveau droit.
## Exécution manuelle — Skill Runner minimal (§13.3, §11.3)
- **A1-10 (L)** Skill Runner, invocation manuelle seulement : construction de la consigne depuis la page rendue en Markdown (blocs éligibles §13.4, contenu imbriqué inclus) + extraits des fichiers de support `shareable` dans le budget + entrée du run ; contrainte d'outils en **intersection seulement** (§13.7 : un skill n'élargit jamais les outils, ne modifie pas les consignes système, ses fichiers sont des données, pas des instructions). La réponse nomme le skill utilisé.
- **A1-11 (M)** Surface chat — UC-02 (§9.2 pour l'éditeur, §11.3) : `POST /api/agent/skills/{id}/run-chat`, menu `/` alimenté par `GET /api/agent/skills/menu` (activés d'abord). _Acceptation UC-02 : `/` → Project Brief Writer → brief au format d'équipe, entièrement depuis la page du skill._
- **A1-12 (M)** Surface éditeur — UC-04 (§7.2, §13.4) : `POST /api/agent/skills/{id}/run-editor`, résultat en diff ou insertion ; éligibilité déclarée par type de bloc (`skill_eligible: bool` dans le registre des types) : texte, H1–H3, citation, callout, listes, toggle, image, bloc synchronisé. _Acceptation UC-04 : les skills personnels d'abord, puis les intégrés, dans le menu de sélection et le menu de bloc._
## Skills intégrés (§20, annexe C) — dépend de SPIKE-A
- **A1-13 (M)** Les 6 actions AI Writing exposées comme skills intégrés (`is_builtin=1`, seed, non supprimables mais désactivables) : Améliorer l'écriture · Corriger · Expliquer · Reformater · Traduire · Résumer ; unification des trois points d'entrée éditeur existants sur le Skill Runner (A1-10).
## Export / import `SKILL.md` (§13.6, §11.3)
- **A1-14 (M)** Export : sérialisation `SKILL.md` (front matter `name` + `description`, corps de page en Markdown) + bundle avec les fichiers de support `shareable=1` ; `GET /api/agent/skills/{id}/download`. Le format `flowdeck-skill` v1 (JSON) reste le format d'échange Flowdeck ↔ Flowdeck (ADR-08).
- **A1-15 (S)** Import : `POST /api/agent/skills/import` accepte `SKILL.md` externe et v1 ; un `SKILL.md` importé devient une page-skill dans la base privée de l'importateur, ses fichiers joints deviennent la propriété `Files`. Suivi de version : ligne `skill_local_downloads` (§10.4) à chaque téléchargement, badge « copie locale périmée » quand l'empreinte de page a changé. _Note : l'écriture directe chez les agents locaux est la question ouverte Q4 (§23) — la Phase 1 livre le téléchargement seul, pas d'utilitaire compagnon._
## API (§11.3) — récapitulatif des routes nouvelles livrées par les tickets ci-dessus
`POST|DELETE /api/agent/pages/{id}/skill` · `POST /api/agent/skills/from-page` · `GET /api/agent/skills/menu` · `PUT /api/agent/skills/{id}/enablement` · `POST …/run-chat` · `POST …/run-editor` · `GET …/download` · `POST /api/agent/skills/import` · `PUT /db/{id}/skills-db` · v2 : `GET /api/v2/skills` expose `page_id`, `description` et la sérialisation `SKILL.md` (`Accept: text/markdown`).
## Ordre conseillé
`SPIKE-A → A1-1 → A1-3 → A1-2/A1-4 → A1-5 → A1-6/A1-7 → A1-8/A1-9 → A1-10 → A1-11/A1-12 → A1-13 → A1-14/A1-15`
## Hors Phase 1 (ne pas ouvrir maintenant)
Routeur automatique et journal de décision (§13.2, Phase 2), migration 40 / `agent_runs` (Phase 2 limitée, Phase 3 complète), page « Mon Flowdeck AI » et instructions-page de l'Agent personnel (Phase 2), déclencheurs événementiels et Custom Agents autonomes (Phase 3), crédits / `ai_usage_ledger` migration 42 et espace fichiers migration 43 (Phase 4). Les questions Q1 (crédits), Q2 (modèle par défaut), Q5 (Slack vs messagerie existante) et Q6 (rétention des runs) ne bloquent **pas** la Phase 1.