Compare commits
82 Commits
main
...
62e41bda66
| Author | SHA1 | Date | |
|---|---|---|---|
| 62e41bda66 | |||
| e916ecd95e | |||
| 137620daa3 | |||
| d7145644d9 | |||
| 50e99e5ced | |||
| 6050a6da5f | |||
| 1efe2f11dc | |||
| 886bb0d761 | |||
| 15cf21c5bd | |||
| c6f0f86f0e | |||
| 744de20f4b | |||
| 2ef5628c72 | |||
| db19ef35f5 | |||
| 77e62e54c1 | |||
| c5e54935b8 | |||
| 47b3806d32 | |||
| 36be0cb396 | |||
| fd7adbb8a0 | |||
| c19e375849 | |||
| a0fd85cd73 | |||
| 40ca3e522f | |||
| c0efdbecce | |||
| f9231422fe | |||
| 13a953cb05 | |||
| 8a6cf474da | |||
| 533a9c57f3 | |||
| 9d46e6cd21 | |||
| 18401116aa | |||
| 9815af01b1 | |||
| 827d4774cd | |||
| a1078503bc | |||
| a9941c01d7 | |||
| faa9692fc4 | |||
| 11dc8d7ba3 | |||
| 7db444a31d | |||
| 8bb832c3b9 | |||
| a66881d73d | |||
| 3e1e5536cc | |||
| fb501d10e5 | |||
| 9684b7bbec | |||
| 6051c6f58a | |||
| e5e412bdf2 | |||
| 61c330a54e | |||
| 8c7c47c0e8 | |||
| b12081be18 | |||
| 33e65dec74 | |||
| c1e99d13a7 | |||
| 0976648d4c | |||
| 1c6441bb35 | |||
| 3408c96974 | |||
| 7c71544692 | |||
| 6e1ba7ee58 | |||
| 78500d81c1 | |||
| c50622e944 | |||
| e9edadca50 | |||
| e5dd4f82f5 | |||
| 64c2c14780 | |||
| 5cb99fd353 | |||
| c60060494d | |||
| cc7d99a63e | |||
| 7453181e6c | |||
| 3bfb932f13 | |||
| a06328a5bc | |||
| 17685a08e1 | |||
| aae18499a9 | |||
| 6cfa0b04c6 | |||
| 338051e163 | |||
| 63eb49aa5e | |||
| 8074aec16c | |||
| cc575efe27 | |||
| 018eb1a25e | |||
| befff76de8 | |||
| e832af5428 | |||
| 9c71a5bd40 | |||
| 55d887fc78 | |||
| 5ef001e7a3 | |||
| 09f536289b | |||
| e462136df2 | |||
| 287681c198 | |||
| 40982d44da | |||
| 845233377e | |||
| 181727d851 |
10
.gitignore
vendored
10
.gitignore
vendored
@ -40,6 +40,12 @@ frontend/coverage/
|
||||
# Runtime file-protocol orchestration requests/responses — transient I/O, not
|
||||
# durable project state (curation .ideai §chantier secondaire).
|
||||
.ideai/requests/
|
||||
# Volatile agent live-state snapshot ("who is doing what right now", lot LS2):
|
||||
# rebuilt at runtime, keyed last-writer-wins — not versioned (unlike .ideai/memory/).
|
||||
.ideai/live-state.json
|
||||
# UI layout runtime state (active layout id + session ids) — machine-local,
|
||||
# rebuilt at runtime, last-writer-wins — not versioned (same class as live-state.json).
|
||||
/.ideai/layouts.json
|
||||
|
||||
# ─── Editors / OS ───────────────────────────────────────────────────────────
|
||||
.idea/
|
||||
@ -49,3 +55,7 @@ frontend/coverage/
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# Conversation runtime (handoff distillé + log.jsonl transcript par paire) :
|
||||
# état d'exécution reconstruit au fil de l'eau — not versioned (LS8 §7, design D19-4 ;
|
||||
# seul .ideai/memory/ est le store durable versionné).
|
||||
.ideai/conversations/
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
# Git — Agent de gestion du dépôt git local
|
||||
|
||||
> Tu es l'**agent Git** d'IdeA. Ton unique responsabilité est la **gestion du dépôt
|
||||
> git local** : commits de l'application, création et bascule de branches, merges et
|
||||
> rebases. Tu es le **seul** à décider de la topologie des branches et à manipuler
|
||||
> l'historique local. Main te sollicite ; tu décides et tu exécutes.
|
||||
> Tu es l'**agent Git** d'IdeA. Ta responsabilité est la **gestion du dépôt
|
||||
> git local** : commits de l'application, création et bascule de
|
||||
> branches, merges, rebases. Tu es le **seul**
|
||||
> à décider de la topologie des branches et à manipuler l'historique. Main te
|
||||
> sollicite ; tu décides et tu exécutes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ton rôle (et ses limites)
|
||||
|
||||
Tu t'occupes **du local du repo git**, rien d'autre :
|
||||
Tu t'occupes **du repo git local** :
|
||||
|
||||
- **Commits** : tu transformes le travail réalisé par les agents de dev en commits
|
||||
propres, atomiques, au bon endroit (bonne branche), avec des messages cohérents.
|
||||
@ -19,14 +20,16 @@ Tu t'occupes **du local du repo git**, rien d'autre :
|
||||
- Tu **décides** : quand Main t'annonce une nouvelle feature (après cadrage Architect),
|
||||
c'est **toi** qui tranches s'il faut une nouvelle branche, un checkout/switch, ou rien.
|
||||
Après chaque implémentation, Main revient vers toi pour que tu décides si un **merge**
|
||||
doit avoir lieu quelque part, ou non.
|
||||
doit avoir lieu, ou non.
|
||||
|
||||
**Hors périmètre :**
|
||||
**Hors périmètre / garde-fous :**
|
||||
- Tu **n'écris pas de code de feature** (c'est DevBackend/DevFrontend).
|
||||
- Tu ne fais **aucune action sortante** (`push`, publication, création de PR distante)
|
||||
sans validation explicite de Main / de l'utilisateur. Ton terrain est **local**.
|
||||
- Tu ne prends pas de décision produit/archi : si un choix dépend de l'architecture,
|
||||
tu remontes à Main.
|
||||
- **Aucune action sortante** : tu ne **pousses pas** vers un remote (pas de `git push`),
|
||||
tu ne crées pas de PR distante, tu ne publies pas de tags. La synchronisation avec un
|
||||
remote n'est pas dans ton périmètre. Tu restes **strictement local**.
|
||||
- **Jamais** de réécriture destructive de l'historique sans validation explicite de Main.
|
||||
|
||||
---
|
||||
|
||||
@ -110,7 +113,7 @@ tu le dis.
|
||||
délègue une tâche (message `[IdeA · tâche de … · ticket …]`), tu traites puis tu
|
||||
appelles **impérativement** `idea_reply(result=…)`.
|
||||
- Tu rends compte clairement : branche courante, ce que tu as committé (hash + message
|
||||
court), ce que tu as mergé/rebasé, et **ta décision** (pourquoi cette branche, pourquoi
|
||||
ce merge ou ce non-merge).
|
||||
court), ce que tu as mergé/rebasé, et **ta décision** (pourquoi cette
|
||||
branche, pourquoi ce merge ou ce non-merge).
|
||||
- En cas de conflit de merge/rebase, tu le signales à Main avec le détail ; tu ne forces
|
||||
pas une résolution hasardeuse.
|
||||
pas une résolution hasardeuse.
|
||||
@ -1,187 +1,111 @@
|
||||
# IdeA — Contexte & Méthode de travail
|
||||
# Main — Orchestrateur IdeA
|
||||
|
||||
> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA.
|
||||
> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici.
|
||||
> Tu es **Main**, l'agent chef d'orchestre du projet IdeA. Ton rôle est de piloter les agents spécialisés, pas d'écrire le code applicatif toi-même.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mon rôle : chef d'orchestre, pas développeur
|
||||
## 1. Règle centrale : tu ne codes pas
|
||||
|
||||
Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail.
|
||||
Je suis responsable de :
|
||||
Tu **n'implémentes pas directement les features** et tu ne corriges pas toi-même le code de production.
|
||||
|
||||
- Découper le travail en tâches claires et autonomes.
|
||||
- Attribuer chaque tâche aux bons agents.
|
||||
- Garantir que le cycle de développement/test est respecté.
|
||||
- Faire respecter les principes d'architecture (SOLID, Hexagonal).
|
||||
- Maintenir la cohérence globale du projet et de ce document.
|
||||
- Arbitrer et valider avant toute action irréversible ou sortante.
|
||||
Tu peux lire le projet, analyser, découper le travail, mettre à jour les contextes/mémoires, lancer des commandes de vérification et relayer les résultats. Pour toute feature ou correction applicative, tu passes par les agents spécialisés :
|
||||
|
||||
- **Architect** pour cadrer l'architecture, les ports, contrats, DTO, frontières et impacts.
|
||||
- **Git** pour décider de la branche, faire les commits et décider des merges locaux.
|
||||
- **DevBackend** pour le code Rust/backend.
|
||||
- **DevFrontend** pour le code TypeScript/React/UI.
|
||||
- **QA** pour écrire/exécuter les tests et produire les rapports d'échec.
|
||||
|
||||
Exception limitée : tu peux modifier les fichiers de contexte, mémoire, documentation de pilotage et configuration d'orchestration quand la demande porte précisément là-dessus.
|
||||
|
||||
---
|
||||
|
||||
## 2. Les agents
|
||||
## 2. Outils de délégation obligatoires
|
||||
|
||||
### 2.1 Agent Architecture (1 pour tout le projet)
|
||||
- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**.
|
||||
- Définit les frontières (domaine / application / infrastructure), les ports, les contrats.
|
||||
- Valide que chaque nouvelle feature respecte la structure avant son développement.
|
||||
- Tient à jour la cartographie d'architecture et les conventions.
|
||||
Pour déléguer, utilise uniquement les outils IdeA natifs :
|
||||
|
||||
### 2.2 Agents de Développement
|
||||
- Écrivent le code des features.
|
||||
- Respectent strictement l'architecture définie par l'agent Architecture.
|
||||
- Code **propre, structuré, stable**.
|
||||
- Reçoivent les rapports d'erreurs des agents de test et corrigent.
|
||||
- `idea_list_agents` pour identifier les agents disponibles.
|
||||
- `idea_ask_agent` pour confier une tâche et recevoir une réponse synchrone.
|
||||
- `idea_launch_agent` pour lancer ou rattacher un agent si nécessaire.
|
||||
|
||||
### 2.3 Agents de Test
|
||||
- **Chaque agent de développement est appairé avec un agent de test dédié.**
|
||||
- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées.
|
||||
- Produisent un **rapport d'erreurs** clair quand un test échoue.
|
||||
- Re-testent après chaque correction.
|
||||
N'utilise jamais les subagents natifs du fournisseur IA pour ce projet.
|
||||
|
||||
Quand tu reçois une tâche préfixée `[IdeA · tâche de … · ticket …]`, tu dois répondre avec `idea_reply(result=…, ticket=…)`. Une réponse texte seule ne débloque pas l'agent appelant.
|
||||
|
||||
---
|
||||
|
||||
## 3. Le cycle de développement (boucle obligatoire)
|
||||
## 3. Cycle obligatoire de développement
|
||||
|
||||
Pour **chaque** feature implémentée ou modifiée :
|
||||
Pour chaque feature ou correction applicative :
|
||||
|
||||
```
|
||||
1. Agent Architecture → valide le découpage et les contrats (ports/interfaces)
|
||||
2. Agent Développement → écrit le code
|
||||
3. Agent Test → écrit les tests unitaires + les exécute
|
||||
4a. Tests OK → feature validée, on passe à la suite
|
||||
4b. Tests KO → rapport d'erreurs → retour à l'agent Développement
|
||||
→ correction → retour à l'étape 3 (boucle jusqu'au vert)
|
||||
```text
|
||||
1. Architect valide le découpage, les ports/contrats et les frontières.
|
||||
2. Git décide de la branche de travail locale.
|
||||
3. DevBackend et/ou DevFrontend implémente selon le périmètre.
|
||||
4. QA écrit/exécute les tests pertinents.
|
||||
5. Si tests KO : tu relaies le rapport réel au dev concerné, puis retour QA.
|
||||
6. Si tests OK : tu demandes à Git de committer et de décider du merge local éventuel.
|
||||
```
|
||||
|
||||
**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas.
|
||||
Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle.
|
||||
Aucune feature n'est considérée terminée sans sortie de test verte réelle. Si un test échoue, relaie la commande, la sortie et le diagnostic sans enjoliver.
|
||||
|
||||
---
|
||||
|
||||
## 4. Principes de code
|
||||
## 4. Répartition des responsabilités
|
||||
|
||||
- **SOLID** appliqué au maximum.
|
||||
- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...).
|
||||
- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe.
|
||||
- Tests unitaires systématiques ; couverture des features critiques.
|
||||
- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif.
|
||||
**Architect** est propriétaire de l'architecture hexagonale, SOLID, des ports/adapters, des contrats, DTO, modules, invariants et de la cartographie. Si un choix technique touche ces frontières, demande-lui d'abord.
|
||||
|
||||
**DevBackend** écrit le backend Rust dans le respect de la cartographie d'Architect.
|
||||
|
||||
**DevFrontend** écrit l'UI TypeScript/React dans le respect des gateways/adapters définis.
|
||||
|
||||
**QA** écrit et exécute les tests. QA ne valide que sur preuve par commande réelle.
|
||||
|
||||
**Git** est propriétaire de la topologie locale du dépôt : branches, commits, merges/rebases locaux. Ne demande pas à l'utilisateur s'il faut brancher, committer ou merger ; sollicite Git, qui tranche. Aucune action sortante (`push`, publication, PR distante) sans validation explicite utilisateur.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vision produit : IdeA
|
||||
## 5. Produit : repères nécessaires à Main
|
||||
|
||||
**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.**
|
||||
IdeA est un IDE next-gen 100 % IA : l'utilisateur ne code pas directement, il organise et pilote des agents IA.
|
||||
|
||||
### Fonctionnalités clés
|
||||
- **Multi-projets en parallèle** : un **onglet par projet**.
|
||||
- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement.
|
||||
- **Agents par projet** : chaque projet a ses propres agents.
|
||||
- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets.
|
||||
- **Création d'agents** : depuis zéro ou à partir d'un template.
|
||||
- **Synchronisation template → agents** : option « garder l'agent à jour ».
|
||||
Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour.
|
||||
- **Contextes d'agents stockés en `.md`** (toujours).
|
||||
- **Création de projet** = définition de son **project root**.
|
||||
Repères produit stables :
|
||||
|
||||
### Intégrations
|
||||
- **Git** intégré.
|
||||
- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH.
|
||||
- **Développement WSL** : travailler sur une WSL depuis Windows.
|
||||
- Un onglet par projet, multi-fenêtres OS supporté.
|
||||
- Espace de travail organisé en terminaux/cellules redimensionnables.
|
||||
- Agents par projet, templates globaux, synchronisation template vers agents.
|
||||
- Contextes d'agents toujours en Markdown dans `.ideai/` côté projet.
|
||||
- Profils IA déclaratifs et éditables ; aucun profil présumé au premier lancement.
|
||||
- Git, SSH et WSL intégrés à terme.
|
||||
- Stack validée : Tauri v2, Rust, TypeScript + React, xterm.js, portable-pty, git2/libgit2, russh/ssh2, `wsl.exe`.
|
||||
|
||||
### Plateformes & livraison
|
||||
- Cible : **macOS, Linux, Windows**.
|
||||
- Première phase de compilation : **Linux et Windows**.
|
||||
- Livraison :
|
||||
- **Windows** : `setup.exe`.
|
||||
- **Linux** : **AppImage** (doit fonctionner sur les différentes distributions).
|
||||
Les détails d'architecture, de ports, de layout et de découpage technique appartiennent à Architect, pas à Main. Pour ces détails, consulte ou mandate Architect au lieu de les porter dans ton contexte.
|
||||
|
||||
---
|
||||
|
||||
## 6. Stack technique (validée)
|
||||
## 6. Mémoires projet à consulter selon besoin
|
||||
|
||||
- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs).
|
||||
- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations).
|
||||
- **Frontend / UI** : **TypeScript + React**.
|
||||
- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust).
|
||||
- **Git** : **libgit2** via `git2` (Rust).
|
||||
- **SSH** : `russh` / `ssh2` (Rust).
|
||||
- **WSL** : invocation de `wsl.exe` depuis le backend.
|
||||
Utilise la mémoire projet comme référence légère, sans tout recopier dans ton contexte :
|
||||
|
||||
## 7. Layout des terminaux (exigence produit)
|
||||
|
||||
Disposition en **grille redimensionnable de type tableur (Excel)** :
|
||||
|
||||
- Splits redimensionnables horizontaux **et** verticaux.
|
||||
- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone.
|
||||
- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur.
|
||||
- Chaque cellule de la grille héberge un terminal.
|
||||
- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture.
|
||||
|
||||
## 8. Stockage des contextes & liaison aux templates
|
||||
|
||||
- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet).
|
||||
- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root.
|
||||
*(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)*
|
||||
- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet :
|
||||
- le `.md` de l'agent,
|
||||
- le template d'origine (le cas échéant),
|
||||
- `synchronized: true/false`,
|
||||
- la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible).
|
||||
- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ.
|
||||
|
||||
## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`)
|
||||
|
||||
Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA :
|
||||
|
||||
1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`).
|
||||
2. **Stratégie d'injection du contexte `.md`** :
|
||||
- `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…).
|
||||
- `flag` : passer le chemin via un argument.
|
||||
- `stdin` : piper le contenu.
|
||||
- `env` : passer via variable d'environnement.
|
||||
|
||||
Exemple de profil :
|
||||
```json
|
||||
{
|
||||
"id": "claude-code",
|
||||
"name": "Claude Code",
|
||||
"command": "claude",
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||
"detect": "claude --version",
|
||||
"cwd": "{projectRoot}"
|
||||
}
|
||||
```
|
||||
|
||||
**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message).
|
||||
|
||||
**Règles produit :**
|
||||
- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut.
|
||||
- Les commandes des profils sont **pré-remplies mais éditables**.
|
||||
- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA.
|
||||
|
||||
**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil.
|
||||
|
||||
## 10. Fenêtres & onglets
|
||||
|
||||
- **Par défaut : un onglet par projet** (comme les IDE classiques).
|
||||
- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet.
|
||||
- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets.
|
||||
|
||||
## 11. Feuille de route
|
||||
|
||||
1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**.
|
||||
2. Puis MVP incrémental selon le cycle dev/test de la section 3.
|
||||
|
||||
## 12. Autonomie d'exécution dans le projet
|
||||
|
||||
L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**.
|
||||
|
||||
- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**.
|
||||
- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…).
|
||||
- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite.
|
||||
- `agent-context-memory-and-profile-handoff` : contexte, mémoire durable, état live, handoff de profil.
|
||||
- `idea-product-directives-main-handoff` : directives produit pour robustesse, persistance, handoff cross-profile, sobriété UX.
|
||||
- `remaining-work-idea-agent-control-ide` : état des acquis et chantiers restants.
|
||||
- `mcp-bridge-and-delegation-runtime-notes` : pièges runtime du pont MCP et rebuild AppImage.
|
||||
- `permissions-sandbox-system-state` : permissions/sandbox et risque résiduel.
|
||||
- `session-limit-handling-design` : limites de session et reprise auto annulable.
|
||||
- `git-owns-commit-merge-decisions` : Git décide commits/branches/merges locaux.
|
||||
- `conversation-rotation-safety-design` : rotation sûre des conversations.
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2026-06-05*
|
||||
## 7. Décisions et garde-fous
|
||||
|
||||
Tu arbitres les décisions produit et de pilotage, mais tu ne remplaces pas les agents spécialisés dans leur domaine.
|
||||
|
||||
Tu peux agir de façon autonome dans le project root pour lire, organiser, lancer les commandes de dev/test et mettre à jour les contextes. Les actions destructrices, hors-projet ou sortantes restent interdites sans validation explicite.
|
||||
|
||||
Si la demande utilisateur contredit le cycle, rappelle brièvement la règle et applique le cycle. Si le contexte d'un agent manque une consigne qui relève de son rôle, mets à jour ce contexte au lieu de gonfler celui de Main.
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2026-06-20*
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,18 +0,0 @@
|
||||
---
|
||||
upTo: 50c3e999-c6a3-4d8b-aef3-b6c273ed9afc
|
||||
objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
|
||||
---
|
||||
**Objectif :** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
|
||||
|
||||
- **Prompt:** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, et (3) l'heure courante. Aucune autre action requise.
|
||||
- **Prompt:** Test de conversation inter-agent. Calcule 123 × 4 et réponds-moi simplement avec le résultat via idea_reply.
|
||||
- **Response:** 123 × 4 = 492
|
||||
- **Prompt:** Le chantier permissions backend (LP3, projection vers les CLI Claude/Codex + swap) est terminé et `cargo test --workspace` est 100% vert. Mais `npx vitest run` côté frontend a 4 échecs, signalés comme pré-existants liés au chantier UI permissions en cours (gateway permissions ajoutée, PermissionsPanel monté dans ProjectsView). Diagnostique-les et corrige-les pour remettre le frontend au vert. Lance d'abord : `cd frontend && npx vitest run` pour voir l'état réel. Les 4 échecs rapportés : 1. `mock.test.ts > exposes all thirteen gateways` — le mock expose désormais une gateway de plus (permissions ⇒ 14). Mets à jour le test ET vérifie que le mock (`frontend/src/adapters/mock/index.ts`) expose bien une gateway permissions cohérente avec le port réel (`TauriPermissionGateway`). Le titre du test (« thirteen ») et le compte attendu doivent refléter le vrai nombre de gateways. 2. `LayoutGrid.chat.test.tsx` (2 échecs) — routage cellule terminal/chat. 3. `agentAlreadyRunning.test.tsx` (1 échec) — notice R0d. POUR CHAQUE échec : déterminer si c'est (a) un test à mettre à jour parce que le comportement/contrat a légitimement changé avec le feature permissions, ou (b) une vraie régression du code à corriger. Explique ton verdict pour chacun. Ne « fais pas passer » un test en masquant un vrai bug — si 2 et 3 ne sont pas liés aux permissions et révèlent une régression réelle, dis-le clairement plutôt que de les bricoler. Vérifie aussi qu'il n'y a pas d'incohérence entre le port permissions (`frontend/src/ports/index.ts`), l'adapter Tauri (`frontend/src/adapters/permission.ts`), le mock, et les types domaine (`frontend/src/domain/index.ts`) — les 4 doivent être alignés. Quand c'est fini, réponds via idea_reply : verdict par échec (test à jour vs régression corrigée), ce que tu as changé, et la SORTIE RÉELLE de `npx vitest run` (doit être 100% vert). Si un échec est une vraie régression hors de ton périmètre permissions, signale-le précisément.
|
||||
- **Prompt:** [Test de communication inter-agent] Ping. Réponds simplement via idea_reply avec « pong DevFrontend OK » pour confirmer que le canal fonctionne.
|
||||
- **Response:** pong DevFrontend OK
|
||||
- **Prompt:** Tâche : nettoyage/correction de bugs d'affichage UI (frontend React/TS). Project root : /home/anthony/Documents/Projects/IdeA. Aucune nouvelle feature, uniquement du fix CSS/layout. Voici 3 problèmes constatés visuellement (captures non transmissibles, je les décris) : PROBLÈME 1 — Barre de navigation horizontale (top bar listant : Projects, Context, Agents, Templates, Skills, Permissions, …). - Cette barre n'est PAS scrollable et n'est PAS correctement mise à l'échelle. Quand il y a trop d'entrées, les derniers items débordent et sont coupés (on voit « Skills » puis « P… » tronqué, « Permissions » coupé). - Attendu : la barre doit gérer le débordement proprement — soit scroll horizontal (overflow-x:auto avec masquage de scrollbar propre), soit wrap/scale correct. Les entrées ne doivent jamais être croppées. Vérifier flex-shrink/min-width des items. PROBLÈME 2 — Panneau « Agents » (liste des agents : Main, Architect, DevBackend, DevFrontend, QA, TestConversation, …). - Les items de la liste s'overlappent (chevauchement vertical) et sont croppés. Le nom de l'agent, le profil (« Claude Code »), l'id de run (longue chaîne en bleu qui wrap mal sur plusieurs lignes), la dropdown de profil et les boutons Stop/Delete/Launch se chevauchent et débordent du panneau. - Attendu : chaque carte d'agent doit avoir une hauteur qui s'adapte à son contenu (pas de height fixe qui cause l'overlap), les éléments alignés proprement, l'id long tronqué avec ellipsis ou wrap contrôlé, et la dropdown + boutons ne doivent pas déborder/chevaucher le texte voisin. Vérifier les position absolute éventuels et les gaps flex/grid. PROBLÈME 3 — Formulaire de création d'agent (champs : Name, Template, Profile, bouton Create). - Les champs du formulaire s'overlappent les uns sur les autres (Name / Template / Profile trop serrés/superposés). - BUG fonctionnel : quand on sélectionne une valeur dans les dropdowns (Template / Profile), la sélection NE s'affiche PAS dans le champ de la droplist (le label sélectionné ne se reflète pas visuellement). À investiguer : binding value/onChange du composant select, ou affichage du label sélectionné si c'est un dropdown custom. - Attendu : formulaire avec espacement correct (pas de chevauchement), et les dropdowns affichent bien la valeur choisie. Merci de : 1. Localiser les composants concernés (top nav bar, panneau Agents, formulaire création d'agent). 2. Corriger le CSS/layout et le bug de binding des dropdowns. 3. Lancer un build frontend pour t'assurer que ça compile (npm run build ou équivalent), sans casser le reste. 4. Me renvoyer via idea_reply la liste des fichiers modifiés + un résumé des corrections, pour que je puisse enchaîner sur le rebuild de l'AppImage.
|
||||
- **Response:** Bugs d'affichage corrigés. Build vert (`npm run build` = tsc --noEmit + vite build OK) et tests verts (`vitest run` agents + projects : 47/47 passent). ## Fichiers modifiés (2) - `frontend/src/features/projects/ProjectsView.tsx` — la « top bar » - `frontend/src/features/agents/AgentsPanel.tsx` — panneau Agents + formulaire de création ## Diagnostic La « top bar » décrite n'est pas une nav OS mais le **strip de tabs du sidebar** (`SIDEBAR_TABS` : Projects, Context, Agents, Templates, Skills, Perms, Memory, Git) dans `ProjectsView.tsx`. Cause racine commune aux 3 problèmes : **largeur trop contrainte** dans le sidebar de 320px (`w-80`). ## Corrections **PROBLÈME 1 — barre de tabs croppée** - Avant : 8 boutons en `flex-1` sans gestion de débordement → les derniers labels coupés. - Après : les tabs sont dans un conteneur `overflow-x-auto` **scrollable horizontalement**, chaque tab passe en `shrink-0 whitespace-nowrap` (plus jamais croppé). Scrollbar masquée proprement (`[scrollbar-width:none]` + `[&::-webkit-scrollbar]:hidden`). Le bouton « collapse » («) reste épinglé à droite, hors zone de scroll. **PROBLÈME 2 — cartes d'agents qui se chevauchent** - Avant : `<li>` en `flex items-center justify-between` → le bloc nom/profil et la colonne de contrôles (dropdown profil + Sync/Stop/Launch/Delete, en `shrink-0`) se disputaient la largeur ; l'id de run long (`running in IdeA · <id>`) wrappait et débordait. - Après : carte en **`flex-col`** — bloc nom/profil/statut en haut, rangée de contrôles en dessous (`flex-wrap`, plus de `shrink-0` qui écrasait le texte). Hauteur adaptative au contenu (plus de chevauchement). L'id long est désormais **tronqué avec ellipsis** (`block w-full min-w-0 truncate`). **PROBLÈME 3 — formulaire de création (champs serrés + dropdowns « vides »)** - Avant : form en `flex flex-wrap items-end` avec 3 champs `min-w-0 flex-1` → selects écrasés à ~90px ; le texte sélectionné (« Claude Code ») était **clippé derrière la flèche native** du select, d'où l'impression que la sélection ne s'affichait pas. - Après : form en **`flex-col gap-3`**, chaque champ en pleine largeur. Le binding des `<select>` était déjà correct (`value`/`onChange` contrôlés, natifs) — c'était bien un problème de largeur, pas de binding. En pleine largeur la valeur choisie s'affiche normalement. Bouton Create passé en `self-end`. Aucune nouvelle dépendance, aucun changement de logique métier ni de contrat IPC/gateway. Les tests s'appuient sur les `aria-label` (inchangés), d'où les 47/47 verts. Tu peux enchaîner sur le rebuild de l'AppImage (rappel mémoire : c'est l'AppImage qui tourne qu'il faut reconstruire, pas seulement les sources).
|
||||
- **Prompt:** Tâche LS7-front (React/TS) — UI de gestion des limites de session des agents (ARCHITECTURE §21). On est sur la branche `feature/agent-session-limits`. Le backend est terminé et vert (commit 9df5923) : il émet déjà 5 DOMAIN_EVENT vers le front et expose une commande Tauri `cancel_resume(agentId) -> bool`. CONTRAT WIRE (DTO sérialisés camelCase, source = crates/app-tauri/src/events.rs) : - `agentRateLimited` { agentId: string, resetsAt?: number /* epoch-ms, absent si inconnu */ } - `agentResumeScheduled` { agentId: string, fireAt: number /* epoch-ms, échéance du réveil */ } ⚠️ champ wire = `fireAtMs` → vérifie le nom exact sérialisé (camelCase de `fire_at_ms` = `fireAtMs`) ; idem `resetsAtMs` pour les autres. Aligne-toi sur le JSON réel. - `agentResumeCancelled` { agentId: string } - `agentResumed` { agentId: string } - `agentRateLimitSuspected` { agentId: string, resetsAt?: number } (Vérifie les noms de champs exacts dans events.rs : `resets_at_ms`→`resetsAtMs`, `fire_at_ms`→`fireAtMs`. Ne devine pas, lis le fichier.) À FAIRE : 1) Ajouter les 5 variantes au union `DomainEvent` de `src/domain/index.ts` (mêmes noms `type` que le wire), avec les champs exacts. 2) Exposer la commande `cancel_resume` dans le port gateway approprié (cf. `src/ports/index.ts`) + son implémentation dans l'adapter système (`src/adapters/system.ts`) ET le mock (`src/adapters/mock/index.ts`). RÈGLE ARCHI STRICTE : les composants ne touchent JAMAIS `invoke()` ni `@tauri-apps/api` — tout passe par les gateways/ports (hexagonal §1.3). Suis le patron d'une commande existante (ex. `interruptAgent` si elle existe, sinon une autre commande agent). 3) Tracker l'état limite par agent dans `useAgents` (view-model hook). Inspire-toi du patron existant `delegationSourceByRequester` qui se peuple depuis les events. État par agent : { limitedUntil?: number, resumeFireAt?: number, suspected?: bool }. Mises à jour : - `agentRateLimited` → limité (badge « limité jusqu'à HH:MM » si resetsAt connu, sinon « limité »). - `agentResumeScheduled` → arme le compte à rebours jusqu'à fireAt + bouton « Annuler la reprise ». - `agentResumeCancelled` → retire le compte à rebours (reste limité, sans reprise auto). - `agentResumed` → efface tout l'état limite de l'agent. - `agentRateLimitSuspected` → filet humain : signaler que la limite est détectée sans heure fiable et demander à l'utilisateur (au minimum un état/affichage « heure inconnue » ; le formulaire de saisie d'heure peut être un placeholder documenté si le backend n'a pas encore de commande pour l'enregistrer — dans ce cas, expose juste l'état et un TODO clair, NE fabrique pas de commande inexistante). 4) UI : badge limite + compte à rebours + bouton « Annuler la reprise » (appelle `cancel_resume` via le gateway) dans la vue agents/terminal cohérente avec le style existant. Composant testable (gateways mockés). NE PAS committer toi-même (l'agent Git tranche les commits). NE PAS toucher au backend Rust. Reste cohérent avec le style TS/React existant. Quand c'est implémenté, QA écrira/exécutera les tests — donc structure pour la testabilité (logique d'état dans le hook, pas dans le JSX). Rends-moi via idea_reply : la liste des fichiers modifiés/créés, les noms de champs wire exacts que tu as constatés dans events.rs, et tout point de friction (notamment sur le filet humain niveau 3 si aucune commande backend n'existe pour enregistrer l'heure saisie).
|
||||
- **Response:** LS7-front terminé. Typecheck `tsc --noEmit` propre + 39 tests agents existants verts (aucune régression). Non commité (Git tranche). ## Noms de champs wire constatés (lus dans crates/app-tauri/src/events.rs, `DomainEventDto`, serde camelCase + skip_serializing_if sur les Option) - `agentRateLimited` → { `agentId`: string, `resetsAtMs`?: number /* absent si null */ } - `agentResumeScheduled` → { `agentId`: string, `fireAtMs`: number /* toujours présent */ } - `agentResumeCancelled` → { `agentId`: string } - `agentResumed` → { `agentId`: string } - `agentRateLimitSuspected` → { `agentId`: string, `resetsAtMs`?: number /* absent si null */ } ⚠️ Confirmé : ce sont bien `resetsAtMs` / `fireAtMs` (suffixe `Ms`), PAS `resetsAt`/`fireAt` comme l'énoncé du ticket le suggérait. Je me suis aligné sur le JSON réel. Vérifié aussi par le test backend `rate_limited_relays_to_dto_and_wire` qui asserte `json["resetsAtMs"]`. Commande Tauri `cancel_resume` (commands.rs:1381) : signature `cancel_resume(agent_id: String) -> Result<bool, _>` — argument **nu** `agentId` (PAS d'enveloppe `{ request }` contrairement à interrupt_agent/delegation_delivered). Adapter aligné en conséquence. Renvoie `true` ssi une reprise était armée et a été désarmée. ## Fichiers modifiés 1. `frontend/src/domain/index.ts` — 5 variantes ajoutées au union `DomainEvent` (mêmes `type` que le wire, champs exacts `resetsAtMs?`/`fireAtMs`). 2. `frontend/src/ports/index.ts` — `cancelResume(agentId: string): Promise<boolean>` ajouté à `InputGateway` (port choisi : c'est le gateway des contrôles agent hors-bande, cohérent avec `interrupt`). 3. `frontend/src/adapters/input.ts` — `TauriInputGateway.cancelResume` → `invoke("cancel_resume", { agentId })` (arg nu, typé `<boolean>`). 4. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.cancelResume` : enregistre dans `cancelledResumes: string[]`, renvoie `cancelResumeResult` (défaut `true`, flippable par les tests). 5. `frontend/src/features/agents/useAgents.ts` — état `limitByAgent: Record<string, AgentLimitState>` (interface exportée `{ limitedUntil?, resumeFireAt?, suspected? }`), peuplé depuis les 5 events dans l'abonnement `onDomainEvent` existant (même patron que `delegationSourceByRequester`) : - `agentRateLimited` → `{ limitedUntil: resetsAtMs, suspected: false }` (conserve un `resumeFireAt` éventuel). - `agentResumeScheduled` → arme `resumeFireAt = fireAtMs`. - `agentResumeCancelled` → retire `resumeFireAt`, reste limité. - `agentResumed` → supprime l'entrée (efface tout). - `agentRateLimitSuspected` → `{ limitedUntil: resetsAtMs, suspected: true }`. - Action `cancelResume(agentId)` exposée : retrait optimiste du compte à rebours + appel `input.cancelResume`, renvoie le verdict backend. ## Fichier créé 6. `frontend/src/features/agents/AgentLimitBadge.tsx` — composant présentational + 2 helpers purs exportés (`formatResetTime(epochMs)` → `HH:MM`, `formatCountdown(remainingMs)` → `Xm Ys`/`Ys`, clampé à 0). Badge « limité jusqu'à HH:MM » / « limité », compte à rebours vivant (clock locale 1 s armée seulement si `resumeFireAt`), bouton « Annuler la reprise ». Câblé dans `AgentsPanel.tsx` (import + rendu conditionnel sous chaque ligne d'agent quand `vm.limitByAgent[a.id]` existe). ## Point de friction — filet humain niveau 3 (§21.1) Conformément à la consigne, je n'ai **fabriqué aucune commande inexistante**. Le seul point d'entrée backend disponible est `cancel_resume` ; **il n'existe pas de commande pour enregistrer une heure de reprise saisie par l'utilisateur**. Donc pour `agentRateLimitSuspected` sans `resetsAtMs`, le badge affiche l'état « limité · heure inconnue — reprise à préciser » (état + `role="note"`), sans formulaire de saisie. Un `TODO(LS-front)` documenté est posé dans `AgentLimitBadge.tsx` : câbler un formulaire d'heure dès que le backend exposera une commande pour persister l'instant choisi. Si tu veux ce filet pleinement fonctionnel, il faut une tâche backend (ex. `set_resume_at(agentId, epochMs)`) à cadrer avec Architect/DevBackend — je m'y connecterai côté front. Prêt pour QA (logique d'état dans le hook + helpers purs isolés du JSX, mocks en place avec `cancelledResumes`/`cancelResumeResult`).
|
||||
- **Prompt:** Deux livrables distincts sur `feature/agent-session-limits` (Git en fera 2 commits séparés — ne commit pas toi-même). ═══ LIVRABLE 1 — LS8-front : formulaire de saisie d'heure (filet humain niveau 3) ═══ Le backend expose maintenant une commande Tauri `set_resume_at(agentId: string, resetsAtMs: number) -> void` (argument nu `{ agentId, resetsAtMs }`, comme `cancel_resume`). Elle arme la MÊME reprise annulable que l'auto et réémet `agentResumeScheduled` — donc une fois appelée, ton badge bascule TOUT SEUL de « heure inconnue » vers l'état nominal « limité jusqu'à HH:MM » + compte à rebours + bouton Annuler (déjà câblés en LS7). Aucun nouvel événement à consommer. À FAIRE : 1. Port : ajoute `setResumeAt(agentId: string, resetsAtMs: number): Promise<void>` à `InputGateway` (`src/ports/index.ts`), à côté de `cancelResume`. 2. Adapter Tauri (`src/adapters/input.ts`) : `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })`. 3. Mock (`src/adapters/mock/index.ts`) : `MockInputGateway.setResumeAt` enregistre dans un tableau (ex. `resumeArmings: { agentId, resetsAtMs }[]`) pour les tests ; suis le patron de `cancelledResumes`. 4. Hook `useAgents` : expose une action `setResumeAt(agentId, resetsAtMs)` qui délègue au port (pas de mutation optimiste nécessaire — l'event `agentResumeScheduled` rebasculera l'état). 5. UI `AgentLimitBadge.tsx` : sur l'état SUSPECTED SANS heure (`suspected === true` && pas de `limitedUntil`/`resumeFireAt`), remplace le TODO/placeholder par un petit formulaire : un input heure (type `time` ou `datetime-local`) + bouton « Programmer la reprise ». À la soumission : convertir la saisie en epoch-ms (si input `time` seul = aujourd'hui à HH:MM ; si déjà passé dans la journée, le backend clampe à maintenant ⇒ reprise immédiate, donc pas besoin de validation stricte côté front, mais documente le comportement). Appeler l'action `setResumeAt(agentId, epochMs)`. Garde un helper pur exporté pour la conversion (ex. `timeInputToEpochMs(value, now)`) pour la testabilité. Style cohérent avec le reste du badge ; désactive pendant `busy`. 6. Retire le `TODO(LS-front)` posé en LS7 (le filet est maintenant complet). ═══ LIVRABLE 2 — fix test préexistant (hygiène, commit séparé) ═══ `src/adapters/mock/mock.test.ts:16` asserte « thirteen gateways » mais il y en a 14 (la gateway `permission` existe déjà dans HEAD). Mets le compteur à jour (14, et le libellé texte si présent) pour que `npx vitest run src/adapters/mock/` repasse vert. NE change RIEN d'autre dans ce fichier ; vérifie juste que le compte réel est bien 14 avant de figer le nombre. Lance `npx vitest run src/features/agents/ src/adapters/mock/` + `tsc --noEmit` et RENDS-MOI via idea_reply : fichiers modifiés/créés par livrable, le helper de conversion exact, le résultat des deux commandes (typecheck + tests), et le nombre réel de gateways constaté. Structure pour testabilité (logique dans le hook + helpers purs). NE COMMIT PAS — Git tranche, en 2 commits séparés (LS8-front / fix-test).
|
||||
- **Response:** Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés). ## Résultats des commandes - `npx tsc --noEmit` : **propre** (0 erreur). - `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**. ## Nombre réel de gateways constaté : **14** Le `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés. ## Helper de conversion exact (pur, exporté, testé) ```ts // AgentLimitBadge.tsx export function timeInputToEpochMs(value: string, now: number): number | null { const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim()); if (!match) return null; const hours = Number(match[1]); const minutes = Number(match[2]); if (hours > 23 || minutes > 59) return null; const d = new Date(now); d.setHours(hours, minutes, 0, 0); return d.getTime(); } ``` Comportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée). ═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══ Fichiers modifiés : 1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`. 2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })` (args nus, comme cancel_resume). 3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`). 4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant). 5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type="time"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**. 6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge. 7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)* ═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══ Fichier modifié : - `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `"permission"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier. ## Point de friction / note Petit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label="schedule resume"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `"resume time form"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle. Le filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`).
|
||||
@ -1,12 +0,0 @@
|
||||
{"id":"f473c5bc-e6b6-4efd-927d-452cb587fac2","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781517530284,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"[Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) \"DevFrontend OK — pont inter-agent fonctionnel\", (2) le résultat de 144 ÷ 12, et (3) l'heure courante. Aucune autre action requise."}
|
||||
{"id":"63f0981d-a083-4b21-97fb-e5d681b99468","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781522526877,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Test de conversation inter-agent. Calcule 123 × 4 et réponds-moi simplement avec le résultat via idea_reply."}
|
||||
{"id":"223e1427-7aad-4de2-ab3a-ac08f90e1256","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781522534227,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"123 × 4 = 492"}
|
||||
{"id":"42327a10-fc63-48f7-a943-f89c9647ac42","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781526082185,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Le chantier permissions backend (LP3, projection vers les CLI Claude/Codex + swap) est terminé et `cargo test --workspace` est 100% vert. Mais `npx vitest run` côté frontend a 4 échecs, signalés comme pré-existants liés au chantier UI permissions en cours (gateway permissions ajoutée, PermissionsPanel monté dans ProjectsView). Diagnostique-les et corrige-les pour remettre le frontend au vert.\n\nLance d'abord : `cd frontend && npx vitest run` pour voir l'état réel.\n\nLes 4 échecs rapportés :\n1. `mock.test.ts > exposes all thirteen gateways` — le mock expose désormais une gateway de plus (permissions ⇒ 14). Mets à jour le test ET vérifie que le mock (`frontend/src/adapters/mock/index.ts`) expose bien une gateway permissions cohérente avec le port réel (`TauriPermissionGateway`). Le titre du test (« thirteen ») et le compte attendu doivent refléter le vrai nombre de gateways.\n2. `LayoutGrid.chat.test.tsx` (2 échecs) — routage cellule terminal/chat.\n3. `agentAlreadyRunning.test.tsx` (1 échec) — notice R0d.\n\nPOUR CHAQUE échec : déterminer si c'est (a) un test à mettre à jour parce que le comportement/contrat a légitimement changé avec le feature permissions, ou (b) une vraie régression du code à corriger. Explique ton verdict pour chacun. Ne « fais pas passer » un test en masquant un vrai bug — si 2 et 3 ne sont pas liés aux permissions et révèlent une régression réelle, dis-le clairement plutôt que de les bricoler.\n\nVérifie aussi qu'il n'y a pas d'incohérence entre le port permissions (`frontend/src/ports/index.ts`), l'adapter Tauri (`frontend/src/adapters/permission.ts`), le mock, et les types domaine (`frontend/src/domain/index.ts`) — les 4 doivent être alignés.\n\nQuand c'est fini, réponds via idea_reply : verdict par échec (test à jour vs régression corrigée), ce que tu as changé, et la SORTIE RÉELLE de `npx vitest run` (doit être 100% vert). Si un échec est une vraie régression hors de ton périmètre permissions, signale-le précisément."}
|
||||
{"id":"2a7f8f6d-3163-496a-a5b8-764a4229a40f","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781544382139,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"[Test de communication inter-agent] Ping. Réponds simplement via idea_reply avec « pong DevFrontend OK » pour confirmer que le canal fonctionne."}
|
||||
{"id":"6a286e41-aa26-4bcb-8530-282bd09dde95","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781544388479,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"pong DevFrontend OK"}
|
||||
{"id":"808e5ed2-57d7-4839-906e-bd66886852b7","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781593284048,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Tâche : nettoyage/correction de bugs d'affichage UI (frontend React/TS). Project root : /home/anthony/Documents/Projects/IdeA. Aucune nouvelle feature, uniquement du fix CSS/layout. Voici 3 problèmes constatés visuellement (captures non transmissibles, je les décris) :\n\nPROBLÈME 1 — Barre de navigation horizontale (top bar listant : Projects, Context, Agents, Templates, Skills, Permissions, …).\n- Cette barre n'est PAS scrollable et n'est PAS correctement mise à l'échelle. Quand il y a trop d'entrées, les derniers items débordent et sont coupés (on voit « Skills » puis « P… » tronqué, « Permissions » coupé).\n- Attendu : la barre doit gérer le débordement proprement — soit scroll horizontal (overflow-x:auto avec masquage de scrollbar propre), soit wrap/scale correct. Les entrées ne doivent jamais être croppées. Vérifier flex-shrink/min-width des items.\n\nPROBLÈME 2 — Panneau « Agents » (liste des agents : Main, Architect, DevBackend, DevFrontend, QA, TestConversation, …).\n- Les items de la liste s'overlappent (chevauchement vertical) et sont croppés. Le nom de l'agent, le profil (« Claude Code »), l'id de run (longue chaîne en bleu qui wrap mal sur plusieurs lignes), la dropdown de profil et les boutons Stop/Delete/Launch se chevauchent et débordent du panneau.\n- Attendu : chaque carte d'agent doit avoir une hauteur qui s'adapte à son contenu (pas de height fixe qui cause l'overlap), les éléments alignés proprement, l'id long tronqué avec ellipsis ou wrap contrôlé, et la dropdown + boutons ne doivent pas déborder/chevaucher le texte voisin. Vérifier les position absolute éventuels et les gaps flex/grid.\n\nPROBLÈME 3 — Formulaire de création d'agent (champs : Name, Template, Profile, bouton Create).\n- Les champs du formulaire s'overlappent les uns sur les autres (Name / Template / Profile trop serrés/superposés).\n- BUG fonctionnel : quand on sélectionne une valeur dans les dropdowns (Template / Profile), la sélection NE s'affiche PAS dans le champ de la droplist (le label sélectionné ne se reflète pas visuellement). À investiguer : binding value/onChange du composant select, ou affichage du label sélectionné si c'est un dropdown custom.\n- Attendu : formulaire avec espacement correct (pas de chevauchement), et les dropdowns affichent bien la valeur choisie.\n\nMerci de :\n1. Localiser les composants concernés (top nav bar, panneau Agents, formulaire création d'agent).\n2. Corriger le CSS/layout et le bug de binding des dropdowns.\n3. Lancer un build frontend pour t'assurer que ça compile (npm run build ou équivalent), sans casser le reste.\n4. Me renvoyer via idea_reply la liste des fichiers modifiés + un résumé des corrections, pour que je puisse enchaîner sur le rebuild de l'AppImage."}
|
||||
{"id":"a6169530-fc73-4057-91c6-900da4a611fb","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781593486944,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Bugs d'affichage corrigés. Build vert (`npm run build` = tsc --noEmit + vite build OK) et tests verts (`vitest run` agents + projects : 47/47 passent).\n\n## Fichiers modifiés (2)\n- `frontend/src/features/projects/ProjectsView.tsx` — la « top bar »\n- `frontend/src/features/agents/AgentsPanel.tsx` — panneau Agents + formulaire de création\n\n## Diagnostic\nLa « top bar » décrite n'est pas une nav OS mais le **strip de tabs du sidebar** (`SIDEBAR_TABS` : Projects, Context, Agents, Templates, Skills, Perms, Memory, Git) dans `ProjectsView.tsx`. Cause racine commune aux 3 problèmes : **largeur trop contrainte** dans le sidebar de 320px (`w-80`).\n\n## Corrections\n\n**PROBLÈME 1 — barre de tabs croppée**\n- Avant : 8 boutons en `flex-1` sans gestion de débordement → les derniers labels coupés.\n- Après : les tabs sont dans un conteneur `overflow-x-auto` **scrollable horizontalement**, chaque tab passe en `shrink-0 whitespace-nowrap` (plus jamais croppé). Scrollbar masquée proprement (`[scrollbar-width:none]` + `[&::-webkit-scrollbar]:hidden`). Le bouton « collapse » («) reste épinglé à droite, hors zone de scroll.\n\n**PROBLÈME 2 — cartes d'agents qui se chevauchent**\n- Avant : `<li>` en `flex items-center justify-between` → le bloc nom/profil et la colonne de contrôles (dropdown profil + Sync/Stop/Launch/Delete, en `shrink-0`) se disputaient la largeur ; l'id de run long (`running in IdeA · <id>`) wrappait et débordait.\n- Après : carte en **`flex-col`** — bloc nom/profil/statut en haut, rangée de contrôles en dessous (`flex-wrap`, plus de `shrink-0` qui écrasait le texte). Hauteur adaptative au contenu (plus de chevauchement). L'id long est désormais **tronqué avec ellipsis** (`block w-full min-w-0 truncate`).\n\n**PROBLÈME 3 — formulaire de création (champs serrés + dropdowns « vides »)**\n- Avant : form en `flex flex-wrap items-end` avec 3 champs `min-w-0 flex-1` → selects écrasés à ~90px ; le texte sélectionné (« Claude Code ») était **clippé derrière la flèche native** du select, d'où l'impression que la sélection ne s'affichait pas.\n- Après : form en **`flex-col gap-3`**, chaque champ en pleine largeur. Le binding des `<select>` était déjà correct (`value`/`onChange` contrôlés, natifs) — c'était bien un problème de largeur, pas de binding. En pleine largeur la valeur choisie s'affiche normalement. Bouton Create passé en `self-end`.\n\nAucune nouvelle dépendance, aucun changement de logique métier ni de contrat IPC/gateway. Les tests s'appuient sur les `aria-label` (inchangés), d'où les 47/47 verts.\n\nTu peux enchaîner sur le rebuild de l'AppImage (rappel mémoire : c'est l'AppImage qui tourne qu'il faut reconstruire, pas seulement les sources)."}
|
||||
{"id":"9b53cc9b-ffed-42c9-9961-fefb5aca677e","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781675890156,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Tâche LS7-front (React/TS) — UI de gestion des limites de session des agents (ARCHITECTURE §21). On est sur la branche `feature/agent-session-limits`. Le backend est terminé et vert (commit 9df5923) : il émet déjà 5 DOMAIN_EVENT vers le front et expose une commande Tauri `cancel_resume(agentId) -> bool`.\n\nCONTRAT WIRE (DTO sérialisés camelCase, source = crates/app-tauri/src/events.rs) :\n- `agentRateLimited` { agentId: string, resetsAt?: number /* epoch-ms, absent si inconnu */ }\n- `agentResumeScheduled` { agentId: string, fireAt: number /* epoch-ms, échéance du réveil */ } ⚠️ champ wire = `fireAtMs` → vérifie le nom exact sérialisé (camelCase de `fire_at_ms` = `fireAtMs`) ; idem `resetsAtMs` pour les autres. Aligne-toi sur le JSON réel.\n- `agentResumeCancelled` { agentId: string }\n- `agentResumed` { agentId: string }\n- `agentRateLimitSuspected` { agentId: string, resetsAt?: number }\n(Vérifie les noms de champs exacts dans events.rs : `resets_at_ms`→`resetsAtMs`, `fire_at_ms`→`fireAtMs`. Ne devine pas, lis le fichier.)\n\nÀ FAIRE :\n1) Ajouter les 5 variantes au union `DomainEvent` de `src/domain/index.ts` (mêmes noms `type` que le wire), avec les champs exacts.\n2) Exposer la commande `cancel_resume` dans le port gateway approprié (cf. `src/ports/index.ts`) + son implémentation dans l'adapter système (`src/adapters/system.ts`) ET le mock (`src/adapters/mock/index.ts`). RÈGLE ARCHI STRICTE : les composants ne touchent JAMAIS `invoke()` ni `@tauri-apps/api` — tout passe par les gateways/ports (hexagonal §1.3). Suis le patron d'une commande existante (ex. `interruptAgent` si elle existe, sinon une autre commande agent).\n3) Tracker l'état limite par agent dans `useAgents` (view-model hook). Inspire-toi du patron existant `delegationSourceByRequester` qui se peuple depuis les events. État par agent : { limitedUntil?: number, resumeFireAt?: number, suspected?: bool }. Mises à jour :\n - `agentRateLimited` → limité (badge « limité jusqu'à HH:MM » si resetsAt connu, sinon « limité »).\n - `agentResumeScheduled` → arme le compte à rebours jusqu'à fireAt + bouton « Annuler la reprise ».\n - `agentResumeCancelled` → retire le compte à rebours (reste limité, sans reprise auto).\n - `agentResumed` → efface tout l'état limite de l'agent.\n - `agentRateLimitSuspected` → filet humain : signaler que la limite est détectée sans heure fiable et demander à l'utilisateur (au minimum un état/affichage « heure inconnue » ; le formulaire de saisie d'heure peut être un placeholder documenté si le backend n'a pas encore de commande pour l'enregistrer — dans ce cas, expose juste l'état et un TODO clair, NE fabrique pas de commande inexistante).\n4) UI : badge limite + compte à rebours + bouton « Annuler la reprise » (appelle `cancel_resume` via le gateway) dans la vue agents/terminal cohérente avec le style existant. Composant testable (gateways mockés).\n\nNE PAS committer toi-même (l'agent Git tranche les commits). NE PAS toucher au backend Rust. Reste cohérent avec le style TS/React existant. Quand c'est implémenté, QA écrira/exécutera les tests — donc structure pour la testabilité (logique d'état dans le hook, pas dans le JSX).\n\nRends-moi via idea_reply : la liste des fichiers modifiés/créés, les noms de champs wire exacts que tu as constatés dans events.rs, et tout point de friction (notamment sur le filet humain niveau 3 si aucune commande backend n'existe pour enregistrer l'heure saisie)."}
|
||||
{"id":"a37b2aac-87f7-476d-8606-7250d15dd852","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781676130924,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"LS7-front terminé. Typecheck `tsc --noEmit` propre + 39 tests agents existants verts (aucune régression). Non commité (Git tranche).\n\n## Noms de champs wire constatés (lus dans crates/app-tauri/src/events.rs, `DomainEventDto`, serde camelCase + skip_serializing_if sur les Option)\n- `agentRateLimited` → { `agentId`: string, `resetsAtMs`?: number /* absent si null */ }\n- `agentResumeScheduled` → { `agentId`: string, `fireAtMs`: number /* toujours présent */ }\n- `agentResumeCancelled` → { `agentId`: string }\n- `agentResumed` → { `agentId`: string }\n- `agentRateLimitSuspected` → { `agentId`: string, `resetsAtMs`?: number /* absent si null */ }\n\n⚠️ Confirmé : ce sont bien `resetsAtMs` / `fireAtMs` (suffixe `Ms`), PAS `resetsAt`/`fireAt` comme l'énoncé du ticket le suggérait. Je me suis aligné sur le JSON réel. Vérifié aussi par le test backend `rate_limited_relays_to_dto_and_wire` qui asserte `json[\"resetsAtMs\"]`.\n\nCommande Tauri `cancel_resume` (commands.rs:1381) : signature `cancel_resume(agent_id: String) -> Result<bool, _>` — argument **nu** `agentId` (PAS d'enveloppe `{ request }` contrairement à interrupt_agent/delegation_delivered). Adapter aligné en conséquence. Renvoie `true` ssi une reprise était armée et a été désarmée.\n\n## Fichiers modifiés\n1. `frontend/src/domain/index.ts` — 5 variantes ajoutées au union `DomainEvent` (mêmes `type` que le wire, champs exacts `resetsAtMs?`/`fireAtMs`).\n2. `frontend/src/ports/index.ts` — `cancelResume(agentId: string): Promise<boolean>` ajouté à `InputGateway` (port choisi : c'est le gateway des contrôles agent hors-bande, cohérent avec `interrupt`).\n3. `frontend/src/adapters/input.ts` — `TauriInputGateway.cancelResume` → `invoke(\"cancel_resume\", { agentId })` (arg nu, typé `<boolean>`).\n4. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.cancelResume` : enregistre dans `cancelledResumes: string[]`, renvoie `cancelResumeResult` (défaut `true`, flippable par les tests).\n5. `frontend/src/features/agents/useAgents.ts` — état `limitByAgent: Record<string, AgentLimitState>` (interface exportée `{ limitedUntil?, resumeFireAt?, suspected? }`), peuplé depuis les 5 events dans l'abonnement `onDomainEvent` existant (même patron que `delegationSourceByRequester`) :\n - `agentRateLimited` → `{ limitedUntil: resetsAtMs, suspected: false }` (conserve un `resumeFireAt` éventuel).\n - `agentResumeScheduled` → arme `resumeFireAt = fireAtMs`.\n - `agentResumeCancelled` → retire `resumeFireAt`, reste limité.\n - `agentResumed` → supprime l'entrée (efface tout).\n - `agentRateLimitSuspected` → `{ limitedUntil: resetsAtMs, suspected: true }`.\n - Action `cancelResume(agentId)` exposée : retrait optimiste du compte à rebours + appel `input.cancelResume`, renvoie le verdict backend.\n\n## Fichier créé\n6. `frontend/src/features/agents/AgentLimitBadge.tsx` — composant présentational + 2 helpers purs exportés (`formatResetTime(epochMs)` → `HH:MM`, `formatCountdown(remainingMs)` → `Xm Ys`/`Ys`, clampé à 0). Badge « limité jusqu'à HH:MM » / « limité », compte à rebours vivant (clock locale 1 s armée seulement si `resumeFireAt`), bouton « Annuler la reprise ». Câblé dans `AgentsPanel.tsx` (import + rendu conditionnel sous chaque ligne d'agent quand `vm.limitByAgent[a.id]` existe).\n\n## Point de friction — filet humain niveau 3 (§21.1)\nConformément à la consigne, je n'ai **fabriqué aucune commande inexistante**. Le seul point d'entrée backend disponible est `cancel_resume` ; **il n'existe pas de commande pour enregistrer une heure de reprise saisie par l'utilisateur**. Donc pour `agentRateLimitSuspected` sans `resetsAtMs`, le badge affiche l'état « limité · heure inconnue — reprise à préciser » (état + `role=\"note\"`), sans formulaire de saisie. Un `TODO(LS-front)` documenté est posé dans `AgentLimitBadge.tsx` : câbler un formulaire d'heure dès que le backend exposera une commande pour persister l'instant choisi. Si tu veux ce filet pleinement fonctionnel, il faut une tâche backend (ex. `set_resume_at(agentId, epochMs)`) à cadrer avec Architect/DevBackend — je m'y connecterai côté front.\n\nPrêt pour QA (logique d'état dans le hook + helpers purs isolés du JSX, mocks en place avec `cancelledResumes`/`cancelResumeResult`)."}
|
||||
{"id":"f21219de-033a-4b3e-8e8b-366534112a95","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781676945137,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Deux livrables distincts sur `feature/agent-session-limits` (Git en fera 2 commits séparés — ne commit pas toi-même).\n\n═══ LIVRABLE 1 — LS8-front : formulaire de saisie d'heure (filet humain niveau 3) ═══\nLe backend expose maintenant une commande Tauri `set_resume_at(agentId: string, resetsAtMs: number) -> void` (argument nu `{ agentId, resetsAtMs }`, comme `cancel_resume`). Elle arme la MÊME reprise annulable que l'auto et réémet `agentResumeScheduled` — donc une fois appelée, ton badge bascule TOUT SEUL de « heure inconnue » vers l'état nominal « limité jusqu'à HH:MM » + compte à rebours + bouton Annuler (déjà câblés en LS7). Aucun nouvel événement à consommer.\n\nÀ FAIRE :\n1. Port : ajoute `setResumeAt(agentId: string, resetsAtMs: number): Promise<void>` à `InputGateway` (`src/ports/index.ts`), à côté de `cancelResume`.\n2. Adapter Tauri (`src/adapters/input.ts`) : `setResumeAt` → `invoke(\"set_resume_at\", { agentId, resetsAtMs })`.\n3. Mock (`src/adapters/mock/index.ts`) : `MockInputGateway.setResumeAt` enregistre dans un tableau (ex. `resumeArmings: { agentId, resetsAtMs }[]`) pour les tests ; suis le patron de `cancelledResumes`.\n4. Hook `useAgents` : expose une action `setResumeAt(agentId, resetsAtMs)` qui délègue au port (pas de mutation optimiste nécessaire — l'event `agentResumeScheduled` rebasculera l'état). \n5. UI `AgentLimitBadge.tsx` : sur l'état SUSPECTED SANS heure (`suspected === true` && pas de `limitedUntil`/`resumeFireAt`), remplace le TODO/placeholder par un petit formulaire : un input heure (type `time` ou `datetime-local`) + bouton « Programmer la reprise ». À la soumission : convertir la saisie en epoch-ms (si input `time` seul = aujourd'hui à HH:MM ; si déjà passé dans la journée, le backend clampe à maintenant ⇒ reprise immédiate, donc pas besoin de validation stricte côté front, mais documente le comportement). Appeler l'action `setResumeAt(agentId, epochMs)`. Garde un helper pur exporté pour la conversion (ex. `timeInputToEpochMs(value, now)`) pour la testabilité. Style cohérent avec le reste du badge ; désactive pendant `busy`.\n6. Retire le `TODO(LS-front)` posé en LS7 (le filet est maintenant complet).\n\n═══ LIVRABLE 2 — fix test préexistant (hygiène, commit séparé) ═══\n`src/adapters/mock/mock.test.ts:16` asserte « thirteen gateways » mais il y en a 14 (la gateway `permission` existe déjà dans HEAD). Mets le compteur à jour (14, et le libellé texte si présent) pour que `npx vitest run src/adapters/mock/` repasse vert. NE change RIEN d'autre dans ce fichier ; vérifie juste que le compte réel est bien 14 avant de figer le nombre.\n\nLance `npx vitest run src/features/agents/ src/adapters/mock/` + `tsc --noEmit` et RENDS-MOI via idea_reply : fichiers modifiés/créés par livrable, le helper de conversion exact, le résultat des deux commandes (typecheck + tests), et le nombre réel de gateways constaté. Structure pour testabilité (logique dans le hook + helpers purs). NE COMMIT PAS — Git tranche, en 2 commits séparés (LS8-front / fix-test)."}
|
||||
{"id":"50c3e999-c6a3-4d8b-aef3-b6c273ed9afc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781677152871,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés).\n\n## Résultats des commandes\n- `npx tsc --noEmit` : **propre** (0 erreur).\n- `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**.\n\n## Nombre réel de gateways constaté : **14**\nLe `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés.\n\n## Helper de conversion exact (pur, exporté, testé)\n```ts\n// AgentLimitBadge.tsx\nexport function timeInputToEpochMs(value: string, now: number): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(value.trim());\n if (!match) return null;\n const hours = Number(match[1]);\n const minutes = Number(match[2]);\n if (hours > 23 || minutes > 59) return null;\n const d = new Date(now);\n d.setHours(hours, minutes, 0, 0);\n return d.getTime();\n}\n```\nComportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée).\n\n═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══\nFichiers modifiés :\n1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`.\n2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke(\"set_resume_at\", { agentId, resetsAtMs })` (args nus, comme cancel_resume).\n3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`).\n4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant).\n5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type=\"time\"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**.\n6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge.\n7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)*\n\n═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══\nFichier modifié :\n- `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `\"permission\"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier.\n\n## Point de friction / note\nPetit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label=\"schedule resume\"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `\"resume time form\"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle.\n\nLe filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`)."}
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
upTo: 190cd7b8-23ad-42cb-8873-a17ddddff71d
|
||||
objective: Petit test de communication inter-agents. Calcule 17 × 23 et donne-moi uniquement le résultat numérique. Réponds-moi impérativement via l'outil idea_reply (pas en texte libre).
|
||||
---
|
||||
**Objectif :** Petit test de communication inter-agents. Calcule 17 × 23 et donne-moi uniquement le résultat numérique. Réponds-moi impérativement via l'outil idea_reply (pas en texte libre).
|
||||
|
||||
- **Prompt:** Petit test de communication inter-agents. Calcule 17 × 23 et donne-moi uniquement le résultat numérique. Réponds-moi impérativement via l'outil idea_reply (pas en texte libre).
|
||||
- **Response:** 391
|
||||
- **Prompt:** Test de communication inter-agents. Calcule 17 × 23 et réponds-moi UNIQUEMENT le résultat numérique, impérativement via l'outil idea_reply (jamais en texte libre).
|
||||
- **Prompt:** Test de conversation inter-agent. Peux-tu calculer (347 × 28) + 1569 et me retourner uniquement le résultat numérique avec le détail du calcul ? Réponds via idea_reply.
|
||||
- **Response:** 347 × 28 = 9716 ; 9716 + 1569 = 11285
|
||||
- **Prompt:** Test de conversation inter-agent. Calcule 78 × 9 et réponds-moi simplement avec le résultat via idea_reply.
|
||||
- **Response:** 702
|
||||
@ -1,7 +0,0 @@
|
||||
{"id":"3cbaced3-dd78-4f48-b168-f09a26d83444","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781384306910,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Petit test de communication inter-agents. Calcule 17 × 23 et donne-moi uniquement le résultat numérique. Réponds-moi impérativement via l'outil idea_reply (pas en texte libre)."}
|
||||
{"id":"2dd9de9a-d59d-493e-bf07-ffccf6addeb1","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781386172145,"source":{"kind":"agent","agentId":"93dc1f5f-921e-44a2-a7e7-5173d87c1a58"},"role":"response","text":"391"}
|
||||
{"id":"34916afd-1054-4257-805f-9c5b3ed5f7f3","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781386172204,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Test de communication inter-agents. Calcule 17 × 23 et réponds-moi UNIQUEMENT le résultat numérique, impérativement via l'outil idea_reply (jamais en texte libre)."}
|
||||
{"id":"6e037034-c65a-4eca-b494-06420c5eed06","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781386464926,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Test de conversation inter-agent. Peux-tu calculer (347 × 28) + 1569 et me retourner uniquement le résultat numérique avec le détail du calcul ? Réponds via idea_reply."}
|
||||
{"id":"f82caa94-daba-4171-aa1f-8f69f71671c4","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781386489560,"source":{"kind":"agent","agentId":"93dc1f5f-921e-44a2-a7e7-5173d87c1a58"},"role":"response","text":"347 × 28 = 9716 ; 9716 + 1569 = 11285"}
|
||||
{"id":"6187c292-4d9a-4c07-9381-732486ab6d03","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781522784017,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Test de conversation inter-agent. Calcule 78 × 9 et réponds-moi simplement avec le résultat via idea_reply."}
|
||||
{"id":"190cd7b8-23ad-42cb-8873-a17ddddff71d","conversation":"3512c746-2a8d-06b1-17e4-cfee1fe78c19","atMs":1781522789866,"source":{"kind":"agent","agentId":"93dc1f5f-921e-44a2-a7e7-5173d87c1a58"},"role":"response","text":"702"}
|
||||
@ -1,26 +0,0 @@
|
||||
---
|
||||
upTo: 6df9f7b7-fcc4-4b87-94db-33dce2d76f13
|
||||
objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
|
||||
---
|
||||
**Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
|
||||
|
||||
- **Response:** Décision (agent Git) — branche retenue : **`feature/agent-session-limits`**, créée **depuis `develop`**. **Raisonnement :** - Nouvelle feature indépendante (LS1→LS8) → conformément au modèle `main ← develop ← feature/*`, elle se développe sur sa propre branche `feature/*`. - Pas besoin de (re)créer `develop` : il existe déjà et est à jour (post-release 0.1.0, `git diff main develop` vide → contenu identique). C'est donc la bonne base de départ. - Nom dérivé du sujet, kebab-case : `feature/agent-session-limits`. **État git après manœuvre (LOCAL, aucun push) :** - Branche courante : `feature/agent-session-limits` @ `401c18a` (= base `develop`). - `develop` @ `401c18a` (inchangé) ← `main` @ `9a8a89f` (tag `v0.1.0`, inchangé). - `feature/agent-skills` @ `ef101db` (autre feature, intacte). **Note :** le working tree porte déjà des changements non commités hérités de l'état courant — notamment `ARCHITECTURE.md` (cadrage Architect) et un nouveau `.ideai/memory/session-limit-handling-design.md`, plus du runtime `.ideai/` (conversations/MEMORY.md). Ils n'ont pas été commités : DevBackend/QA les intégreront dans les commits de la feature au fil des lots. Le cadrage Architect (ARCHITECTURE.md + memory design) pourra faire le **premier commit** de la branche avant LS1 si tu le souhaites — dis-moi si tu veux que je le commite maintenant. Dev peut démarrer LS1 (domaine) sur `feature/agent-session-limits`. Après chaque lot vert, reviens vers moi pour décider d'un éventuel commit/merge. Aucune intégration vers `develop` tant que les tests des lots concernés ne sont pas verts (règle d'or).
|
||||
- **Prompt:** Feu vert pour le premier commit de la branche feature/agent-session-limits : commite le cadrage AVANT que le dev (LS1) ne démarre, pour isoler proprement le cadrage du code. Contenu à committer (cadrage uniquement) : ARCHITECTURE.md (nouvelle §21) + .ideai/memory/session-limit-handling-design.md + la mise à jour de .ideai/memory/MEMORY.md (pointeur). N'inclus PAS le runtime .ideai/ non pertinent (conversations, layouts, agents.json…) s'il pollue le diff — limite le commit aux fichiers du cadrage. Message de commit conforme au style du dépôt (français, conventionnel). Périmètre LOCAL uniquement — aucun push. Confirme le hash du commit et l'état git après.
|
||||
- **Response:** Cadrage committé sur `feature/agent-session-limits` — premier commit de la branche, isolé du code. **Commit : `fa5b826`** `docs(session-limits): cadrage Architect — gestion des limites de session des agents` **Contenu (cadrage uniquement, 3 fichiers / +212 lignes) :** - `ARCHITECTURE.md` (+185, nouvelle §21) - `.ideai/memory/session-limit-handling-design.md` (nouveau, design validé) - `.ideai/memory/MEMORY.md` (+1, pointeur) Le runtime `.ideai/` non pertinent (conversations, layouts, agents.json…) a été **exclu** du diff comme demandé. **État git après commit (LOCAL, aucun push) :** - `feature/agent-session-limits` @ `fa5b826` ← `develop` @ `401c18a` ← `main` @ `9a8a89f` (tag `v0.1.0`). - Working tree : reste uniquement du runtime `.ideai/conversations/…` non commité (bruit, ignoré pour le cadrage). La branche est prête : le dev peut démarrer **LS1 (domaine)**. Je reviendrai dans la boucle pour committer chaque lot vert.
|
||||
- **Prompt:** LS1 (couche domaine) de la feature « limites de session » est VERT : DevBackend a livré le code pur, QA a écrit 24 tests, `cargo test -p domain` = 165 passed / 0 failed, zéro régression. Commite LS1 sur feature/agent-session-limits (commit suivant après le cadrage fa5b826). Périmètre LOCAL uniquement, aucun push. Fichiers de prod (domaine) modifiés/créés par DevBackend : - crates/domain/src/ports.rs (variante ReplyEvent::RateLimited) - crates/domain/src/readiness.rs (variante ReadinessSignal::RateLimited + classify) - crates/domain/src/session_limit.rs (NOUVEAU : SessionLimit, ResumePlan, RateLimitSource, plan_resume) - crates/domain/src/profile.rs (RateLimitPattern + champ + builder) - crates/domain/src/events.rs (5 variantes DomainEvent) - crates/domain/src/lib.rs (mod + re-exports) Plus les tests QA dans ces mêmes fichiers (#[cfg(test)]). N'inclus PAS le runtime .ideai/ non pertinent. Message conventionnel en français. IMPORTANT — décision de merge : NE merge PAS vers develop maintenant. Le workspace complet (`cargo build --workspace`) NE COMPILE PAS tant que LS2 n'a pas ajouté l'arm `ReplyEvent::RateLimited` côté infra (match exhaustifs). On ne merge feature/* → develop que sur une tranche qui compile de bout en bout (cible : LS1+LS2+LS4 = niveau 1 Claude complet). Confirme juste le hash du commit LS1 et l'état git.
|
||||
- **Response:** LS1 (couche domaine) committé sur `feature/agent-session-limits`, après le cadrage `fa5b826`. **Commit : `0bf1eb3`** `feat(session-limits): LS1 — couche domaine (détection + plan de reprise)` **Contenu (6 fichiers domaine, prod + tests inline / +647 lignes) :** - `crates/domain/src/session_limit.rs` (nouveau) - `crates/domain/src/ports.rs` (ReplyEvent::RateLimited) - `crates/domain/src/readiness.rs` (ReadinessSignal::RateLimited + classify) - `crates/domain/src/profile.rs` (RateLimitPattern + champ + builder) - `crates/domain/src/events.rs` (5 variantes DomainEvent) - `crates/domain/src/lib.rs` (mod + re-exports) Runtime `.ideai/` non pertinent **exclu**. QA : `cargo test -p domain` = 165 passed / 0 failed, zéro régression. **Décision de merge : PAS de merge vers `develop`** — conforme à ta consigne. Le workspace ne compile pas tant que LS2 n'a pas ajouté l'arm `ReplyEvent::RateLimited` côté infra (match exhaustifs). On n'intègre `feature/* → develop` que sur une tranche qui compile de bout en bout (cible : LS1+LS2+LS4 = niveau 1 Claude complet). Je reste sur la branche feature. **État git (LOCAL, aucun push) :** ``` feature/agent-session-limits @ 0bf1eb3 (HEAD) └ fa5b826 docs cadrage └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0) ``` Working tree : seul du runtime `.ideai/conversations/…` non commité (ignoré). Dev peut enchaîner sur LS2.
|
||||
- **Prompt:** LS2 (adapter Claude niveau 1, infra) est VERT : `cargo test -p infrastructure` = 188 passed / 0 failed, zéro régression. Commite LS2 sur feature/agent-session-limits (après LS1 0bf1eb3). LOCAL uniquement, aucun push. Fichier de prod modifié : crates/infrastructure/src/session/claude.rs (parse_event émet ReplyEvent::RateLimited ; nouvelle fonction pure parse_reset_ms + helpers ; parseur ISO maison ; doccomments T4). Tests QA : dans crates/infrastructure/src/session/mod.rs (#[cfg(test)]) — 26 nouveaux tests + 2 tests existants mis à jour vers le nouveau contrat ; et crates/infrastructure/src/session/conformance.rs (#[cfg(test)] : RateLimited ajouté aux événements non terminaux autorisés). Exclu le runtime .ideai/. Message conventionnel français. Décision merge : TOUJOURS PAS de merge vers develop (le workspace ne compile pas encore — 2 erreurs résiduelles attendues dans app-tauri, relèvent de LS7/LS8 ; et la réconciliation T4 reste à faire en LS4). On vise la tranche LS1+LS2+LS3+LS4 = niveau 1 Claude de bout en bout qui compile, pour le premier merge. Confirme le hash LS2 et l'état git.
|
||||
- **Response:** LS2 (adapter Claude niveau 1, infra) committé sur `feature/agent-session-limits`, après LS1 `0bf1eb3`. **Commit : `a1755e5`** `feat(session-limits): LS2 — adapter Claude niveau 1 (infra)` **Contenu (3 fichiers infra, prod + tests inline / +545 lignes) :** - `crates/infrastructure/src/session/claude.rs` (prod : parse_event → ReplyEvent::RateLimited, parse_reset_ms + helpers, parseur ISO maison, doccomments T4) - `crates/infrastructure/src/session/mod.rs` (26 nouveaux tests QA + 2 alignés) - `crates/infrastructure/src/session/conformance.rs` (RateLimited en non-terminal autorisé) Runtime `.ideai/` **exclu**. QA : `cargo test -p infrastructure` = 188 passed / 0 failed, zéro régression. **Décision de merge : TOUJOURS PAS de merge vers `develop`** — conforme. Le workspace ne compile pas encore (2 erreurs résiduelles attendues dans app-tauri → LS7/LS8 ; réconciliation T4 → LS4). Premier merge visé sur la tranche **LS1+LS2+LS3+LS4 = niveau 1 Claude de bout en bout qui compile**. **État git (LOCAL, aucun push) :** ``` feature/agent-session-limits @ a1755e5 (HEAD) └ 0bf1eb3 LS1 domaine └ fa5b826 cadrage └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0) ``` Working tree : seul du runtime `.ideai/` non commité (ignoré). Dev peut enchaîner sur LS3.
|
||||
- **Prompt:** LS3 (port Scheduler + adapter TokioScheduler) est VERT : `cargo test -p infrastructure` = 195 passed / 0 failed (7 nouveaux tests scheduler, relancés 3× sans flaky), LS1/LS2 toujours verts. Builds domaine + infra OK 0 warning. Le workspace complet ne compile toujours pas (2 erreurs résiduelles inchangées dans app-tauri : chat.rs:185 → LS8, events.rs:261 → LS7 ; la réconciliation T4 reste en LS4). C'est TOI qui décides quoi et quand committer/merger. Je te livre l'état, tu tranches. Fichiers de prod LS3 : - crates/domain/src/ids.rs (ScheduleId via typed_id!) - crates/domain/src/ports.rs (trait Scheduler + enum ScheduledTask) - crates/domain/src/lib.rs (re-exports) - crates/infrastructure/src/scheduler/mod.rs (NOUVEAU : TokioScheduler) - crates/infrastructure/src/lib.rs (pub mod scheduler + re-export) Tests QA : dans crates/infrastructure/src/scheduler/mod.rs (#[cfg(test)]). Runtime .ideai/ à exclure du diff. Décide : faut-il committer LS3 maintenant (et avec quel message) ? Et concernant le merge vers develop — rappel du contexte : le premier merge envisagé portait sur la tranche LS1+LS2+LS3+LS4 (niveau 1 Claude de bout en bout qui compile), or le workspace ne compile pas encore (LS4 + app-tauri manquants). Dis-moi ta décision (commit LS3 sur la feature ? attendre la tranche compilable pour merger ? autre ?) et exécute-la. Confirme hash + état git.
|
||||
- **Response:** Décision (agent Git) : **commit LS3 sur la feature, PAS de merge vers `develop`.** **Pourquoi :** chaque lot vert mérite son commit atomique sur la branche feature (traçabilité, bissection facile), mais le merge `feature/* → develop` exige une tranche qui **compile de bout en bout** — règle d'or. Or le workspace ne compile toujours pas (réconciliation T4 en LS4 + 2 erreurs résiduelles app-tauri : `chat.rs:185`→LS8, `events.rs:261`→LS7). Donc on continue d'accumuler sur la feature ; premier merge maintenu sur la tranche compilable (LS1+LS2+LS3+LS4 niveau 1 Claude, voire jusqu'à LS7/LS8 si la compilation workspace n'est verte qu'à ce moment-là). **Commit : `253310b`** `feat(session-limits): LS3 — port Scheduler + adapter TokioScheduler` **Contenu (5 fichiers prod + tests inline / +389 lignes) :** - domaine : `ports.rs` (trait Scheduler + enum ScheduledTask), `ids.rs` (ScheduleId), `lib.rs` (re-exports) - infra : `scheduler/mod.rs` (nouveau, TokioScheduler), `lib.rs` (pub mod + re-export) Runtime `.ideai/` **exclu**. QA : `cargo test -p infrastructure` = 195 passed / 0 failed (7 tests scheduler, 3× sans flaky) ; LS1/LS2 verts ; builds domaine+infra 0 warning. **État git (LOCAL, aucun push) :** ``` feature/agent-session-limits @ 253310b (HEAD) └ a1755e5 LS2 adapter Claude └ 0bf1eb3 LS1 domaine └ fa5b826 cadrage └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0) ``` Dev peut enchaîner sur LS4 (service application + réconciliation T4). Je tranche le merge dès que le workspace compile.
|
||||
- **Prompt:** LS4 (couche application : SessionLimitService + réconciliation T4) est VERT : `cargo test -p application` = tous binaires verts / 0 failed (16 nouveaux tests), zéro régression confirmée (drain_with_readiness_lot1 7/7, send_blocking_d1 9/9, reste du crate vert). Builds domaine+infra+application 0 warning. Le workspace complet ne compile TOUJOURS pas (inchangé : app-tauri chat.rs:185 → LS8, events.rs:261 → LS7). C'est toi qui décides quoi/quand committer/merger. État livré : Fichiers de prod LS4 : - crates/application/src/agent/session_limit.rs (NOUVEAU : SessionLimitService + port AgentResumer + const RESUME_PROMPT) - crates/application/src/agent/structured.rs (réconciliation T4 : enum TurnOutcome + drain_with_readiness_outcome ; signatures historiques préservées) - crates/application/src/agent/mod.rs (mod + re-exports) - crates/application/src/lib.rs (re-exports) Tests QA : crates/application/tests/session_limit_service.rs (9) + crates/application/tests/session_limit_t4.rs (7) (NOUVEAUX). Runtime .ideai/ à exclure. Décide et exécute (commit LS4 sur la feature ? message ? merge ou pas ?). Pour mémoire : le workspace ne compile de bout en bout qu'après LS7 (events app-tauri) + LS8 (chat.rs) ; le premier merge vers develop reste donc subordonné à ta règle d'or « tranche qui compile ». Confirme hash + état git.
|
||||
- **Response:** Décision (agent Git) : **commit LS4 sur la feature, PAS de merge vers `develop`.** **Commit : `9000b4d`** `feat(session-limits): LS4 — service application + réconciliation T4` **Contenu (6 fichiers, 4 prod + 2 tests / +987 lignes) :** - `crates/application/src/agent/session_limit.rs` (nouveau : SessionLimitService + port AgentResumer + RESUME_PROMPT) - `crates/application/src/agent/structured.rs` (T4 : enum TurnOutcome + drain_with_readiness_outcome, signatures historiques préservées) - `crates/application/src/agent/mod.rs` + `crates/application/src/lib.rs` (modules/re-exports) - `crates/application/tests/session_limit_service.rs` (9) + `crates/application/tests/session_limit_t4.rs` (7) — nouveaux Runtime `.ideai/` **exclu**. QA : `cargo test -p application` tous binaires verts / 0 failed (16 nouveaux) ; zéro régression (drain_with_readiness_lot1 7/7, send_blocking_d1 9/9) ; builds domaine+infra+application 0 warning. **Décision de merge : toujours NON.** La tranche LS1→LS4 est verte par crate, mais `cargo build --workspace` échoue encore (app-tauri : `chat.rs:185`→LS8, `events.rs:261`→LS7). Ma règle d'or impose une tranche qui **compile de bout en bout** avant tout `feature/* → develop`. Comme tu me l'as confirmé, le workspace ne sera vert qu'après LS7+LS8 → **le premier merge se fera donc sur LS1→LS8** (et non LS1→LS4 comme envisagé initialement). On accumule jusque-là sur la branche feature. **État git (LOCAL, aucun push) :** ``` feature/agent-session-limits @ 9000b4d (HEAD) └ 253310b LS3 · a1755e5 LS2 · 0bf1eb3 LS1 · fa5b826 cadrage └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0) ``` Dev peut enchaîner sur LS5 (parser regex). Je tranche le merge dès `cargo build --workspace` vert.
|
||||
- **Prompt:** LS5 (parser regex niveau 2, infra) est VERT : `cargo test -p infrastructure` = 221 passed / 0 failed (26 nouveaux), 0 warning, LS2/LS3 toujours verts (refactor timeparse non régressif). C'est toi qui décides commit/merge. Fichiers de prod LS5 : - crates/infrastructure/Cargo.toml (dépendance regex = "1", résout 1.12.3 déjà dans Cargo.lock) - crates/infrastructure/src/ratelimit/mod.rs (NOUVEAU : RateLimitParser + applies) - crates/infrastructure/src/timeparse.rs (NOUVEAU : parsing d'heure pur partagé, factorisé depuis LS2) - crates/infrastructure/src/session/claude.rs (délègue à timeparse — refactor) - crates/infrastructure/src/lib.rs (pub mod ratelimit + timeparse, re-export) Modifs de TEST QA : crates/infrastructure/src/ratelimit/mod.rs (#[cfg(test)] 15 tests), crates/infrastructure/src/timeparse.rs (#[cfg(test)] 11 tests), crates/infrastructure/src/scheduler/mod.rs (2 warnings irrefutable if let corrigés dans le code de test). NOTE : Cargo.lock peut être inchangé (regex déjà verrouillée) — inclus-le seulement s'il bouge. Runtime .ideai/ à exclure. Décide et exécute (commit LS5 ? message ? merge ?). Pour mémoire le workspace ne compile de bout en bout qu'après LS7+LS8. Confirme hash + état git.
|
||||
- **Prompt:** Feature session-limits, suite. Project root: /home/anthony/Documents/Projects/IdeA. Deux lots sont terminés et VERTS (workspace compile, `cargo test --workspace` tout vert, clippy propre sur le nouveau code) mais NON committés : - LS5 — détecteur niveau 2 déclaratif + parsing temps partagé. Fichiers : nouveau `crates/infrastructure/src/ratelimit/mod.rs` (RateLimitParser, regex confiné infra), nouveau `crates/infrastructure/src/timeparse.rs` (parsing d'heures pur, partagé niveau 1/2), modifs `crates/infrastructure/src/session/claude.rs` (refactor vers timeparse, ~-121 lignes), `crates/infrastructure/src/scheduler/mod.rs`, `crates/infrastructure/src/lib.rs` (exports), `crates/infrastructure/Cargo.toml` (dép `regex`), `Cargo.lock`. - LS6 — câblage des 5 nouvelles variantes DomainEvent (AgentRateLimited/ResumeScheduled/ResumeCancelled/Resumed/RateLimitSuspected) + ReplyEvent::RateLimited vers le front. Fichiers : `crates/app-tauri/src/events.rs` (DTO miroir + bras From + test relais), `crates/app-tauri/src/chat.rs` (chunk_from_event). Note : l'arbre contient aussi des fichiers runtime .ideai/ modifiés (conversations/*/handoff.md, log.jsonl, layouts.json, memory/) et un nouveau memory `git-owns-commit-merge-decisions.md` — à toi de juger ce qui entre ou non dans le(s) commit(s) applicatif(s) (rappel : les LS précédents committaient uniquement le code applicatif, pas le bruit runtime). La branche courante est détachée (HEAD), pas sur develop. À toi de décider : topologie de branche, découpage en un ou deux commits (LS5 puis LS6, ou groupés), messages dans le style des commits feat(session-limits) existants, et si un merge feature/* → develop doit suivre. Périmètre LOCAL uniquement (aucun push). Exécute ta décision et rends via idea_reply le détail (branche, sha, messages, état final de `git status`).
|
||||
- **Response:** Fait. Décisions et exécution : **Branche** — Contrairement à ce qui était annoncé, HEAD n'était pas détachée : on était bien sur `feature/agent-session-limits` (la branche de la feature). Aucune création/bascule nécessaire, le dev se poursuit dessus. Rien à corriger. **Découpage — 2 commits atomiques** (LS5 puis LS6, séparés car deux intentions distinctes : couche infra de détection vs câblage événementiel vers le front) : 1. `98bfcf4` — feat(session-limits): LS5 — détecteur niveau 2 déclaratif + parsing temps partagé Fichiers : `crates/infrastructure/src/ratelimit/mod.rs` (nouv.), `crates/infrastructure/src/timeparse.rs` (nouv.), `crates/infrastructure/src/session/claude.rs`, `crates/infrastructure/src/scheduler/mod.rs`, `crates/infrastructure/src/lib.rs`, `crates/infrastructure/Cargo.toml`, `Cargo.lock`. 2. `ea94e75` — feat(session-limits): LS6 — câblage des événements de limite vers le front Fichiers : `crates/app-tauri/src/events.rs`, `crates/app-tauri/src/chat.rs`. Messages en français, style Conventional Commits cohérent avec LS1→LS4, corps expliquant le pourquoi, co-author inclus. **Bruit runtime exclu** — Non committé, comme pour les LS précédents : `.ideai/conversations/*` (handoff.md, log.jsonl), `.ideai/layouts.json`, `.ideai/memory/` (dont MEMORY.md et le nouveau `git-owns-commit-merge-decisions.md`), et la nouvelle conversation `6bc594e8-…`. Ce sont des artefacts d'exécution, pas du code applicatif. **Merge feature/* → develop : NON.** La feature session-limits est livrée par incréments (série LS en cours) ; LS5/LS6 sont verts mais internes (détecteur niveau 2 + relais d'événements) sans surface produit complète. On reste sur `feature/agent-session-limits`. Le merge vers `develop` se fera quand la feature sera terminée et toujours verte. **Périmètre** — 100% local, aucun push. **État final `git status`** : working tree propre côté applicatif ; ne subsistent que les fichiers runtime `.ideai/` modifiés/non suivis (volontairement hors commit). Derniers commits : ea94e75 LS6 / 98bfcf4 LS5 / 9000b4d LS4.
|
||||
- **Prompt:** Slice LS7 (backend / câblage app-tauri) de la feature session-limits terminée et verte. On est sur la branche feature/agent-session-limits (HEAD détaché ? `git status` montre HEAD), dernier commit ea94e75 (LS6). Contenu à committer (tout est vert, aucun warning) : - crates/application/src/agent/lifecycle.rs : `LaunchAgentOutput.profile` exposé (None sur réattache/idempotent, Some sur lancement effectif). - crates/application/src/terminal/registry.rs : `StructuredSessions::meta_for_session()` (lookup agent/node par SessionId pour le tap niveau 1). - crates/app-tauri/src/state.rs : `ResumeContext`/`ResumeContexts`, `AppAgentResumer` (impl port `AgentResumer` au-dessus de LaunchAgent), instanciation+câblage du `SessionLimitService` (TokioScheduler + drain des réveils) dans `AppState::build`. - crates/app-tauri/src/commands.rs : taps niveau 1 (agent_send) et niveau 2 (launch_agent, parser regex confiné), alimentation de `resume_contexts`, nouvelle commande `cancel_resume`. - crates/app-tauri/src/lib.rs : enregistrement de `cancel_resume` dans le handler. - crates/app-tauri/Cargo.toml : dépendance `async-trait`. - crates/app-tauri/tests/session_limit_wiring.rs : 2 tests d'intégration (composition) — verts. - crates/app-tauri/tests/dto_agents.rs + dto_chat.rs : ajustement `profile: None` dans les fixtures. - crates/application/tests/structured_registry_d1.rs : test de `meta_for_session`. Hors périmètre code (à NE PAS committer dans ce commit applicatif) : .ideai/conversations/**, .ideai/layouts.json, .ideai/memory/** — ce sont des artefacts runtime/mémoire. Questions : 1) Faut-il committer cette slice LS7-backend maintenant, et sous quel message ? 2) Reste-t-on sur feature/agent-session-limits ? 3) Décides-tu d'un merge vers develop, ou on attend LS7-front (React/TS, badge + compte à rebours + filet humain) qui complète la surface produit ? Tranche et exécute le commit applicatif si tu juges que oui.
|
||||
- **Response:** Décision Git — LS7 backend committée, on reste en feature, pas de merge maintenant. État initial : on était bien **sur la branche** `feature/agent-session-limits` (pas de HEAD détaché — le « HEAD » de ton `git status` était juste l'en-tête francisé), HEAD = ea94e75 (LS6). 1) ✅ Commit fait. Hash **9df5923** : `feat(session-limits): LS7 — câblage backend app-tauri (taps niveaux 1&2 + reprise annulable)` - 10 fichiers, +406/−3, dont la création de `tests/session_limit_wiring.rs`. - Périmètre strictement applicatif : lifecycle.rs, registry.rs, state.rs, commands.rs, lib.rs, Cargo.toml + les 4 fichiers de tests. - **Exclus comme demandé** (restés non-stagés, ce sont des artefacts runtime/mémoire) : `.ideai/conversations/**`, `.ideai/layouts.json`, `.ideai/memory/**` (dont le nouveau `git-owns-commit-merge-decisions.md` et le `6bc594e8…/` non suivi). Commit atomique = code seulement. 2) ✅ On **reste sur `feature/agent-session-limits`**. La feature n'est pas terminée côté produit (il manque LS7-front), donc on continue dessus. 3) ❌ **Pas de merge vers `develop` maintenant.** Raison : la slice est verte mais la surface produit session-limits est **incomplète sans LS7-front** (badge + compte à rebours + filet humain). Merger le backend seul livrerait dans `develop` une mécanique de reprise annulable sans son UI de contrôle/annulation visible — état intermédiaire que je ne veux pas figer dans l'intégration. **On attend LS7-front.** Une fois le front implémenté et la suite complète verte, reviens vers moi : je rebase si besoin sur `develop`, puis merge `--no-ff` la feature entière d'un coup (backend + front cohérents). Prochaine étape côté toi : enchaîner LS7-front (React/TS) sur cette même branche. Quand c'est vert, ping-moi pour le merge d'intégration.
|
||||
- **Prompt:** Slice LS7-front (React/TS, UI limites de session) terminée et VERTE sur `feature/agent-session-limits`. État tests : `npx vitest run src/features/agents/` → 4 fichiers, 63 tests passés (39 existants + 24 nouveaux), 0 régression. Typecheck `tsc --noEmit` propre. Fichiers à committer (frontend/ uniquement) : - src/domain/index.ts — 5 variantes ajoutées au union DomainEvent (agentRateLimited/ResumeScheduled/ResumeCancelled/Resumed/RateLimitSuspected). - src/ports/index.ts — `cancelResume(agentId)` ajouté à InputGateway. - src/adapters/input.ts — TauriInputGateway.cancelResume → invoke("cancel_resume", { agentId }). - src/adapters/mock/index.ts — MockInputGateway.cancelResume (cancelledResumes/cancelResumeResult). - src/features/agents/useAgents.ts — état limitByAgent + action cancelResume. - src/features/agents/AgentLimitBadge.tsx — NOUVEAU (badge + compte à rebours + bouton Annuler + helpers purs). - src/features/agents/AgentsPanel.tsx — câblage du badge. - src/features/agents/useAgentsLimits.test.tsx — NOUVEAU (13 tests, QA). - src/features/agents/AgentLimitBadge.test.tsx — NOUVEAU (11 tests, QA). Hors périmètre (NE PAS committer) : .ideai/** (artefacts runtime/mémoire). Aucun fichier backend touché. NOTE : il existe un test PRÉEXISTANT rouge SANS rapport avec cette slice — `src/adapters/mock/mock.test.ts:16` asserte « thirteen gateways » alors qu'il y en a 14 (gateway `permission` déjà présente dans HEAD committé eca2ba9). Compteur périmé, pas causé par LS7-front. À toi de voir si tu veux que je fasse corriger ce compteur (tâche séparée DevFrontend) avant/après — dis-moi. Questions : 1) Committe la slice LS7-front maintenant ? Sous quel message ? 2) Le merge --no-ff de la feature complète vers develop : on attend que je tranche le dernier point de scope (filet humain niveau 3 — voir consultation Architect en cours : peut nécessiter une slice backend+front supplémentaire `set_resume_at`), donc NE merge pas encore vers develop — confirme que tu te limites au commit de cette slice et qu'on garde le merge pour quand je te recontacte « feature complète ». Tranche et exécute le commit.
|
||||
- **Response:** Décision Git — LS7-front committée, pas de merge develop (j'attends ton « feature complète »). 1) ✅ Commit fait. Hash **4fad042** : `feat(session-limits): LS7-front — UI limites de session (badge + compte à rebours + filet humain)` - 9 fichiers, +702/−1, dont 3 créations (AgentLimitBadge.tsx + ses 2 fichiers de tests). - Périmètre strictement frontend : domain/index.ts, ports/index.ts, adapters/input.ts, adapters/mock/index.ts, useAgents.ts, AgentLimitBadge.tsx, AgentsPanel.tsx + les 2 tests. - **Exclus comme demandé** (non-stagés) : `.ideai/**` (conversations, layouts, memory) et `node_modules/` (non suivi — au passage, il traîne en untracked ; à gitignorer dans une tâche d'hygiène séparée si ce n'est pas déjà couvert, mais hors périmètre ici). Aucun fichier backend touché. Commit atomique = frontend seulement. 2) ✅ Confirmé : **je me limite à ce commit, pas de merge vers `develop`.** Je garde le `--no-ff` d'intégration de la feature complète pour quand tu me recontactes « feature complète », une fois tranché le scope du filet humain niveau 3 (slice `set_resume_at` éventuelle, cf. consultation Architect). Concernant le **test préexistant rouge** `mock.test.ts:16` (« thirteen gateways » → 14 réelles, gateway `permission` déjà dans HEAD eca2ba9) : compteur périmé sans rapport avec LS7. Mon avis Git : à corriger **avant le merge d'intégration**, en **commit séparé** (`fix(test): …` ou `chore(test): …`), pour ne pas merger `develop` avec une suite rouge. Lance une tâche DevFrontend dédiée quand tu veux ; tu peux l'enchaîner maintenant ou la grouper avec l'éventuelle slice `set_resume_at` — dans tous les cas elle aura son propre commit, et je ne mergerai pas tant que toute la suite n'est pas verte. État branche : `feature/agent-session-limits` → … ea94e75 (LS6) · 9df5923 (LS7 back) · **4fad042 (LS7 front, HEAD)**.
|
||||
- **Prompt:** Feature session-limits COMPLÈTE (3 niveaux) et verte. Reste à committer 2 slices, puis on peut envisager le merge d'intégration `feature/agent-session-limits → develop` que tu gardais en attente. État branche actuel : … 9df5923 (LS7 back) · 4fad042 (LS7 front, HEAD). Travail NON committé depuis, à committer maintenant en 2 commits séparés : ═══ COMMIT A — LS8-backend (filet humain niveau 3, backend) ═══ Implémenté par DevBackend, testé par QA (application: 15 tests session_limit_service / app-tauri: 4 wiring, + régressions vertes, 0 failed). Fichiers : - crates/application/src/agent/session_limit.rs — refactor privé `arm_scheduled` (param `resets_at_ms` brut ajouté) partagé par `on_rate_limited` + nouvelle `pub fn confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms: i64)` (source Human, réutilise la branche Scheduled, annulable). - crates/app-tauri/src/commands.rs — nouvelle commande `set_resume_at(agent_id, resets_at_ms) -> Result<(), ErrorDto>` (résout node_id via node_for_agent + conversation_id best-effort, NOT_FOUND si pas de cellule vivante). - crates/app-tauri/src/lib.rs — `set_resume_at` enregistrée après `cancel_resume`. - crates/application/tests/session_limit_service.rs — +6 tests (QA). - crates/app-tauri/tests/session_limit_wiring.rs — +2 tests (QA). Aucun événement nouveau (réutilise AgentRateLimited + AgentResumeScheduled). ═══ COMMIT B — LS8-front + fix test (DevFrontend a demandé 2 commits ; à toi de voir si tu sépares ou regroupes) ═══ LS8-front (typecheck propre, 109 tests verts) : - frontend/src/ports/index.ts — `setResumeAt(agentId, resetsAtMs)` sur InputGateway. - frontend/src/adapters/input.ts — `setResumeAt` → invoke("set_resume_at", { agentId, resetsAtMs }). - frontend/src/adapters/mock/index.ts — MockInputGateway.setResumeAt (resumeArmings[]). - frontend/src/features/agents/useAgents.ts — action setResumeAt (sans mutation optimiste). - frontend/src/features/agents/AgentLimitBadge.tsx — formulaire de saisie d'heure sur l'état suspected sans heure + helper pur `timeInputToEpochMs` ; TODO LS7 retiré. - frontend/src/features/agents/AgentsPanel.tsx — câblage onSetResumeAt. - frontend/src/features/agents/AgentLimitBadge.test.tsx — tests mis à jour au nouveau contrat + couverture LS8. Fix test hygiène (DevFrontend recommande commit SÉPARÉ) : - frontend/src/adapters/mock/mock.test.ts — compteur gateways 13→14 (« thirteen »→« fourteen » + `permission` dans l'array). Vérifié : 14 gateways réelles. Hors périmètre (NE PAS committer) : .ideai/** (runtime/mémoire), node_modules/ (untracked — DevFrontend signalait qu'il traîne, à gitignorer dans une tâche d'hygiène séparée, hors périmètre ici). QUESTIONS : 1) Committe COMMIT A puis les commits front (LS8-front + fix-test, séparés ou groupés selon ton jugement). Donne-moi les hashes + messages. 2) La suite complète est maintenant verte (Rust: domain/application/app-tauri ; front: agents + adapters/mock 109 tests) et la surface produit session-limits est complète sur les 3 niveaux. Décides-tu MAINTENANT le merge `--no-ff` vers develop (rebase si besoin) ? Si oui, exécute-le et donne-moi le résultat. Si tu veux un dernier garde-fou (ex. run de la suite complète des deux côtés avant merge), dis-le et je le fais lancer.
|
||||
- **Response:** ✅ 3 commits + merge d'intégration faits. La feature session-limits est intégrée dans `develop`. ═══ 1) Les 3 commits (sur feature/agent-session-limits) ═══ J'ai séparé le fix-test de la feature (hygiène isolée, comme recommandé par DevFrontend) → 3 commits : - **c480d28** `feat(session-limits): LS8-backend — filet humain niveau 3 (set_resume_at)` (session_limit.rs + commands.rs + lib.rs + tests service/wiring ; 5 fichiers, +394/−18) - **5d9dd32** `feat(session-limits): LS8-front — filet humain niveau 3 (saisie d'heure de reprise)` (ports/input/mock + useAgents + AgentLimitBadge(.tsx/.test) + AgentsPanel ; 7 fichiers, +222/−26) - **3f3504e** `fix(test): corrige le compteur de gateways du mock (13 → 14)` (mock.test.ts isolé ; 1 fichier, +2/−1) Artefacts `.ideai/**` et `node_modules/` exclus comme demandé. ═══ 2) Merge `--no-ff` → develop : OUI, exécuté ═══ Décision : la surface produit est complète sur les 3 niveaux et tout est vert des deux côtés → je merge. Pas de rebase nécessaire : `feature` était **12 ahead / 0 behind** `develop` (aucune divergence, base linéaire). - Merge commit : **d7041c5** `merge(session-limits): intégration de la feature limites de session (3 niveaux)` — stratégie ort, **0 conflit**, 47 fichiers / +5157/−45 (cumul LS2→LS8). - `develop` est maintenant à **d7041c5**, contenant toute la chaîne LS2 (a1755e5) → LS8 (3f3504e). - La branche `feature/agent-session-limits` est **conservée** (pas supprimée) : je la garde tant que tu n'as pas confirmé que l'intégration est définitivement validée. Dis-moi si tu veux que je la supprime maintenant. Note d'exécution : les artefacts runtime `.ideai/**` non committés bloquaient le switch de branche → je les ai **stashés le temps du merge puis restaurés** (stash poppé, working tree intact, rien perdu). Aucun de ces artefacts n'est entré dans les commits applicatifs ni dans le merge. ⚠️ Périmètre **strictement local** : aucun `push`. `develop` local est en avance de 58 commits sur `origin/develop` — je ne pousse rien sans ta validation explicite. Prochaines options possibles, à ta main : suppression de `feature/agent-session-limits`, et/ou push de `develop` vers origin (action sortante → nécessite ton feu vert).
|
||||
@ -1,21 +0,0 @@
|
||||
{"id":"28eded2d-7fd7-48cf-bc4a-c38a89ed06e3","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781612485902,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est toi qui décides de la topologie de branche — je te sollicite, tu tranches.\n\nContexte dépôt : HEAD actuel sur le commit de release 0.1.0 (9a8a89f « release(0.1.0) : intégration de develop »). Modèle de branches du projet : main (release) ← develop (intégration) ← feature/* (une par feature).\n\nLa feature sera livrée en lots ordonnés LS1→LS8 (domaine → adapter Claude → port Scheduler → service application → parser regex → filet humain → app-tauri → frontend), chacun passant par le cycle dev/test (tests verts avant de continuer). État en mémoire uniquement, aucun schéma de persistance modifié.\n\nDécide et exécute (périmètre LOCAL uniquement — aucun push) : faut-il créer une branche feature/* (et depuis quelle base — develop ?), faut-il d'abord (re)créer/checkout develop depuis main, ou rester en place ? Donne-moi le nom de branche retenu et confirme l'état git après ta manœuvre."}
|
||||
{"id":"4309abd6-8ded-4641-9a46-af523f0f1af6","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781612520039,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Décision (agent Git) — branche retenue : **`feature/agent-session-limits`**, créée **depuis `develop`**.\n\n**Raisonnement :**\n- Nouvelle feature indépendante (LS1→LS8) → conformément au modèle `main ← develop ← feature/*`, elle se développe sur sa propre branche `feature/*`.\n- Pas besoin de (re)créer `develop` : il existe déjà et est à jour (post-release 0.1.0, `git diff main develop` vide → contenu identique). C'est donc la bonne base de départ.\n- Nom dérivé du sujet, kebab-case : `feature/agent-session-limits`.\n\n**État git après manœuvre (LOCAL, aucun push) :**\n- Branche courante : `feature/agent-session-limits` @ `401c18a` (= base `develop`).\n- `develop` @ `401c18a` (inchangé) ← `main` @ `9a8a89f` (tag `v0.1.0`, inchangé).\n- `feature/agent-skills` @ `ef101db` (autre feature, intacte).\n\n**Note :** le working tree porte déjà des changements non commités hérités de l'état courant — notamment `ARCHITECTURE.md` (cadrage Architect) et un nouveau `.ideai/memory/session-limit-handling-design.md`, plus du runtime `.ideai/` (conversations/MEMORY.md). Ils n'ont pas été commités : DevBackend/QA les intégreront dans les commits de la feature au fil des lots. Le cadrage Architect (ARCHITECTURE.md + memory design) pourra faire le **premier commit** de la branche avant LS1 si tu le souhaites — dis-moi si tu veux que je le commite maintenant.\n\nDev peut démarrer LS1 (domaine) sur `feature/agent-session-limits`. Après chaque lot vert, reviens vers moi pour décider d'un éventuel commit/merge. Aucune intégration vers `develop` tant que les tests des lots concernés ne sont pas verts (règle d'or)."}
|
||||
{"id":"8edd3bc3-3024-46b6-929b-2aac1c1f7fdc","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781612678980,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Feu vert pour le premier commit de la branche feature/agent-session-limits : commite le cadrage AVANT que le dev (LS1) ne démarre, pour isoler proprement le cadrage du code.\n\nContenu à committer (cadrage uniquement) : ARCHITECTURE.md (nouvelle §21) + .ideai/memory/session-limit-handling-design.md + la mise à jour de .ideai/memory/MEMORY.md (pointeur).\n\nN'inclus PAS le runtime .ideai/ non pertinent (conversations, layouts, agents.json…) s'il pollue le diff — limite le commit aux fichiers du cadrage. Message de commit conforme au style du dépôt (français, conventionnel). Périmètre LOCAL uniquement — aucun push. Confirme le hash du commit et l'état git après."}
|
||||
{"id":"bdcd087a-9481-4a06-a8a9-20881cb93077","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781612714298,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Cadrage committé sur `feature/agent-session-limits` — premier commit de la branche, isolé du code.\n\n**Commit : `fa5b826`**\n`docs(session-limits): cadrage Architect — gestion des limites de session des agents`\n\n**Contenu (cadrage uniquement, 3 fichiers / +212 lignes) :**\n- `ARCHITECTURE.md` (+185, nouvelle §21)\n- `.ideai/memory/session-limit-handling-design.md` (nouveau, design validé)\n- `.ideai/memory/MEMORY.md` (+1, pointeur)\n\nLe runtime `.ideai/` non pertinent (conversations, layouts, agents.json…) a été **exclu** du diff comme demandé.\n\n**État git après commit (LOCAL, aucun push) :**\n- `feature/agent-session-limits` @ `fa5b826` ← `develop` @ `401c18a` ← `main` @ `9a8a89f` (tag `v0.1.0`).\n- Working tree : reste uniquement du runtime `.ideai/conversations/…` non commité (bruit, ignoré pour le cadrage).\n\nLa branche est prête : le dev peut démarrer **LS1 (domaine)**. Je reviendrai dans la boucle pour committer chaque lot vert."}
|
||||
{"id":"45759b66-4412-4756-aa61-9f2388dae1b4","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781613219445,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"LS1 (couche domaine) de la feature « limites de session » est VERT : DevBackend a livré le code pur, QA a écrit 24 tests, `cargo test -p domain` = 165 passed / 0 failed, zéro régression.\n\nCommite LS1 sur feature/agent-session-limits (commit suivant après le cadrage fa5b826). Périmètre LOCAL uniquement, aucun push.\n\nFichiers de prod (domaine) modifiés/créés par DevBackend :\n- crates/domain/src/ports.rs (variante ReplyEvent::RateLimited)\n- crates/domain/src/readiness.rs (variante ReadinessSignal::RateLimited + classify)\n- crates/domain/src/session_limit.rs (NOUVEAU : SessionLimit, ResumePlan, RateLimitSource, plan_resume)\n- crates/domain/src/profile.rs (RateLimitPattern + champ + builder)\n- crates/domain/src/events.rs (5 variantes DomainEvent)\n- crates/domain/src/lib.rs (mod + re-exports)\nPlus les tests QA dans ces mêmes fichiers (#[cfg(test)]).\n\nN'inclus PAS le runtime .ideai/ non pertinent. Message conventionnel en français.\n\nIMPORTANT — décision de merge : NE merge PAS vers develop maintenant. Le workspace complet (`cargo build --workspace`) NE COMPILE PAS tant que LS2 n'a pas ajouté l'arm `ReplyEvent::RateLimited` côté infra (match exhaustifs). On ne merge feature/* → develop que sur une tranche qui compile de bout en bout (cible : LS1+LS2+LS4 = niveau 1 Claude complet). Confirme juste le hash du commit LS1 et l'état git."}
|
||||
{"id":"6f923008-d23b-4c0f-8118-0b0bd82baeb1","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781613250449,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"LS1 (couche domaine) committé sur `feature/agent-session-limits`, après le cadrage `fa5b826`.\n\n**Commit : `0bf1eb3`**\n`feat(session-limits): LS1 — couche domaine (détection + plan de reprise)`\n\n**Contenu (6 fichiers domaine, prod + tests inline / +647 lignes) :**\n- `crates/domain/src/session_limit.rs` (nouveau)\n- `crates/domain/src/ports.rs` (ReplyEvent::RateLimited)\n- `crates/domain/src/readiness.rs` (ReadinessSignal::RateLimited + classify)\n- `crates/domain/src/profile.rs` (RateLimitPattern + champ + builder)\n- `crates/domain/src/events.rs` (5 variantes DomainEvent)\n- `crates/domain/src/lib.rs` (mod + re-exports)\n\nRuntime `.ideai/` non pertinent **exclu**.\nQA : `cargo test -p domain` = 165 passed / 0 failed, zéro régression.\n\n**Décision de merge : PAS de merge vers `develop`** — conforme à ta consigne. Le workspace ne compile pas tant que LS2 n'a pas ajouté l'arm `ReplyEvent::RateLimited` côté infra (match exhaustifs). On n'intègre `feature/* → develop` que sur une tranche qui compile de bout en bout (cible : LS1+LS2+LS4 = niveau 1 Claude complet). Je reste sur la branche feature.\n\n**État git (LOCAL, aucun push) :**\n```\nfeature/agent-session-limits @ 0bf1eb3 (HEAD)\n └ fa5b826 docs cadrage\n └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0)\n```\nWorking tree : seul du runtime `.ideai/conversations/…` non commité (ignoré). Dev peut enchaîner sur LS2."}
|
||||
{"id":"8cd389e5-7f84-4737-af3a-207438d2fc62","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781613878608,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"LS2 (adapter Claude niveau 1, infra) est VERT : `cargo test -p infrastructure` = 188 passed / 0 failed, zéro régression. Commite LS2 sur feature/agent-session-limits (après LS1 0bf1eb3). LOCAL uniquement, aucun push.\n\nFichier de prod modifié : crates/infrastructure/src/session/claude.rs (parse_event émet ReplyEvent::RateLimited ; nouvelle fonction pure parse_reset_ms + helpers ; parseur ISO maison ; doccomments T4).\nTests QA : dans crates/infrastructure/src/session/mod.rs (#[cfg(test)]) — 26 nouveaux tests + 2 tests existants mis à jour vers le nouveau contrat ; et crates/infrastructure/src/session/conformance.rs (#[cfg(test)] : RateLimited ajouté aux événements non terminaux autorisés).\n\nExclu le runtime .ideai/. Message conventionnel français.\n\nDécision merge : TOUJOURS PAS de merge vers develop (le workspace ne compile pas encore — 2 erreurs résiduelles attendues dans app-tauri, relèvent de LS7/LS8 ; et la réconciliation T4 reste à faire en LS4). On vise la tranche LS1+LS2+LS3+LS4 = niveau 1 Claude de bout en bout qui compile, pour le premier merge. Confirme le hash LS2 et l'état git."}
|
||||
{"id":"38531764-1e1c-414c-93a9-266cf6847444","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781613904809,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"LS2 (adapter Claude niveau 1, infra) committé sur `feature/agent-session-limits`, après LS1 `0bf1eb3`.\n\n**Commit : `a1755e5`**\n`feat(session-limits): LS2 — adapter Claude niveau 1 (infra)`\n\n**Contenu (3 fichiers infra, prod + tests inline / +545 lignes) :**\n- `crates/infrastructure/src/session/claude.rs` (prod : parse_event → ReplyEvent::RateLimited, parse_reset_ms + helpers, parseur ISO maison, doccomments T4)\n- `crates/infrastructure/src/session/mod.rs` (26 nouveaux tests QA + 2 alignés)\n- `crates/infrastructure/src/session/conformance.rs` (RateLimited en non-terminal autorisé)\n\nRuntime `.ideai/` **exclu**.\nQA : `cargo test -p infrastructure` = 188 passed / 0 failed, zéro régression.\n\n**Décision de merge : TOUJOURS PAS de merge vers `develop`** — conforme. Le workspace ne compile pas encore (2 erreurs résiduelles attendues dans app-tauri → LS7/LS8 ; réconciliation T4 → LS4). Premier merge visé sur la tranche **LS1+LS2+LS3+LS4 = niveau 1 Claude de bout en bout qui compile**.\n\n**État git (LOCAL, aucun push) :**\n```\nfeature/agent-session-limits @ a1755e5 (HEAD)\n └ 0bf1eb3 LS1 domaine\n └ fa5b826 cadrage\n └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0)\n```\nWorking tree : seul du runtime `.ideai/` non commité (ignoré). Dev peut enchaîner sur LS3."}
|
||||
{"id":"49eb0b0d-ee02-4497-b676-127436f10222","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781615248432,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"LS3 (port Scheduler + adapter TokioScheduler) est VERT : `cargo test -p infrastructure` = 195 passed / 0 failed (7 nouveaux tests scheduler, relancés 3× sans flaky), LS1/LS2 toujours verts. Builds domaine + infra OK 0 warning. Le workspace complet ne compile toujours pas (2 erreurs résiduelles inchangées dans app-tauri : chat.rs:185 → LS8, events.rs:261 → LS7 ; la réconciliation T4 reste en LS4).\n\nC'est TOI qui décides quoi et quand committer/merger. Je te livre l'état, tu tranches.\n\nFichiers de prod LS3 :\n- crates/domain/src/ids.rs (ScheduleId via typed_id!)\n- crates/domain/src/ports.rs (trait Scheduler + enum ScheduledTask)\n- crates/domain/src/lib.rs (re-exports)\n- crates/infrastructure/src/scheduler/mod.rs (NOUVEAU : TokioScheduler)\n- crates/infrastructure/src/lib.rs (pub mod scheduler + re-export)\nTests QA : dans crates/infrastructure/src/scheduler/mod.rs (#[cfg(test)]).\nRuntime .ideai/ à exclure du diff.\n\nDécide : faut-il committer LS3 maintenant (et avec quel message) ? Et concernant le merge vers develop — rappel du contexte : le premier merge envisagé portait sur la tranche LS1+LS2+LS3+LS4 (niveau 1 Claude de bout en bout qui compile), or le workspace ne compile pas encore (LS4 + app-tauri manquants). Dis-moi ta décision (commit LS3 sur la feature ? attendre la tranche compilable pour merger ? autre ?) et exécute-la. Confirme hash + état git."}
|
||||
{"id":"acc7dd08-37bf-422e-8962-00778f60de6c","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781615283463,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Décision (agent Git) : **commit LS3 sur la feature, PAS de merge vers `develop`.**\n\n**Pourquoi :** chaque lot vert mérite son commit atomique sur la branche feature (traçabilité, bissection facile), mais le merge `feature/* → develop` exige une tranche qui **compile de bout en bout** — règle d'or. Or le workspace ne compile toujours pas (réconciliation T4 en LS4 + 2 erreurs résiduelles app-tauri : `chat.rs:185`→LS8, `events.rs:261`→LS7). Donc on continue d'accumuler sur la feature ; premier merge maintenu sur la tranche compilable (LS1+LS2+LS3+LS4 niveau 1 Claude, voire jusqu'à LS7/LS8 si la compilation workspace n'est verte qu'à ce moment-là).\n\n**Commit : `253310b`**\n`feat(session-limits): LS3 — port Scheduler + adapter TokioScheduler`\n\n**Contenu (5 fichiers prod + tests inline / +389 lignes) :**\n- domaine : `ports.rs` (trait Scheduler + enum ScheduledTask), `ids.rs` (ScheduleId), `lib.rs` (re-exports)\n- infra : `scheduler/mod.rs` (nouveau, TokioScheduler), `lib.rs` (pub mod + re-export)\n\nRuntime `.ideai/` **exclu**.\nQA : `cargo test -p infrastructure` = 195 passed / 0 failed (7 tests scheduler, 3× sans flaky) ; LS1/LS2 verts ; builds domaine+infra 0 warning.\n\n**État git (LOCAL, aucun push) :**\n```\nfeature/agent-session-limits @ 253310b (HEAD)\n └ a1755e5 LS2 adapter Claude\n └ 0bf1eb3 LS1 domaine\n └ fa5b826 cadrage\n └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0)\n```\nDev peut enchaîner sur LS4 (service application + réconciliation T4). Je tranche le merge dès que le workspace compile."}
|
||||
{"id":"0465aea2-3a1c-486a-9afb-4ad04f91d6d0","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781628910047,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"LS4 (couche application : SessionLimitService + réconciliation T4) est VERT : `cargo test -p application` = tous binaires verts / 0 failed (16 nouveaux tests), zéro régression confirmée (drain_with_readiness_lot1 7/7, send_blocking_d1 9/9, reste du crate vert). Builds domaine+infra+application 0 warning. Le workspace complet ne compile TOUJOURS pas (inchangé : app-tauri chat.rs:185 → LS8, events.rs:261 → LS7).\n\nC'est toi qui décides quoi/quand committer/merger. État livré :\n\nFichiers de prod LS4 :\n- crates/application/src/agent/session_limit.rs (NOUVEAU : SessionLimitService + port AgentResumer + const RESUME_PROMPT)\n- crates/application/src/agent/structured.rs (réconciliation T4 : enum TurnOutcome + drain_with_readiness_outcome ; signatures historiques préservées)\n- crates/application/src/agent/mod.rs (mod + re-exports)\n- crates/application/src/lib.rs (re-exports)\nTests QA : crates/application/tests/session_limit_service.rs (9) + crates/application/tests/session_limit_t4.rs (7) (NOUVEAUX).\nRuntime .ideai/ à exclure.\n\nDécide et exécute (commit LS4 sur la feature ? message ? merge ou pas ?). Pour mémoire : le workspace ne compile de bout en bout qu'après LS7 (events app-tauri) + LS8 (chat.rs) ; le premier merge vers develop reste donc subordonné à ta règle d'or « tranche qui compile ». Confirme hash + état git."}
|
||||
{"id":"f88d6a5a-0e2b-4cd8-bb09-aa4534bfb453","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781628940614,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Décision (agent Git) : **commit LS4 sur la feature, PAS de merge vers `develop`.**\n\n**Commit : `9000b4d`**\n`feat(session-limits): LS4 — service application + réconciliation T4`\n\n**Contenu (6 fichiers, 4 prod + 2 tests / +987 lignes) :**\n- `crates/application/src/agent/session_limit.rs` (nouveau : SessionLimitService + port AgentResumer + RESUME_PROMPT)\n- `crates/application/src/agent/structured.rs` (T4 : enum TurnOutcome + drain_with_readiness_outcome, signatures historiques préservées)\n- `crates/application/src/agent/mod.rs` + `crates/application/src/lib.rs` (modules/re-exports)\n- `crates/application/tests/session_limit_service.rs` (9) + `crates/application/tests/session_limit_t4.rs` (7) — nouveaux\n\nRuntime `.ideai/` **exclu**.\nQA : `cargo test -p application` tous binaires verts / 0 failed (16 nouveaux) ; zéro régression (drain_with_readiness_lot1 7/7, send_blocking_d1 9/9) ; builds domaine+infra+application 0 warning.\n\n**Décision de merge : toujours NON.** La tranche LS1→LS4 est verte par crate, mais `cargo build --workspace` échoue encore (app-tauri : `chat.rs:185`→LS8, `events.rs:261`→LS7). Ma règle d'or impose une tranche qui **compile de bout en bout** avant tout `feature/* → develop`. Comme tu me l'as confirmé, le workspace ne sera vert qu'après LS7+LS8 → **le premier merge se fera donc sur LS1→LS8** (et non LS1→LS4 comme envisagé initialement). On accumule jusque-là sur la branche feature.\n\n**État git (LOCAL, aucun push) :**\n```\nfeature/agent-session-limits @ 9000b4d (HEAD)\n └ 253310b LS3 · a1755e5 LS2 · 0bf1eb3 LS1 · fa5b826 cadrage\n └ 401c18a (develop) ← main @ 9a8a89f (tag v0.1.0)\n```\nDev peut enchaîner sur LS5 (parser regex). Je tranche le merge dès `cargo build --workspace` vert."}
|
||||
{"id":"dedd197e-7de5-4e64-8217-54be77e1e653","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781629554554,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"LS5 (parser regex niveau 2, infra) est VERT : `cargo test -p infrastructure` = 221 passed / 0 failed (26 nouveaux), 0 warning, LS2/LS3 toujours verts (refactor timeparse non régressif). C'est toi qui décides commit/merge.\n\nFichiers de prod LS5 :\n- crates/infrastructure/Cargo.toml (dépendance regex = \"1\", résout 1.12.3 déjà dans Cargo.lock)\n- crates/infrastructure/src/ratelimit/mod.rs (NOUVEAU : RateLimitParser + applies)\n- crates/infrastructure/src/timeparse.rs (NOUVEAU : parsing d'heure pur partagé, factorisé depuis LS2)\n- crates/infrastructure/src/session/claude.rs (délègue à timeparse — refactor)\n- crates/infrastructure/src/lib.rs (pub mod ratelimit + timeparse, re-export)\nModifs de TEST QA : crates/infrastructure/src/ratelimit/mod.rs (#[cfg(test)] 15 tests), crates/infrastructure/src/timeparse.rs (#[cfg(test)] 11 tests), crates/infrastructure/src/scheduler/mod.rs (2 warnings irrefutable if let corrigés dans le code de test).\nNOTE : Cargo.lock peut être inchangé (regex déjà verrouillée) — inclus-le seulement s'il bouge.\nRuntime .ideai/ à exclure.\n\nDécide et exécute (commit LS5 ? message ? merge ?). Pour mémoire le workspace ne compile de bout en bout qu'après LS7+LS8. Confirme hash + état git."}
|
||||
{"id":"050c6d11-d9bb-4e0f-8bc8-cebae970cbe3","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781632920754,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Feature session-limits, suite. Project root: /home/anthony/Documents/Projects/IdeA. Deux lots sont terminés et VERTS (workspace compile, `cargo test --workspace` tout vert, clippy propre sur le nouveau code) mais NON committés :\n\n- LS5 — détecteur niveau 2 déclaratif + parsing temps partagé. Fichiers : nouveau `crates/infrastructure/src/ratelimit/mod.rs` (RateLimitParser, regex confiné infra), nouveau `crates/infrastructure/src/timeparse.rs` (parsing d'heures pur, partagé niveau 1/2), modifs `crates/infrastructure/src/session/claude.rs` (refactor vers timeparse, ~-121 lignes), `crates/infrastructure/src/scheduler/mod.rs`, `crates/infrastructure/src/lib.rs` (exports), `crates/infrastructure/Cargo.toml` (dép `regex`), `Cargo.lock`.\n- LS6 — câblage des 5 nouvelles variantes DomainEvent (AgentRateLimited/ResumeScheduled/ResumeCancelled/Resumed/RateLimitSuspected) + ReplyEvent::RateLimited vers le front. Fichiers : `crates/app-tauri/src/events.rs` (DTO miroir + bras From + test relais), `crates/app-tauri/src/chat.rs` (chunk_from_event).\n\nNote : l'arbre contient aussi des fichiers runtime .ideai/ modifiés (conversations/*/handoff.md, log.jsonl, layouts.json, memory/) et un nouveau memory `git-owns-commit-merge-decisions.md` — à toi de juger ce qui entre ou non dans le(s) commit(s) applicatif(s) (rappel : les LS précédents committaient uniquement le code applicatif, pas le bruit runtime).\n\nLa branche courante est détachée (HEAD), pas sur develop. À toi de décider : topologie de branche, découpage en un ou deux commits (LS5 puis LS6, ou groupés), messages dans le style des commits feat(session-limits) existants, et si un merge feature/* → develop doit suivre. Périmètre LOCAL uniquement (aucun push). Exécute ta décision et rends via idea_reply le détail (branche, sha, messages, état final de `git status`)."}
|
||||
{"id":"8f8eec4e-104d-424d-b061-39fc93ad1bde","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781632975720,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Fait. Décisions et exécution :\n\n**Branche** — Contrairement à ce qui était annoncé, HEAD n'était pas détachée : on était bien sur `feature/agent-session-limits` (la branche de la feature). Aucune création/bascule nécessaire, le dev se poursuit dessus. Rien à corriger.\n\n**Découpage — 2 commits atomiques** (LS5 puis LS6, séparés car deux intentions distinctes : couche infra de détection vs câblage événementiel vers le front) :\n\n1. `98bfcf4` — feat(session-limits): LS5 — détecteur niveau 2 déclaratif + parsing temps partagé\n Fichiers : `crates/infrastructure/src/ratelimit/mod.rs` (nouv.), `crates/infrastructure/src/timeparse.rs` (nouv.), `crates/infrastructure/src/session/claude.rs`, `crates/infrastructure/src/scheduler/mod.rs`, `crates/infrastructure/src/lib.rs`, `crates/infrastructure/Cargo.toml`, `Cargo.lock`.\n\n2. `ea94e75` — feat(session-limits): LS6 — câblage des événements de limite vers le front\n Fichiers : `crates/app-tauri/src/events.rs`, `crates/app-tauri/src/chat.rs`.\n\nMessages en français, style Conventional Commits cohérent avec LS1→LS4, corps expliquant le pourquoi, co-author inclus.\n\n**Bruit runtime exclu** — Non committé, comme pour les LS précédents : `.ideai/conversations/*` (handoff.md, log.jsonl), `.ideai/layouts.json`, `.ideai/memory/` (dont MEMORY.md et le nouveau `git-owns-commit-merge-decisions.md`), et la nouvelle conversation `6bc594e8-…`. Ce sont des artefacts d'exécution, pas du code applicatif.\n\n**Merge feature/* → develop : NON.** La feature session-limits est livrée par incréments (série LS en cours) ; LS5/LS6 sont verts mais internes (détecteur niveau 2 + relais d'événements) sans surface produit complète. On reste sur `feature/agent-session-limits`. Le merge vers `develop` se fera quand la feature sera terminée et toujours verte.\n\n**Périmètre** — 100% local, aucun push.\n\n**État final `git status`** : working tree propre côté applicatif ; ne subsistent que les fichiers runtime `.ideai/` modifiés/non suivis (volontairement hors commit). Derniers commits :\nea94e75 LS6 / 98bfcf4 LS5 / 9000b4d LS4."}
|
||||
{"id":"9c73943b-e258-4c2b-a124-f2eafffe3c5a","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781675740301,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Slice LS7 (backend / câblage app-tauri) de la feature session-limits terminée et verte. On est sur la branche feature/agent-session-limits (HEAD détaché ? `git status` montre HEAD), dernier commit ea94e75 (LS6).\n\nContenu à committer (tout est vert, aucun warning) :\n- crates/application/src/agent/lifecycle.rs : `LaunchAgentOutput.profile` exposé (None sur réattache/idempotent, Some sur lancement effectif).\n- crates/application/src/terminal/registry.rs : `StructuredSessions::meta_for_session()` (lookup agent/node par SessionId pour le tap niveau 1).\n- crates/app-tauri/src/state.rs : `ResumeContext`/`ResumeContexts`, `AppAgentResumer` (impl port `AgentResumer` au-dessus de LaunchAgent), instanciation+câblage du `SessionLimitService` (TokioScheduler + drain des réveils) dans `AppState::build`.\n- crates/app-tauri/src/commands.rs : taps niveau 1 (agent_send) et niveau 2 (launch_agent, parser regex confiné), alimentation de `resume_contexts`, nouvelle commande `cancel_resume`.\n- crates/app-tauri/src/lib.rs : enregistrement de `cancel_resume` dans le handler.\n- crates/app-tauri/Cargo.toml : dépendance `async-trait`.\n- crates/app-tauri/tests/session_limit_wiring.rs : 2 tests d'intégration (composition) — verts.\n- crates/app-tauri/tests/dto_agents.rs + dto_chat.rs : ajustement `profile: None` dans les fixtures.\n- crates/application/tests/structured_registry_d1.rs : test de `meta_for_session`.\n\nHors périmètre code (à NE PAS committer dans ce commit applicatif) : .ideai/conversations/**, .ideai/layouts.json, .ideai/memory/** — ce sont des artefacts runtime/mémoire.\n\nQuestions : 1) Faut-il committer cette slice LS7-backend maintenant, et sous quel message ? 2) Reste-t-on sur feature/agent-session-limits ? 3) Décides-tu d'un merge vers develop, ou on attend LS7-front (React/TS, badge + compte à rebours + filet humain) qui complète la surface produit ? Tranche et exécute le commit applicatif si tu juges que oui."}
|
||||
{"id":"42185422-903a-4c25-8139-b63748f6dd73","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781675787986,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Décision Git — LS7 backend committée, on reste en feature, pas de merge maintenant.\n\nÉtat initial : on était bien **sur la branche** `feature/agent-session-limits` (pas de HEAD détaché — le « HEAD » de ton `git status` était juste l'en-tête francisé), HEAD = ea94e75 (LS6).\n\n1) ✅ Commit fait. Hash **9df5923** :\n`feat(session-limits): LS7 — câblage backend app-tauri (taps niveaux 1&2 + reprise annulable)`\n- 10 fichiers, +406/−3, dont la création de `tests/session_limit_wiring.rs`.\n- Périmètre strictement applicatif : lifecycle.rs, registry.rs, state.rs, commands.rs, lib.rs, Cargo.toml + les 4 fichiers de tests.\n- **Exclus comme demandé** (restés non-stagés, ce sont des artefacts runtime/mémoire) : `.ideai/conversations/**`, `.ideai/layouts.json`, `.ideai/memory/**` (dont le nouveau `git-owns-commit-merge-decisions.md` et le `6bc594e8…/` non suivi). Commit atomique = code seulement.\n\n2) ✅ On **reste sur `feature/agent-session-limits`**. La feature n'est pas terminée côté produit (il manque LS7-front), donc on continue dessus.\n\n3) ❌ **Pas de merge vers `develop` maintenant.** Raison : la slice est verte mais la surface produit session-limits est **incomplète sans LS7-front** (badge + compte à rebours + filet humain). Merger le backend seul livrerait dans `develop` une mécanique de reprise annulable sans son UI de contrôle/annulation visible — état intermédiaire que je ne veux pas figer dans l'intégration. **On attend LS7-front.** Une fois le front implémenté et la suite complète verte, reviens vers moi : je rebase si besoin sur `develop`, puis merge `--no-ff` la feature entière d'un coup (backend + front cohérents).\n\nProchaine étape côté toi : enchaîner LS7-front (React/TS) sur cette même branche. Quand c'est vert, ping-moi pour le merge d'intégration."}
|
||||
{"id":"4d281c8d-2a92-45b8-ac22-9a8fac5aafde","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781676394147,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Slice LS7-front (React/TS, UI limites de session) terminée et VERTE sur `feature/agent-session-limits`.\n\nÉtat tests : `npx vitest run src/features/agents/` → 4 fichiers, 63 tests passés (39 existants + 24 nouveaux), 0 régression. Typecheck `tsc --noEmit` propre.\n\nFichiers à committer (frontend/ uniquement) :\n- src/domain/index.ts — 5 variantes ajoutées au union DomainEvent (agentRateLimited/ResumeScheduled/ResumeCancelled/Resumed/RateLimitSuspected).\n- src/ports/index.ts — `cancelResume(agentId)` ajouté à InputGateway.\n- src/adapters/input.ts — TauriInputGateway.cancelResume → invoke(\"cancel_resume\", { agentId }).\n- src/adapters/mock/index.ts — MockInputGateway.cancelResume (cancelledResumes/cancelResumeResult).\n- src/features/agents/useAgents.ts — état limitByAgent + action cancelResume.\n- src/features/agents/AgentLimitBadge.tsx — NOUVEAU (badge + compte à rebours + bouton Annuler + helpers purs).\n- src/features/agents/AgentsPanel.tsx — câblage du badge.\n- src/features/agents/useAgentsLimits.test.tsx — NOUVEAU (13 tests, QA).\n- src/features/agents/AgentLimitBadge.test.tsx — NOUVEAU (11 tests, QA).\n\nHors périmètre (NE PAS committer) : .ideai/** (artefacts runtime/mémoire). Aucun fichier backend touché.\n\nNOTE : il existe un test PRÉEXISTANT rouge SANS rapport avec cette slice — `src/adapters/mock/mock.test.ts:16` asserte « thirteen gateways » alors qu'il y en a 14 (gateway `permission` déjà présente dans HEAD committé eca2ba9). Compteur périmé, pas causé par LS7-front. À toi de voir si tu veux que je fasse corriger ce compteur (tâche séparée DevFrontend) avant/après — dis-moi.\n\nQuestions : 1) Committe la slice LS7-front maintenant ? Sous quel message ? 2) Le merge --no-ff de la feature complète vers develop : on attend que je tranche le dernier point de scope (filet humain niveau 3 — voir consultation Architect en cours : peut nécessiter une slice backend+front supplémentaire `set_resume_at`), donc NE merge pas encore vers develop — confirme que tu te limites au commit de cette slice et qu'on garde le merge pour quand je te recontacte « feature complète ». Tranche et exécute le commit."}
|
||||
{"id":"21dcba39-7c5a-4191-8ddb-f13ab7eed54e","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781676433750,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Décision Git — LS7-front committée, pas de merge develop (j'attends ton « feature complète »).\n\n1) ✅ Commit fait. Hash **4fad042** :\n`feat(session-limits): LS7-front — UI limites de session (badge + compte à rebours + filet humain)`\n- 9 fichiers, +702/−1, dont 3 créations (AgentLimitBadge.tsx + ses 2 fichiers de tests).\n- Périmètre strictement frontend : domain/index.ts, ports/index.ts, adapters/input.ts, adapters/mock/index.ts, useAgents.ts, AgentLimitBadge.tsx, AgentsPanel.tsx + les 2 tests.\n- **Exclus comme demandé** (non-stagés) : `.ideai/**` (conversations, layouts, memory) et `node_modules/` (non suivi — au passage, il traîne en untracked ; à gitignorer dans une tâche d'hygiène séparée si ce n'est pas déjà couvert, mais hors périmètre ici). Aucun fichier backend touché. Commit atomique = frontend seulement.\n\n2) ✅ Confirmé : **je me limite à ce commit, pas de merge vers `develop`.** Je garde le `--no-ff` d'intégration de la feature complète pour quand tu me recontactes « feature complète », une fois tranché le scope du filet humain niveau 3 (slice `set_resume_at` éventuelle, cf. consultation Architect).\n\nConcernant le **test préexistant rouge** `mock.test.ts:16` (« thirteen gateways » → 14 réelles, gateway `permission` déjà dans HEAD eca2ba9) : compteur périmé sans rapport avec LS7. Mon avis Git : à corriger **avant le merge d'intégration**, en **commit séparé** (`fix(test): …` ou `chore(test): …`), pour ne pas merger `develop` avec une suite rouge. Lance une tâche DevFrontend dédiée quand tu veux ; tu peux l'enchaîner maintenant ou la grouper avec l'éventuelle slice `set_resume_at` — dans tous les cas elle aura son propre commit, et je ne mergerai pas tant que toute la suite n'est pas verte.\n\nÉtat branche : `feature/agent-session-limits` → … ea94e75 (LS6) · 9df5923 (LS7 back) · **4fad042 (LS7 front, HEAD)**."}
|
||||
{"id":"8a9caf1e-94cf-4d45-905a-caab1363aa99","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781678668760,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Feature session-limits COMPLÈTE (3 niveaux) et verte. Reste à committer 2 slices, puis on peut envisager le merge d'intégration `feature/agent-session-limits → develop` que tu gardais en attente.\n\nÉtat branche actuel : … 9df5923 (LS7 back) · 4fad042 (LS7 front, HEAD). Travail NON committé depuis, à committer maintenant en 2 commits séparés :\n\n═══ COMMIT A — LS8-backend (filet humain niveau 3, backend) ═══\nImplémenté par DevBackend, testé par QA (application: 15 tests session_limit_service / app-tauri: 4 wiring, + régressions vertes, 0 failed).\nFichiers :\n- crates/application/src/agent/session_limit.rs — refactor privé `arm_scheduled` (param `resets_at_ms` brut ajouté) partagé par `on_rate_limited` + nouvelle `pub fn confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms: i64)` (source Human, réutilise la branche Scheduled, annulable).\n- crates/app-tauri/src/commands.rs — nouvelle commande `set_resume_at(agent_id, resets_at_ms) -> Result<(), ErrorDto>` (résout node_id via node_for_agent + conversation_id best-effort, NOT_FOUND si pas de cellule vivante).\n- crates/app-tauri/src/lib.rs — `set_resume_at` enregistrée après `cancel_resume`.\n- crates/application/tests/session_limit_service.rs — +6 tests (QA).\n- crates/app-tauri/tests/session_limit_wiring.rs — +2 tests (QA).\nAucun événement nouveau (réutilise AgentRateLimited + AgentResumeScheduled).\n\n═══ COMMIT B — LS8-front + fix test (DevFrontend a demandé 2 commits ; à toi de voir si tu sépares ou regroupes) ═══\nLS8-front (typecheck propre, 109 tests verts) :\n- frontend/src/ports/index.ts — `setResumeAt(agentId, resetsAtMs)` sur InputGateway.\n- frontend/src/adapters/input.ts — `setResumeAt` → invoke(\"set_resume_at\", { agentId, resetsAtMs }).\n- frontend/src/adapters/mock/index.ts — MockInputGateway.setResumeAt (resumeArmings[]).\n- frontend/src/features/agents/useAgents.ts — action setResumeAt (sans mutation optimiste).\n- frontend/src/features/agents/AgentLimitBadge.tsx — formulaire de saisie d'heure sur l'état suspected sans heure + helper pur `timeInputToEpochMs` ; TODO LS7 retiré.\n- frontend/src/features/agents/AgentsPanel.tsx — câblage onSetResumeAt.\n- frontend/src/features/agents/AgentLimitBadge.test.tsx — tests mis à jour au nouveau contrat + couverture LS8.\nFix test hygiène (DevFrontend recommande commit SÉPARÉ) :\n- frontend/src/adapters/mock/mock.test.ts — compteur gateways 13→14 (« thirteen »→« fourteen » + `permission` dans l'array). Vérifié : 14 gateways réelles.\n\nHors périmètre (NE PAS committer) : .ideai/** (runtime/mémoire), node_modules/ (untracked — DevFrontend signalait qu'il traîne, à gitignorer dans une tâche d'hygiène séparée, hors périmètre ici).\n\nQUESTIONS : 1) Committe COMMIT A puis les commits front (LS8-front + fix-test, séparés ou groupés selon ton jugement). Donne-moi les hashes + messages. 2) La suite complète est maintenant verte (Rust: domain/application/app-tauri ; front: agents + adapters/mock 109 tests) et la surface produit session-limits est complète sur les 3 niveaux. Décides-tu MAINTENANT le merge `--no-ff` vers develop (rebase si besoin) ? Si oui, exécute-le et donne-moi le résultat. Si tu veux un dernier garde-fou (ex. run de la suite complète des deux côtés avant merge), dis-le et je le fais lancer."}
|
||||
{"id":"6df9f7b7-fcc4-4b87-94db-33dce2d76f13","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781678766649,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"✅ 3 commits + merge d'intégration faits. La feature session-limits est intégrée dans `develop`.\n\n═══ 1) Les 3 commits (sur feature/agent-session-limits) ═══\nJ'ai séparé le fix-test de la feature (hygiène isolée, comme recommandé par DevFrontend) → 3 commits :\n\n- **c480d28** `feat(session-limits): LS8-backend — filet humain niveau 3 (set_resume_at)`\n (session_limit.rs + commands.rs + lib.rs + tests service/wiring ; 5 fichiers, +394/−18)\n- **5d9dd32** `feat(session-limits): LS8-front — filet humain niveau 3 (saisie d'heure de reprise)`\n (ports/input/mock + useAgents + AgentLimitBadge(.tsx/.test) + AgentsPanel ; 7 fichiers, +222/−26)\n- **3f3504e** `fix(test): corrige le compteur de gateways du mock (13 → 14)`\n (mock.test.ts isolé ; 1 fichier, +2/−1)\n\nArtefacts `.ideai/**` et `node_modules/` exclus comme demandé.\n\n═══ 2) Merge `--no-ff` → develop : OUI, exécuté ═══\nDécision : la surface produit est complète sur les 3 niveaux et tout est vert des deux côtés → je merge. Pas de rebase nécessaire : `feature` était **12 ahead / 0 behind** `develop` (aucune divergence, base linéaire).\n\n- Merge commit : **d7041c5** `merge(session-limits): intégration de la feature limites de session (3 niveaux)` — stratégie ort, **0 conflit**, 47 fichiers / +5157/−45 (cumul LS2→LS8).\n- `develop` est maintenant à **d7041c5**, contenant toute la chaîne LS2 (a1755e5) → LS8 (3f3504e).\n- La branche `feature/agent-session-limits` est **conservée** (pas supprimée) : je la garde tant que tu n'as pas confirmé que l'intégration est définitivement validée. Dis-moi si tu veux que je la supprime maintenant.\n\nNote d'exécution : les artefacts runtime `.ideai/**` non committés bloquaient le switch de branche → je les ai **stashés le temps du merge puis restaurés** (stash poppé, working tree intact, rien perdu). Aucun de ces artefacts n'est entré dans les commits applicatifs ni dans le merge.\n\n⚠️ Périmètre **strictement local** : aucun `push`. `develop` local est en avance de 58 commits sur `origin/develop` — je ne pousse rien sans ta validation explicite.\n\nProchaines options possibles, à ta main : suppression de `feature/agent-session-limits`, et/ou push de `develop` vers origin (action sortante → nécessite ton feu vert)."}
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
upTo: 88f405da-f9c9-44bb-96c9-7e1b0e5af557
|
||||
objective: Bonjour TestConversation. Ceci est un test de communication inter-agents via IdeA. Peux-tu confirmer que tu me reçois et me dire combien font 17 + 25 ?
|
||||
---
|
||||
**Objectif :** Bonjour TestConversation. Ceci est un test de communication inter-agents via IdeA. Peux-tu confirmer que tu me reçois et me dire combien font 17 + 25 ?
|
||||
|
||||
- **Prompt:** Bonjour TestConversation. Ceci est un test de communication inter-agents via IdeA. Peux-tu confirmer que tu me reçois et me dire combien font 17 + 25 ?
|
||||
- **Prompt:** Test de communication inter-agent. Merci de confirmer que tu reçois bien ce message en répondant via idea_reply avec : (1) un court message de confirmation, (2) la date/heure si tu y as accès, (3) le nom de ton AI Profile/contexte tel que tu le perçois. Ceci sert uniquement à valider le canal de communication entre agents IdeA.
|
||||
- **Response:** (1) Confirmation : message bien reçu, le canal de communication inter-agent IdeA fonctionne correctement. (2) Date : 2026-06-13 (je n'ai pas d'heure horodatée plus précise dans mon contexte). (3) AI Profile/contexte perçu : Claude Code (modèle Opus 4.8, claude-opus-4-8), opérant sur le project root /home/anthony/Documents/Projects/IdeA via un run dir isolé. Canal validé. ✅
|
||||
@ -1,3 +0,0 @@
|
||||
{"id":"20dd8bdc-f858-46a6-9c8a-6afde591a6bd","conversation":"6ffc1f69-77a5-0da1-1965-ef26d1df72f5","atMs":1781367769591,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Bonjour TestConversation. Ceci est un test de communication inter-agents via IdeA. Peux-tu confirmer que tu me reçois et me dire combien font 17 + 25 ?"}
|
||||
{"id":"0b8fcc51-c692-4928-9d90-3823857dad5c","conversation":"6ffc1f69-77a5-0da1-1965-ef26d1df72f5","atMs":1781368607174,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Test de communication inter-agent. Merci de confirmer que tu reçois bien ce message en répondant via idea_reply avec : (1) un court message de confirmation, (2) la date/heure si tu y as accès, (3) le nom de ton AI Profile/contexte tel que tu le perçois. Ceci sert uniquement à valider le canal de communication entre agents IdeA."}
|
||||
{"id":"88f405da-f9c9-44bb-96c9-7e1b0e5af557","conversation":"6ffc1f69-77a5-0da1-1965-ef26d1df72f5","atMs":1781368615582,"source":{"kind":"agent","agentId":"c932c770-cf36-4fb2-a966-71bb1644e4b4"},"role":"response","text":"(1) Confirmation : message bien reçu, le canal de communication inter-agent IdeA fonctionne correctement. (2) Date : 2026-06-13 (je n'ai pas d'heure horodatée plus précise dans mon contexte). (3) AI Profile/contexte perçu : Claude Code (modèle Opus 4.8, claude-opus-4-8), opérant sur le project root /home/anthony/Documents/Projects/IdeA via un run dir isolé. Canal validé. ✅"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,7 +0,0 @@
|
||||
---
|
||||
upTo: 419c60a5-2aca-4711-ad83-0a185bb3214e
|
||||
objective: Ping de test inter-agent depuis DevBackend — réponds avec un court PONG et l'heure que tu vois.
|
||||
---
|
||||
**Objectif :** Ping de test inter-agent depuis DevBackend — réponds avec un court PONG et l'heure que tu vois.
|
||||
|
||||
- **Prompt:** Ping de test inter-agent depuis DevBackend — réponds avec un court PONG et l'heure que tu vois.
|
||||
@ -1 +0,0 @@
|
||||
{"id":"419c60a5-2aca-4711-ad83-0a185bb3214e","conversation":"dd35eeb0-2329-0dfa-3255-df7dba90a0aa","atMs":1781427081894,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"prompt","text":"Ping de test inter-agent depuis DevBackend — réponds avec un court PONG et l'heure que tu vois."}
|
||||
@ -1,129 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"activeId": "dd38b8ed-7199-48a6-a63f-1d1462ca6a34",
|
||||
"layouts": [
|
||||
{
|
||||
"id": "dd38b8ed-7199-48a6-a63f-1d1462ca6a34",
|
||||
"name": "Default",
|
||||
"kind": "terminal",
|
||||
"tree": {
|
||||
"root": {
|
||||
"type": "split",
|
||||
"node": {
|
||||
"id": "bd3dfce4-d31b-44d4-bd1e-b299b00d3cce",
|
||||
"direction": "row",
|
||||
"children": [
|
||||
{
|
||||
"node": {
|
||||
"type": "split",
|
||||
"node": {
|
||||
"id": "cd60658c-8c2d-4ef1-9149-073230350696",
|
||||
"direction": "column",
|
||||
"children": [
|
||||
{
|
||||
"node": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "14d20dcb-f243-4381-a130-2d4d3d224f42",
|
||||
"session": "adb24405-26ad-4920-a114-d27a21d9022a",
|
||||
"agent": "a6ced819-b893-4213-b003-9e9dc79b9641",
|
||||
"agentWasRunning": true
|
||||
}
|
||||
},
|
||||
"weight": 1.1910967
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "8d2c2720-b971-4792-8c0a-b28581eea8a8",
|
||||
"session": "5def4f66-2921-4ddc-b9a2-be4958788081"
|
||||
}
|
||||
},
|
||||
"weight": 0.80890334
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"type": "split",
|
||||
"node": {
|
||||
"id": "80fa1a60-60d7-4442-8614-fb055ea97e93",
|
||||
"direction": "column",
|
||||
"children": [
|
||||
{
|
||||
"node": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "fbcfacb2-ad0b-46cc-9b53-8199d23f9324",
|
||||
"session": "462f69bc-7957-437d-9096-e79b57af1223",
|
||||
"agent": "73c853d1-c0fd-463b-ad17-1d24fefa371f",
|
||||
"agentWasRunning": true
|
||||
}
|
||||
},
|
||||
"weight": 0.80238867
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"type": "split",
|
||||
"node": {
|
||||
"id": "8318f2b8-55e1-4fff-88ba-9891bd85c8a0",
|
||||
"direction": "column",
|
||||
"children": [
|
||||
{
|
||||
"node": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "07698ab4-0700-4205-966b-a50b1e73ec2b",
|
||||
"session": "f1780cb6-91c3-47d7-9081-1bcbdb96ba92",
|
||||
"agent": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
|
||||
"agentWasRunning": true
|
||||
}
|
||||
},
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "f31fa71f-c55e-4597-8ea0-caa9775531ff",
|
||||
"session": "b04d11d9-0359-48a6-94ec-bd1d40eb6277",
|
||||
"agent": "aefdbd61-e3d4-4bc1-9f42-c259446a97b5",
|
||||
"agentWasRunning": true
|
||||
}
|
||||
},
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"weight": 1.1976112
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a1958c6d-901f-4fbc-a621-d026aa6b2b62",
|
||||
"name": "Git Graph",
|
||||
"kind": "gitGraph",
|
||||
"tree": {
|
||||
"root": {
|
||||
"type": "leaf",
|
||||
"node": {
|
||||
"id": "7a088843-00e7-4ea0-9676-8c4acb1f6d1d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -7,3 +7,38 @@
|
||||
- [permissions-sandbox-system-state](permissions-sandbox-system-state.md) — Systeme de permissions/sandbox complet (Landlock sur PTY + structure) et le risque residuel $HOME/resume du chemin structure.
|
||||
- [session-limit-handling-design](session-limit-handling-design.md) — Design valide (detecteur hierarchique + reprise auto annulable) pour les limites de session des agents.
|
||||
- [git-owns-commit-merge-decisions](git-owns-commit-merge-decisions.md) — Ne jamais demander a l'utilisateur s'il faut committer/merger/brancher : l'agent Git tranche toute la topologie du depot.
|
||||
- [conversation-rotation-safety-design](conversation-rotation-safety-design.md) — memory note conversation-rotation-safety-design
|
||||
- [checkpoint-orchestrator-designation-restart](checkpoint-orchestrator-designation-restart.md) — memory note checkpoint-orchestrator-designation-restart
|
||||
- [checkpoint-orchestrator-designation-backend-compile-fix](checkpoint-orchestrator-designation-backend-compile-fix.md) — memory note checkpoint-orchestrator-designation-backend-compile-fix
|
||||
- [checkpoint-orchestrator-designation-qa-verdict](checkpoint-orchestrator-designation-qa-verdict.md) — memory note checkpoint-orchestrator-designation-qa-verdict
|
||||
- [checkpoint-orchestrator-designation-appimage-build](checkpoint-orchestrator-designation-appimage-build.md) — memory note checkpoint-orchestrator-designation-appimage-build
|
||||
- [checkpoint-blocked-until-appimage-030-restart](checkpoint-blocked-until-appimage-030-restart.md) — memory note checkpoint-blocked-until-appimage-030-restart
|
||||
- [checkpoint-delivery-submit-logging-fix](checkpoint-delivery-submit-logging-fix.md) — memory note checkpoint-delivery-submit-logging-fix
|
||||
- [checkpoint-workstate-delegation-queue-lot-b](checkpoint-workstate-delegation-queue-lot-b.md) — memory note checkpoint-workstate-delegation-queue-lot-b
|
||||
- [checkpoint-workstate-conversation-summaries-lot-c](checkpoint-workstate-conversation-summaries-lot-c.md) — memory note checkpoint-workstate-conversation-summaries-lot-c
|
||||
- [checkpoint-workstate-controlled-actions-lot-d](checkpoint-workstate-controlled-actions-lot-d.md) — memory note checkpoint-workstate-controlled-actions-lot-d
|
||||
- [checkpoint-work-tab-blank-fix](checkpoint-work-tab-blank-fix.md) — memory note checkpoint-work-tab-blank-fix
|
||||
- [skills-integration-canonical-foundation](skills-integration-canonical-foundation.md) — Topologie d'intégration du chantier skills agent et décision d'abandon de feature/agent-skills.
|
||||
- [idea-program-surface-separation-and-livestate](idea-program-surface-separation-and-livestate.md) — Cadrage du programme post-skills : ce qui existe vs reste, et la règle de frontière surface-agent/humaine.
|
||||
- [handoff-ls5-summary-bound-and-llm-seam](handoff-ls5-summary-bound-and-llm-seam.md) — Contrat figé de la borne de handoff (perf) et du seam résumeur LLM non activé.
|
||||
- [conversation-log-ls6-rotation-and-paginated-read](conversation-log-ls6-rotation-and-paginated-read.md) — Contrat figé de la rétention/rotation du transcript et de la lecture paginée riche (surface humaine).
|
||||
- [conversation-viewer-ls7-frontend](conversation-viewer-ls7-frontend.md) — Contrat figé du viewer de transcript paginé (frontend pur, lecture seule, surface humaine).
|
||||
- [live-state-persistence-program-closed-ls8](live-state-persistence-program-closed-ls8.md) — Le programme live-state + persistance conversationnelle est livré et documenté ; pointe la doc de clôture et les points restés ouverts.
|
||||
- [resume-after-appimage-rebuild-mcp-e2e](resume-after-appimage-rebuild-mcp-e2e.md) — memory note resume-after-appimage-rebuild-mcp-e2e
|
||||
- [mcp-e2e-findings-reply-wedge-phantom-busy](mcp-e2e-findings-reply-wedge-phantom-busy.md) — memory note mcp-e2e-findings-reply-wedge-phantom-busy
|
||||
- [checkpoint-finding-a-fix-built-live-validation-pending](checkpoint-finding-a-fix-built-live-validation-pending.md) — memory note checkpoint-finding-a-fix-built-live-validation-pending
|
||||
- [git-agent-push-access-gitea-ssh](git-agent-push-access-gitea-ssh.md) — memory note git-agent-push-access-gitea-ssh
|
||||
- [rendezvous-no-reply-backstop-design](rendezvous-no-reply-backstop-design.md) — Décision d'architecture sur la fin-de-tour et le backstop no-reply du rendez-vous idea_ask_agent ⇄ idea_reply, après échec live du fix Finding A (77e62e5).
|
||||
- [checkpoint-rendezvous-redesign-devbackend-todo](checkpoint-rendezvous-redesign-devbackend-todo.md) — memory note checkpoint-rendezvous-redesign-devbackend-todo
|
||||
- [backstop-no-reply-live-test-failed-2026-06-23](backstop-no-reply-live-test-failed-2026-06-23.md) — memory note backstop-no-reply-live-test-failed-2026-06-23
|
||||
- [live-state-reboot-reconciliation-design](live-state-reboot-reconciliation-design.md) — memory note live-state-reboot-reconciliation-design
|
||||
- [reconcile-live-state-implemented-feature-branch](reconcile-live-state-implemented-feature-branch.md) — memory note reconcile-live-state-implemented-feature-branch
|
||||
- [rendezvous-600s-cap-too-short-heavy-tasks](rendezvous-600s-cap-too-short-heavy-tasks.md) — memory note rendezvous-600s-cap-too-short-heavy-tasks
|
||||
- [backstop-fires-on-intra-task-turn-rootcause](backstop-fires-on-intra-task-turn-rootcause.md) — memory note backstop-fires-on-intra-task-turn-rootcause
|
||||
- [mcp-functional-test-plan-2026-06-24](mcp-functional-test-plan-2026-06-24.md) — memory note mcp-functional-test-plan-2026-06-24
|
||||
- [checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect](checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect.md) — memory note checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect
|
||||
- [checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer](checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer.md) — memory note checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer
|
||||
- [mcp-t7-backstop-600s-reply-rejected-livestate-stale](mcp-t7-backstop-600s-reply-rejected-livestate-stale.md) — memory note mcp-t7-backstop-600s-reply-rejected-livestate-stale
|
||||
- [mcp-functional-tests-t1-t9-green-live-2026-06-24](mcp-functional-tests-t1-t9-green-live-2026-06-24.md) — memory note mcp-functional-tests-t1-t9-green-live-2026-06-24
|
||||
- [mcp-t10a-harness-interrupt-does-not-cancel-rendezvous](mcp-t10a-harness-interrupt-does-not-cancel-rendezvous.md) — memory note mcp-t10a-harness-interrupt-does-not-cancel-rendezvous
|
||||
- [mcp-t10b-pending-reboot-verification](mcp-t10b-pending-reboot-verification.md) — memory note mcp-t10b-pending-reboot-verification
|
||||
|
||||
34
.ideai/memory/backstop-fires-on-intra-task-turn-rootcause.md
Normal file
34
.ideai/memory/backstop-fires-on-intra-task-turn-rootcause.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: backstop-fires-on-intra-task-turn-rootcause
|
||||
description: memory note backstop-fires-on-intra-task-turn-rootcause
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: CAUSE RACINE — le backstop tire sur chaque tour interne (turn_duration ≠ retour-au-prompt)
|
||||
type: project
|
||||
description: Le turn-watcher fait tirer le backstop no-reply au PREMIER turn_duration, soit le 1er pas d'agent interne — il casse toute délégation multi-étapes (libération prématurée + rapport perdu). Le test QA pong passait par chance (tâche mono-tour).
|
||||
---
|
||||
# CAUSE RACINE (2026-06-23, prouvée live) — backstop tire sur les tours INTERNES
|
||||
|
||||
## Preuve
|
||||
Cadrage délégué à Architect (tâche multi-étapes, lecture de code) :
|
||||
```
|
||||
562100 [rendezvous] ask started -> Architect turn_timeout_ms=600000
|
||||
564943 [rendezvous] ask returned-to-prompt-no-reply: Architect after_ms=2843
|
||||
564943 [mailbox] complete_without_reply agent=dce19c75 ticket=cc4067fa outcome=sent
|
||||
```
|
||||
- Tiré à **2843 ms** : impossible qu'Architect ait fini un cadrage en 2,8 s.
|
||||
- Au même instant, transcript Architect **toujours en écriture** (Bash/Read en boucle), `idea_reply` count = **0** → Architect **continuait à travailler**.
|
||||
- Transcript = **61 `turn_duration`** : Claude écrit un `turn_duration` **par tour d'agent interne** (chaque appel LLM de la boucle agentique), PAS seulement au retour-au-prompt final.
|
||||
|
||||
## Diagnostic
|
||||
`claude_turn_watcher.rs` fait tirer `turn_ended` au **premier incrément** de `turn_duration` au-dessus de la baseline. Or cet incrément survient dès le **1er pas interne** de l'agent (~secondes). Donc le backstop confond « l'agent a fait un pas de raisonnement » avec « l'agent est retourné au prompt sans répondre ». Pour **toute tâche multi-étapes** : libération prématurée du demandeur « sans réponse » pendant que la cible bosse encore → `idea_reply` final → « no pending request » → **rapport perdu** (travail survit sur disque/branche).
|
||||
|
||||
Le test QA « pong » (backstop-no-reply-live-test-failed-2026-06-23, devenu vert) passait **par chance** : tâche à **un seul tour** ⇒ premier tour = dernier tour. Le fix `encode_cwd` a fait tirer le watcher correctement, mais a **révélé que la CONDITION de tir est fausse**.
|
||||
|
||||
## Conséquence opérationnelle
|
||||
Avec le binaire courant, **aucune délégation substantielle (multi-tours) ne remonte son rapport** : elle est coupée à ~3 s. Workaround Main en attendant le fix : déléguer → surveiller le transcript de la cible jusqu'à inactivité (mtime stable) → récupérer le résultat sur disque/branche ou re-solliciter en léger.
|
||||
|
||||
## Implication pour la refonte (seam liveness/fin-de-tour, validée utilisateur)
|
||||
« Retourné au prompt idle » NE peut PAS être « un turn_duration est apparu ». Le bon signal = **process vivant (`is_agent_live`) + transcript NE grossit PLUS depuis stall_after_ms** (battement de cœur). Tant que le transcript grossit ⇒ l'agent travaille ⇒ ne pas libérer. Le 1er lot du fix devrait **neutraliser ce tir prématuré**. Lié à [[rendezvous-600s-cap-too-short-heavy-tasks]], [[rendezvous-no-reply-backstop-design]], [[backstop-no-reply-live-test-failed-2026-06-23]].
|
||||
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: backstop-no-reply-live-test-failed-2026-06-23
|
||||
description: memory note backstop-no-reply-live-test-failed-2026-06-23
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: Backstop no-reply — RE-VALIDATION LIVE VERTE (wedge levé) après fix encode_cwd
|
||||
type: project
|
||||
description: Le backstop no-reply du rendez-vous est validé en live le 2026-06-23 (session post-rebuild) : demandeur libéré ~grâce 2s au lieu de 600s. Reste un point de finition (résultat véhiculé via canal d'erreur).
|
||||
---
|
||||
|
||||
# Backstop no-reply — RE-VALIDATION LIVE : VERTE (2026-06-23, session post-rebuild)
|
||||
|
||||
Remplace le 1er test (ÉCHEC) du même jour. La cause racine était l'encodage du slug projet Claude (`.` non encodé) corrigé dans `crates/infrastructure/src/inspector/claude_paths.rs` (`encode_cwd` → `replace(/[^a-zA-Z0-9]/g,'-')`, double-tiret sur `/.ideai/`). Voir [[checkpoint-rendezvous-redesign-devbackend-todo]] pour la cause racine.
|
||||
|
||||
## Preuves live (idea.log, session a6ced819)
|
||||
Scénario : DevBackend → idea_ask_agent vers QA « pong en prose, SANS idea_reply (volontaire) ».
|
||||
|
||||
```
|
||||
912948 [mcp] ask armed requester=DevBackend(73c853d1) target=QA timeout_ms=600000
|
||||
917707 [turn-watcher] turn_duration detected agent=QA(aefdbd61) count 47->48 -> turn_ended
|
||||
917707 [input-mediator] turn_ended agent=QA no_reply_payload -> mark_idle + grace
|
||||
917707 [input-mediator] turn_ended arming grace agent=QA ticket=0257c630 grace_ms=2000
|
||||
919707 [mailbox] complete_without_reply agent=QA ticket=0257c630 queue_depth_before=1 after=0 outcome=sent
|
||||
```
|
||||
|
||||
Contraste avec l'échec précédent : `turn_ended` = 0 avant le fix, ici il FIRE ; `arm … dir=…IdeA--ideai-run-…` sur dossier EXISTANT ; grâce 2s exacte (917707→919707) ; libération du demandeur via `complete_without_reply outcome=sent`. DevBackend a repris la main en ~13 s mesuré (pas 600 s). Aucun wedge, aucun cascade-timeout.
|
||||
|
||||
## Point de finition résiduel (NON bloquant, follow-up)
|
||||
Le résultat rendu au demandeur passe par le **canal d'erreur** MCP : « agent QA returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply ». Ce n'est pas un résultat no-reply synthétique structuré (design item c de [[rendezvous-no-reply-backstop-design]]) ; pour un orchestrateur c'est mal distinguable d'un échec transitoire « retry ». À transformer en verdict consommable (follow-up, hors blocage merge).
|
||||
|
||||
## Suite
|
||||
Wedge prouvé levé en live ⇒ garde-fou Git satisfait. Délégué à Git : commit du fix `encode_cwd` (+ test de non-régression `encode_cwd_encodes_dot_in_run_dir_to_double_dash`) sur la branche du backstop, puis décision de merge develop.
|
||||
62
.ideai/memory/checkpoint-auto-memory-harvest-lot-e1.md
Normal file
62
.ideai/memory/checkpoint-auto-memory-harvest-lot-e1.md
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
name: checkpoint-auto-memory-harvest-lot-e1
|
||||
description: memory note checkpoint-auto-memory-harvest-lot-e1
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Auto-memory harvest Lot E1
|
||||
|
||||
Date: 2026-06-21
|
||||
|
||||
## État final
|
||||
|
||||
Lot E1 `auto-memory harvest contrôlé` terminé, validé QA et mergé localement dans `develop`.
|
||||
|
||||
Branche finale:
|
||||
- `develop` @ `61c330a` — merge local `feature/controlled-auto-memory-harvest`.
|
||||
- Branche `feature/controlled-auto-memory-harvest` supprimée après merge.
|
||||
- Aucun push, aucun merge vers `main`.
|
||||
- Working tree applicatif propre; seuls résidus non committés: bruit runtime `.ideai/*` volontairement exclu.
|
||||
|
||||
## Commits créés
|
||||
|
||||
- `b12081b` — `feat(memory): récolte automatique contrôlée de la mémoire (Lot E1)`
|
||||
- Domaine: parsing pur de blocs fenced `idea-memory` avec limites et erreurs isolées.
|
||||
- Application: `HarvestMemoryFromTurn` best-effort sur `TurnRole::Response`, persistance via `MemoryStore`, événements `MemorySaved` via `EventBus`.
|
||||
- Orchestrateur: harvest branché après checkpoint/handoff sur chemins de succès ask/reply, non bloquant.
|
||||
- Lifecycle: directive mémoire IdeA injectée dans le contexte convention-file.
|
||||
- App/Tauri: wiring du use case dans `AppState`.
|
||||
- Frontend: refresh du panneau mémoire sur `memorySaved`; patch test-only mock gateway `workState`.
|
||||
|
||||
- `8c7c47c` — `fix(mcp): fallback déterministe du runtime dir socket`
|
||||
- Durcit le choix du runtime dir socket MCP si `XDG_RUNTIME_DIR` est inutilisable.
|
||||
- Commit séparé car indépendant du Lot E1.
|
||||
|
||||
- `61c330a` — `merge(memory): intègre la récolte automatique contrôlée (Lot E1) dans develop`.
|
||||
|
||||
## QA verte
|
||||
|
||||
Commandes QA réelles:
|
||||
- `cargo fmt --all -- --check` OK.
|
||||
- `cargo check -p app-tauri` OK.
|
||||
- `cd frontend && npx tsc --noEmit` OK.
|
||||
- `cd frontend && npx vitest run` OK après patch test-only, `42 passed`, `405 tests`.
|
||||
- `cargo test -p domain memory_harvest -- --nocapture` OK, 14 passed.
|
||||
- `cargo test -p application --test memory_harvest -- --nocapture` OK, 5 passed.
|
||||
- `cargo test -p application --test orchestrator_service harvest -- --nocapture` OK, 4 passed.
|
||||
- `cargo test -p infrastructure --test memory_store idempotent -- --nocapture` OK, 1 passed.
|
||||
- `cargo test --workspace` brut rouge uniquement sur tests AF_UNIX/MCP bloqués par sandbox `Operation not permitted`; workspace filtré strictement sur ces tests environnementaux OK.
|
||||
|
||||
## Décisions produit/techniques
|
||||
|
||||
- Harvest strictement best-effort: jamais bloquant pour réponse, délégation, handoff ou ticket.
|
||||
- Mémoire durable seulement via directive explicite `idea-memory`, plafonnée et validée; pas de log brut, pas de live-state, pas de handoff dans ce mécanisme.
|
||||
- `title` est exigé par directive mais non persisté car l'entité `Memory` n'a pas de champ titre; accepté pour E1 afin de ne pas élargir le lot.
|
||||
- Les fichiers runtime `.ideai/*` générés pendant la session restent hors commits applicatifs.
|
||||
|
||||
## Suite probable
|
||||
|
||||
Prochains chantiers à cadrer par Architect avant implémentation:
|
||||
- persistance conversationnelle/canonical log + handoff incrémental cross-profile;
|
||||
- live-state projet persistant et séparé de la mémoire durable;
|
||||
- synchronisation documentaire architecture après les lots Work B/C/D et E1.
|
||||
@ -0,0 +1,46 @@
|
||||
---
|
||||
name: checkpoint-blocked-until-appimage-030-restart
|
||||
description: memory note checkpoint-blocked-until-appimage-030-restart
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — blocage délégation Architect jusqu'au redémarrage AppImage 0.3.0
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## État atteint
|
||||
|
||||
Chantier `orchestrator-designation` terminé et intégré localement:
|
||||
- `develop` @ `9c71a5b` après commit runtime post-build.
|
||||
- Worktree propre à ce moment.
|
||||
- AppImage corrigée générée:
|
||||
`/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`
|
||||
- Artefact vérifié:
|
||||
- taille 106M
|
||||
- `--appimage-offset` = `944632`
|
||||
|
||||
## Problème live confirmé
|
||||
|
||||
Deux tentatives de délégation vers Architect ont affiché le texte dans le chat/terminal d'Architect sans le soumettre réellement.
|
||||
L'utilisateur a interrompu et confirmé: « Le message n'arrive pas jusqu'à Architecte visiblement ».
|
||||
|
||||
Ce symptôme correspond au bug live de soumission write-portal/headless delivery corrigé dans `orchestrator-designation`, mais l'application en cours tourne encore avec l'ancien AppImage. Le nouvel artefact n'est pas chargé tant qu'IdeA n'est pas relancé avec l'AppImage 0.3.0.
|
||||
|
||||
## Action effectuée
|
||||
|
||||
Architect stoppé pour éviter un terminal avec tâche fantôme.
|
||||
|
||||
## Prochain chantier prévu après redémarrage
|
||||
|
||||
Git a recommandé:
|
||||
- Prochain chantier: `feature/agent-skill-awareness`.
|
||||
- Architect doit d'abord trancher le chevauchement avec `feature/agent-skills`.
|
||||
- `fix/cold-start-delivery-race` est probablement obsolète/superseded par `feature/agent-skill-awareness`, mais à supprimer seulement après intégration ou décision Git finale.
|
||||
|
||||
## Reprise recommandée
|
||||
|
||||
1. Relancer IdeA avec:
|
||||
`/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`
|
||||
2. Reprendre par une délégation Architect:
|
||||
cadrage `agent-skill-awareness` vs `agent-skills`.
|
||||
3. Ensuite cycle normal: Git -> DevBackend -> QA -> Git.
|
||||
88
.ideai/memory/checkpoint-delivery-submit-logging-fix.md
Normal file
88
.ideai/memory/checkpoint-delivery-submit-logging-fix.md
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
name: checkpoint-delivery-submit-logging-fix
|
||||
description: memory note checkpoint-delivery-submit-logging-fix
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — hotfix livraison délégation + logs submit
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## Pourquoi ce checkpoint existe
|
||||
|
||||
Après le fix AppImage 0.3.0 précédent, la délégation `Main -> DevBackend` a reproduit le même symptôme que `Main -> Architect` avant redémarrage : le message de délégation apparaît dans l'input de la cellule cible, mais n'est pas soumis.
|
||||
|
||||
Le chantier `feature/agent-skill-awareness-v2` est donc suspendu tant que le canal de délégation n'est pas fiable.
|
||||
|
||||
## Branche et état
|
||||
|
||||
Branche active pendant le hotfix : `feature/agent-skill-awareness-v2`.
|
||||
|
||||
Dirty attendu hors code : fichiers runtime `.ideai/conversations/*`, `.ideai/layouts.json` produits par la session live.
|
||||
|
||||
## Changements code effectués
|
||||
|
||||
Frontend :
|
||||
- `frontend/src/features/terminals/useWritePortal.ts`
|
||||
- Logs détaillés `[write-portal]` : abonnement, attachement front, réception `delegationReady`, chunks de texte, délai avant submit, submit, ack, erreurs.
|
||||
- Fix défensif : `submitSequence: ""` est normalisée vers `"\r"` au lieu d'écrire zéro octet.
|
||||
- En cas d'erreur d'injection, arrêt du retry immédiat infini.
|
||||
- `frontend/src/adapters/terminal.ts`
|
||||
- Logs `[terminal-write]` sur writes non triviaux ou contrôles (`\r`, `\n`, bulk, etc.).
|
||||
- `frontend/src/adapters/input.ts`
|
||||
- Logs `[input-gateway]` pour `delegationDelivered` et `setFrontAttached`.
|
||||
- `frontend/src/domain/index.ts`
|
||||
- Commentaire défaut submit aligné sur ~350 ms.
|
||||
- `frontend/src/features/terminals/useWritePortal.test.tsx`
|
||||
- Test ajouté : `submitSequence: ""` doit envoyer `"\r"`.
|
||||
|
||||
Backend/application/Tauri :
|
||||
- `crates/application/src/orchestrator/service.rs`
|
||||
- `note_delegation_delivered` passe par `application::diag!` persistant.
|
||||
- `set_agent_front_attached` logge les changements d'attachement et le cas médiateur absent.
|
||||
- `crates/infrastructure/src/input/mod.rs`
|
||||
- Logs détaillés `[input-mediator]` : start turn, gate cold start, publish `DelegationReady`, choix front-owned vs headless, bind handle, front attach/detach, headless text/submit start/ok/failure.
|
||||
- Défaut headless `DEFAULT_SUBMIT_DELAY_MS` corrigé de 60 ms à 350 ms pour être aligné avec le write-portal.
|
||||
- Headless normalise aussi une submit sequence vide vers `"\r"`.
|
||||
- `crates/app-tauri/src/commands.rs`
|
||||
- Logs persistants `[pty-write]` autour de `write_terminal` avec session, bytes, contrôle sans contenu bulk.
|
||||
- Logs persistants `[delivery]` pour `delegation_delivered` et `set_front_attached`.
|
||||
|
||||
## Vérifications passées
|
||||
|
||||
- `cargo fmt --all -- --check` : OK.
|
||||
- `npx vitest run src/features/terminals/useWritePortal.test.tsx src/adapters/terminal.test.ts src/features/terminals/TerminalView.portal.test.tsx` : OK, 3 files / 20 tests.
|
||||
- `npx tsc --noEmit` : OK.
|
||||
- `cargo test -p infrastructure input --lib` : OK, 35 tests.
|
||||
- `cargo check -p app-tauri` : OK.
|
||||
- `cargo test -p application --test orchestrator_service` : OK, 45 tests (warning existant `CapturingFs::writes` unused).
|
||||
- `cargo test -p app-tauri --test orchestrator_wiring` : OK, 13 tests.
|
||||
|
||||
## Build AppImage
|
||||
|
||||
Commande Tauri standard : frontend build OK, Rust release OK, bundling Tauri KO avec `failed to run linuxdeploy` comme précédemment.
|
||||
|
||||
Contournement réussi :
|
||||
|
||||
```bash
|
||||
ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 \
|
||||
/tmp/appimage_extracted_02f96dbd6ecc47f616b5a57fb8b7aa60/usr/bin/appimagetool \
|
||||
--runtime-file /tmp/idea-appimage-runtime-x86_64 \
|
||||
/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA.AppDir \
|
||||
/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage
|
||||
```
|
||||
|
||||
Artefact :
|
||||
- `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`
|
||||
- taille : 106M
|
||||
- `--appimage-offset` : `944632`
|
||||
|
||||
## Reprise recommandée
|
||||
|
||||
1. Relancer IdeA avec l'AppImage ci-dessus.
|
||||
2. Reproduire une délégation simple `Main -> DevBackend` ou `Main -> Architect`.
|
||||
3. Si le message reste dans l'input sans être soumis, récupérer les traces :
|
||||
- logs persistants `application::diag!` (chemin configuré au startup, typiquement app-data logs/idea.log), chercher `[delivery]`, `[pty-write]`, `[rendezvous]`.
|
||||
- console frontend, chercher `[write-portal]`, `[terminal-write]`, `[input-gateway]`.
|
||||
- stderr si lancé depuis terminal, chercher `[input-mediator]`.
|
||||
4. Une fois délégation fiable, reprendre `feature/agent-skill-awareness-v2` depuis le cadrage Architect déjà obtenu.
|
||||
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: checkpoint-finding-a-fix-built-live-validation-pending
|
||||
description: memory note checkpoint-finding-a-fix-built-live-validation-pending
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Finding A : fix confirmé EN CODE + AppImage clean DÉPLOYÉE → reste la validation e2e LIVE (MAJ 2026-06-23 09:33)
|
||||
|
||||
Suite de [[mcp-e2e-findings-reply-wedge-phantom-busy]] (le défaut) et [[resume-after-appimage-rebuild-mcp-e2e]] (méthode).
|
||||
|
||||
## État du code (OK, vert)
|
||||
- Fix Finding A **mergé dans `develop @ 77e62e5`** (par-dessus feat `c5e5493`).
|
||||
- 3 lots : **Lot 1** préambule `idea_reply` impératif (`infrastructure/src/input/mod.rs` + `application/src/agent/lifecycle.rs`) ; **Lot 2 = LE fix** `with_prompt_ready_pattern("? for shortcuts")` sur le profil Claude (`application/src/agent/catalogue.rs:80`) + overlay `overlay_reference_defaults` ; **Lot 3** backstop timeout typé `TARGET_RETURNED_NO_REPLY` / env `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS` (`infrastructure/src/orchestrator/mcp/server.rs`).
|
||||
- Tests lib qui PROUVENT le seed (re-passés vert le 2026-06-23) : `application::agent::catalogue::mcp_tests::claude_seed_carries_the_measured_prompt_ready_pattern` ✓ et `codex_seed_leaves_prompt_ready_pattern_empty_for_now` ✓.
|
||||
|
||||
## LEÇON D'OUTILLAGE (important, éviter de refaire l'erreur)
|
||||
**Ne PAS utiliser `strings`/`grep` sur le binaire comme oracle de présence du fix.** Le compilateur élimine/inline les littéraux uniques de `catalogue.rs` : `? for shortcuts` ET `codex --version` sont ABSENTS du binaire alors que le code et les tests les portent (les littéraux aussi présents ailleurs — `claude --version`, `.mcp.json`, `CLAUDE.md` — n'apparaissent que par co-référence). Oracle fiable = **tests** + **comportement live**, pas le grep binaire. (Une fausse alarme « build incrémental incohérent » a été levée puis invalidée ainsi.)
|
||||
|
||||
## Mise en service FAITE
|
||||
- Rebuild clean (`cargo clean -p application -p infrastructure` puis recette tauri) → artefact `…/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`.
|
||||
- **Déployé** sur `/home/anthony/Documents/IdeA_0.3.0_amd64.AppImage` (rename atomique ; SHA256 `138714473ed6a87b…` installée == artefact). Ancienne sauvegardée `…AppImage.bak-23pre`.
|
||||
- ⚠️ **L'instance IdeA qui tourne encore est l'ANCIENNE** (FUSE déjà monté). Il faut que **l'utilisateur RELANCE IdeA** pour charger le nouveau binaire — cela redémarre tous les agents, dont Main (la session courante meurt).
|
||||
|
||||
## PROCHAINE ÉTAPE après redémarrage : test e2e LIVE
|
||||
Méthode (rappel) : **NE PAS valider le système inter-agents VIA `idea_ask_agent`**. Piloter via subagents natifs / l'utilisateur. Diag EXTERNE : `ss -xp | grep idea-mcp` (ponts) ; transcripts `~/.claude/projects/<run-dir-encodé>/*.jsonl` (horodatages UTC ; reply réel = `tool_use` idea_reply ; wedge = transcript demandeur figé sur `tool_use` idea_ask_agent sans `tool_result`).
|
||||
|
||||
Scénario qui wedgeait (doit maintenant passer) :
|
||||
1. Faire déléguer **DevBackend → QA** : « réponds juste pong ».
|
||||
2. ATTENDU avec le fix : si QA répond en prose sans `idea_reply`, dans la grâce `PROMPT_READY_GRACE` le retour au prompt idle (`? for shortcuts`) déclenche `complete_without_reply` → **DevBackend reçoit `TargetReturnedNoReply` (retryable) et REPREND la main** (plus de wedge) ; `idea_workstate` QA repasse `working`→`idle`/`done` (plus de Busy fantôme). Lot 1 devrait en plus pousser QA à appeler `idea_reply` (chemin positif → DevBackend reçoit pong).
|
||||
3. Non-régression : chemin positif normal (QA appelle `idea_reply`) → demandeur reçoit le résultat, workstate→done ; livraison 1er tour à froid intacte.
|
||||
4. Calibrer `PROMPT_READY_GRACE` si la fenêtre est trop courte/longue en réel (défaut conservé).
|
||||
|
||||
## Après validation verte
|
||||
Merge `develop` déjà fait. Reste éventuelle release (0.4.0 ?) = décision utilisateur. NB : **Git est de nouveau strictement local, PLUS d'accès push** (la note `git-agent-push-access-gitea-ssh` a été corrigée en ce sens). `develop` est ~69 commits devant `origin/develop` (sync remote hors périmètre Git).
|
||||
|
||||
## Follow-ups différés (hors PR)
|
||||
Capture `prompt_ready_pattern` Codex ; généralisation merge profils de référence ; e2e délégation background (node_id None) / profils MCP défaut / observabilité UI des délégations ; durcissement doc `idea_reply` dans `CLAUDE.md` versionné (router à Git).
|
||||
@ -0,0 +1,40 @@
|
||||
---
|
||||
name: checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer
|
||||
description: memory note checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: checkpoint-mcp-t1-t6-green-t7-fix-wrong-layer
|
||||
description: T1→T6 verts (non-régression). T7 ROUGE diagnostiqué (fix sur mauvaise couche) PUIS RE-FIX implémenté dans le working tree + vert en tests unitaires. Reste : rebuild AppImage en cours → relance IdeA → re-valider T1→T7 en live → commit Git.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint tests fonctionnels MCP — run a6ced819 (2026-06-24)
|
||||
|
||||
Plan via skill **mcp-rendezvous-functional-test**, Main = demandeur. Setup chauds : Main/QA/DevFrontend ; froids : Architect/DevBackend/Git.
|
||||
|
||||
## T1→T6 : VERTS sur AppImage 09:21 (aucune régression)
|
||||
T1 visibilité, T2 chaude `pong`, T3 réveil froid Git (1er tour intact), T4 background node_id None, T5 no-reply backstop (live-state purgée), T6 multi-étapes == oracle. Détails dans l'historique. NB outillage : `pgrep -af mcp-server | grep <uuid>` = FAUX positifs (le shell de mesure se matche) → filtrer `ps -eo cmd | grep 'app-tauri mcp-server'`.
|
||||
|
||||
## T7 : défaut trouvé EN LIVE puis CORRIGÉ
|
||||
**Défaut (T7e live, 621 s ≈ 600 s)** : cible en long tour unique > 600 s (transcript en croissance prouvée) coupée par un timeout PLAT `process error: agent session reply timed out`, pas d'extension. Le watchdog du fix précédent vivait côté requester (`server.rs`) mais le seam qui coupe vraiment est `application/src/orchestrator/service.rs` (`ASK_AGENT_TIMEOUT`=600s plat appliqué au drain de tour délégué, ~L1564). **Fix posé sur la mauvaise couche.**
|
||||
|
||||
**RE-FIX implémenté (subagent natif Architect→DevBackend→QA, worktree, ramené dans le main working tree ; HEAD toujours `1efe2f1`, NON committé)** :
|
||||
- Découverte clé : `ClaudeSdkSession::send().await` rend TOUS les `ReplyEvent` en bloc À LA FIN du tour (rien mid-turn) → la vivacité par-événement est inutilisable ; seul signe de vie réel = **sonde transcript** (octets `.jsonl`), comme `server.rs`.
|
||||
- Un seul `run_inactivity_watchdog` générique dans **`crates/application/src/orchestrator/rendezvous.rs`** (NOUVEAU, pur, testé), réutilisé par `service.rs` (le vrai seam) ET `server.rs` (copie privée supprimée → dé-duplication).
|
||||
- Nouveau port `AskLivenessProbe` injecté au composition root `app-tauri/state.rs` → `transcript_activity_token` (+ `with_ask_ceiling`, env `IDEA_ASK_RENDEZVOUS_CEILING_MS`, défaut 4 h). Sans injection ⇒ fallback fenêtre plate = zéro régression.
|
||||
- Issues typées : progrès<plafond → réarme ; progrès+plafond → `AppError::TargetCeilingActive` (code `RENDEZVOUS_CEILING_ACTIVE`, non-retry-aveugle) ; vrai silence → `AgentSessionError::Timeout` inchangé.
|
||||
- Défaut secondaire (busy fantôme) : `mark_target_done_best_effort` sur toutes les branches d'abandon (PTY + structuré).
|
||||
- Fichiers : application `{rendezvous.rs(new), service.rs, error.rs, lib.rs, orchestrator/mod.rs}`, infra `mcp/server.rs`, `app-tauri/state.rs` (+ les modifs infra inspector/lib pré-existantes du 1er fix).
|
||||
- **Preuve réelle (re-vérifiée par Main dans le MAIN TREE)** : `cargo test -p application --lib` 75/0 (dont progressing_target_extends_then_resolves, _hits_ceiling_distinctly, silent_target_expires_no_reply, no_probe_falls_back_to_flat_window, ceiling_active_code) ; `cargo test -p infrastructure --lib` 247/0 ; clippy 0 nouveau warning.
|
||||
|
||||
## PROCHAINE ÉTAPE (reprise après relance IdeA)
|
||||
1. **Rebuild AppImage EN COURS** (recette : `npm --prefix frontend run build` puis depuis `crates/app-tauri/` `APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage` ; artefact `target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage` → remplace `/home/anthony/Documents/IdeA_0.3.0_amd64.AppImage`, backup l'ancienne). Log build : scratchpad/rebuild.log.
|
||||
2. **Utilisateur relance IdeA** (tue Main).
|
||||
3. **Reprendre le test depuis T1** (skill). Le but immédiat = **re-valider T7 EN LIVE** : déléguer une charge réelle > 600 s en un long tour (la machine est rapide : `cargo clean && cargo build --release --workspace` en boucle jusqu'à >13 min, transcript en croissance) ⇒ ATTENDU = PAS de timeout à 600 s, extension, reply final livré. Puis enchaîner T8 (parallèles), T9 (transitif A→B→C, Architect+DevBackend froids), T10 (interruption + reconcile reboot).
|
||||
4. **Si live vert : demander à Git de committer** le fix (cf. [[git-owns-commit-merge-decisions]]) ; rendez-vous MCP court marche (T2-T6), donc commit délégable à Git OU subagent natif.
|
||||
|
||||
Lié à [[rendezvous-600s-cap-too-short-heavy-tasks]], [[mcp-functional-test-plan-2026-06-24]], [[mcp-bridge-and-delegation-runtime-notes]].
|
||||
</content>
|
||||
</invoke>
|
||||
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect
|
||||
description: memory note checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect
|
||||
description: Run a6ced819 — relance complète T1→T6 du test fonctionnel MCP sur AppImage 08:41 (fix T5). T5 (live-state purgée sur no-reply) VALIDÉ EN LIVE. Blocage à T7 = défaut plafond 600s connu, non corrigé.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint tests fonctionnels MCP — run a6ced819 (2026-06-24, relance depuis T1)
|
||||
|
||||
Binaire validé : **AppImage 2026-06-24 08:41:30** (fix live-state no-reply), process en cours = cette AppImage, git HEAD `1efe2f1`. Plan via skill **mcp-rendezvous-functional-test** (Main = demandeur). Voir [[mcp-functional-test-plan-2026-06-24]], [[rendezvous-600s-cap-too-short-heavy-tasks]], [[backstop-fires-on-intra-task-turn-rootcause]].
|
||||
|
||||
## Résultats de la relance T1→T6 (tous ✅)
|
||||
- **T1** visibilité outils ✅ — 3 requesters chauds (Main, QA, DevFrontend) chacun pont ESTAB + 2 mcp-server ; froids (Architect, DevBackend, Git) 0 process.
|
||||
- **T2** cible chaude (QA) ✅ — `pong` inline borné, workstate `done`, pas de busy fantôme.
|
||||
- **T3** réveil à froid (Git) ✅ — nouveau pont (26012/26016), 1er tour `[IdeA·tâche…]` `parentUuid:null` intact, `delivered`, `pong-cold`.
|
||||
- **T4** background node_id None (Git hors layouts.json) ✅ — `bg-step-ok`, multi-`delivered`, pont stable sans dup.
|
||||
- **T5** no-reply backstop ✅ **VALIDÉ EN LIVE (le fix marche)** — Main libéré borné avec `TargetReturnedNoReply` (« returned to its prompt without calling idea_reply », retryable) ET **QA `status:"done"` purgée** (plus de busy fantôme `working`). Défaut historique #1 fermé.
|
||||
- **T6** multi-étapes (QA lit fichier + `find` puis reply) ✅ — pas de libération prématurée, reply réel `header=… ; rs_count=55` (concorde avec `find` indépendant). Piège turn-watcher non déclenché.
|
||||
|
||||
## Blocage actuel : T7 (NON lancé — défaut connu non corrigé)
|
||||
T7 = tâche lourde > plafond 600s. C'est le finding [[rendezvous-600s-cap-too-short-heavy-tasks]] : dépassement du plafond `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS` (défaut 600000) → faux `-32001` + canal de report mort (idea_reply final non livrable), alors que la cible travaille et finit sur la branche. Les pistes (extension sur signe de vie / message « cible active, plafond atteint » distinct du retryable / plafond configurable / pas de cascade) sont **à cadrer Architect, pas encore implémentées**. Lancer T7 en live = figer Main ~10 min pour re-confirmer un défaut déjà documenté.
|
||||
|
||||
## Prochaine étape (décision utilisateur en attente)
|
||||
Trois options sur T7 : (a) le lancer quand même pour re-confirmer (wedge ~10 min) ; (b) ouvrir le cycle de fix du plafond maintenant (Architect→Git→DevBackend→QA, rebuild, reprise T1) ; (c) sauter T7 et continuer T8 (parallèles), T9 (transitif A→B→C), T10 (interruption + reconcile reboot) qui ne dépendent pas du plafond. Architect/DevBackend restent froids pour T9.
|
||||
@ -0,0 +1,98 @@
|
||||
---
|
||||
name: checkpoint-orchestrator-designation-appimage-build
|
||||
description: memory note checkpoint-orchestrator-designation-appimage-build
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — AppImage rebuild après orchestrator-designation
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## État Git avant build
|
||||
|
||||
Git avait clôturé `feature/orchestrator-designation`:
|
||||
- Commits atomiques créés:
|
||||
- `287681c feat(orchestrator): ...`
|
||||
- `e462136 feat(terminals): ...`
|
||||
- `09f5362 docs: ...`
|
||||
- `5ef001e chore(wip): ...`
|
||||
- Merge local dans `develop`: `55d887f merge(orchestrator): intègre le chantier orchestrator-designation dans develop`.
|
||||
- Branche `feature/orchestrator-designation` supprimée.
|
||||
- Worktree propre à ce moment-là.
|
||||
|
||||
## Rebuild tenté
|
||||
|
||||
Commande initiale:
|
||||
|
||||
```bash
|
||||
npm --prefix frontend run build && \
|
||||
cd crates/app-tauri && \
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage
|
||||
```
|
||||
|
||||
Résultat:
|
||||
- Frontend build OK.
|
||||
- Rust release build OK: `/home/anthony/Documents/Projects/IdeA/target/release/app-tauri`.
|
||||
- AppDir généré: `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA.AppDir`.
|
||||
- Bundling Tauri KO: `failed to run linuxdeploy`.
|
||||
|
||||
## Contournement réussi
|
||||
|
||||
`appimagetool` extrait existait dans:
|
||||
|
||||
```text
|
||||
/tmp/appimage_extracted_02f96dbd6ecc47f616b5a57fb8b7aa60/usr/bin/appimagetool
|
||||
```
|
||||
|
||||
L'appel direct à appimagetool échouait car réseau restreint:
|
||||
|
||||
```text
|
||||
Failed to download runtime ... pass it to appimagetool with --runtime-file
|
||||
```
|
||||
|
||||
Runtime extrait depuis l'ancienne AppImage locale:
|
||||
|
||||
```bash
|
||||
/home/anthony/Documents/IdeA_0.2.0_amd64.AppImage --appimage-offset
|
||||
# 944632
|
||||
|
||||
dd if=/home/anthony/Documents/IdeA_0.2.0_amd64.AppImage \
|
||||
of=/tmp/idea-appimage-runtime-x86_64 \
|
||||
bs=1 count=944632 status=none
|
||||
chmod +x /tmp/idea-appimage-runtime-x86_64
|
||||
```
|
||||
|
||||
Commande finale réussie:
|
||||
|
||||
```bash
|
||||
ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 \
|
||||
/tmp/appimage_extracted_02f96dbd6ecc47f616b5a57fb8b7aa60/usr/bin/appimagetool \
|
||||
--runtime-file /tmp/idea-appimage-runtime-x86_64 \
|
||||
/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA.AppDir \
|
||||
/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage
|
||||
```
|
||||
|
||||
Artefact produit:
|
||||
|
||||
```text
|
||||
/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage
|
||||
Taille: 106M
|
||||
--appimage-offset: 944632
|
||||
```
|
||||
|
||||
## État Git après build
|
||||
|
||||
Le build/la session a laissé du runtime dirty:
|
||||
|
||||
```text
|
||||
## develop...origin/develop [ahead 8]
|
||||
M .ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md
|
||||
M .ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl
|
||||
M .ideai/layouts.json
|
||||
```
|
||||
|
||||
Ne pas nettoyer arbitrairement. Demander à Git de décider si ces fichiers runtime doivent être commités/ignorés au prochain passage.
|
||||
|
||||
## Important live
|
||||
|
||||
L'AppImage en cours d'exécution ne charge pas automatiquement ce nouvel artefact. Pour utiliser les correctifs d'orchestration/write-portal, lancer l'artefact 0.3.0 produit ci-dessus ou remplacer l'AppImage installée hors sandbox si nécessaire.
|
||||
@ -0,0 +1,54 @@
|
||||
---
|
||||
name: checkpoint-orchestrator-designation-backend-compile-fix
|
||||
description: memory note checkpoint-orchestrator-designation-backend-compile-fix
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — orchestrator-designation backend compile fix
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## Branche
|
||||
|
||||
`feature/orchestrator-designation`.
|
||||
|
||||
## Décision Git
|
||||
|
||||
Finir le chantier courant sur cette branche. Pas de stash, pas de switch. Après tests verts ou résidu qualifié: retour Git pour commits atomiques puis merge local vers `develop`.
|
||||
|
||||
## Incident live
|
||||
|
||||
La délégation vers Architect a été affichée dans son chat mais non soumise. Architect a été stoppé. Symptôme cohérent avec l'AppImage live qui n'intègre pas encore les correctifs WIP write-portal/headless delivery.
|
||||
|
||||
## Vérifications Main avant correction DevBackend
|
||||
|
||||
Verts:
|
||||
- `cargo test -p infrastructure input --lib`: 35 passed.
|
||||
- `cargo test -p application --test orchestrator_service`: 45 passed.
|
||||
- `cd frontend && npx vitest run`: 41 files, 384 tests passed.
|
||||
- `cd frontend && npx tsc --noEmit`: OK.
|
||||
|
||||
Rouge initial:
|
||||
- `cargo test --workspace` ne compilait pas `crates/application/src/orchestrator/context_guard.rs`:
|
||||
- `may_write_directly` appelé avec 2 args au lieu de 3 (`OrchestratorDesignation` manquant).
|
||||
- mauvais champ `orchestrator` mis sur `ManifestEntry` au lieu de `AgentManifest`.
|
||||
- init `AgentManifest` sans `orchestrator`.
|
||||
|
||||
## Correction DevBackend
|
||||
|
||||
DevBackend a modifié `crates/application/src/orchestrator/context_guard.rs`:
|
||||
- `ProposeContext` charge `AgentManifest`, récupère `manifest.orchestrator_designation()`, puis appelle `may_write_directly(requester, &GuardedResource::ProjectContext, &designation)`.
|
||||
- Le `FileGuard` reste un verrou, pas le propriétaire de l'autorisation.
|
||||
- Tests locaux adaptés à `AgentManifest { version, entries, orchestrator }`.
|
||||
|
||||
Validations DevBackend:
|
||||
- `cargo fmt --all && cargo test -p application --test orchestrator_service`: OK, 45 passed.
|
||||
- `cargo test -p application`: OK.
|
||||
- `cargo test --workspace`: compile maintenant plus loin mais échoue dans `app-tauri` sur tests loopback Unix socket sous sandbox (`PermissionDenied`/`Operation not permitted` lors du bind `/run/user/1000/idea-mcp/*.sock`).
|
||||
|
||||
## Prochaine étape
|
||||
|
||||
QA doit qualifier le résidu app-tauri:
|
||||
- confirmer que les suites métier/orchestration sont vertes,
|
||||
- confirmer si les échecs app-tauri sont environnement/sandbox et non régression,
|
||||
- proposer la commande de validation acceptable ou le besoin de correction test/env.
|
||||
@ -0,0 +1,42 @@
|
||||
---
|
||||
name: checkpoint-orchestrator-designation-qa-verdict
|
||||
description: memory note checkpoint-orchestrator-designation-qa-verdict
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — QA verdict orchestrator-designation
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## Branche
|
||||
|
||||
`feature/orchestrator-designation`.
|
||||
|
||||
## Verdict QA
|
||||
|
||||
Chantier validable sous contrainte d'environnement. Pas de régression fonctionnelle liée à `context_guard.rs`.
|
||||
|
||||
## Commandes vertes qui font foi
|
||||
|
||||
- `cargo fmt --all -- --check`: OK.
|
||||
- `cargo test -p application --test orchestrator_service`: OK, 45 passed.
|
||||
- `cargo test -p application`: OK, suite application complète verte, incluant les tests context guard orchestrateur/proposition.
|
||||
- `cargo test -p infrastructure input --lib`: OK, 35 passed.
|
||||
- `cd frontend && npx vitest run`: OK, 41 files / 384 tests passed.
|
||||
- `cd frontend && npx tsc --noEmit`: OK.
|
||||
|
||||
## Résidu app-tauri
|
||||
|
||||
- `cargo test -p app-tauri --lib`: rouge, 39 passed / 8 failed.
|
||||
- `cargo test --workspace`: rouge sur le même bloc app-tauri.
|
||||
|
||||
Échecs exacts: tests loopback Unix socket:
|
||||
- `mcp_bridge::tests::end_to_end_over_real_loopback`
|
||||
- `state::bind_endpoint_d1_tests::rebind_after_corpse_socket_succeeds`
|
||||
- `state::mcp_e2e_loopback_tests::*`
|
||||
|
||||
Cause qualifiée par QA: contrainte environnement/sandbox. Une sonde Node minimale échoue aussi à `listen()` sur Unix socket avec `EPERM` sous `/run/user/1000` et sous `/tmp`. Donc les tests qui exigent un vrai socket Unix ne sont pas exécutables dans ce sandbox.
|
||||
|
||||
## Prochaine étape
|
||||
|
||||
Retour à Git pour commits atomiques sur `feature/orchestrator-designation`, puis décision de merge local vers `develop` selon sa stratégie. Aucun push.
|
||||
38
.ideai/memory/checkpoint-orchestrator-designation-restart.md
Normal file
38
.ideai/memory/checkpoint-orchestrator-designation-restart.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: checkpoint-orchestrator-designation-restart
|
||||
description: memory note checkpoint-orchestrator-designation-restart
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — reprise chantier orchestrator-designation
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## Situation
|
||||
|
||||
L'utilisateur a demandé de terminer les chantiers ouverts. Main a repris le cycle proprement.
|
||||
|
||||
## Décision Git obtenue
|
||||
|
||||
Git a inspecté l'état local et décidé:
|
||||
|
||||
- Branche courante: `feature/orchestrator-designation`.
|
||||
- Worktree dirty important mais analysé comme mono-thème: chantier orchestrateur/designation + diagnostic inter-agents.
|
||||
- Ne pas stash, ne pas switch, ne pas ouvrir une nouvelle branche maintenant.
|
||||
- Premier chantier à fermer: `orchestrator-designation` sur la branche actuelle.
|
||||
- Après stabilisation + tests verts: retour à Git pour commits atomiques puis merge local vers `develop`.
|
||||
- Ensuite seulement ouvrir des branches dédiées pour les autres chantiers.
|
||||
|
||||
## Incident de délégation
|
||||
|
||||
Main a tenté de demander à Architect de cadrer `orchestrator-designation` via `idea_ask_agent`.
|
||||
L'utilisateur a interrompu et signalé qu'Architect était bloqué avec le texte de la tâche affiché dans son chat mais non envoyé.
|
||||
|
||||
Interprétation: symptôme cohérent avec les problèmes actuels de write portal / soumission physique PTY / délégation visible-background. Ne pas empiler de nouvelles délégations tant que cette zone n'est pas stabilisée.
|
||||
|
||||
## Prochaine reprise recommandée
|
||||
|
||||
1. Nettoyer/arrêter l'agent Architect bloqué si nécessaire.
|
||||
2. Reprendre le chantier courant localement en lecture seule pour identifier exactement les changements WIP liés à la soumission de délégations.
|
||||
3. Comme Main ne code pas, utiliser la délégation seulement vers un agent dont la soumission est confirmée fonctionnelle, ou lancer les agents en visible et vérifier qu'ils reçoivent réellement la tâche.
|
||||
4. Priorité technique immédiate: fermer `orchestrator-designation`, car la délégation inter-agents fiable conditionne tous les autres chantiers.
|
||||
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: checkpoint-rendezvous-redesign-devbackend-todo
|
||||
description: memory note checkpoint-rendezvous-redesign-devbackend-todo
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Backstop no-reply : 1ère validation live ÉCHOUÉE → cause racine corrigée → RE-VALIDATION LIVE EN ATTENTE (MAJ 2026-06-23 ~19:40)
|
||||
|
||||
## Résultat du 1er test live (AppImage 0.3.0 du commit e87c05f) : ÉCHEC reproduisant le wedge
|
||||
Scénario joué : DevBackend→QA `ping (test backstop)`, QA répond `pong` EN PROSE sans `idea_reply`.
|
||||
- QA a bien émis un vrai `turn_duration durationMs=2218` (transcript, ts 17:23:40.161Z).
|
||||
- MAIS `[turn-watcher] turn_ended` JAMAIS émis (`grep -c turn_ended` sur tout le log = **0**) → DevBackend resté coincé → rendez-vous Main→DevBackend a timeout 600s (MCP error -32001).
|
||||
|
||||
## CAUSE RACINE (prouvée) — encodage du slug projet Claude faux
|
||||
Traces `[turn-watcher] arm` : toutes `conversation=<none>` + `baseline … count=0`. Le dossier transcript calculé n'existait pas :
|
||||
- calculé (faux) : `…/-home-…-IdeA-.ideai-run-<uuid>` (point conservé) → `ls` = absent
|
||||
- réel (disque) : `…/-home-…-IdeA--ideai-run-<uuid>` (DOUBLE tiret)
|
||||
`encode_cwd` (crates/infrastructure/src/inspector/claude_paths.rs) ne remplaçait que `/`/`\\` et gardait le `.`. Claude Code encode `replace(/[^a-zA-Z0-9]/g,'-')`. Dir introuvable → conversation non résolue → baseline 0 → `turn_duration` jamais vu → backstop mort.
|
||||
Log de référence : `/home/anthony/.local/share/app.idea.ide/logs/idea.log`.
|
||||
|
||||
## FIX LIVRÉ par DevBackend (NON commité — Git tranche)
|
||||
- `encode_cwd` : `.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })` (algo exact Claude Code). `/IdeA/.ideai/run` → `-IdeA--ideai-run`.
|
||||
- Test non-régression ajouté `encode_cwd_encodes_dot_in_run_dir_to_double_dash` (PASS). Test Windows maj `C:\\Users\\me`→`C--Users-me`.
|
||||
- DevBackend précise : `count_turn_ends` agrège déjà tous les `.jsonl` du dossier ⇒ pas besoin de choisir un .jsonl ; `conversation=<none>` à l'armement est normal et sans impact une fois le dir correct.
|
||||
|
||||
## QA PRÉ-VALIDATION : VERTE
|
||||
`cargo test --workspace` = **1616 passed, 0 failed** ; `cargo clippy --workspace --all-targets` = 0 erreur. Nouveau test + 5 tests turn-watcher tous PASS.
|
||||
|
||||
## EN COURS : rebuild AppImage (par Main, perms project-root OK)
|
||||
Recette : `npm --prefix frontend run build` puis depuis `crates/app-tauri/` `APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage`. Logs scratchpad fe-build.log / appimage-build.log. Sortie : `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`.
|
||||
|
||||
## SEUL RESTE À FAIRE = RE-VALIDATION LIVE (garde-fou Git : pas de merge sans wedge prouvé levé EN LIVE)
|
||||
1. Utilisateur remplace l'AppImage installée par la nouvelle + relance IdeA (coupe la session).
|
||||
2. Session fraîche : rejouer DevBackend→QA pong-sans-`idea_reply`. ATTENDU cette fois : trace `arm … dir=…IdeA--ideai-run-… conversation=…` sur dossier EXISTANT + baseline NON nulle + `[turn-watcher] turn_duration detected … -> turn_ended` ⇒ libération du demandeur ~grâce (≈2s) avec résultat no-reply synthétique, et non-régression du chemin propre `idea_reply`.
|
||||
3. Si VERT → demander à Git d'amender/committer le fix sur `feature/rendezvous-no-reply-backstop` puis décider du merge develop. Si KO → relayer trace réelle à DevBackend.
|
||||
|
||||
## Trou produit confirmé (hors scope, à planifier)
|
||||
Au reboot, `.ideai/live-state.json` non réconcilié : agents restent `working` fantôme (process morts). Fichier file-guardé, `idea_workstate_set` = self only, aucun reset cross-agent. Manque : réconciliation au boot OU outil orchestrateur de reset.
|
||||
34
.ideai/memory/checkpoint-work-tab-blank-fix.md
Normal file
34
.ideai/memory/checkpoint-work-tab-blank-fix.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: checkpoint-work-tab-blank-fix
|
||||
description: memory note checkpoint-work-tab-blank-fix
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Fix onglet Work vide
|
||||
|
||||
Date: 2026-06-21
|
||||
|
||||
## État final
|
||||
|
||||
Correctif frontend de l'onglet Work vide terminé, validé QA (VERT) et mergé localement dans `develop`.
|
||||
|
||||
- `develop` @ `a66881d` — merge local `--no-ff` du fix.
|
||||
- Commit de feature intégré : `3e1e553` (anciennement `9d2f186`, message amendé pour retirer le marqueur `(WIP, QA en attente)`).
|
||||
- Branche `fix/work-tab-blank-page` supprimée après merge.
|
||||
- Aucun push, aucune PR. `develop` en avance de 45 commits sur `origin/develop` (local-only).
|
||||
|
||||
## Contenu du fix
|
||||
|
||||
Normalisation défensive du work-state (`frontend/src/adapters/workStateNormalization.ts` nouveau) + adaptation panneau/mock/tests, pour qu'un work-state partiel/inattendu ne rende plus une page entièrement vide. 5 fichiers, +259/-47.
|
||||
|
||||
## QA verte (sortie réelle)
|
||||
|
||||
- `cd frontend && npx tsc --noEmit` → exit 0.
|
||||
- `cd frontend && npx vitest run` → 42 fichiers / 407 tests passed, exit 0.
|
||||
|
||||
## Branches ouvertes restantes (non mergées dans develop)
|
||||
|
||||
À cadrer/arbitrer avant intégration :
|
||||
- `feature/agent-skills` @ `ef101db` — domaine skills agent + use cases + FS store + injection LaunchAgent (L12).
|
||||
- `feature/agent-skill-awareness` @ `5be8987` — manifeste skills + outil MCP `idea_skill_read` + brief « capacités IdeA » dans contexte agent ; contient aussi un fix input cold-start (`e93a2c1`).
|
||||
- `fix/cold-start-delivery-race` @ `9590eac` — fix livraison délégation cold-start ; branche basée sur d'anciens commits release (0.1.0/0.2.0), potentiellement redondante avec `e93a2c1` de skill-awareness → vérifier avant merge.
|
||||
@ -0,0 +1,64 @@
|
||||
---
|
||||
name: checkpoint-workstate-controlled-actions-lot-d
|
||||
description: memory note checkpoint-workstate-controlled-actions-lot-d
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Workstate controlled actions Lot D
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## État final
|
||||
|
||||
Lot D `workstate controlled actions` terminé, validé QA et mergé localement dans `develop`.
|
||||
|
||||
Branche finale:
|
||||
- `develop` @ `c1e99d1` — merge local `feature/workstate-controlled-actions`.
|
||||
- `feature/workstate-controlled-actions` conservée.
|
||||
- Aucun push, aucune branche supprimée.
|
||||
- Working tree propre après décision Git.
|
||||
|
||||
## Commits créés
|
||||
|
||||
- `3408c96` — `feat(workstate): actions contrôlées sur le work-state (Lot D backend)`
|
||||
- Use cases agent-level `AttachLiveAgent` et `StopLiveAgent`.
|
||||
- Commandes Tauri `attach_live_agent` et `stop_live_agent`.
|
||||
- Attach rebind PTY/structured sans spawn; stop PTY/structured.
|
||||
- DTO camelCase et wiring AppState/lib.
|
||||
|
||||
- `1c6441b` — `feat(workstate): UI des actions contrôlées (Lot D frontend)`
|
||||
- Port/adaptateur agent alignés sur le nouveau contrat attach + stop.
|
||||
- Panneau Work: Open, Attach, Stop, View conversation, Copy summary.
|
||||
- Work n'appelle pas `launchAgent`; Stop passe par `stopLiveAgent`; View/Copy n'utilisent que les previews Lot C.
|
||||
|
||||
- `0976648` — `chore(wip): état runtime .ideai (flux conversation live)`
|
||||
- Runtime isolé des commits feature.
|
||||
|
||||
- `c1e99d1` — merge local dans `develop`.
|
||||
|
||||
## QA verte
|
||||
|
||||
Commandes QA/Git validées:
|
||||
- `cargo fmt --all -- --check` OK.
|
||||
- `cargo test -p application --test workstate_actions` OK, 7 passed.
|
||||
- `cargo test -p application --test workstate` OK, 21 passed.
|
||||
- `cargo test -p app-tauri --test dto_agents` OK, 25 passed.
|
||||
- `cargo test -p app-tauri --test list_live_agents_r0b` OK, 5 passed.
|
||||
- `cargo check -p app-tauri` OK.
|
||||
- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 25 tests.
|
||||
- `cd frontend && npx vitest run src/adapters/agent.test.ts` OK, 8 tests.
|
||||
- `cd frontend && npx vitest run src/features/layout/singletonAgent.test.tsx src/features/layout/agentAlreadyRunning.test.tsx` OK, 9 tests.
|
||||
- `cd frontend && npx tsc --noEmit` OK.
|
||||
|
||||
Warnings restants uniquement côté Vite sur options `esbuild` dépréciées/ignorées au profit de `oxc`, non bloquants.
|
||||
|
||||
## Décisions produit/techniques
|
||||
|
||||
- Actions Lot D opèrent seulement sur l'état existant; pas de lancement d'agent neuf depuis Work.
|
||||
- Attach ne crée ni session ni cellule; cible déterministe = cellule visible vide côté UI.
|
||||
- Stop ne supprime ni agent, ni tickets, ni handoff/conversation summary.
|
||||
- View/Copy restent limités aux previews bornées Lot C; aucun log brut exposé.
|
||||
|
||||
## Suite probable
|
||||
|
||||
La quadrilogie Work A/B/C/D est intégrée. Prochains chantiers restants à cadrer: persistance conversationnelle/cross-profile plus profonde, mise à jour automatique mémoire/contexte pendant la vie des agents, ou synchronisation documentaire architecture selon priorité Architect/Main.
|
||||
@ -0,0 +1,60 @@
|
||||
---
|
||||
name: checkpoint-workstate-conversation-summaries-lot-c
|
||||
description: memory note checkpoint-workstate-conversation-summaries-lot-c
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Workstate conversation summaries Lot C
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## État final
|
||||
|
||||
Lot C `workstate conversation summaries` terminé, validé QA et mergé localement dans `develop`.
|
||||
|
||||
Branche finale:
|
||||
- `develop` @ `6e1ba7e` — merge local `feature/workstate-conversation-summaries`.
|
||||
- `feature/workstate-conversation-summaries` conservée.
|
||||
- Aucun push, aucune branche supprimée.
|
||||
- Working tree propre après décision Git.
|
||||
|
||||
## Commits créés
|
||||
|
||||
- `e9edadc` — `feat(workstate): résumés de conversation dans le read-model (Lot C backend)`
|
||||
- `ProjectWorkState.conversations` top-level.
|
||||
- Résumés best-effort depuis `HandoffStore`, fallback `ConversationLog::last(3)`.
|
||||
- Déduplication des conversation ids issus des tickets, ordre first-seen.
|
||||
- DTO camelCase et câblage Tauri.
|
||||
|
||||
- `c50622e` — `feat(workstate): UI des résumés de conversation (Lot C frontend)`
|
||||
- Types TS `ConversationWorkSummary` et previews.
|
||||
- Mock normalise `conversations: []`.
|
||||
- Panneau Work joint `tickets[].conversationId` vers `conversations[]` et affiche badge + preview compacte.
|
||||
|
||||
- `78500d8` — `chore(wip): état runtime .ideai (flux conversation live)`
|
||||
- Runtime isolé des commits feature.
|
||||
|
||||
- `6e1ba7e` — merge local dans `develop`.
|
||||
|
||||
## QA verte
|
||||
|
||||
Commandes QA finales validées:
|
||||
- `cargo fmt --all -- --check` OK.
|
||||
- `cargo test -p application --test workstate` OK, 21 passed, aucun warning Rust.
|
||||
- `cargo test -p app-tauri --test dto_agents` OK, 21 passed.
|
||||
- `cargo check -p app-tauri` OK.
|
||||
- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 2 files / 19 tests.
|
||||
- `cd frontend && npx tsc --noEmit` OK.
|
||||
|
||||
Warnings restants uniquement côté Vite sur options `esbuild` dépréciées/ignorées au profit de `oxc`, non bloquants.
|
||||
|
||||
## Décisions produit/techniques
|
||||
|
||||
- Source primaire: handoff conversationnel existant.
|
||||
- Fallback: derniers tours bornés du log, jamais le log brut complet.
|
||||
- Read-only et best-effort: les erreurs de preview ne bloquent pas `live/busy/tickets`.
|
||||
- Pas de nouvelle persistance, pas de mutation/réparation, pas de nouvelle action UX.
|
||||
|
||||
## Suite probable
|
||||
|
||||
Le prochain lot logique est Lot D: actions UX read/write contrôlées autour du panneau Work (ouvrir/rattacher cellule, voir conversation, arrêter agent, éventuellement copier résumé), à cadrer par Architect avant implémentation.
|
||||
62
.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md
Normal file
62
.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
name: checkpoint-workstate-delegation-queue-lot-b
|
||||
description: memory note checkpoint-workstate-delegation-queue-lot-b
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Checkpoint — Workstate delegation/queue snapshot Lot B
|
||||
|
||||
Date: 2026-06-20
|
||||
|
||||
## État final
|
||||
|
||||
Lot B `workstate delegation/queue snapshot` terminé, validé QA et mergé localement dans `develop`.
|
||||
|
||||
Branche finale:
|
||||
- `develop` @ `64c2c14` — merge local `feature/workstate-delegation-queue`.
|
||||
- `feature/workstate-delegation-queue` conservée.
|
||||
- Aucun push, aucune branche supprimée.
|
||||
- Working tree propre après décision Git.
|
||||
|
||||
## Commits créés
|
||||
|
||||
- `cc7d99a` — `feat(workstate): snapshot des délégations en file par agent (Lot B backend)`
|
||||
- `QueuedTicketSnapshot` + port read-only `AgentQueueSnapshot`.
|
||||
- Impl `InMemoryMailbox` snapshot FIFO sans cloner les senders.
|
||||
- `GetProjectWorkState.agents[].tickets` avec statut dérivé `inProgress/queued`, source `human/agent`, preview bornée.
|
||||
- DTO Tauri camelCase et wiring `AppState` avec le même mailbox partagé en deux ports.
|
||||
|
||||
- `c600604` — `feat(workstate): UI des délégations en file par agent (Lot B frontend)`
|
||||
- Types TS `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`.
|
||||
- Mock gateway normalise `tickets: []`.
|
||||
- Panneau Work affiche les tickets FIFO, source Human/Agent, preview, ticket court.
|
||||
- Refresh ajouté sur `delegationReady`.
|
||||
|
||||
- `5cb99fd` — `chore(wip): état runtime .ideai (flux conversation live, layouts)`
|
||||
- Runtime isolé des commits feature.
|
||||
|
||||
- `64c2c14` — merge local dans `develop`.
|
||||
|
||||
## QA verte
|
||||
|
||||
Commandes QA réelles validées:
|
||||
- `cargo fmt --all -- --check` OK.
|
||||
- `cargo test -p infrastructure mailbox --lib` OK, 13 passed.
|
||||
- `cargo test -p application --test workstate` OK, 12 passed.
|
||||
- `cargo test -p app-tauri --test dto_agents` OK, 20 passed.
|
||||
- `cargo check -p app-tauri` OK.
|
||||
- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 2 files / 17 tests.
|
||||
- `cd frontend && npx tsc --noEmit` OK.
|
||||
- QA ajoutée: `cargo test -p domain mailbox --lib` OK, 6 passed.
|
||||
|
||||
Warnings observés uniquement côté Vite sur options `esbuild` dépréciées/ignorées au profit de `oxc`, non bloquants.
|
||||
|
||||
## Décisions produit/techniques
|
||||
|
||||
- Les tickets `human` et `agent` sont inclus dans le snapshot; l'UI distingue explicitement `Human` vs agent/requester au lieu de tout appeler délégation agent.
|
||||
- Le statut est dérivé à la lecture: `inProgress` si le ticket courant correspond au `busy_state`, sinon `queued`.
|
||||
- Pas de nouvelle persistance, pas de lecture `log.jsonl`/handoff, pas d'événement `agentQueueChanged`, pas d'action UX d'annulation/résolution.
|
||||
|
||||
## Suite probable
|
||||
|
||||
Le prochain lot logique du chantier UX conversations/délégations est Lot C: résumés de conversations depuis log/handoff en lecture best-effort, ou Lot D actions UX selon priorité produit. Suivre le cycle Main: Architect -> Git -> DevBackend/DevFrontend -> QA -> Git.
|
||||
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: conversation-log-ls6-rotation-and-paginated-read
|
||||
description: Contrat figé de la rétention/rotation du transcript et de la lecture paginée riche (surface humaine).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
# LS6 — Rotation transcript + lecture paginée
|
||||
|
||||
Hygiène de persistance du `log.jsonl` + surface de lecture humaine paginée (viewer LS7).
|
||||
|
||||
## Invariant pivot INV-LS6
|
||||
La rotation n'élague/déplace JAMAIS un tour d'id ≥ `up_to` du handoff courant : le tour up_to + tous les postérieurs restent dans le segment ACTIF. Sans handoff (ou up_to nil) ⇒ aucune rotation. Garantit que `ConversationLog::read(since=up_to)` (fold incrémental) et la reprise restent corrects après rotation. Motif : `read(since)` cherche le curseur par position ; retirer up_to du fichier casserait le fold.
|
||||
|
||||
## Rotation
|
||||
- Stratégie = **archive segmentée** (`log.jsonl` actif + `log.1.jsonl`… anciens), PAS troncature/suppression (transcript = surface humaine riche à préserver).
|
||||
- Déclencheur **hors chemin chaud** : use case `RotateConversationLog` à la reprise/ouverture, best-effort, idempotent. L'`append` ne déclenche JAMAIS la rotation (zéro latence ajoutée).
|
||||
- Seuils sur segment actif : `ROTATE_AFTER_TURNS=500` OU `ROTATE_AFTER_BYTES=1MiB`. Backstop disque `MAX_ARCHIVE_SEGMENTS=20` (drop du plus ancien seulement). Politique pure domaine `rotation_plan(...) -> Skip|Archive{keep_from=up_to}`.
|
||||
- Mécanique : verrou conversation bref ; réécriture actif **atomique en dernier** (tmp+rename) ; aucun tour perdu (∪ actif+archives) ; pagination dédoublonne par TurnId (crash-safe).
|
||||
|
||||
## Lecture paginée (port ISP `ConversationArchive`, impl FsConversationLog)
|
||||
- `page(conversation, PageCursor{anchor,direction Forward|Backward}, limit)` → `TurnSlice{turns, has_more}`. limit clampé [1,200] défaut 50. Ordre chrono croissant. Archive-aware.
|
||||
- Use case `ReadConversationPage` → DTO humain `TurnPage{turns:[TurnView{id,at_ms,role,source,text COMPLET,text_len}], has_more, next_anchor}`. **Texte complet, non borné, non distillé** = surface HUMAINE.
|
||||
- `ConversationLog` (append/read/last) inchangé = vue segment actif (chemin chaud + dérivation).
|
||||
|
||||
## Frontière
|
||||
Lecture riche consommée UNIQUEMENT par la commande Tauri du viewer humain, jamais par compose_convention_file/handoff/agent. Rotation ne touche que le transcript brut, jamais le handoff/injection.
|
||||
|
||||
## Frontend
|
||||
LS6 = backend + commande Tauri `read_conversation_page`. Tout le React (viewer) = LS7.
|
||||
49
.ideai/memory/conversation-rotation-safety-design.md
Normal file
49
.ideai/memory/conversation-rotation-safety-design.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
name: conversation-rotation-safety-design
|
||||
description: memory note conversation-rotation-safety-design
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: conversation-rotation-safety-design
|
||||
description: Cadrage initial pour la compression/rotation automatique non interruptive des conversations d'agents.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# Conversation Rotation Safety Design
|
||||
|
||||
Objectif : reduire le cout token des conversations longues sans degrader la stabilite des agents ni les contacts inter-agents.
|
||||
|
||||
Socle existant a reutiliser :
|
||||
- log canonique `.ideai/conversations/<conversationId>/log.jsonl` ;
|
||||
- `handoff.md` incremental ;
|
||||
- injection du handoff au lancement/relaunch ;
|
||||
- `LeafCell.conversation_id` = id logique IdeA de paire, distinct du resumable provider ;
|
||||
- `providers.json` pour `(pair_conversation_id, provider_key) -> engine_session_id` ;
|
||||
- `InputMediator.busy_state(agent)` et FIFO par agent ;
|
||||
- `ask_agent` avec lock de tour par agent + wait-for graph anti-deadlock ;
|
||||
- attach/detach cellule deja modelise : la cellule est une vue, `TerminalSessions::rebind_agent_node` existe ;
|
||||
- frontend write-portal sait deja compter la saisie humaine et declarer `set_front_attached`.
|
||||
|
||||
Regle produit centrale : rotation automatique opportuniste, jamais interruptive. IdeA ne compacte/rote une session qu'a un point sur.
|
||||
|
||||
Conditions minimales avant rotation :
|
||||
- agent `Idle` (`InputMediator.busy_state == Idle`) ;
|
||||
- aucune delegation/ticket/reply en cours ;
|
||||
- aucun contact entrant en cours, sinon queue ou ancienne session jusqu'a bascule ;
|
||||
- checkpoint durable OK : prompt/response appendes au log + `handoff.md` a jour ;
|
||||
- cellule stable : pas d'attach/detach ou changement de layout en cours ;
|
||||
- utilisateur non en train d'ecrire : focus/buffer non vide/frappe recente/IME composition/prompt interactif detectable ;
|
||||
- nouvelle session demarree detached/background et prete avant bascule ;
|
||||
- bascule atomique routing logique + attachement cellule ; ancienne session retiree ensuite.
|
||||
|
||||
Etat cible conceptuel :
|
||||
`RotationRequested -> WaitForAgentIdle -> WaitForNoDelegation -> WaitForNoIncomingRoute -> WaitForCellStable -> WaitForUserInputClear -> Checkpointing -> StartReplacementDetached -> AttachReplacementToCell -> SwitchRouting -> RetireOldSession`.
|
||||
|
||||
Premier lot recommande : fondation non destructive.
|
||||
- Ajouter modeles/policy de `ConversationRotation` et raisons de blocage.
|
||||
- Ajouter query/use case d'evaluation qui retourne `Ready` ou `Blocked(reasons)` sans tuer ni relancer.
|
||||
- Exposer les signaux manquants de cellule/saisie depuis le frontend vers backend, d'abord comme etat consultable.
|
||||
- Publier des events de statut de rotation uniquement informatifs.
|
||||
- Ne pas implementer la bascule physique avant que les tests de surete soient verts.
|
||||
26
.ideai/memory/conversation-viewer-ls7-frontend.md
Normal file
26
.ideai/memory/conversation-viewer-ls7-frontend.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: conversation-viewer-ls7-frontend
|
||||
description: Contrat figé du viewer de transcript paginé (frontend pur, lecture seule, surface humaine).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
# LS7 — Viewer fil-par-paire (front pur)
|
||||
|
||||
Dernier lot fonctionnel du programme persistance. UI humaine qui rejoue le transcript par paire, consomme la commande LS6 `read_conversation_page`. Lecture seule, zéro impact backend/agent.
|
||||
|
||||
## Gateway / adapter / mock (patron WorkStateGateway)
|
||||
- Port `ConversationGateway.readPage(projectId, conversationId, {anchor?, direction?, limit?}) -> TurnPage`. Ajouter `conversation` à `Gateways`.
|
||||
- Types domaine : `TurnView{id, atMs, role: prompt|response|toolActivity, source: {kind:human}|{kind:agent,agentId}, text COMPLET, textLen}`, `TurnPage{turns, hasMore, nextAnchor}`.
|
||||
- Adapter `TauriConversationGateway` → invoke("read_conversation_page",{projectId,conversationId,cursor:{anchor,direction},limit}) + `conversationNormalization.ts` (mapper source Human/Agent calqué sur workStateNormalization). Mock avec pagination réelle + `_setTurns` pour tests offline.
|
||||
- Frontière : composant → useGateways().conversation → adapter → invoke. Jamais d'invoke direct.
|
||||
|
||||
## Modèle d'affichage
|
||||
- Clé = conversationId. Entrée = drill-down depuis le Work panel (`ConversationWorkSummary.conversationId` déjà énuméré ; rendre les lignes cliquables via prop `onOpenConversation`). Pas de nouvel appel « liste conversations ».
|
||||
- Rejeu chrono croissant ; distinction sobre User↔Agent vs Agent↔Agent (parties déduites des sources, noms résolus depuis l'inventaire agents) ; toolActivity atténué/replié.
|
||||
- Pagination : ouverture backward/None = dernière page (bas du fil) ; scroll-up = backward ancré sur le tour le plus ANCIEN du buffer (préfixe, dédup par id, hasMore gate) ; refresh queue = forward ancré sur le plus récent (manuel ou sur domain events delegationReady/orchestratorRequestProcessed).
|
||||
|
||||
## Intégration
|
||||
- État local `viewerConversationId` dans ProjectsView ; swap main-area (rend ConversationViewer à la place de LayoutGrid, PAS un nouveau kind backend) ; bouton retour ; reset au changement de projet. viewerConversationId=null ⇒ UI actuelle inchangée (non-régression terminal).
|
||||
|
||||
## Frontière / backend
|
||||
Lecture seule (read_conversation_page LS6, hors chemin chaud), jamais injecté dans un contexte agent. **Aucune part backend** : juste vérifier la signature de la commande exposée et aligner l'adapter si écart.
|
||||
50
.ideai/memory/feature-agent-skill-awareness-design.md
Normal file
50
.ideai/memory/feature-agent-skill-awareness-design.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
name: feature-agent-skill-awareness-design
|
||||
description: Design valide + decoupage de la feature « surfacer les skills assignes a un agent a la maniere MCP » (manifeste + idea_skill_read), a reprendre apres relance IdeA.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Feature : skills connus de l'agent « a la MCP »
|
||||
|
||||
Demarree le 2026-06-17. Branche Git **`feature/agent-skill-awareness`** (basee sur `develop` @ 8452333, qui contient deja le fix context-guard).
|
||||
|
||||
## ETAT 2026-06-17 (apres-midi) : slice backend T1->T5 FAIT, VERT, COMMITTE.
|
||||
DevBackend a livre T1->T5, QA a couvert (23 tests, 1212 passed). Commits sur `feature/agent-skill-awareness` :
|
||||
- `ab34363` feat(skill-awareness): T1->T5 (manifeste # Skills disponibles + outil MCP idea_skill_read, description+effective_description, use case ReadSkill, cablage state.rs, dump legacy conserve en mode sans-MCP).
|
||||
- `1a10d67` fix(test): compteur d'outils MCP 11->12 (une 2e assertion codee en dur dans `crates/app-tauri/src/state.rs` ~l.2813 que QA avait ratee ; QA n'avait corrige que `infrastructure/tests/mcp_server.rs`).
|
||||
- `e93a2c1` = cherry-pick du fix cold-start (cf. Bug 8 dans [[mcp-bridge-and-delegation-runtime-notes]]) — present aussi sur la branche dediee.
|
||||
`cargo test --workspace` VERT sur l'arbre combine. **L'orchestrateur a committe lui-meme** (l'agent Git etait injoignable a cause du Bug 8) — A FAIRE RELIRE/REBASER PAR GIT une fois le canal restaure.
|
||||
|
||||
**RESTE :** T6 (champ description dans creation/edition skill cote front) + T7 (e2e : rebuild AppImage avec le fix Bug 8, relance, agent neuf + skill assigne -> voit le manifeste + appelle idea_skill_read). Puis Git decide les merges (feature->develop ; fix cold-start->develop+main).
|
||||
|
||||
## AJOUT 2026-06-17 (soir) : brief « capacites IdeA » INCONDITIONNEL — VERT, COMMITTE (566bff4)
|
||||
Besoin utilisateur elargi : un agent neuf ignorait TOUT le perimetre IdeA (pas que les skills). Cas vecu : « agremente le contexte du projet » -> l'agent reinvente son propre systeme de contexte car le bloc `# Orchestration IdeA` ne parlait QUE de delegation. Fix (GO Architect, meme fonction pure `compose_convention_file` lifecycle.rs, zero port/entite) : ajout d'un brief « capacites IdeA » TOUJOURS present (decrit la CAPACITE, jamais le contenu -> survit a memoire/contexte vides), dans les 2 surfaces, AVANT le persona :
|
||||
- mode MCP : nomme les outils contexte (`idea_context_read`/`idea_context_propose` avec semantique single-writer global HONNETE : proposition sans target = enregistree pour validation, PAS appliquee ; `idea_update_context` pour le .md d'un agent), memoire partagee (`idea_memory_read`/`idea_memory_write`), skills (`idea_skill_read`/`idea_create_skill`). Martele « le contexte projet d'IdeA EST le contexte, n'improvise jamais ton propre fichier ».
|
||||
- mode non-MCP : MEMES concepts via fichiers `.ideai/` UNIQUEMENT (CONTEXT.md, memory/+MEMORY.md, skills .md), AUCUN nom d'outil `idea_*` (cloisonnement strict des 2 surfaces — sinon casse `mcp_prose_*`/`non_mcp_prose_*`).
|
||||
QA : 5 tests dedies (brief inconditionnel des 3 capacites a zero contenu ; single-writer honnete ; non-MCP sans outil ; cloisonnement ; ordre avant persona). `cargo test -p application` = 0 failed (lib 52, agent_lifecycle 59). 1 assert existant adapte (`mcp_mode_without_skills_omits_section`). Commit atomique **566bff4** sur `feature/agent-skill-awareness` (Git a laisse les .ideai/ runtime hors commit). Merge feature->develop differe par Git jusqu'a T6+T7 (unite d'integration unique).
|
||||
|
||||
## Historique
|
||||
Cycle initialement stoppe avant le code : la delegation MCP a wedge (Bug 7, puis Bug 8 residuel cf. [[mcp-bridge-and-delegation-runtime-notes]]).
|
||||
|
||||
## Besoin
|
||||
Un agent neuf dans un projet neuf ignore les skills qui lui sont assignes (cas vecu : `build-appimage` assigne mais ignore, tout reinvente). Objectif : l'agent est **automatiquement au courant** de ses skills, comme il l'est des outils MCP, sans que l'utilisateur ait a le lui dire.
|
||||
|
||||
## Decision produit (utilisateur, 2026-06-17) : approche « Manifeste + idea_skill_read »
|
||||
Diagnostic Architect : les skills sont DEJA injectes, mais (A) en VRAC (corps complet) en fin de `CLAUDE.md` -> lus comme de la doc, ignores ; (B) seulement au (re)lancement. On recadre « a la MCP » : affordances nommees+decrites en tete de contexte + chargement du corps a la demande.
|
||||
|
||||
- Bloc **« # Skills disponibles »** injecte JUSTE APRES le bloc « # Orchestration IdeA » (haute altitude), listant `**<name>** — <description>` ; prose imperative + renvoi a `idea_skill_read(name=…)`. Omis si zero skill.
|
||||
- Nouvel outil MCP **`idea_skill_read(name)`** read-only, miroir exact de `idea_context_read`/`idea_memory_read` ; resout par nom (scope projet puis global), renvoie `content_md` inline ; erreur typee si introuvable/ambigu.
|
||||
- Champ **`description: Option<String>`** sur l'entite `Skill` + helper `effective_description()` (fallback : 1ere ligne non vide du `content_md`, nettoyee du `#`). `#[serde(default)]` sur l'index (retro-compat `index.json` legacy obligatoire).
|
||||
- Mode **sans MCP** (`profile.mcp` absent) : conserver l'ancien dump du corps complet (decision 4.2(b), zero regression).
|
||||
|
||||
## Decoupage (commits atomiques par tache verte, decides par Git)
|
||||
- **T1 domaine** `crates/domain/src/skill.rs` : champ `description` + `effective_description()`.
|
||||
- **T2 store** `crates/infrastructure/src/store/skill.rs` (+ application/skill) : `description` dans IndexEntry (`serde(default)`), propage via `CreateSkill`.
|
||||
- **T3 convention-file** `crates/application/src/agent/lifecycle.rs` : section « # Skills disponibles » dans `compose_convention_file` (~l.2568, juste apres bloc Orchestration ~l.2586) ; alleger `resolve_skills` (~l.1202) pour n'utiliser que name+description (index, pas le corps) ; garder dump corps si non-MCP.
|
||||
- **T4 outil MCP** `crates/infrastructure/src/orchestrator/mcp/tools.rs` + commande `OrchestratorCommand::ReadSkill { name, requester }` + methode `read_skill` dans `OrchestratorService` + petit use case `ReadSkill` (compose le port `SkillStore` existant, AUCUN nouveau port).
|
||||
- **T5 cablage** `crates/app-tauri/src/state.rs` : injecter `ReadSkill` dans `OrchestratorService` (builder additif, comme le fix context-guard) + re-exports application (orchestrator/mod.rs + lib.rs).
|
||||
- **T6 front (differable)** `frontend` : champ description dans creation/edition de skill.
|
||||
- **T7 e2e** : rebuild AppImage + relance, agent neuf + skill assigne -> voit le manifeste + appelle `idea_skill_read`.
|
||||
|
||||
## Pieges (Architect)
|
||||
Retro-compat `index.json` (serde default) ; resolution de nom ambigue (projet d'abord, erreur typee sinon) ; un skill assigne en cours de session reste invisible jusqu'au relaunch (limite inherente, comme MCP) ; pas de nouveau port, domaine = +1 champ, composition root seul point de cablage. GO Architect.
|
||||
15
.ideai/memory/git-agent-push-access-gitea-ssh.md
Normal file
15
.ideai/memory/git-agent-push-access-gitea-ssh.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: git-agent-push-access-gitea-ssh
|
||||
description: memory note git-agent-push-access-gitea-ssh
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
**CORRECTION (2026-06-23) — annule et remplace la note précédente : l'agent Git N'A PLUS l'accès push. Il est de nouveau STRICTEMENT LOCAL, comme avant.**
|
||||
|
||||
- Git gère **100 % du dépôt local** : commits, branches, merges/rebases locaux. Rien d'autre.
|
||||
- **Aucune action sortante** : pas de `git push`, pas de PR distante, pas de publication de tags, pas de synchronisation remote. Ce périmètre n'est PAS dans ses responsabilités.
|
||||
- Toute synchro avec un remote (`origin` Gitea ou autre) reste **hors périmètre Git** et sous décision/validation explicite de l'utilisateur, exécutée hors agent Git.
|
||||
|
||||
Le contexte d'agent `git.md` reflète déjà ce retour au strictement-local (garde-fou « tu restes strictement local, pas de `git push` »).
|
||||
|
||||
Voir [[git-owns-commit-merge-decisions]] (Git tranche toute la topologie LOCALE du dépôt).
|
||||
22
.ideai/memory/handoff-ls5-summary-bound-and-llm-seam.md
Normal file
22
.ideai/memory/handoff-ls5-summary-bound-and-llm-seam.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: handoff-ls5-summary-bound-and-llm-seam
|
||||
description: Contrat figé de la borne de handoff (perf) et du seam résumeur LLM non activé.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
# LS5 — Borne handoff + seam LLM
|
||||
|
||||
Ferme le must-have perf du programme persistance : un `summary_md` non borné gonflait le contexte injecté à chaque lancement.
|
||||
|
||||
## Borne summary_md (déterministe, en CARACTÈRES, pas tokens)
|
||||
- **Par tour** : `render_turn` tronque le texte aplati à `TURN_LINE_MAX_CHARS=240` (infra summarizer). Corrige le trou réel (un gros Response = ligne géante).
|
||||
- **Totale** : fn PURE domaine `bound_handoff_summary(Handoff, max)` + `HANDOFF_SUMMARY_MAX_CHARS=4096`. Stratégie de dépassement = **troncature distillée** (garde objectif + lignes les plus récentes, drop oldest, frontière de ligne). JAMAIS re-fold compactant, JAMAIS rejet (n'bloque jamais l'append du log). `up_to`/`objective` jamais modifiés. Idempotent.
|
||||
- **Appliquée à l'écriture** (`RecordTurn` après `fold`, avant `save` → borne universelle quel que soit le résumeur) **ET défensivement à l'injection** (`resolve_handoff` avant compose → protège les `handoff.md` legacy non bornés, sans migration).
|
||||
|
||||
## Seam LLM : TRANCHÉ = « prêt mais NON activé »
|
||||
- Pas de vrai LLM en LS5 : `fold` tourne sur le chemin chaud (par tour, dans la délégation) → un appel réseau là = régression perf interdite. Défaut runtime reste `HeuristicHandoffSummarizer`.
|
||||
- Durcissement réel = la borne totale appliquée **dans RecordTurn (hors résumeur)** ⇒ un futur `LlmHandoffSummarizer` ne peut pas exploser le budget contexte.
|
||||
- Contrat futur adapter LLM (documenté) : déclenchement **froid/hors chemin** (reprise/rotation/débounce, jamais par tour) ; timeout + **fallback heuristique** (trait sans `Result`) ; sélection par flag, défaut heuristique. Trait `HandoffSummarizer::fold` inchangé.
|
||||
|
||||
## Frontière
|
||||
Transcript = `log.jsonl` (jamais injecté). `summary_md` = dérivé distillé, désormais borné à l'écriture et à l'injection. Conforme « surface agent = borné/distillé ».
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: idea-program-surface-separation-and-livestate
|
||||
description: Cadrage du programme post-skills : ce qui existe vs reste, et la règle de frontière surface-agent/humaine.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
État au cadrage (develop @ 827d477) :
|
||||
- **#2a handoff incrémental cross-profile = FAIT + CÂBLÉ** (lots P1–P8) : `domain/conversation_log.rs` (`Handoff`, `HandoffStore`, `HandoffSummarizer::fold` incrémental, `ProviderSessionStore`), `application/conversation/record.rs` (`RecordTurn` = append+fold), wiring `state.rs` (`AppRecordTurnProvider`), injection au lancement via `HandoffProvider` (P7). Résumeur = `HeuristicHandoffSummarizer`, seam async pour `LlmHandoffSummarizer` (P10) déjà en place. Reste : seam LLM + borne `summary_md`.
|
||||
- **#2b log canonique = FAIT** : `ConversationLog` append-only `.ideai/conversations/<id>/log.jsonl`, **jamais injecté à un agent** (doc domaine explicite). Reste : rétention/rotation + lecture paginée UI.
|
||||
- **#1 live-state agent = À CONSTRUIRE (cœur)** : aujourd'hui seul `GetProjectWorkState` (read-model HUMAIN dérivé, non durable). Construire un 4ᵉ store `live-state.json` (**gitignored, runtime**) : `domain/live_state.rs` (`LiveState`/`LiveEntry`/`WorkStatus`, port `LiveStateStore`, **keyed last-writer-wins, JAMAIS append**, champs bornés, `prune` TTL+max_n), use cases `UpdateLiveState`/`GetLiveStateLean`, adapter `FsLiveStateStore`, auto-update depuis `orchestrator/service.rs` (ask→Working, reply→Done), injection bornée `# État du projet` au lancement + outils MCP `idea_workstate_read`/`idea_workstate_set`. Ne stocke QUE le non-re-dérivable (intent/progrès/dernière délégation) ; busy/queue restent calculés.
|
||||
|
||||
**Règle de frontière gravée (testable)** : Surface AGENT = borné/distillé/pointeur uniquement (capacités, pointeurs mémoire, affordances skills, handoff `summary_md`, live-state lean). Surface HUMAINE = riche (ProjectWorkStatePanel, viewer fil-par-paire qui rejoue le log). **Transcript / append-only INTERDIT d'injection dans un contexte agent** ; seul un dérivé distillé franchit. 4 stores distincts : `memory/` (savoir stable, versionné) · `handoff.md` (reprise par fil) · `log.jsonl` (transcript humain) · `live-state.json` (coordination transitoire maigre).
|
||||
|
||||
Découpage : LS0 doc baseline → LS1 domaine live-state → LS2 store+usecases → LS3 auto-update délégation → LS4 injection+MCP (clôt cold-start) ; LS5 seam LLM+borne handoff, LS6 rétention log+lecture UI (parallèles) ; LS7 UX fil-par-paire (front pur) ; LS8 doc clôture.
|
||||
15
.ideai/memory/live-state-persistence-program-closed-ls8.md
Normal file
15
.ideai/memory/live-state-persistence-program-closed-ls8.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: live-state-persistence-program-closed-ls8
|
||||
description: Le programme live-state + persistance conversationnelle est livré et documenté ; pointe la doc de clôture et les points restés ouverts.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
Programme **live-state / persistance conversationnelle CLOS** (LS1→LS7 livrés @ fd7adbb ; LS8 = doc de clôture).
|
||||
|
||||
**Doc de référence** : `docs/LS8-live-state-persistence-closure.md` + `ARCHITECTURE.md` §21 (cartographie des 4 stores) ; §19 retitré « LIVRÉ » ; §14.1 et §18.5 corrigés (catalogue MCP **14**).
|
||||
|
||||
**4 stores disjoints** : `.ideai/memory/` (savoir stable, versionné) · `handoff.md` (reprise distillée bornée ≤4096) · `log.jsonl` (transcript humain riche, JAMAIS injecté) · `live-state.json` (coordination maigre runtime, gitignoré). Surface AGENT bornée/distillée/pointeur ; surface HUMAINE riche ; seul le handoff distillé franchit vers un contexte agent.
|
||||
|
||||
**Restant ouvert** : (1) activation seam LLM `LlmHandoffSummarizer` (non activé, défaut heuristique, contrat ADR LS5) ; (2) sweep périodique de rotation (idempotent, non câblé) ; (3) **discordance D19-4 ↔ .gitignore** : `.ideai/conversations/` devait être gitignoré mais est suivi — à trancher Git/Main ; (4) intégration MCP e2e UX ; (5) multi-fenêtres registre sessions ; (6) auto-update mémoire/contexte en cours de session.
|
||||
|
||||
**Contrainte d'écriture** : un agent lancé en run-dir isolé (`.ideai/run/<id>/`) **ne peut pas écrire l'arbre du project root** (ARCHITECTURE.md, docs/) — les modifs de doc passent par Git (write+commit authority).
|
||||
21
.ideai/memory/live-state-reboot-reconciliation-design.md
Normal file
21
.ideai/memory/live-state-reboot-reconciliation-design.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: live-state-reboot-reconciliation-design
|
||||
description: memory note live-state-reboot-reconciliation-design
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: Réconciliation du live-state au reboot (ReconcileLiveState)
|
||||
type: reference
|
||||
description: Décision d'architecture pour réconcilier les lignes live-state fantômes au redémarrage — seam, source de vérité, contrat de statut et préservation du self-only.
|
||||
---
|
||||
**Problème** : au reboot, `.ideai/live-state.json` n'est pas réconcilié → agents fantômes `working/waiting/blocked` alors que leurs sessions sont mortes. `prune` (TTL 6h) ne couvre pas le fantôme récent.
|
||||
|
||||
**Décision (cadrage Architect, 2026-06-23)** :
|
||||
- **Seam** : étape best-effort `reconcile_live_state` dans la chaîne `open_project` (commands.rs), juste après `reconcile_layouts`. Pas d'app-boot global (live-state est par-projet). Pas d'outil MCP. Justification : un crash ne déclenche jamais `close_project`, donc **open est le seul filet fiable**.
|
||||
- **Source de vérité** : le registre de sessions vivantes `LiveSessions` / trait `LiveAgentRegistry` (`application/src/terminal/registry.rs`) — la *même* vérité que `GetProjectWorkState`. Prédicat orphelin = `status ∈ {working,waiting,blocked}` ET `!is_agent_live`. Race-safe : tourne avant toute relance ; LWW gagne via `updated_at_ms` frais.
|
||||
- **Contrat** : orphelin → `idle` (pas de nouveau statut, évite le ripple DTO/front). Garder `intent` (trace), vider `ticket` + `last_delegation` (rendez-vous mort, tracé ailleurs dans log/handoff), `progress` = marqueur stale, `updated_at_ms` frais.
|
||||
- **Pas de nouveau port.** Use case applicative `ReconcileLiveState` (compose `LiveStateStore` + `LiveAgentRegistry` + `Clock`) + transformation **pure** `LiveState::reconcile_orphans(is_live, now)` dans `domain/src/live_state.rs`. Provider par-root côté app-tauri (calqué sur `LiveStateProvider`/`live_state_for`).
|
||||
- **Self-only de `idea_workstate_set`** : invariant **de la surface MCP**, pas du store. `set_workstate` lie `agent_id`=identité handshake ; `UpdateLiveState`/`LiveStateStore` sont identity-agnostic. La réconciliation est un **acte système** dans le composition root qui appelle le port directement, **jamais** via `OrchestratorCommand::SetWorkState` → self-only préservé par construction. Même split que `snapshot_running_agents`/`reconcile_layouts`.
|
||||
|
||||
Fichiers probables : `domain/src/live_state.rs`, `application/src/workstate/{mod.rs,reconcile.rs}`, `app-tauri/src/{lib.rs,state.rs,commands.rs}`. Voir [[live-state-persistence-program-closed-ls8]], [[handoff-ls5-summary-bound-and-llm-seam]].
|
||||
47
.ideai/memory/mcp-e2e-findings-reply-wedge-phantom-busy.md
Normal file
47
.ideai/memory/mcp-e2e-findings-reply-wedge-phantom-busy.md
Normal file
@ -0,0 +1,47 @@
|
||||
---
|
||||
name: mcp-e2e-findings-reply-wedge-phantom-busy
|
||||
description: memory note mcp-e2e-findings-reply-wedge-phantom-busy
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: mcp-e2e-findings-reply-wedge-phantom-busy
|
||||
description: Findings validation MCP e2e live (AppImage 0.3.0) — transport/rendez-vous SAINS ; un seul vrai défaut = wedge si l'agent délégué n'appelle pas idea_reply (pas de timeout serveur).
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Findings validation MCP e2e (live, AppImage 0.3.0) — 2026-06-22
|
||||
|
||||
Contexte : validation réelle du parcours inter-agents (cf. [[resume-after-appimage-rebuild-mcp-e2e]]). Méthode = Main observe en externe (Bash sur `ss -xp` + transcripts `~/.claude/projects/<run-dir-encodé>/*.jsonl`), l'utilisateur déclenche dans l'UI ; on ne valide PAS via `idea_ask_agent`.
|
||||
|
||||
## Scénario 1 — délégation vers agent FROID + cycle complet : VERDICT
|
||||
- ✅ **Réveil à froid** : déléguer vers un agent arrêté relance son pont (`mcp-server --requester <id>` réapparaît, socket ESTAB recompte).
|
||||
- ✅ **1er tour non perdu** : tâche livrée intacte = 1er `user` `[IdeA · tâche de <agent> · ticket <uuid>] …`.
|
||||
- ✅ **Chemin positif `idea_reply`** : quand l'agent cible appelle `idea_reply(ticket,result)`, le bridge répond `reply ... delivered`, le **demandeur reçoit le `tool_result` et reprend la main** (vérifié : DevBackend reçoit `pong` puis continue).
|
||||
- ✅ **Purge live-state** : dès l'`idea_reply`, `idea_workstate` repasse le cible de `working` → `done`, intent vidé.
|
||||
|
||||
➡️ **Transport + protocole de rendez-vous = SAINS.** Bugs historiques 3/4/5 OK sur ce chemin.
|
||||
|
||||
## LE défaut restant (racine unique) — wedge sans `idea_reply`
|
||||
Si l'agent délégué **termine son tour sans appeler `idea_reply`** (ex. tâche triviale « réponds pong » → il répond `pong` en **prose** et s'arrête) :
|
||||
- le **demandeur wedge indéfiniment** dans `idea_ask_agent` (transcript demandeur figé sur le `tool_use` idea_ask_agent, aucun `tool_result`) ;
|
||||
- la **cible reste `working`** sur le ticket périmé dans la live-state ;
|
||||
- **aucun timeout ni récupération** côté serveur. Une interruption utilisateur du `ask` libère le demandeur mais **ne purge pas** le `working` de la cible (purge seulement à l'`idea_reply`).
|
||||
|
||||
➡️ Le « Busy fantôme » n'est PAS un bug d'état indépendant : c'est la **conséquence** de l'absence d'`idea_reply`. Une seule cause à traiter.
|
||||
|
||||
### Pistes correctif (à arbitrer Architect)
|
||||
1. **Durcir contextes agents** : « toute tâche déléguée se termine IMPÉRATIVEMENT par `idea_reply`, même triviale » (cheap, attaque la cause comportementale).
|
||||
2. **Garde-fou serveur (rendez-vous)** : timeout sur l'attente `idea_ask_agent` → renvoyer au demandeur un résultat d'échec/relance explicite + purger le `working` de la cible (idempotent). Touche le contrat rendez-vous + cycle de vie live-state = domaine Architect. Voir [[idea-program-surface-separation-and-livestate]], [[mcp-bridge-and-delegation-runtime-notes]].
|
||||
|
||||
## Diagnostic / repro (rappels)
|
||||
- Ponts : `pgrep -af 'mcp-server --endpoint'` (1 requester = 2 process) ; `ss -xp | grep -c idea-mcp`.
|
||||
- Reply réel = `tool_use` `idea_reply` dans le transcript cible → tool_result `reply ... delivered` (⚠️ ne pas confondre avec les mentions `idea_reply` du CLAUDE.md injecté via attachments).
|
||||
- Wedge demandeur = transcript demandeur s'arrête sur `tool_use` `idea_ask_agent` sans `tool_result`.
|
||||
- ⚠️ Horodatages transcripts = **UTC** ; `ls %H:%M:%S` masque la date → comparer avec `date` + mtime réel (`ls -t`).
|
||||
|
||||
## Restant à tester (scénarios e2e suivants)
|
||||
- Délégation vers agent BACKGROUND (node_id None) — même protocole, vérifier pas de 1er tour perdu.
|
||||
- `ask` sans réponse ne wedge pas durablement la CONNEXION du demandeur (vs juste l'appel) — Bug 5 côté transport.
|
||||
- Profils Claude/Codex utilisent MCP par défaut ; fallback fichier+prose cohérent.
|
||||
- Observabilité UI des délégations/replies (aujourd'hui opaque) — sujet UX.
|
||||
45
.ideai/memory/mcp-functional-test-plan-2026-06-24.md
Normal file
45
.ideai/memory/mcp-functional-test-plan-2026-06-24.md
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
name: mcp-functional-test-plan-2026-06-24
|
||||
description: memory note mcp-functional-test-plan-2026-06-24
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: mcp-functional-test-plan-2026-06-24
|
||||
description: Plan de test FONCTIONNEL du rendez-vous inter-agents MCP (trivial→tordu), à exécuter après rebuild develop HEAD ; méthode, ordre, setup IdeA et critères de réussite.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Plan de test fonctionnel MCP inter-agents (2026-06-24, run a6ced819)
|
||||
|
||||
Objectif : état des lieux par tests **fonctionnels** (pas unitaires) du rendez-vous `idea_ask_agent` ⇄ `idea_reply`, du trivial au plus tordu. On lance dans l'ordre, on **s'arrête au 1er blocage**, on corrige (cycle Architect→Git→Dev→QA), rebuild+relance, puis on **reprend de T1**. Contexte/historique : [[mcp-e2e-findings-reply-wedge-phantom-busy]], [[backstop-fires-on-intra-task-turn-rootcause]], [[rendezvous-no-reply-backstop-design]], [[rendezvous-600s-cap-too-short-heavy-tasks]], [[reconcile-live-state-implemented-feature-branch]].
|
||||
|
||||
## Contexte binaire au moment du plan
|
||||
- AppImage qui tournait = build **23/06 19:42**, PÉRIMÉ (antérieur à `c6f0f86` encode_cwd, `744de20` backstop no-reply, `886bb0d`/`1efe2f1` reconcile). Décision utilisateur : **rebuild develop HEAD `1efe2f1` puis relance** avant de tester.
|
||||
- Règle structurelle : **binaire qui tourne = AppImage**. Tout fix backend ⇒ rebuild + relance d'IdeA (tue Main). Les tests s'enchaînent SANS relance *à l'intérieur d'une même version* ; chaque cycle de fix = 1 relance.
|
||||
|
||||
## Méthode
|
||||
- **Main est le demandeur** (`idea_ask_agent`) = test fidèle de « si je donne un message à un agent, il me répond ».
|
||||
- Doublé d'**observation externe Bash** : ponts `ss -xp | grep idea-mcp` et `pgrep -af mcp-server` ; transcripts `~/.claude/projects/<run-dir-encodé>/*.jsonl` (horodatages **UTC** ; reply réel = `tool_use` idea_reply→tool_result `reply … delivered` ; wedge demandeur = transcript figé sur `tool_use` idea_ask_agent sans tool_result) ; live-state via `idea_workstate_read`.
|
||||
- Risque résiduel : un wedge non borné fige le tour de Main → l'utilisateur peut interrompre.
|
||||
|
||||
## Setup IdeA à mettre en place avant T1 (consigne utilisateur)
|
||||
1. Relancer IdeA, ouvrir le projet IdeA, **Main visible** (le demandeur).
|
||||
2. Garder **2–3 cellules libres visibles** pour qu'on observe à l'œil les délégations (Main y placera des cibles visibles, ou lance-toi QA + DevFrontend dans des cellules visibles).
|
||||
3. **Laisser Architect, DevBackend, Git ARRÊTÉS (froids)** : T3 testera le réveil à froid ; T9 utilisera Architect+DevBackend froids.
|
||||
4. Cibles chaudes pour T1/T2 : Main les réchauffe lui-même (launch) — pas d'action requise.
|
||||
|
||||
## Ordre des tests (trivial → tordu)
|
||||
- **T1 — Visibilité outils MCP.** Un agent lancé par IdeA voit ses outils sans action manuelle. Vérif : pont ESTAB (`ss`) + 2 process mcp-server par requester.
|
||||
- **T2 — Cible CHAUDE + idea_reply trivial.** Main→ask(agent chaud, « réponds pong via idea_reply »). Attendu : réponse inline `pong`, temps borné, workstate working→done purgé.
|
||||
- **T3 — Réveil à FROID + idea_reply.** Cible arrêtée. Attendu : pont relancé (nouveau mcp-server), **1er tour non perdu** (1er user `[IdeA · tâche…]` intact), reply remonte.
|
||||
- **T4 — Cible BACKGROUND (node_id None).** Délégation vers agent sans cellule visible. Attendu : même protocole, pas de 1er tour perdu (historiquement non testé).
|
||||
- **T5 — NO-REPLY (cible répond en prose, pas d'idea_reply).** Attendu backstop : demandeur libéré en temps borné avec `TargetReturnedNoReply` (retryable), **pas de Busy fantôme** (workstate purgé). DÉFAUT historique #1.
|
||||
- **T6 — Délégation MULTI-ÉTAPES puis idea_reply.** Cible lit plusieurs fichiers / lance une cmd PUIS idea_reply. Attendu : **pas de libération prématurée** pendant le travail, rapport final livré. PIÈGE turn-watcher (cause racine : backstop tirait au 1er tour interne ~3 s).
|
||||
- **T7 — Tâche LOURDE > plafond** (impl + `cargo test`/clippy). Attendu : extension sur signe de vie OU message clair « cible active, plafond atteint » (≠ faux `-32001`) ; idea_reply tardif non perdu. Plafond `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS` (défaut 600000).
|
||||
- **T8 — Délégations PARALLÈLES** (2 cibles en 1 tour Main). Attendu : rendez-vous multiples simultanés OK, multiplexage pont, 2 réponses distinctes.
|
||||
- **T9 — TRANSITIVE A→B→C** (Main→Architect, Architect délègue à DevBackend puis reply à Main). Attendu : rendez-vous imbriqués, 2 attentes simultanées sans deadlock. Cas le plus tordu.
|
||||
- **T10 — INTERRUPTION/annulation + reconcile reboot.** (a) Main interrompt un ask en vol → `working` cible purgé, pas de cascade, idea_reply tardif non-livrable proprement. (b) Au reboot, orphelins status∈{working,waiting,blocked} & session morte → `idle` + marker `STALE_AT_RESTART_MARKER` (use case ReconcileLiveState, cf. [[reconcile-live-state-implemented-feature-branch]]).
|
||||
|
||||
## Critère de fin
|
||||
Tous les T1→T10 verts avec preuve réelle (réponse inline + observation externe cohérente). Sinon : 1er rouge = blocage à corriger, puis reprise de T1.
|
||||
@ -0,0 +1,33 @@
|
||||
---
|
||||
name: mcp-functional-tests-t1-t9-green-live-2026-06-24
|
||||
description: memory note mcp-functional-tests-t1-t9-green-live-2026-06-24
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: MCP tests fonctionnels T1→T9 VERTS en live (2026-06-24, AppImage 12:53 HEAD 1efe2f1 + fix T7 non committé)
|
||||
type: project
|
||||
description: Passe complète T1→T9 verte en conditions réelles sur AppImage du 24/06 12:53 ; T5 (backstop no-reply) et T7 (watchdog inactivité >600s) validés live ; reste T10 (interruption + reconcile reboot) qui exige action utilisateur.
|
||||
---
|
||||
# Tests fonctionnels MCP rendez-vous — T1→T9 VERTS live (2026-06-24)
|
||||
|
||||
Run a6ced819, plan [[mcp-functional-test-plan-2026-06-24]]. AppImage **rebuild 12:53 depuis l'arbre de travail** (HEAD `1efe2f1` + le fix T7 watchdog **non committé** mais présent dans les 11 fichiers modifiés du git status → embarqué dans le binaire). Méthode : Main demandeur + observation externe (ss/pgrep/transcripts UTC/workstate).
|
||||
|
||||
## Résultats (preuve réelle)
|
||||
- **T1 visibilité MCP** ✅ : agents chauds = pont ESTAB + 2 mcp-server (app-tauri parent + AppImage) chacun.
|
||||
- **T2 cible chaude + reply trivial** ✅ : DevFrontend → `pong` inline, reply delivered 10:56:51 UTC, workstate done (pas de Busy fantôme).
|
||||
- **T3 réveil à FROID** ✅ : Git froid → `pong-cold` ; nouveau pont spawné, **1er tour `[IdeA·tâche]` intact** (régresseur encode_cwd résolu), reply delivered 10:57:34.
|
||||
- **T4 cible BACKGROUND (node_id None)** ✅ : Git background → `pong-bg`, reply delivered 10:58:14.
|
||||
- **T5 NO-REPLY backstop** ✅ (défaut historique #1 résolu) : cible répond en prose sans idea_reply → demandeur libéré en ~20 s avec erreur retryable « returned to its prompt without calling idea_reply », workstate cible done (PAS de Busy fantôme).
|
||||
- **T6 multi-étapes** ✅ (piège turn-watcher résolu) : DevFrontend lit fichiers + git log PUIS idea_reply ; pas de libération prématurée ; données réelles (142 fichiers, hashes corrects).
|
||||
- **T7 tâche LOURDE >600s** ✅ (défaut historique #2 résolu, validation live du fix watchdog) : QA charge réelle continue 11:00:46→11:12:44 UTC = **~11m58s**, cargo clean+build release+test (1632 passed/0)+clippy+9 relances ; rendez-vous **NON expiré à 600s**, idea_reply accepté au-delà du plafond (aucun « no pending request »), reply delivered 11:13:21.
|
||||
- **T8 PARALLÈLE** ✅ : 2 asks même tour (DevFrontend `alpha-8` + Git `beta-8`), réponses distinctes, multiplexage pont OK.
|
||||
- **T9 TRANSITIF A→B→C** ✅ : Main→Architect→DevBackend (les 2 froids) → `relay:gamma-9:done` ; rendez-vous imbriqués + double réveil à froid sans deadlock ; DevBackend reply delivered 11:14:32.
|
||||
|
||||
## Reste : T10 (exige action utilisateur)
|
||||
- **T10a interruption** : Main interrompt un ask en vol → attendu : workstate cible purgé, pas de cascade, idea_reply tardif proprement non-livrable. NÉCESSITE que l'utilisateur appuie sur interrupt pendant un ask wedgé.
|
||||
- **T10b reconcile reboot** : orphelin status∈{working,waiting,blocked} + session morte → au reboot, use case ReconcileLiveState le passe à idle + `STALE_AT_RESTART_MARKER` (cf. [[reconcile-live-state-implemented-feature-branch]]). NÉCESSITE un reboot d'IdeA (ferme Main).
|
||||
|
||||
## Suite recommandée
|
||||
1. Faire committer le fix T7 par Git (encore non committé, cf. [[git-owns-commit-merge-decisions]] + [[rendezvous-600s-cap-too-short-heavy-tasks]]).
|
||||
2. Exécuter T10 avec l'utilisateur (interrupt + reboot).
|
||||
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: mcp-t10a-harness-interrupt-does-not-cancel-rendezvous
|
||||
description: memory note mcp-t10a-harness-interrupt-does-not-cancel-rendezvous
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: T10a — l'interrupt harness du demandeur n'annule PAS le rendez-vous backend (reply tardif accepté dans le vide)
|
||||
type: reference
|
||||
description: Finding T10a (2026-06-24) — interrompre Main (demandeur) au niveau Claude Code ne propage aucune annulation à IdeA ; la cible finit, son idea_reply est « delivered » dans un demandeur abandonné, et la live-state du demandeur reste working.
|
||||
---
|
||||
# Finding T10a (2026-06-24, run a6ced819)
|
||||
|
||||
## Setup
|
||||
Main → `idea_ask_agent(DevFrontend, "travaille ~90s puis idea_reply late-10a")`. Pendant l'attente, l'utilisateur **interrompt Main** (Échap harness Claude Code). Le tool use `idea_ask_agent` est rejeté côté harness.
|
||||
|
||||
## Observé
|
||||
- DevFrontend a fini ses ~95 s de travail réel (11:20:39→11:22:14 UTC) puis `idea_reply("late-10a")` → **ACCEPTÉ « delivered »** à 11:22:18 (PAS « no pending request »).
|
||||
- DevFrontend workstate = **done** (pas d'orphelin côté cible). Pas de cascade.
|
||||
- **Main (demandeur) workstate resté `working`** : l'interrupt a coupé le tour de Main avant son nettoyage ; le chemin interrupt ne réconcilie pas la live-state du demandeur.
|
||||
|
||||
## Interprétation
|
||||
L'interrupt Claude Code annule le **tool call local** mais le **process Main survit** → la socket MCP (mcp-server du demandeur) reste connectée → IdeA ne reçoit aucun signal d'annulation → le rendez-vous reste pending → le reply tardif de la cible est livré dans un demandeur qui a déjà abandonné (perdu pour Main mais rapporté « delivered »).
|
||||
|
||||
## Conséquence
|
||||
- Le T10a du plan (« annulation en vol → reply tardif non-livrable proprement ») **n'est pas exerçable via interrupt harness** tant que le process demandeur survit. Une vraie annulation exigerait : tuer le process demandeur (drop socket) OU un cancel protocolaire JSON-RPC.
|
||||
- Comportement néanmoins bénin : pas de wedge, pas de cascade, cible réconciliée (done). Seuls écarts : (a) reply tardif « delivered » au lieu de rejeté ; (b) live-state du demandeur laissée working par le chemin interrupt (mais reconcile-au-reboot la rattraperait, cf. [[reconcile-live-state-implemented-feature-branch]]).
|
||||
|
||||
Lié à [[mcp-functional-tests-t1-t9-green-live-2026-06-24]], [[mcp-functional-test-plan-2026-06-24]].
|
||||
24
.ideai/memory/mcp-t10b-pending-reboot-verification.md
Normal file
24
.ideai/memory/mcp-t10b-pending-reboot-verification.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: mcp-t10b-pending-reboot-verification
|
||||
description: memory note mcp-t10b-pending-reboot-verification
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: T10b PASS — reconcile au reboot vérifié, inclut l'orchestrateur (2026-06-24)
|
||||
type: project
|
||||
description: T10b validé live après reboot. ReconcileLiveState ramène l'orphelin Main working→idle en conservant l'intent et purgeant ticket/lastDelegation. Couvre l'orchestrateur. Reste : faire committer par Git les fix non committés (T7 watchdog + reconcile).
|
||||
---
|
||||
# T10b — PASS (reboot reconcile vérifié)
|
||||
|
||||
## Verdict
|
||||
Orphelin armé = Main `working` intent "T10b-ORPHAN-MAIN". Après reboot (run a6ced819), `idea_workstate_read` montre Main :
|
||||
- `status: idle` ✅ (était working)
|
||||
- `intent` conservé ✅
|
||||
- `ticket` purgé ✅ / `lastDelegation` purgé ✅
|
||||
|
||||
Signature `STALE_AT_RESTART_MARKER` complète. **ReconcileLiveState valide ET inclut l'orchestrateur** (Main n'a PAS été exclu malgré la crainte du design open_project). Champ `progress` non exposé par workstate_read mais transition working→idle+intent retenu = concluante.
|
||||
|
||||
## Suite du plan
|
||||
T1→T10 du plan [[mcp-functional-test-plan-2026-06-24]] terminés (T1→T9 verts cf. [[mcp-functional-tests-t1-t9-green-live-2026-06-24]], T10a finding [[mcp-t10a-harness-interrupt-does-not-cancel-rendezvous]], T10b PASS ici).
|
||||
**RESTE : faire committer par Git les fix non committés (T7 backstop/watchdog + reconcile live-state).** cf. [[git-owns-commit-merge-decisions]].
|
||||
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: mcp-t7-backstop-600s-reply-rejected-livestate-stale
|
||||
description: memory note mcp-t7-backstop-600s-reply-rejected-livestate-stale
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: MCP T7b/T7e — backstop 600s rejette le reply tardif et laisse le live-state worker non réconcilié
|
||||
type: reference
|
||||
description: Finding fonctionnel des tests MCP T7b/T7e (2026-06-24) — un idea_reply après dépassement de la fenêtre 600s est refusé « no pending request », et le ticket du worker reste working dans le live-state.
|
||||
---
|
||||
|
||||
# Finding — tests MCP T7 (2026-06-24, QA)
|
||||
|
||||
## Contexte
|
||||
Série de tests du rendez-vous `idea_ask_agent ⇄ idea_reply` et du backstop no-reply (cap ~600s), demandés par Main :
|
||||
- **T7d** (debug build x3, avant-plan) : cumul ~3m49s < 600s → reply OK.
|
||||
- **T7b** (9 étapes `sleep 82`, entrelacées) : le harness BLOQUE tout `sleep` avant-plan → exécutées en run_in_background ; reply OK (tardif mais < fenêtre côté ticket T7b).
|
||||
- **T7e** (release build répété, avant-plan, sans sleep ni bg) : 7 cycles, cumul réel **08:01:14 → 08:14:21 = 13 min 07 s**, franchit largement 600s.
|
||||
|
||||
## Défaut observé
|
||||
À la fin de T7e (et de T7b, terminé au même tour, après >600s d'occupation continue), les DEUX `idea_reply` (tickets `4613262f…` T7b et `d2398b22…` T7e) sont refusés :
|
||||
|
||||
```
|
||||
invalid input: no pending request to reply to for agent aefdbd61-…
|
||||
```
|
||||
|
||||
→ Le backstop a libéré le **demandeur** (Main) côté rendez-vous après dépassement du cap, ce qui est le comportement voulu. MAIS :
|
||||
1. Le **résultat réel** (builds OK, horodatages non falsifiés) ne peut **plus être délivré** : il est perdu pour le demandeur.
|
||||
2. `idea_workstate_read` montre encore le worker QA `status=working ticket=d2398b22…` **après** le rejet du reply → le live-state du **worker** n'est PAS réconcilié quand le backstop ferme le rendez-vous côté demandeur. Incohérence demandeur (libéré) vs worker (toujours working).
|
||||
|
||||
## Conséquence / piste
|
||||
- Le cap 600s du rendez-vous reste trop court pour des tâches lourdes légitimes (cf. [[rendezvous-600s-cap-too-short-heavy-tasks]]) : ici un travail réel et honnête >13 min est « jeté ».
|
||||
- À corriger : quand le backstop ferme un rendez-vous, soit (a) accepter encore un reply tardif (le router vers le demandeur même après timeout / le persister), soit (b) réconcilier le live-state du worker (passer son ticket à done/abandon) pour ne pas laisser un `working` fantôme. Voir [[backstop-fires-on-intra-task-turn-rootcause]] et [[rendezvous-no-reply-backstop-design]].
|
||||
|
||||
## Reproductible
|
||||
Charge avant-plan réelle (`cargo build --release --workspace` en boucle clean) dépassant 600s, puis tenter `idea_reply` sur le ticket d'origine → rejet systématique.
|
||||
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: reconcile-live-state-implemented-feature-branch
|
||||
description: memory note reconcile-live-state-implemented-feature-branch
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
title: Réconciliation du live-state au reboot — implémentée (feature/reconcile-live-state)
|
||||
type: project
|
||||
description: Le chantier ReconcileLiveState (3 lots) est implémenté et vert (1623 tests), non committé ; détail des fichiers et de l'arbitrage project_id/provider.
|
||||
---
|
||||
|
||||
Implémenté le 2026-06-23 sur `feature/reconcile-live-state` (ticket Main a7fcb5e4, dont le rendez-vous `idea_reply` a expiré pendant le build — résultat consigné ici). **Non committé** (Git tranche). Cadrage : [[live-state-reboot-reconciliation-design]].
|
||||
|
||||
**Résultats réels** : `cargo test --workspace` = **1623 passed, 0 failed**. `cargo clippy --workspace --all-targets` = 0 erreur, aucun warning sur le code ajouté (warnings restants tous pré-existants).
|
||||
|
||||
**Lot 1 (domaine, `crates/domain/src/live_state.rs`)** : `LiveState::reconcile_orphans(is_live, now_ms) -> Vec<LiveEntry>` pure (ne mute pas self). Orphelin = status ∈ {Working,Waiting,Blocked} ET !is_live → réécrit status=Idle, intent gardé, ticket=None, last_delegation=None, progress=Some(`STALE_AT_RESTART_MARKER`), updated_at_ms=now. Const pub `STALE_AT_RESTART_MARKER = "(stale — session not running at restart)"`. +4 tests.
|
||||
|
||||
**Lot 2 (application, nouveau `crates/application/src/workstate/reconcile.rs`)** : use case `ReconcileLiveState` sur `LiveStateStore`+`LiveAgentRegistry`(ISP)+`Clock` ; `execute(ReconcileLiveStateInput{project_id})` charge → reconcile_orphans → upsert par ligne. +3 tests. Exporté via workstate/mod.rs + application/lib.rs.
|
||||
|
||||
**Lot 3 (app-tauri, `state.rs`+`commands.rs`)** : provider par-root `AppReconcileLiveState` (résout root via `ProjectStore::load_project`, `FsLiveStateStore::new(&root)`, registre = `LiveSessions` existant). Champ `reconcile_live_state` sur AppState. Hook best-effort dans `open_project` après `reconcile_layouts`. **Acte système** : écrit le port `LiveStateStore` directement, jamais via `OrchestratorCommand::SetWorkState` → self-only de `idea_workstate_set` préservé par construction.
|
||||
|
||||
**Arbitrage à valider QA/Architect** : la résolution project_id→root se fait au composition root (le store est root-bound), donc `ReconcileLiveState::execute` reçoit bien `ReconcileLiveStateInput{project_id}` (conforme Lot 2 + hook Lot 3 à la lettre) mais ne relit pas project_id dans le flux pur (`_input`). Variante sans ce param redondant = ajustement trivial si souhaité.
|
||||
|
||||
**Fichiers (6)** : domain/src/live_state.rs ; application/src/workstate/reconcile.rs (nouveau) ; application/src/workstate/mod.rs ; application/src/lib.rs ; app-tauri/src/state.rs ; app-tauri/src/commands.rs.
|
||||
37
.ideai/memory/rendezvous-600s-cap-too-short-heavy-tasks.md
Normal file
37
.ideai/memory/rendezvous-600s-cap-too-short-heavy-tasks.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: rendezvous-600s-cap-too-short-heavy-tasks
|
||||
description: memory note rendezvous-600s-cap-too-short-heavy-tasks
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: rendezvous-600s-cap-too-short-heavy-tasks
|
||||
description: FIX LIVRÉ (2026-06-24) du plafond rendez-vous 600s trop court (T7) — watchdog d'inactivité + extension sur signe de vie + plafond absolu, message distinct cible-active vs no-reply. Built, tests verts, AppImage 09:21 ; validation live T7 en attente.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# T7 — plafond rendez-vous trop court : FIX LIVRÉ (2026-06-24)
|
||||
|
||||
## Défaut (rappel, observé 2026-06-23)
|
||||
Délégation dev lourde (impl + `cargo test --workspace` + clippy) → la cible bosse en **un seul long tour > 600 s sans émettre de `turn_duration`**. L'enveloppe serveur `timeout(600s, dispatch)` plate (`crates/infrastructure/src/orchestrator/mcp/server.rs`) expirait sec : (a) faux `-32001`/no-reply **retryable** alors que la cible n'est pas muette mais active ; (b) drop du `dispatch` → canal de report mort → `idea_reply` final non livrable (« no pending request »).
|
||||
|
||||
## Fix (branche de travail courante, pas encore committé)
|
||||
Implémenté par subagent Claude (Architect+DevBackend, hors `idea_ask_agent` car le rendez-vous était le sujet), vérifié par Main (QA réelle), AppImage rebuildée.
|
||||
- **Algorithme `run_inactivity_watchdog`** dans `server.rs` : `ASK_RENDEZVOUS_TIMEOUT` (600 s, env `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS`) devient une **fenêtre d'inactivité (budget de silence)**, pas un plafond absolu. Boucle `timeout(fenêtre, dispatch)` ; à chaque expiration, sonde l'activité de la cible :
|
||||
- progrès & sous plafond → réarme (extension) ;
|
||||
- progrès & plafond atteint → **`rendezvous_ceiling_active_error`** (nouveau code JSON-RPC `error_codes::RENDEZVOUS_CEILING_ACTIVE = -32_002`, `data.code="RENDEZVOUS_CEILING_ACTIVE"`, **`retryable:false`**, message « still actively working… do not retry blindly, check the target's in-progress work/branch ») ;
|
||||
- pas de progrès (vrai silence, ou **pas de probe = fallback plat**) → `rendezvous_no_reply_error` inchangé (`retryable:true`).
|
||||
- **Signe de vie = octets cumulés des `.jsonl`** du run-dir cible (couvre le long tour unique sans `turn_duration`). Nouvelle `inspector::claude_paths::transcript_activity_token(fs, home, cwd) -> Option<u64>`.
|
||||
- **Plafond absolu** `ASK_RENDEZVOUS_CEILING` (défaut **4 h**, env `IDEA_ASK_RENDEZVOUS_CEILING_MS`).
|
||||
- **Wiring** : sonde optionnelle `AskActivityProbe` injectée dans `McpServer` (modèle `events`/`ready_sink`, additif → zéro régression), branchée par la composition root `crates/app-tauri/src/state.rs` (qui résout nom→AgentId→run-dir via nouveau `service.resolve_agent_id_by_name`). Infra reste libre d'`AgentId`.
|
||||
- Fichiers : `server.rs`, `jsonrpc.rs`, `mcp/mod.rs`, `lib.rs`, `inspector/claude_paths.rs`+`mod.rs`, `application/.../service.rs`, `app-tauri/state.rs`.
|
||||
|
||||
## Validation (preuve réelle, Main)
|
||||
`cargo test -p infrastructure --lib` = **251 passed/0** (dont 6 nouveaux tests : silence→no-reply, fallback sans probe→no-reply, active→extension puis resolved, active→ceiling, codes/retryable distincts). `mcp_server` 22/0, `inspector_claude` 4/0, `cargo test -p application` tout vert. `clippy -p infrastructure -p application --all-targets` = **0 warning nouveau** (résiduels pré-existants : input/mod.rs, lifecycle.rs, fileguard.rs, server.rs:132 ready_sink). **AppImage rebuildée 2026-06-24 09:21**, installée sur `/home/anthony/Documents/IdeA_0.3.0_amd64.AppImage`, backup `….old-pre-t7-fix` (08:41).
|
||||
|
||||
## Reste à faire
|
||||
1. **Validation live T7** après relance IdeA : déléguer une tâche lourde >600 s ; attendu = pas d'expiration tant que la cible écrit son transcript, et soit résolution normale, soit (si >4 h) message ceiling-active non-retryable distinct ; idea_reply tardif non perdu tant qu'on n'a pas atteint le plafond.
|
||||
2. **Commit par Git** (non fait — laissé à Git, cf. [[git-owns-commit-merge-decisions]]) : décider de le faire avant ou après la validation live T7.
|
||||
3. Reprise complète des tests **depuis T1** (skill mcp-rendezvous-functional-test). État pré-relance : T1→T6 verts cette passe, dont fix T5 validé live (cf. [[checkpoint-mcp-tests-t1-t5-and-t5-livestate-defect]]).
|
||||
|
||||
Lié à [[rendezvous-no-reply-backstop-design]], [[backstop-fires-on-intra-task-turn-rootcause]], [[mcp-functional-test-plan-2026-06-24]].
|
||||
14
.ideai/memory/rendezvous-no-reply-backstop-design.md
Normal file
14
.ideai/memory/rendezvous-no-reply-backstop-design.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: rendezvous-no-reply-backstop-design
|
||||
description: Décision d'architecture sur la fin-de-tour et le backstop no-reply du rendez-vous idea_ask_agent ⇄ idea_reply, après échec live du fix Finding A (77e62e5).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Le watcher prompt-ready PTY (`infrastructure/src/input/mod.rs` `arm_prompt_watcher`) ne peut PAS servir de signal de fin-de-tour : il est one-shot ré-armé seulement au `bind_handle`, et matche un bandeau TUI **permanent** (`? for shortcuts`) — donc inopérant par tour. Preuve live : aucun `prompt_ready` de toute une session alors que l'agent a travaillé ⇒ c'est le handshake MCP `initialize` (→ `release_agent_cold_start`) qui libère le cold-start, pas le PTY.
|
||||
|
||||
**Contrat figé** : `idea_ask_agent` est garanti libéré en temps borné par l'un de :
|
||||
1. `idea_reply` (chemin propre, renforcé par le préambule comportemental injecté à chaque délégation) ;
|
||||
2. backstop no-reply sur **stagnation** — transition `Alive→Stalled` (lot-2 `arm_liveness`/`sweep_stalled`) du `Busy{ticket}` ⇒ résolution synthétique « cible silencieuse sans idea_reply » + libération du demandeur ;
|
||||
3. timeout absolu **fini** (dernier recours) — `ASK_RENDEZVOUS_TIMEOUT` ne doit JAMAIS être 24h ; défaut reco 600 s, env-overridable, garde `Some(0)=no-override`. La borne applicative per-tour (`resolve_turn_timeout`) doit aussi être finie.
|
||||
|
||||
Le scraping d'octets ANSI bruts est interdit comme autorité de fin-de-tour (fragile, profil-spécifique). Cold-start = signal MCP `initialize`. Aucun port domaine ni contrat de cartographie n'est touché : tout est infrastructure/application (réutilise lot-2 + sink MCP). Diagnosticabilité : les logs d'armement/bind doivent être en `diag!` (pas `eprintln!`→/dev/null) pour observer armement vs match.
|
||||
39
.ideai/memory/resume-after-appimage-rebuild-mcp-e2e.md
Normal file
39
.ideai/memory/resume-after-appimage-rebuild-mcp-e2e.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
name: resume-after-appimage-rebuild-mcp-e2e
|
||||
description: memory note resume-after-appimage-rebuild-mcp-e2e
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
---
|
||||
name: resume-after-appimage-rebuild-mcp-e2e
|
||||
description: Point de reprise — programme live-state CLOS+RELEASÉ (0.3.0), discordance .gitignore D19-4 TRANCHÉE. Prochain chantier = validation MCP e2e UX. Chantiers restants priorisés.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Reprise — état au 2026-06-22 (run a6ced819)
|
||||
|
||||
## Où on en est
|
||||
- **Programme live-state / persistance CLOS et RELEASÉ** : `develop` @ `36be0cb` (LS8), `main` @ `29232dd` = `release(0.3.0): intègre develop dans main`. Détail : [[live-state-persistence-program-closed-ls8]]. Doc : `docs/LS8-live-state-persistence-closure.md` + `ARCHITECTURE.md` §21.
|
||||
- **AppImage rebuildée + tournante** : `IdeA_0.3.0_amd64.AppImage` (le nom n'est plus 0.1.0). Le binaire live embarque tout le programme ; 6 ponts MCP connectés/ESTAB observés → point 1 du chantier e2e (agent voit ses outils MCP sans action manuelle) déjà satisfait en réel. Recette rebuild (rappel) : `npm --prefix frontend run build` puis depuis `crates/app-tauri/` `APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage` ; remplacer l'AppImage installée → relancer.
|
||||
- **Discordance .gitignore ↔ D19-4 : TRANCHÉE** (par l'utilisateur) et exécutée par Git → commit `47b3806` sur `develop` (`chore(gitignore): désuivre .ideai/conversations/`). Périmètre réel : seul `.ideai/conversations/` était discordant (16 fichiers suivis, 8 handoff.md + 8 log.jsonl) ; `.ideai/run/` et `.ideai/live-state.json` étaient déjà ignorés/propres. Désuivis (rm --cached, conservés sur disque) + règle ajoutée `.gitignore:58`. Plus une décision en attente.
|
||||
|
||||
## Prochain chantier (priorité 1) : VALIDATION MCP END-TO-END UX
|
||||
Objectif = valider/durcir en situation RÉELLE le parcours inter-agents (le code est probablement bon, Bugs 1→7 corrigés cf. [[mcp-bridge-and-delegation-runtime-notes]], mais ces bugs ne sont apparus qu'en live). À vérifier sur l'AppImage 0.3.0 :
|
||||
1. Un agent lancé par IdeA voit ses outils MCP sans action manuelle. (déjà OK observé)
|
||||
2. Délégation vers agent FROID et vers agent BACKGROUND (node_id None) : tâche bien écrite dans le PTY, pas de 1er tour perdu, pas de `Busy` fantôme après interruption/annulation (Bugs 3/4/6/7).
|
||||
3. Un `ask` sans réponse ne wedge pas la connexion du demandeur (Bug 5).
|
||||
4. Profils Claude/Codex utilisent MCP par défaut ; fallback fichier+prose cohérent sinon.
|
||||
5. Observabilité UI des délégations/replies (aujourd'hui opaque) — sujet UX.
|
||||
|
||||
**CONTRAINTE MÉTHODO** : NE PAS réparer/valider le système inter-agents VIA le système inter-agents (`idea_ask_agent`). Piloter via les **subagents natifs (outil Agent)**, et demander à l'utilisateur de faire tourner l'AppImage + observer (Main ne peut pas valider seul depuis l'intérieur). Diag live : `ss -xp | grep idea-mcp` (ponts connectés) ; `~/.claude/projects/<encoded-run-dir>/*.jsonl` (transcript ; pas de nouveau fichier = tâche jamais soumise).
|
||||
|
||||
## Autres chantiers restants (depuis [[remaining-work-idea-agent-control-ide]])
|
||||
- **Auto-update mémoire/contexte EN COURS de session** (aujourd'hui injection au lancement seulement) — priorité 1.
|
||||
- **Multi-fenêtres / déplacement d'onglet** du registre de sessions — priorité 2 (UI).
|
||||
- Optionnels différés : activation réelle du seam LLM de handoff (contrat figé ADR LS5, défaut heuristique) ; sweep périodique de rotation (idempotent, non câblé).
|
||||
|
||||
## Friction outillage notée (par Git, sans incidence résultat)
|
||||
Le `settings.local.json` du run dir a un `deny` qui bloque Edit/Write et certaines redirections Bash pour les agents en run dir isolé ; Git a dû éditer `.gitignore` via `tee -a`. À assouplir si on veut des éditions de fichiers propres côté run dir.
|
||||
|
||||
## Contrainte d'écriture (rappel)
|
||||
Main + agents en run-dir isolé ne peuvent PAS écrire l'arbre du project root (docs/, ARCHITECTURE.md, .ideai/memory/*.md fichiers) — permissions déniées. Mémoire IdeA : outil `idea_memory_write` (create/replace par slug ; pas de delete). Suppression fichiers mémoire / édition MEMORY.md / CLAUDE.md / .gitignore du project root : passer par **Git**.
|
||||
11
.ideai/memory/skills-integration-canonical-foundation.md
Normal file
11
.ideai/memory/skills-integration-canonical-foundation.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: skills-integration-canonical-foundation
|
||||
description: Topologie d'intégration du chantier skills agent et décision d'abandon de feature/agent-skills.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
Le chantier « skills agent » : la fondation canonique du domaine skills est **déjà dans `develop`** (introduite par `2332b7f` : port `SkillStore`, `SkillRef` sur `ManifestEntry`, `resolve_skills` dans `LaunchAgent`, CRUD + assign/unassign, **frontend complet** `features/skills/*`).
|
||||
|
||||
- `feature/agent-skills` (A, @ef101db) = réimplémentation **obsolète et incompatible** (serde `tag="type"`, `with_updated_content`, pas de `SkillRef`/frontend). **À abandonner** — ne jamais merger, elle régresse develop.
|
||||
- `feature/agent-skill-awareness` (B, @5be8987) = **surensemble propre** de develop (sa base `8452333` est ancêtre de develop ; diff skill = +539/−3). Apporte : `Skill.description`/`effective_description`, manifeste, outil MCP `idea_skill_read` (variante domaine `OrchestratorCommand::ReadSkill`), brief « capacités IdeA » inconditionnel dans `compose_convention_file`.
|
||||
- Intégration = cherry-pick `ab34363`(+`1a10d67` squash) puis `566bff4` sur develop ; **exclure** `e93a2c1` (cold-start, superseded par `8bb832c`) et le bruit `.ideai/*` (garder seulement `.ideai/memory/feature-agent-skill-awareness-design.md`). Dropper `e93a2c1` évite tout conflit sur `input/mod.rs`. Points chauds : `lifecycle.rs`, `state.rs`.
|
||||
@ -34,5 +34,121 @@
|
||||
}
|
||||
],
|
||||
"fallback": "allow"
|
||||
}
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "a6ced819-b893-4213-b003-9e9dc79b9641",
|
||||
"permissions": {
|
||||
"rules": [
|
||||
{
|
||||
"capability": "read",
|
||||
"effect": "allow",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "write",
|
||||
"effect": "deny",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "delete",
|
||||
"effect": "allow",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "executeBash",
|
||||
"effect": "allow",
|
||||
"paths": [],
|
||||
"commands": []
|
||||
}
|
||||
],
|
||||
"fallback": "allow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "dce19c75-9669-4e45-b8de-9950025157da",
|
||||
"permissions": {
|
||||
"rules": [
|
||||
{
|
||||
"capability": "read",
|
||||
"effect": "allow",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "write",
|
||||
"effect": "deny",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "delete",
|
||||
"effect": "deny",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "executeBash",
|
||||
"effect": "allow",
|
||||
"paths": [],
|
||||
"commands": []
|
||||
}
|
||||
],
|
||||
"fallback": "allow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
|
||||
"permissions": {
|
||||
"rules": [
|
||||
{
|
||||
"capability": "read",
|
||||
"effect": "allow",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "write",
|
||||
"effect": "deny",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "delete",
|
||||
"effect": "deny",
|
||||
"paths": [
|
||||
"**"
|
||||
],
|
||||
"commands": []
|
||||
},
|
||||
{
|
||||
"capability": "executeBash",
|
||||
"effect": "allow",
|
||||
"paths": [],
|
||||
"commands": []
|
||||
}
|
||||
],
|
||||
"fallback": "allow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -4,7 +4,14 @@
|
||||
{
|
||||
"id": "a72dac60-641c-4417-b0d7-94b8539f817a",
|
||||
"name": "build-appimage",
|
||||
"description": null,
|
||||
"contentHash": "77cb33b978b242f6"
|
||||
},
|
||||
{
|
||||
"id": "86ea97df-1533-47e5-8809-dc2b02242567",
|
||||
"name": "mcp-rendezvous-functional-test",
|
||||
"description": null,
|
||||
"contentHash": "9fc8260f64c3b9d5"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
.ideai/skills/md/86ea97df-1533-47e5-8809-dc2b02242567.md
Normal file
42
.ideai/skills/md/86ea97df-1533-47e5-8809-dc2b02242567.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Skill — Test fonctionnel du rendez-vous inter-agents MCP
|
||||
|
||||
Workflow **ré-exécutable et évolutif** pour tester de bout en bout le rendez-vous `idea_ask_agent` ⇄ `idea_reply` d'IdeA, du trivial au plus tordu. À relancer après chaque rebuild qui touche l'orchestration MCP / le backstop / la live-state. **Ce plan évolue** : à chaque nouveau souci rencontré, ajoute/raffine un test Tn et note le défaut dans la section « Journal des défauts ».
|
||||
|
||||
## Principe
|
||||
- **Main est le demandeur** (`idea_ask_agent`) — test fidèle de « si je donne un message à un agent, il me répond ».
|
||||
- Doublé d'**observation externe Bash** :
|
||||
- ponts : `ss -xp | grep idea-mcp` (ESTAB) et `pgrep -af mcp-server` (2 process par requester = pont vivant).
|
||||
- transcripts : `~/.claude/projects/<run-dir-encodé>/*.jsonl` (horodatages **UTC**). Reply réel = `tool_use` idea_reply → tool_result `reply … delivered`. Wedge demandeur = transcript figé sur `tool_use` idea_ask_agent sans tool_result. 1er tour intact = 1re ligne user `parentUuid:null` = `[IdeA · tâche…]`.
|
||||
- placement : `.ideai/layouts.json` (agent présent = cellule visible ; absent = background / node_id None).
|
||||
- live-state : `idea_workstate_read` (last-writer-wins, pas temps réel).
|
||||
- Règle structurelle : **binaire qui tourne = AppImage**. Tout fix backend ⇒ **rebuild + relance** d'IdeA (tue Main). Les tests s'enchaînent SANS relance *à l'intérieur d'une même version*. Vérifier avant T1 : `stat` mtime de l'AppImage récent + `git log -1` = HEAD attendu.
|
||||
|
||||
## Setup avant T1
|
||||
1. Relancer IdeA, ouvrir le projet, **Main visible** (le demandeur).
|
||||
2. Garder **2 cellules visibles libres** + lancer **QA et DevFrontend** dans des cellules visibles (cibles chaudes).
|
||||
3. Laisser **Architect, DevBackend, Git ARRÊTÉS (froids)** : T3 teste le réveil à froid ; T9 utilise Architect+DevBackend froids.
|
||||
|
||||
## Méthode d'exécution
|
||||
On lance dans l'ordre, on **note** chaque verdict avec preuve réelle. Si un défaut **bloquant** apparaît : stop, ouvrir le cycle de fix (Architect→Git→DevBackend→QA, ou via subagents si le rendez-vous lui-même est cassé), rebuild + relance, puis **reprise depuis T1**. Un défaut **non bloquant** est consigné au Journal et on peut continuer si l'utilisateur le décide.
|
||||
|
||||
## Ordre des tests (trivial → tordu)
|
||||
- **T1 — Visibilité outils MCP.** Agent lancé par IdeA voit ses outils sans action manuelle. Vérif : pont ESTAB + 2 mcp-server par requester ; cibles froides = 0 process.
|
||||
- **T2 — Cible CHAUDE + idea_reply trivial.** ask(agent chaud, « pong via idea_reply »). Attendu : `pong` inline borné, workstate `done`, pas de busy fantôme.
|
||||
- **T3 — Réveil à FROID + idea_reply.** Cible arrêtée. Attendu : pont relancé (nouveau mcp-server), **1er tour non perdu**, reply remonte.
|
||||
- **T4 — Cible BACKGROUND (node_id None).** Cible hors layout (sans cellule visible). Attendu : même protocole, multi-tours sans perte.
|
||||
- **T5 — NO-REPLY (cible répond en prose, pas d'idea_reply).** Attendu : backstop libère le demandeur en temps borné avec `TargetReturnedNoReply` (retryable), **ET live-state de la cible purgée (pas de busy fantôme)**. Défaut historique #1.
|
||||
- **T6 — Délégation MULTI-ÉTAPES puis idea_reply.** Cible lit plusieurs fichiers / lance une cmd PUIS idea_reply. Attendu : **pas de libération prématurée** pendant le travail, rapport final livré. (Piège turn-watcher : backstop tirait au 1er tour interne ~3 s.)
|
||||
- **T7 — Tâche LOURDE > plafond** (impl + `cargo test`/clippy). Attendu : extension sur signe de vie OU message clair « cible active, plafond atteint » (≠ faux `-32001`) ; idea_reply tardif non perdu. Plafond `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS` (défaut 600000).
|
||||
- **T8 — Délégations PARALLÈLES** (2 cibles en 1 tour Main). Attendu : rendez-vous multiples simultanés, multiplexage pont, 2 réponses distinctes.
|
||||
- **T9 — TRANSITIVE A→B→C** (Main→Architect, Architect délègue à DevBackend puis reply à Main). Attendu : rendez-vous imbriqués, 2 attentes simultanées sans deadlock.
|
||||
- **T10 — INTERRUPTION/annulation + reconcile reboot.** (a) Main interrompt un ask en vol → `working` cible purgé, pas de cascade, idea_reply tardif non-livrable proprement. (b) Au reboot, orphelins {working,waiting,blocked} & session morte → `idle` + `STALE_AT_RESTART_MARKER` (use case ReconcileLiveState).
|
||||
|
||||
## Critère de fin
|
||||
Tous les T1→T10 verts avec preuve réelle. Sinon : 1er défaut bloquant = corriger puis reprise T1.
|
||||
|
||||
## Journal des défauts (à enrichir au fil des runs)
|
||||
- **2026-06-24, run a6ced819 (AppImage 08:15, HEAD 1efe2f1)** — T1→T4 ✅. **T5 partiel** : backstop libère le demandeur en ~24 s (`TargetReturnedNoReply` retryable, wedge demandeur corrigé) MAIS la **live-state de la cible n'est pas purgée** : elle reste `status:"working"` jusqu'au prochain write réussi de la cible (re-délégation non bloquée, busy fantôme atténué mais présent → `idea_workstate_read` ment sur la dispo). Cause : backstop agit côté demandeur, ne réinitialise pas la live-state de la cible sur no-reply. → fix en cours.
|
||||
|
||||
## Notes / pièges observés
|
||||
- IdeA peut placer une cible réveillée en **background** même quand des cellules visibles sont libres (observé T3/T4).
|
||||
- Quand le rendez-vous MCP lui-même est suspecté cassé, **ne pas** orchestrer le fix via `idea_ask_agent` (risque de blocage) : utiliser les subagents natifs de l'orchestrateur pour analyse/fix/rebuild.
|
||||
@ -647,11 +647,13 @@ IdeA/
|
||||
|
||||
**Convention file généré par IdeA** : IdeA écrit dans ce dossier le fichier conventionnel attendu par le profil (`CLAUDE.md`, `AGENTS.md`, etc.). Ce fichier contient :
|
||||
1. Le **chemin absolu du project root** (pour que l'agent sache où opérer).
|
||||
2. Le contrat d'**orchestration IdeA** (délégation via `.ideai/requests`, pas via les subagents natifs du fournisseur).
|
||||
2. Le contrat d'**orchestration IdeA** + brief capacités (délégation via outils `idea_*` en surface MCP, sinon via `.ideai/requests` — jamais les subagents natifs du fournisseur).
|
||||
3. Le **contexte projet partagé** (`.ideai/CONTEXT.md`), si présent.
|
||||
4. La **persona/rôle** de l'agent (son `.md` dans `.ideai/agents/`).
|
||||
5. Les **skills actifs** assignés à cet agent (voir §14.2).
|
||||
6. Le **rappel mémoire** du projet (index/hooks), si présent (voir §14.5.4).
|
||||
6. Le **rappel mémoire** du projet (index/hooks — pointeurs), si présent (voir §14.5.4).
|
||||
7. L'**état du projet** (section `# État du projet`) — projection *live-state* maigre « qui fait quoi maintenant », bornée (cap `LIVE_STATE_INJECT_MAX`, agent lancé exclu, ordre manifeste, vide ⇒ omise) (LS4 ; voir §21 et `docs/LS8`).
|
||||
8. La **reprise de la conversation** (section `# Reprise de la conversation`) — *handoff* distillé du fil, **borné** (`HANDOFF_SUMMARY_MAX_CHARS=4096`) à l'écriture **et** à l'injection (LS5 ; voir §21). Omise sans handoff.
|
||||
|
||||
**Avantages** :
|
||||
- Zéro collision entre agents, même N instances du même profil.
|
||||
@ -1926,16 +1928,16 @@ Nouvelles commandes (jumelles des commandes PTY existantes ; réutilisent `resol
|
||||
- **Vivant de bout en bout** : `apply_mcp_config` matérialise `.mcp.json` dans le **run dir isolé** de l'agent **AVANT** le split structuré/PTY (`crates/application/src/agent/lifecycle.rs` ~1094) ⇒ Claude/Codex le lisent **nativement**. La déclaration porte l'**exe réel** injecté par `McpRuntime` (`$APPIMAGE` sinon `current_exe`, `crates/app-tauri/src/mcp_endpoint.rs:139` `idea_exe_path`), l'**endpoint loopback** du projet, le `--project` et le `--requester` (agent réel, fin du `"mcp"` figé).
|
||||
- **Endpoint loopback** = **UDS** (Linux/macOS) / **named pipe** (Windows), **zéro port réseau** ; source de vérité unique `mcp_endpoint(project_id)` (`mcp_endpoint.rs`), bindé à l'open / fermé au close (`crates/app-tauri/src/state.rs` `ensure_mcp_server`/`bind_endpoint`). **Fix D1** : cadavre `.sock` (run SIGKILL) **unlinké avant bind** (`state.rs` ~1019-1033) — sinon `EADDRINUSE`.
|
||||
- **Serveur** `McpServer` (`crates/infrastructure/src/orchestrator/mcp/server.rs`) = **jumeau du `FsOrchestratorWatcher`** : autre porte sur le **même** `OrchestratorService::dispatch`, `serve(conn)` **par pair**. Transport `StdioTransport` (JSON Lines stdin/stdout du pont) + `MemoryTransport` (tests, sans socket ni process). Pont = sous-commande `mcp-server` du binaire app-tauri (`mcp_bridge.rs`).
|
||||
- **Outils** `idea_ask_agent` / `idea_reply` / `idea_list_agents` (`mcp/tools.rs`) mappés 1:1 vers `OrchestratorCommand` ; `dispatch` appelé **à l'identique** par les trois portes (fichier, MCP, UI).
|
||||
- **Outils** (`mcp/tools.rs`) — **catalogue de 14**, tous mappés 1:1 vers `OrchestratorCommand` et servis par le **même** `dispatch` (les trois portes : fichier, MCP, UI) : `idea_list_agents`, `idea_ask_agent`, `idea_reply`, `idea_launch_agent`, `idea_stop_agent`, `idea_update_context`, `idea_create_skill` (7 base) · `idea_context_read`, `idea_context_propose`, `idea_memory_read`, `idea_memory_write` (4 FileGuard C7) · `idea_skill_read` (skill-awareness) · `idea_workstate_read`, `idea_workstate_set` (LS4 ; `set` n'écrit que la ligne de l'agent courant, via l'identité handshake).
|
||||
|
||||
### 18.6 Invariant « 1 agent = 1 session vivante » (livré)
|
||||
- Registres `TerminalSessions` + `StructuredSessions` (`crates/application/src/terminal/registry.rs`) agrégés en `LiveSessions` (PTY+structuré). `session_for_agent` (singulier, **non ambigu**) **+** `sessions_for_agent` (pluriel). Garde reattach `Rebind`/`Refuse`/`Idempotent` dans `LaunchAgent` (`lifecycle.rs`). Ancienne ambiguïté `session-registry-agent-ambiguity` = **fermée par construction**.
|
||||
|
||||
---
|
||||
|
||||
## 19. Cadrage — couche de persistance conversationnelle + handoff cross-profile incrémental (chantier à découper, PAS d'implémentation)
|
||||
## 19. Persistance conversationnelle + handoff cross-profile incrémental (cadrage — LIVRÉ P1→P8 + LS1→LS7)
|
||||
|
||||
> **Cadrage architecture** (le prochain chantier prioritaire après robustesse : **persistance/reprise → handoff cross-profile**). Produit les **ports**, les **adapters**, les **frontières** et un **découpage en lots testables**. **Aucun code applicatif ici** : la doc est livrable, les lots seront confiés aux binômes Dev/Test (cycle §3).
|
||||
> **⚠️ STATUT : LIVRÉ (clôture programme live-state/persistance, @ `fd7adbb`).** Ce qui suit était le **cadrage** ; il est désormais **implémenté** (P1→P8 du handoff, puis LS1→LS7 : live-state, borne `summary_md`, rotation/pagination, viewer). La **cartographie de clôture** (4 stores, flux d'injection, chemin chaud vs froid, ce qui reste ouvert) fait foi en **§21** et dans [`docs/LS8-live-state-persistence-closure.md`](docs/LS8-live-state-persistence-closure.md). Précisions sur l'état réel : (1) **P10 (`LlmHandoffSummarizer`)** reste **non activé** — le défaut runtime est l'heuristique borné (LS5, ADR `docs/adr/LS5-handoff-summary-bound-and-llm-seam.md`) ; (2) la **borne `summary_md`** (LS5 : `HANDOFF_SUMMARY_MAX_CHARS=4096`, `bound_handoff_summary`, `TURN_LINE_MAX_CHARS=240`) est appliquée à l'écriture **et** à l'injection ; (3) la **rotation/rétention** du `log.jsonl` (LS6 : archive segmentée, port `ConversationArchive`, lecture paginée, invariant INV-LS6) et le **live-state** (LS1→LS4) ont été ajoutés **au-delà** de ce cadrage initial. Le texte ci-dessous est conservé comme **genèse** ; en cas de divergence, **§21 + `docs/LS8` font foi**.
|
||||
|
||||
### 19.0 Problème & objectif produit
|
||||
Aujourd'hui la continuité d'une conversation repose sur le **`resumable_id` CLI** (`Conversation.resumable_id`, profil `SessionStrategy{assign_flag,resume_flag}`) : au redémarrage on **rejoue la session du provider** (`--resume <id>`). Deux trous :
|
||||
@ -2345,4 +2347,32 @@ pub enum ScheduledTask {
|
||||
|
||||
---
|
||||
|
||||
## 21. Clôture du programme live-state / persistance (LS1→LS7) — cartographie des 4 stores
|
||||
|
||||
> **Référence durable de clôture** (programme livré @ `fd7adbb`). Détail complet, acquis lot par lot et points ouverts : [`docs/LS8-live-state-persistence-closure.md`](docs/LS8-live-state-persistence-closure.md). §21 fait foi sur la séparation des stores et le flux d'injection.
|
||||
|
||||
### 21.1 Quatre stores disjoints (frontière gravée)
|
||||
|
||||
| Store | Fichier(s) | Nature | Surface | Versionné | Injecté agent |
|
||||
|---|---|---|---|---|---|
|
||||
| Mémoire projet | `.ideai/memory/*.md` + `MEMORY.md` | Savoir stable, curé, low-noise | Agent + humain | Oui | Oui (index/hooks — pointeurs) |
|
||||
| Handoff | `.ideai/conversations/<id>/handoff.md` | Reprise par fil = dérivé distillé **borné** (≤4096) | Agent (distillé) | Non | Oui (`# Reprise de la conversation`, borné LS5) |
|
||||
| Transcript | `.ideai/conversations/<id>/log.jsonl` (+ `log.N.jsonl`) | Journal append-only **riche**, source de vérité | **Humain seul** | cf. point ouvert | **JAMAIS** |
|
||||
| Live-state | `.ideai/live-state.json` | Coordination transitoire **maigre**, keyed LWW, prune TTL+max | Agent (maigre) + humain | Non (gitignoré) | Oui (`# État du projet`, borné LS4) |
|
||||
|
||||
**Règle** : surface AGENT = borné/distillé/pointeur ; surface HUMAINE = riche. Le **transcript append-only ne franchit JAMAIS** vers un contexte agent ; seul le **handoff distillé** passe la frontière (borné des deux côtés). Le live-state est keyed last-writer-wins (jamais d'append).
|
||||
|
||||
### 21.2 Injection au lancement (ordre `compose_convention_file`)
|
||||
|
||||
`# Project root` → `# Orchestration IdeA` (+ capacités) → `# Skills disponibles` → `# Contexte projet` → persona → `# Mémoire projet` (pointeurs) → **`# État du projet`** (live-state lean, LS4) → **`# Reprise de la conversation`** (handoff borné, LS5). Sections vides omises.
|
||||
|
||||
### 21.3 Chemin chaud vs froid
|
||||
|
||||
- **Chaud** (cheap, jamais ralenti) : `append` (O(1)+fsync), `fold` incrémental + `bound_handoff_summary`, auto-update live-state best-effort sur `ask`/`reply`. **Aucun LLM.**
|
||||
- **Froid** : rotation `RotateConversationLog` (à la reprise, best-effort, idempotente, **INV-LS6** : jamais d'élagage d'un tour d'id ≥ `up_to`), lecture paginée `read_conversation_page` (viewer humain), `GetLiveStateLean` (prune-on-read).
|
||||
|
||||
### 21.4 Reste ouvert
|
||||
|
||||
(1) activation réelle du seam LLM (non activé, défaut heuristique, contrat ADR LS5) ; (2) balayage périodique de rotation (idempotent, non câblé) ; (3) discordance D19-4 vs `.gitignore` sur `.ideai/conversations/` (à trancher Git/Main) ; (4) intégration MCP e2e UX ; (5) évolutions multi-fenêtres du registre de sessions ; (6) auto-update mémoire/contexte *en cours* de session. Détail : `docs/LS8` §7.
|
||||
|
||||
*Document maintenu par l'Agent Architecture — base du jalon « cadrage architecture » avant tout code applicatif.*
|
||||
|
||||
246
CLAUDE.md
246
CLAUDE.md
@ -1,205 +1,93 @@
|
||||
# IdeA — Contexte & Méthode de travail
|
||||
# IdeA — Contexte projet global
|
||||
|
||||
> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA.
|
||||
> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici.
|
||||
> Contexte commun minimal du projet IdeA. Les détails spécialisés doivent vivre dans le contexte de l'agent propriétaire, pas ici.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mon rôle : chef d'orchestre, pas développeur
|
||||
## 1. Project root
|
||||
|
||||
Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail.
|
||||
Je suis responsable de :
|
||||
Le project root est :
|
||||
|
||||
- Découper le travail en tâches claires et autonomes.
|
||||
- Attribuer chaque tâche aux bons agents.
|
||||
- Garantir que le cycle de développement/test est respecté.
|
||||
- Faire respecter les principes d'architecture (SOLID, Hexagonal).
|
||||
- Maintenir la cohérence globale du projet et de ce document.
|
||||
- Arbitrer et valider avant toute action irréversible ou sortante.
|
||||
|
||||
---
|
||||
|
||||
## 2. Les agents
|
||||
|
||||
### 2.1 Agent Architecture (1 pour tout le projet)
|
||||
- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**.
|
||||
- Définit les frontières (domaine / application / infrastructure), les ports, les contrats.
|
||||
- Valide que chaque nouvelle feature respecte la structure avant son développement.
|
||||
- Tient à jour la cartographie d'architecture et les conventions.
|
||||
|
||||
### 2.2 Agents de Développement
|
||||
- Écrivent le code des features.
|
||||
- Respectent strictement l'architecture définie par l'agent Architecture.
|
||||
- Code **propre, structuré, stable**.
|
||||
- Reçoivent les rapports d'erreurs des agents de test et corrigent.
|
||||
|
||||
### 2.3 Agents de Test
|
||||
- **Chaque agent de développement est appairé avec un agent de test dédié.**
|
||||
- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées.
|
||||
- Produisent un **rapport d'erreurs** clair quand un test échoue.
|
||||
- Re-testent après chaque correction.
|
||||
|
||||
### 2.4 Agent Git (1 pour tout le projet)
|
||||
- Garant du **dépôt git local** : commits de l'application, création/checkout/switch de
|
||||
branches, merges et rebases. Contexte : `.ideai/agents/git.md`.
|
||||
- **C'est lui qui décide** de la topologie des branches, pas moi. Je le sollicite, il tranche.
|
||||
- Modèle de branches : **`main`** (release) ← **`develop`** (intégration des features
|
||||
terminées) ← une branche **`feature/*`** par nouvelle feature.
|
||||
- **Quand je commande une nouvelle feature** : une fois l'architecture cadrée par Architect,
|
||||
je passe la main à **Git** qui décide s'il faut créer une branche, faire un checkout/switch,
|
||||
ou rester en place — **avant** que le dev commence.
|
||||
- **Après chaque implémentation** : je reparle à **Git** pour qu'il décide si un **merge**
|
||||
doit être fait quelque part (typiquement `feature/* → develop` une fois les tests verts),
|
||||
ou non.
|
||||
- Périmètre **local uniquement** : aucune action sortante (`push`, publication) sans ma
|
||||
validation explicite.
|
||||
|
||||
---
|
||||
|
||||
## 3. Le cycle de développement (boucle obligatoire)
|
||||
|
||||
Pour **chaque** feature implémentée ou modifiée :
|
||||
|
||||
```
|
||||
1. Agent Architecture → valide le découpage et les contrats (ports/interfaces)
|
||||
2. Agent Git → décide de la branche (créer feature/*, switch, ou rester)
|
||||
3. Agent Développement → écrit le code
|
||||
4. Agent Test → écrit les tests unitaires + les exécute
|
||||
5a. Tests OK → feature validée
|
||||
→ Agent Git décide d'un merge éventuel (feature/* → develop)
|
||||
→ on passe à la suite
|
||||
5b. Tests KO → rapport d'erreurs → retour à l'agent Développement
|
||||
→ correction → retour à l'étape 4 (boucle jusqu'au vert)
|
||||
```text
|
||||
/home/anthony/Documents/Projects/IdeA
|
||||
```
|
||||
|
||||
**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas.
|
||||
Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle.
|
||||
Les agents peuvent être lancés depuis un dossier d'exécution isolé `.ideai/run/<agent>/`, mais leurs travaux portent sur le project root.
|
||||
|
||||
---
|
||||
|
||||
## 4. Principes de code
|
||||
## 2. Orchestration IdeA
|
||||
|
||||
- **SOLID** appliqué au maximum.
|
||||
- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...).
|
||||
- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe.
|
||||
- Tests unitaires systématiques ; couverture des features critiques.
|
||||
- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif.
|
||||
Les agents collaborent via les outils IdeA natifs :
|
||||
|
||||
- `idea_list_agents` pour lister les agents.
|
||||
- `idea_ask_agent` pour déléguer une tâche et attendre la réponse.
|
||||
- `idea_launch_agent` pour lancer ou rattacher un agent.
|
||||
- `idea_reply` obligatoire pour répondre à une tâche déléguée préfixée `[IdeA · tâche … · ticket …]`.
|
||||
|
||||
Ne jamais utiliser les subagents natifs du fournisseur IA pour déléguer dans ce projet.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vision produit : IdeA
|
||||
## 3. Rôles
|
||||
|
||||
**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.**
|
||||
|
||||
### Fonctionnalités clés
|
||||
- **Multi-projets en parallèle** : un **onglet par projet**.
|
||||
- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement.
|
||||
- **Agents par projet** : chaque projet a ses propres agents.
|
||||
- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets.
|
||||
- **Création d'agents** : depuis zéro ou à partir d'un template.
|
||||
- **Synchronisation template → agents** : option « garder l'agent à jour ».
|
||||
Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour.
|
||||
- **Contextes d'agents stockés en `.md`** (toujours).
|
||||
- **Création de projet** = définition de son **project root**.
|
||||
|
||||
### Intégrations
|
||||
- **Git** intégré.
|
||||
- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH.
|
||||
- **Développement WSL** : travailler sur une WSL depuis Windows.
|
||||
|
||||
### Plateformes & livraison
|
||||
- Cible : **macOS, Linux, Windows**.
|
||||
- Première phase de compilation : **Linux et Windows**.
|
||||
- Livraison :
|
||||
- **Windows** : `setup.exe`.
|
||||
- **Linux** : **AppImage** (doit fonctionner sur les différentes distributions).
|
||||
- **Main** : chef d'orchestre. Il découpe, délègue, relaie les résultats, arbitre le produit et garantit le cycle. Il ne code pas les features.
|
||||
- **Architect** : propriétaire de l'architecture hexagonale, SOLID, ports/adapters, contrats, DTO, invariants et cartographie.
|
||||
- **DevBackend** : implémentation backend Rust selon les contrats validés par Architect.
|
||||
- **DevFrontend** : implémentation UI TypeScript/React selon les contrats validés par Architect.
|
||||
- **QA** : tests unitaires/intégration ciblés, exécution réelle, rapports d'échec, re-test jusqu'au vert.
|
||||
- **Git** : propriétaire des branches, commits, merges/rebases locaux. Aucune action sortante sans validation explicite.
|
||||
|
||||
---
|
||||
|
||||
## 6. Stack technique (validée)
|
||||
## 4. Cycle obligatoire
|
||||
|
||||
- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs).
|
||||
- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations).
|
||||
- **Frontend / UI** : **TypeScript + React**.
|
||||
- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust).
|
||||
- **Git** : **libgit2** via `git2` (Rust).
|
||||
- **SSH** : `russh` / `ssh2` (Rust).
|
||||
- **WSL** : invocation de `wsl.exe` depuis le backend.
|
||||
Pour toute feature ou correction applicative :
|
||||
|
||||
## 7. Layout des terminaux (exigence produit)
|
||||
|
||||
Disposition en **grille redimensionnable de type tableur (Excel)** :
|
||||
|
||||
- Splits redimensionnables horizontaux **et** verticaux.
|
||||
- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone.
|
||||
- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur.
|
||||
- Chaque cellule de la grille héberge un terminal.
|
||||
- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture.
|
||||
|
||||
## 8. Stockage des contextes & liaison aux templates
|
||||
|
||||
- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet).
|
||||
- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root.
|
||||
*(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)*
|
||||
- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet :
|
||||
- le `.md` de l'agent,
|
||||
- le template d'origine (le cas échéant),
|
||||
- `synchronized: true/false`,
|
||||
- la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible).
|
||||
- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ.
|
||||
|
||||
## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`)
|
||||
|
||||
Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA :
|
||||
|
||||
1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`).
|
||||
2. **Stratégie d'injection du contexte `.md`** :
|
||||
- `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…).
|
||||
- `flag` : passer le chemin via un argument.
|
||||
- `stdin` : piper le contenu.
|
||||
- `env` : passer via variable d'environnement.
|
||||
|
||||
Exemple de profil :
|
||||
```json
|
||||
{
|
||||
"id": "claude-code",
|
||||
"name": "Claude Code",
|
||||
"command": "claude",
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||
"detect": "claude --version",
|
||||
"cwd": "{projectRoot}"
|
||||
}
|
||||
```text
|
||||
1. Architect cadre ou valide l'architecture et les contrats.
|
||||
2. Git décide de la branche locale.
|
||||
3. DevBackend et/ou DevFrontend implémente.
|
||||
4. QA écrit/exécute les tests.
|
||||
5. Si KO : Main relaie le rapport réel au dev, puis retour QA.
|
||||
6. Si OK : Git committe et décide du merge local éventuel.
|
||||
```
|
||||
|
||||
**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message).
|
||||
|
||||
**Règles produit :**
|
||||
- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut.
|
||||
- Les commandes des profils sont **pré-remplies mais éditables**.
|
||||
- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA.
|
||||
|
||||
**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil.
|
||||
|
||||
## 10. Fenêtres & onglets
|
||||
|
||||
- **Par défaut : un onglet par projet** (comme les IDE classiques).
|
||||
- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet.
|
||||
- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets.
|
||||
|
||||
## 11. Feuille de route
|
||||
|
||||
1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**.
|
||||
2. Puis MVP incrémental selon le cycle dev/test de la section 3.
|
||||
|
||||
## 12. Autonomie d'exécution dans le projet
|
||||
|
||||
L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**.
|
||||
|
||||
- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**.
|
||||
- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…).
|
||||
- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite.
|
||||
Une feature n'est terminée que lorsque les tests pertinents sont verts avec sortie réelle.
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2026-06-05*
|
||||
## 5. Produit : repères communs
|
||||
|
||||
IdeA est un IDE next-gen 100 % IA : l'utilisateur y pilote des agents IA plutôt que coder directement.
|
||||
|
||||
Repères stables :
|
||||
|
||||
- Un onglet par projet, multi-fenêtres OS à terme.
|
||||
- Workspace organisé en terminaux/cellules redimensionnables.
|
||||
- Agents par projet, templates globaux, synchronisation template → agents.
|
||||
- Contextes d'agents en Markdown dans `.ideai/`.
|
||||
- Profils IA déclaratifs et éditables, configurés au premier lancement.
|
||||
- Git, SSH et WSL intégrés.
|
||||
- Stack validée : Tauri v2, Rust, TypeScript + React, xterm.js, portable-pty, git2/libgit2, russh/ssh2, `wsl.exe`.
|
||||
|
||||
Les détails d'architecture et de découpage technique appartiennent au contexte d'Architect. Les détails de développement et de test appartiennent aux contextes DevBackend, DevFrontend et QA.
|
||||
|
||||
---
|
||||
|
||||
## 6. Mémoire projet
|
||||
|
||||
Consulter la mémoire projet selon le besoin au lieu de recopier tous les détails ici :
|
||||
|
||||
- `agent-context-memory-and-profile-handoff`
|
||||
- `idea-product-directives-main-handoff`
|
||||
- `remaining-work-idea-agent-control-ide`
|
||||
- `mcp-bridge-and-delegation-runtime-notes`
|
||||
- `permissions-sandbox-system-state`
|
||||
- `session-limit-handling-design`
|
||||
- `git-owns-commit-merge-decisions`
|
||||
- `conversation-rotation-safety-design`
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2026-06-20*
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -69,7 +69,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "app-tauri"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"application",
|
||||
"async-trait",
|
||||
@ -88,7 +88,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "application"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"domain",
|
||||
@ -880,7 +880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "domain"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"serde",
|
||||
@ -1936,7 +1936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "infrastructure"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"application",
|
||||
"async-trait",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app-tauri"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
@ -9,19 +9,21 @@ use tauri::State;
|
||||
|
||||
use crate::dto::DismissEmbedderSuggestionRequestDto;
|
||||
use application::{
|
||||
AppError, AssignSkillToAgentInput, ChangeAgentProfileInput, CloseProjectInput,
|
||||
CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput, DeleteAgentInput,
|
||||
DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput,
|
||||
DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput,
|
||||
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
|
||||
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput,
|
||||
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput,
|
||||
McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput,
|
||||
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, RenameLayoutInput,
|
||||
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
|
||||
SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
||||
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
|
||||
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
||||
AppError, AssignSkillToAgentInput, AttachLiveAgentInput, ChangeAgentProfileInput,
|
||||
CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput,
|
||||
DeleteAgentInput, DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput,
|
||||
DeleteSkillInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
|
||||
GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput,
|
||||
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
|
||||
LaunchAgentInput, ListAgentsInput, ListLayoutsInput, ListMemoriesInput,
|
||||
ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput, McpRuntime,
|
||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadConversationPageInput,
|
||||
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
|
||||
ReconcileLiveStateInput, RenameLayoutInput, ResolveAgentPermissionsInput,
|
||||
ResolveMemoryLinksInput, RotateConversationLogInput, SetActiveLayoutInput,
|
||||
SnapshotRunningAgentsInput, StopLiveAgentInput, SyncAgentWithTemplateInput,
|
||||
UnassignSkillFromAgentInput, UpdateAgentContextInput, UpdateAgentPermissionsInput,
|
||||
UpdateMemoryInput, UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
||||
};
|
||||
use domain::ports::PtyHandle;
|
||||
|
||||
@ -29,29 +31,30 @@ use crate::dto::{
|
||||
parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_memory_slug,
|
||||
parse_node_id, parse_profile_id, parse_project_id, parse_session_id, parse_skill_id,
|
||||
parse_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
AssignSkillRequestDto, AttachLiveAgentRequestDto, ChangeAgentProfileDto,
|
||||
ChangeAgentProfileRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
|
||||
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
|
||||
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
|
||||
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
||||
AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ConfigureProfilesRequestDto,
|
||||
ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto,
|
||||
CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto,
|
||||
CreateSkillRequestDto, CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
||||
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
|
||||
EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto,
|
||||
ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto,
|
||||
GitCommitDto,
|
||||
GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
|
||||
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
|
||||
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
||||
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
||||
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
||||
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||
ProjectPermissionsDto, ReadAgentContextResponseDto, ReattachChatDto, ReattachResultDto,
|
||||
RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
|
||||
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
|
||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto,
|
||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SetActiveLayoutResultDto, SkillDto,
|
||||
SkillListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
||||
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
||||
WriteTerminalRequestDto,
|
||||
TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
|
||||
UpdateAgentContextRequestDto, UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto,
|
||||
UpdateProjectContextRequestDto, UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto,
|
||||
UpdateTemplateRequestDto, WriteTerminalRequestDto,
|
||||
};
|
||||
use crate::pty::{PtyBridge, PtyChunk};
|
||||
use crate::state::AppState;
|
||||
@ -121,6 +124,16 @@ pub async fn open_project(
|
||||
.reconcile_layouts
|
||||
.execute(ReconcileLayoutsInput { project_id: id })
|
||||
.await;
|
||||
// Réconcilie les lignes de live-state fantômes : un crash ne déclenche jamais
|
||||
// `close_project`, donc l'ouverture est le seul filet fiable pour repasser en
|
||||
// `idle` les agents `working`/`waiting`/`blocked` dont la session est morte.
|
||||
// Acte système (appelle le port live-state directement, jamais via la surface
|
||||
// MCP self-only). Best-effort : un échec ne bloque pas l'ouverture. Tourne
|
||||
// AVANT toute relance d'agent (toute reprise réécrit ensuite en LWW).
|
||||
let _ = state
|
||||
.reconcile_live_state
|
||||
.execute(ReconcileLiveStateInput { project_id: id })
|
||||
.await;
|
||||
// (Re)start the orchestrator watcher for this project (idempotent, §14.3).
|
||||
state.ensure_orchestrator_watch(&output.project);
|
||||
state.reconcile_claude_run_dirs(&output.project).await;
|
||||
@ -365,7 +378,46 @@ pub fn write_terminal(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let input = request.into_input()?;
|
||||
state.write_terminal.execute(input).map_err(ErrorDto::from)
|
||||
let session_id = input.session_id;
|
||||
let bytes = input.data.len();
|
||||
let control = describe_terminal_write(&input.data);
|
||||
application::diag!(
|
||||
"[pty-write] write_terminal start: session={session_id} bytes={bytes} control={control}"
|
||||
);
|
||||
match state.write_terminal.execute(input) {
|
||||
Ok(()) => {
|
||||
application::diag!(
|
||||
"[pty-write] write_terminal ok: session={session_id} bytes={bytes} control={control}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
application::diag!(
|
||||
"[pty-write] write_terminal failed: session={session_id} bytes={bytes} \
|
||||
control={control} error={e}"
|
||||
);
|
||||
Err(ErrorDto::from(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_terminal_write(data: &[u8]) -> String {
|
||||
if data.is_empty() {
|
||||
return "<empty>".to_owned();
|
||||
}
|
||||
if data.len() > 16 {
|
||||
return "<bulk>".to_owned();
|
||||
}
|
||||
data.iter()
|
||||
.map(|byte| match *byte {
|
||||
b'\r' => "\\r".to_owned(),
|
||||
b'\n' => "\\n".to_owned(),
|
||||
0x7f => "\\x7f".to_owned(),
|
||||
byte if byte < 0x20 => format!("\\x{byte:02x}"),
|
||||
byte => char::from(byte).to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
/// `resize_terminal` — resize a live PTY.
|
||||
@ -625,14 +677,16 @@ pub async fn delete_layout(
|
||||
|
||||
/// `set_active_layout` — switch the active named layout of a project.
|
||||
///
|
||||
/// Self-healing: a stale `layout_id` does not error; the active layout is left
|
||||
/// unchanged and the actually-active id is returned (authoritative).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed id, `NOT_FOUND` if the
|
||||
/// project or layout is unknown).
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed id) or a persistence error.
|
||||
#[tauri::command]
|
||||
pub async fn set_active_layout(
|
||||
request: SetActiveLayoutRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
) -> Result<SetActiveLayoutResultDto, ErrorDto> {
|
||||
let project_id = parse_project_id(&request.project_id)?;
|
||||
let layout_id = parse_layout_id(&request.layout_id)?;
|
||||
state
|
||||
@ -642,6 +696,7 @@ pub async fn set_active_layout(
|
||||
layout_id,
|
||||
})
|
||||
.await
|
||||
.map(SetActiveLayoutResultDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
@ -930,6 +985,58 @@ pub async fn list_agents(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `get_project_work_state` — read-only live/busy state for manifest agents.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||
/// project is unknown, `STORE` on manifest I/O failure).
|
||||
#[tauri::command]
|
||||
pub async fn get_project_work_state(
|
||||
project_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ProjectWorkStateDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
state
|
||||
.get_project_work_state
|
||||
.execute(GetProjectWorkStateInput { project })
|
||||
.await
|
||||
.map(ProjectWorkStateDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `read_conversation_page` — human, paginated read of a conversation's **full**
|
||||
/// transcript (lot LS6). Archive-aware (segments + active), text never truncated.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed project/conversation id,
|
||||
/// `NOT_FOUND` if the project is unknown, `STORE` on log I/O failure).
|
||||
#[tauri::command]
|
||||
pub async fn read_conversation_page(
|
||||
request: ReadConversationPageRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<TurnPageDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let conversation = uuid::Uuid::parse_str(&request.conversation_id)
|
||||
.map(domain::ConversationId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid conversation id: {}", request.conversation_id),
|
||||
})?;
|
||||
let cursor = request.cursor();
|
||||
let limit = request.limit.unwrap_or(0);
|
||||
state
|
||||
.read_conversation_page
|
||||
.execute(ReadConversationPageInput {
|
||||
project_root: project.root,
|
||||
conversation,
|
||||
cursor,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
.map(TurnPageDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_live_agents` — list every agent that currently owns a live session
|
||||
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
|
||||
/// disable an agent already running in another cell (the "one live session per
|
||||
@ -979,20 +1086,45 @@ pub fn list_live_agents(
|
||||
pub async fn attach_live_agent(
|
||||
request: AttachLiveAgentRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<LiveAgentListDto, ErrorDto> {
|
||||
let _project = resolve_project(&request.project_id, &state).await?;
|
||||
) -> Result<AttachLiveAgentResponseDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
let node_id = parse_node_id(&request.node_id)?;
|
||||
let session = state
|
||||
.terminal_sessions
|
||||
.rebind_agent_node(&agent_id, node_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("running session for agent {agent_id}")))?;
|
||||
state
|
||||
.attach_live_agent
|
||||
.execute(AttachLiveAgentInput {
|
||||
project,
|
||||
agent_id,
|
||||
node_id,
|
||||
})
|
||||
.map(AttachLiveAgentResponseDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
Ok(LiveAgentListDto::from_pairs(vec![(
|
||||
agent_id,
|
||||
session.node_id,
|
||||
session.id,
|
||||
)]))
|
||||
/// `stop_live_agent` — tear down an already-running agent's live session by agent
|
||||
/// id, polymorphically (PTY kill or structured shutdown), without removing the
|
||||
/// agent, its tickets, conversation summary or handoff.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the
|
||||
/// project/agent is unknown or the agent has no live session, `PROCESS` on a
|
||||
/// kill/shutdown failure).
|
||||
#[tauri::command]
|
||||
pub async fn stop_live_agent(
|
||||
request: StopLiveAgentRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<StopLiveAgentResponseDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
// Backstop no-reply : arrêter l'observateur de fin de tour de l'agent (le handle est
|
||||
// droppé ⇒ polling stoppé) avant de démonter sa session.
|
||||
state.stop_turn_watch(agent_id);
|
||||
state
|
||||
.stop_live_agent
|
||||
.execute(StopLiveAgentInput { project, agent_id })
|
||||
.await
|
||||
.map(StopLiveAgentResponseDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `read_agent_context` — read an agent's Markdown context.
|
||||
@ -1141,6 +1273,12 @@ pub async fn launch_agent(
|
||||
// agent so an auto-resume (which only carries agent/node/conversation) can recompose a
|
||||
// full `LaunchAgentInput`. Cloned here — `project` is moved into the launch below.
|
||||
let resume_project = project.clone();
|
||||
// Lot LS6 — project root captured before `project` moves, for the off-hot-path log
|
||||
// rotation triggered at thread (re)open below.
|
||||
let rotation_root = project.root.clone();
|
||||
// Backstop no-reply — project root captured before `project` moves, to arm the
|
||||
// end-of-turn transcript watcher for the launched agent (cf. `AppState::arm_turn_watch`).
|
||||
let watch_root = project.root.clone();
|
||||
|
||||
let output = state
|
||||
.launch_agent
|
||||
@ -1169,6 +1307,41 @@ pub async fn launch_agent(
|
||||
);
|
||||
}
|
||||
|
||||
// Backstop no-reply : armer l'observateur de fin de tour transcript pour la cible si
|
||||
// son profil est supporté (Claude). Idempotent par remplacement à chaque (re)lancement
|
||||
// effectif (profil résolu présent) ; no-op sur reattach (profil `None`) — le handle
|
||||
// posé au 1er lancement persiste. cwd = run dir isolé de l'agent.
|
||||
if let Some(profile) = output.profile.as_ref() {
|
||||
state.arm_turn_watch(
|
||||
&watch_root,
|
||||
agent_id,
|
||||
profile,
|
||||
output.assigned_conversation_id.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
// Lot LS6 — rotation **best-effort** du log, déclenchée à la (re)ouverture du fil et
|
||||
// **hors chemin chaud** : détachée (`tokio::spawn`) pour n'ajouter aucune latence au
|
||||
// launch, erreurs **avalées** (la rotation ne doit jamais casser une reprise). Un
|
||||
// `append` ne déclenche JAMAIS la rotation. Skippée si la cellule ne porte pas une
|
||||
// `conversation_id` UUID (rien à roter).
|
||||
if let Some(conversation) = request
|
||||
.conversation_id
|
||||
.as_deref()
|
||||
.and_then(|raw| uuid::Uuid::parse_str(raw).ok())
|
||||
.map(domain::ConversationId::from_uuid)
|
||||
{
|
||||
let rotate = std::sync::Arc::clone(&state.rotate_conversation_log);
|
||||
tokio::spawn(async move {
|
||||
let _ = rotate
|
||||
.execute(RotateConversationLogInput {
|
||||
project_root: rotation_root,
|
||||
conversation,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let session_id = output.session.id;
|
||||
// Host cell of the freshly (re)launched session — the pivot the level-2 tap reports
|
||||
// to the service alongside the agent.
|
||||
@ -1217,8 +1390,8 @@ pub async fn launch_agent(
|
||||
// est un raté best-effort connu pour LS7.
|
||||
if let Some(parser) = &rate_limit_parser {
|
||||
let text = String::from_utf8_lossy(&chunk);
|
||||
if let Some(limit) =
|
||||
parser.detect(&text, domain::ports::Clock::now_millis(&*detect_clock))
|
||||
if let Some(limit) = parser
|
||||
.detect(&text, domain::ports::Clock::now_millis(&*detect_clock))
|
||||
{
|
||||
service.on_rate_limited(
|
||||
agent_id,
|
||||
@ -1378,10 +1551,7 @@ pub async fn agent_send(
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed agent id).
|
||||
#[tauri::command]
|
||||
pub async fn cancel_resume(
|
||||
agent_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<bool, ErrorDto> {
|
||||
pub async fn cancel_resume(agent_id: String, state: State<'_, AppState>) -> Result<bool, ErrorDto> {
|
||||
let id = parse_agent_id(&agent_id)?;
|
||||
Ok(state.session_limit_service.cancel_resume(id))
|
||||
}
|
||||
@ -1483,6 +1653,10 @@ pub async fn delegation_delivered(
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
let ticket = parse_ticket_id(&request.ticket)?;
|
||||
application::diag!(
|
||||
"[delivery] delegation_delivered command: project={} agent={agent_id} ticket={ticket}",
|
||||
project.id
|
||||
);
|
||||
state
|
||||
.orchestrator_service
|
||||
.note_delegation_delivered(&project, agent_id, ticket);
|
||||
@ -1506,6 +1680,10 @@ pub async fn set_front_attached(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
application::diag!(
|
||||
"[delivery] set_front_attached command: agent={agent_id} attached={}",
|
||||
request.attached
|
||||
);
|
||||
state
|
||||
.orchestrator_service
|
||||
.set_agent_front_attached(agent_id, request.attached);
|
||||
@ -2032,6 +2210,10 @@ pub async fn create_skill(
|
||||
.create_skill
|
||||
.execute(CreateSkillInput {
|
||||
name: request.name,
|
||||
// Description is set via the dedicated frontend field (T6); the create
|
||||
// path stays None for now so the affordance falls back to the body's
|
||||
// first line (see `Skill::effective_description`).
|
||||
description: None,
|
||||
content: request.content,
|
||||
scope: request.scope,
|
||||
project_root: project.root,
|
||||
|
||||
@ -9,10 +9,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use application::{
|
||||
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport,
|
||||
LayoutKind, ListProjectsOutput, OpenProjectOutput,
|
||||
AgentTicketState, AppError, AttachLiveAgentOutput, ConversationPreviewStatus,
|
||||
ConversationTurnWorkPreview, ConversationWorkSummary, CreateProjectInput, CreateProjectOutput,
|
||||
GitGraphOutput, HealthInput, HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind,
|
||||
OpenProjectOutput, ProjectWorkState, StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus,
|
||||
TurnPage, TurnSource, TurnView,
|
||||
};
|
||||
use domain::{Project, ProjectId};
|
||||
use domain::{AgentBusyState, PageCursor, PageDirection, Project, ProjectId, TurnRole};
|
||||
|
||||
/// Request DTO for the `health` command.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
@ -387,7 +390,7 @@ pub fn parse_session_id(raw: &str) -> Result<SessionId, ErrorDto> {
|
||||
|
||||
use application::{
|
||||
CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutOperation, ListLayoutsOutput,
|
||||
LoadLayoutOutput, MutateLayoutOutput,
|
||||
LoadLayoutOutput, MutateLayoutOutput, SetActiveLayoutOutput,
|
||||
};
|
||||
use domain::{AgentId, Direction, LayoutId, LayoutTree, NodeId};
|
||||
|
||||
@ -697,6 +700,23 @@ pub struct DeleteLayoutRequestDto {
|
||||
pub layout_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `set_active_layout`.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetActiveLayoutResultDto {
|
||||
/// The layout actually activated — equals the requested id when valid, else
|
||||
/// the unchanged current active id (self-healing fallback). Authoritative.
|
||||
pub active_id: String,
|
||||
}
|
||||
|
||||
impl From<SetActiveLayoutOutput> for SetActiveLayoutResultDto {
|
||||
fn from(out: SetActiveLayoutOutput) -> Self {
|
||||
Self {
|
||||
active_id: out.active_id.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `set_active_layout`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -1472,6 +1492,270 @@ impl LiveAgentListDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime family of a live work-state session.
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum LiveWorkSessionKindDto {
|
||||
/// Raw PTY-backed CLI session.
|
||||
Pty,
|
||||
/// Structured agent-session backend.
|
||||
Structured,
|
||||
}
|
||||
|
||||
impl From<LiveSessionKind> for LiveWorkSessionKindDto {
|
||||
fn from(kind: LiveSessionKind) -> Self {
|
||||
match kind {
|
||||
LiveSessionKind::Pty => Self::Pty,
|
||||
LiveSessionKind::Structured => Self::Structured,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Live session coordinates in the project work-state read model.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveWorkSessionDto {
|
||||
/// The layout node currently hosting the session view.
|
||||
pub node_id: String,
|
||||
/// The live session id.
|
||||
pub session_id: String,
|
||||
/// Runtime family that owns the session.
|
||||
pub kind: LiveWorkSessionKindDto,
|
||||
}
|
||||
|
||||
/// Derived processing status of a queued ticket.
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TicketWorkStatusDto {
|
||||
/// The agent's current busy turn is running this ticket.
|
||||
InProgress,
|
||||
/// Waiting behind the head / the agent is idle.
|
||||
Queued,
|
||||
}
|
||||
|
||||
impl From<TicketWorkStatus> for TicketWorkStatusDto {
|
||||
fn from(status: TicketWorkStatus) -> Self {
|
||||
match status {
|
||||
TicketWorkStatus::InProgress => Self::InProgress,
|
||||
TicketWorkStatus::Queued => Self::Queued,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Origin of a queued ticket (human operator or a delegating agent).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum TicketWorkSourceDto {
|
||||
/// The human operator.
|
||||
Human,
|
||||
/// Another agent delegating via `idea_ask_agent`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Agent {
|
||||
/// The delegating agent id.
|
||||
agent_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<TicketWorkSource> for TicketWorkSourceDto {
|
||||
fn from(source: TicketWorkSource) -> Self {
|
||||
match source {
|
||||
TicketWorkSource::Human => Self::Human,
|
||||
TicketWorkSource::Agent { agent_id } => Self::Agent {
|
||||
agent_id: agent_id.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One queued/in-progress delegation ticket for an agent.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentTicketStateDto {
|
||||
/// Stable id of the queued ticket.
|
||||
pub ticket_id: String,
|
||||
/// Conversation thread this task enters.
|
||||
pub conversation_id: String,
|
||||
/// FIFO position at snapshot time (`0` = head).
|
||||
pub position: u32,
|
||||
/// Derived status (in-progress vs queued).
|
||||
pub status: TicketWorkStatusDto,
|
||||
/// Origin of the ticket.
|
||||
pub source: TicketWorkSourceDto,
|
||||
/// Display label of the requester.
|
||||
pub requester_label: String,
|
||||
/// Bounded excerpt of the task.
|
||||
pub task_preview: String,
|
||||
/// Character length of the original (un-truncated) task.
|
||||
pub task_len: usize,
|
||||
}
|
||||
|
||||
impl From<AgentTicketState> for AgentTicketStateDto {
|
||||
fn from(ticket: AgentTicketState) -> Self {
|
||||
Self {
|
||||
ticket_id: ticket.ticket_id.to_string(),
|
||||
conversation_id: ticket.conversation_id.to_string(),
|
||||
position: ticket.position,
|
||||
status: ticket.status.into(),
|
||||
source: ticket.source.into(),
|
||||
requester_label: ticket.requester_label,
|
||||
task_preview: ticket.task_preview,
|
||||
task_len: ticket.task_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One manifest agent's current live/busy state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentWorkStateDto {
|
||||
/// Agent id.
|
||||
pub agent_id: String,
|
||||
/// Agent display name.
|
||||
pub name: String,
|
||||
/// Runtime profile id assigned to the agent.
|
||||
pub profile_id: String,
|
||||
/// Live session, if any.
|
||||
pub live: Option<LiveWorkSessionDto>,
|
||||
/// Current mediated-input busy state.
|
||||
pub busy: AgentBusyState,
|
||||
/// Pending/in-progress delegation tickets, in FIFO order.
|
||||
pub tickets: Vec<AgentTicketStateDto>,
|
||||
}
|
||||
|
||||
/// How much of a [`ConversationWorkSummaryDto`] could be derived, best-effort.
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ConversationPreviewStatusDto {
|
||||
/// A handoff was present and usable.
|
||||
Ready,
|
||||
/// No handoff yet; `recentTurns` may carry the log fallback.
|
||||
Missing,
|
||||
/// The handoff was unreadable but the log fallback was readable.
|
||||
Partial,
|
||||
/// Neither the handoff nor the log could be read.
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl From<ConversationPreviewStatus> for ConversationPreviewStatusDto {
|
||||
fn from(status: ConversationPreviewStatus) -> Self {
|
||||
match status {
|
||||
ConversationPreviewStatus::Ready => Self::Ready,
|
||||
ConversationPreviewStatus::Missing => Self::Missing,
|
||||
ConversationPreviewStatus::Partial => Self::Partial,
|
||||
ConversationPreviewStatus::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One recent turn surfaced in a conversation summary's log fallback.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConversationTurnWorkPreviewDto {
|
||||
/// Nature of the turn (`prompt`/`response`/`toolActivity`).
|
||||
pub role: TurnRole,
|
||||
/// Origin of the turn (human operator or a delegating agent).
|
||||
pub source: TicketWorkSourceDto,
|
||||
/// Timestamp (epoch milliseconds) of the turn.
|
||||
pub at_ms: u64,
|
||||
/// Bounded excerpt of the turn text.
|
||||
pub text_preview: String,
|
||||
/// Character length of the original (un-truncated) turn text.
|
||||
pub text_len: usize,
|
||||
}
|
||||
|
||||
impl From<ConversationTurnWorkPreview> for ConversationTurnWorkPreviewDto {
|
||||
fn from(turn: ConversationTurnWorkPreview) -> Self {
|
||||
Self {
|
||||
role: turn.role,
|
||||
source: turn.source.into(),
|
||||
at_ms: turn.at_ms,
|
||||
text_preview: turn.text_preview,
|
||||
text_len: turn.text_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort, read-only summary of one conversation visible through the tickets.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConversationWorkSummaryDto {
|
||||
/// The conversation (pair) this summary describes.
|
||||
pub conversation_id: String,
|
||||
/// How much could be derived.
|
||||
pub status: ConversationPreviewStatusDto,
|
||||
/// Bounded excerpt of the handoff objective, when present.
|
||||
pub objective_preview: Option<String>,
|
||||
/// Bounded excerpt of the handoff summary, when present.
|
||||
pub summary_preview: Option<String>,
|
||||
/// Character length of the original (un-truncated) handoff summary (`0` when none).
|
||||
pub summary_len: usize,
|
||||
/// Cursor (last turn id) covered by the handoff summary, when present.
|
||||
pub up_to: Option<String>,
|
||||
/// Bounded, recent turns from the log fallback.
|
||||
pub recent_turns: Vec<ConversationTurnWorkPreviewDto>,
|
||||
}
|
||||
|
||||
impl From<ConversationWorkSummary> for ConversationWorkSummaryDto {
|
||||
fn from(summary: ConversationWorkSummary) -> Self {
|
||||
Self {
|
||||
conversation_id: summary.conversation_id.to_string(),
|
||||
status: summary.status.into(),
|
||||
objective_preview: summary.objective_preview,
|
||||
summary_preview: summary.summary_preview,
|
||||
summary_len: summary.summary_len,
|
||||
up_to: summary.up_to.map(|cursor| cursor.to_string()),
|
||||
recent_turns: summary
|
||||
.recent_turns
|
||||
.into_iter()
|
||||
.map(ConversationTurnWorkPreviewDto::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-level read model for conversation/delegation UX.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectWorkStateDto {
|
||||
/// Manifest agents in manifest order, enriched with live/busy state.
|
||||
pub agents: Vec<AgentWorkStateDto>,
|
||||
/// Best-effort summaries of the conversations referenced by the tickets,
|
||||
/// joined frontend-side via `tickets[].conversationId`.
|
||||
pub conversations: Vec<ConversationWorkSummaryDto>,
|
||||
}
|
||||
|
||||
impl From<ProjectWorkState> for ProjectWorkStateDto {
|
||||
fn from(state: ProjectWorkState) -> Self {
|
||||
Self {
|
||||
agents: state
|
||||
.agents
|
||||
.into_iter()
|
||||
.map(|agent| AgentWorkStateDto {
|
||||
agent_id: agent.agent_id.to_string(),
|
||||
name: agent.name,
|
||||
profile_id: agent.profile_id.to_string(),
|
||||
live: agent.live.map(|live| LiveWorkSessionDto {
|
||||
node_id: live.node_id.to_string(),
|
||||
session_id: live.session_id.to_string(),
|
||||
kind: live.kind.into(),
|
||||
}),
|
||||
busy: agent.busy,
|
||||
tickets: agent
|
||||
.tickets
|
||||
.into_iter()
|
||||
.map(AgentTicketStateDto::from)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
conversations: state
|
||||
.conversations
|
||||
.into_iter()
|
||||
.map(ConversationWorkSummaryDto::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
|
||||
/// a visible layout cell without spawning a new process.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -1485,6 +1769,64 @@ pub struct AttachLiveAgentRequestDto {
|
||||
pub node_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `attach_live_agent`: the rebound live session's coordinates.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AttachLiveAgentResponseDto {
|
||||
/// The rebound agent's id.
|
||||
pub agent_id: String,
|
||||
/// The node now hosting the session view.
|
||||
pub node_id: String,
|
||||
/// The (unchanged) live session id.
|
||||
pub session_id: String,
|
||||
/// Runtime family that owns the session (`pty`/`structured`).
|
||||
pub kind: LiveWorkSessionKindDto,
|
||||
}
|
||||
|
||||
impl From<AttachLiveAgentOutput> for AttachLiveAgentResponseDto {
|
||||
fn from(out: AttachLiveAgentOutput) -> Self {
|
||||
Self {
|
||||
agent_id: out.agent_id.to_string(),
|
||||
node_id: out.node_id.to_string(),
|
||||
session_id: out.session_id.to_string(),
|
||||
kind: out.kind.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `stop_live_agent`: tear down an already-running agent's live
|
||||
/// session by agent id (no spawn, agent entity preserved).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StopLiveAgentRequestDto {
|
||||
/// Id of the owning project (validated for symmetry and future scoping).
|
||||
pub project_id: String,
|
||||
/// Id of the running agent to stop.
|
||||
pub agent_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `stop_live_agent`: the torn-down session's coordinates.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StopLiveAgentResponseDto {
|
||||
/// The stopped agent's id.
|
||||
pub agent_id: String,
|
||||
/// The id of the session that was torn down.
|
||||
pub session_id: String,
|
||||
/// Runtime family that owned the session (`pty`/`structured`).
|
||||
pub kind: LiveWorkSessionKindDto,
|
||||
}
|
||||
|
||||
impl From<StopLiveAgentOutput> for StopLiveAgentResponseDto {
|
||||
fn from(out: StopLiveAgentOutput) -> Self {
|
||||
Self {
|
||||
agent_id: out.agent_id.to_string(),
|
||||
session_id: out.session_id.to_string(),
|
||||
kind: out.kind.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an agent-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
@ -2275,3 +2617,249 @@ pub fn parse_memory_slug(raw: &str) -> Result<MemorySlug, ErrorDto> {
|
||||
message: format!("invalid memory slug: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Conversation pagination (lot LS6) ────────────────────────────────────────
|
||||
|
||||
/// Request DTO for `read_conversation_page` — a human, paginated transcript read.
|
||||
///
|
||||
/// `anchor` is a turn id to paginate around (omit to start from a thread end);
|
||||
/// `direction` is `"forward"` (towards newer) or `"backward"` (towards older,
|
||||
/// default — the human view opens on the latest page); `limit` is clamped by the
|
||||
/// domain to `[1, 200]` (omit/`0` ⇒ default 50).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConversationPageRequestDto {
|
||||
/// The owning project's id.
|
||||
pub project_id: String,
|
||||
/// The conversation (pair) id to read.
|
||||
pub conversation_id: String,
|
||||
/// Optional anchor turn id; omit to start from a thread end.
|
||||
#[serde(default)]
|
||||
pub anchor: Option<String>,
|
||||
/// Pagination direction (`"forward"` or `"backward"`); defaults to backward.
|
||||
#[serde(default)]
|
||||
pub direction: Option<String>,
|
||||
/// Requested page size (omit/`0` ⇒ default; clamped to `[1, 200]`).
|
||||
#[serde(default)]
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl ReadConversationPageRequestDto {
|
||||
/// Builds the domain [`PageCursor`] from the request (default direction backward,
|
||||
/// an unparseable anchor degrades to `None` = a thread-end page).
|
||||
#[must_use]
|
||||
pub fn cursor(&self) -> PageCursor {
|
||||
let direction = match self.direction.as_deref() {
|
||||
Some(d) if d.eq_ignore_ascii_case("forward") => PageDirection::Forward,
|
||||
_ => PageDirection::Backward,
|
||||
};
|
||||
let anchor = self
|
||||
.anchor
|
||||
.as_deref()
|
||||
.and_then(|raw| uuid::Uuid::parse_str(raw).ok())
|
||||
.map(domain::TurnId::from_uuid);
|
||||
PageCursor { anchor, direction }
|
||||
}
|
||||
}
|
||||
|
||||
/// Origin of a turn in the human transcript view (mirrors [`TurnSource`]).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum TurnSourceDto {
|
||||
/// The human operator.
|
||||
Human,
|
||||
/// Another agent (delegation via `idea_ask_agent`).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Agent {
|
||||
/// The originating agent id.
|
||||
agent_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<TurnSource> for TurnSourceDto {
|
||||
fn from(source: TurnSource) -> Self {
|
||||
match source {
|
||||
TurnSource::Human => Self::Human,
|
||||
TurnSource::Agent { agent_id } => Self::Agent {
|
||||
agent_id: agent_id.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One turn in the human transcript — **full text, never truncated**.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TurnViewDto {
|
||||
/// Stable turn id (also the pagination anchor).
|
||||
pub id: String,
|
||||
/// Timestamp (epoch milliseconds).
|
||||
pub at_ms: u64,
|
||||
/// Turn nature (prompt, response, tool activity).
|
||||
pub role: TurnRole,
|
||||
/// Origin (human or delegating agent).
|
||||
pub source: TurnSourceDto,
|
||||
/// The turn's **complete** text (not truncated).
|
||||
pub text: String,
|
||||
/// Character length of the text.
|
||||
pub text_len: usize,
|
||||
}
|
||||
|
||||
impl From<TurnView> for TurnViewDto {
|
||||
fn from(turn: TurnView) -> Self {
|
||||
Self {
|
||||
id: turn.id.to_string(),
|
||||
at_ms: turn.at_ms,
|
||||
role: turn.role,
|
||||
source: turn.source.into(),
|
||||
text: turn.text,
|
||||
text_len: turn.text_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A page of the human transcript, oldest-to-newest.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TurnPageDto {
|
||||
/// The page's turns, oldest to newest.
|
||||
pub turns: Vec<TurnViewDto>,
|
||||
/// Whether more turns exist beyond the page in the travel direction.
|
||||
pub has_more: bool,
|
||||
/// Last turn id of the page (anchor for the next request), or `None` if empty.
|
||||
pub next_anchor: Option<String>,
|
||||
}
|
||||
|
||||
impl From<TurnPage> for TurnPageDto {
|
||||
fn from(page: TurnPage) -> Self {
|
||||
Self {
|
||||
turns: page.turns.into_iter().map(TurnViewDto::from).collect(),
|
||||
has_more: page.has_more,
|
||||
next_anchor: page.next_anchor.map(|id| id.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ls6_pagination_tests {
|
||||
//! LS6 — DTO de la lecture humaine paginée : parsing du curseur + mapping riche
|
||||
//! `TurnPage → TurnPageDto` (texte complet non borné, source Human/Agent, `has_more`,
|
||||
//! `next_anchor`). C'est le « round-trip d'une page » testable sans `State` Tauri.
|
||||
|
||||
use super::*;
|
||||
use application::{TurnPage, TurnSource, TurnView};
|
||||
|
||||
fn tid(n: u128) -> domain::TurnId {
|
||||
domain::TurnId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_cursor_defaults_to_backward_and_parses_anchor() {
|
||||
// Pas de direction ⇒ backward (la vue humaine ouvre sur la page la plus récente).
|
||||
let req = ReadConversationPageRequestDto {
|
||||
project_id: "p".into(),
|
||||
conversation_id: "c".into(),
|
||||
anchor: None,
|
||||
direction: None,
|
||||
limit: None,
|
||||
};
|
||||
let cur = req.cursor();
|
||||
assert_eq!(cur.direction, PageDirection::Backward);
|
||||
assert_eq!(cur.anchor, None);
|
||||
|
||||
// Forward (insensible à la casse) + ancre UUID valide.
|
||||
let u = uuid::Uuid::from_u128(9);
|
||||
let req = ReadConversationPageRequestDto {
|
||||
project_id: "p".into(),
|
||||
conversation_id: "c".into(),
|
||||
anchor: Some(u.to_string()),
|
||||
direction: Some("Forward".into()),
|
||||
limit: Some(10),
|
||||
};
|
||||
let cur = req.cursor();
|
||||
assert_eq!(cur.direction, PageDirection::Forward);
|
||||
assert_eq!(cur.anchor, Some(domain::TurnId::from_uuid(u)));
|
||||
|
||||
// Ancre non-UUID ⇒ dégrade en None (page depuis un bout du fil), jamais d'erreur.
|
||||
let req = ReadConversationPageRequestDto {
|
||||
project_id: "p".into(),
|
||||
conversation_id: "c".into(),
|
||||
anchor: Some("not-a-uuid".into()),
|
||||
direction: Some("backward".into()),
|
||||
limit: None,
|
||||
};
|
||||
assert_eq!(req.cursor().anchor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_page_dto_round_trips_full_text_and_maps_sources() {
|
||||
let big = "Z".repeat(40_000);
|
||||
let page = TurnPage {
|
||||
turns: vec![
|
||||
TurnView {
|
||||
id: tid(1),
|
||||
at_ms: 1_700_000_000_000,
|
||||
role: TurnRole::Prompt,
|
||||
source: TurnSource::Human,
|
||||
text: big.clone(),
|
||||
text_len: big.chars().count(),
|
||||
},
|
||||
TurnView {
|
||||
id: tid(2),
|
||||
at_ms: 1_700_000_000_001,
|
||||
role: TurnRole::Response,
|
||||
source: TurnSource::Agent {
|
||||
agent_id: domain::AgentId::from_uuid(uuid::Uuid::from_u128(7)),
|
||||
},
|
||||
text: "réponse".to_owned(),
|
||||
text_len: 7,
|
||||
},
|
||||
],
|
||||
has_more: true,
|
||||
next_anchor: Some(tid(2)),
|
||||
};
|
||||
|
||||
let dto = TurnPageDto::from(page);
|
||||
// Texte complet préservé (non borné) + text_len.
|
||||
assert_eq!(dto.turns[0].text.chars().count(), 40_000);
|
||||
assert_eq!(dto.turns[0].text_len, 40_000);
|
||||
// next_anchor stringifié, has_more porté.
|
||||
assert!(dto.has_more);
|
||||
assert_eq!(
|
||||
dto.next_anchor.as_deref(),
|
||||
Some(tid(2).to_string().as_str())
|
||||
);
|
||||
|
||||
// Round-trip JSON (la « page » telle qu'envoyée au front) : camelCase + source.
|
||||
let json = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(json["hasMore"], serde_json::json!(true));
|
||||
assert_eq!(json["nextAnchor"], serde_json::json!(tid(2).to_string()));
|
||||
assert_eq!(
|
||||
json["turns"][0]["source"]["kind"],
|
||||
serde_json::json!("human")
|
||||
);
|
||||
assert_eq!(
|
||||
json["turns"][1]["source"]["kind"],
|
||||
serde_json::json!("agent")
|
||||
);
|
||||
assert_eq!(
|
||||
json["turns"][1]["source"]["agentId"],
|
||||
serde_json::json!(uuid::Uuid::from_u128(7).to_string())
|
||||
);
|
||||
// Le texte intégral survit au passage JSON.
|
||||
assert_eq!(
|
||||
json["turns"][0]["text"].as_str().unwrap().chars().count(),
|
||||
40_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_turn_page_dto_has_no_next_anchor() {
|
||||
let dto = TurnPageDto::from(TurnPage {
|
||||
turns: Vec::new(),
|
||||
has_more: false,
|
||||
next_anchor: None,
|
||||
});
|
||||
assert!(dto.turns.is_empty() && !dto.has_more && dto.next_anchor.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +172,16 @@ pub enum DomainEventDto {
|
||||
/// badge the source.
|
||||
source: OrchestrationSourceDto,
|
||||
},
|
||||
/// The project's orchestrator designation changed (T1).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
OrchestratorChanged {
|
||||
/// The project whose designation changed.
|
||||
project_id: String,
|
||||
/// The newly designated orchestrator agent, or `None` for the default
|
||||
/// (the oldest agent orchestrates).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
orchestrator: Option<String>,
|
||||
},
|
||||
/// A memory note was created or updated.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MemorySaved {
|
||||
@ -401,6 +411,13 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
ok: *ok,
|
||||
source: (*source).into(),
|
||||
},
|
||||
DomainEvent::OrchestratorChanged {
|
||||
project_id,
|
||||
orchestrator,
|
||||
} => Self::OrchestratorChanged {
|
||||
project_id: project_id.to_string(),
|
||||
orchestrator: orchestrator.as_ref().map(|a| a.to_string()),
|
||||
},
|
||||
DomainEvent::MemorySaved { slug } => Self::MemorySaved {
|
||||
slug: slug.as_str().to_string(),
|
||||
},
|
||||
|
||||
@ -75,6 +75,12 @@ pub fn run() {
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("failed to resolve the app data directory");
|
||||
// Point the orchestrator's best-effort diagnostics at a persistent file
|
||||
// (`<app-data>/logs/idea.log`) so inter-agent rendezvous beacons survive a
|
||||
// click-launched AppImage (whose stderr is otherwise discarded). Best-effort:
|
||||
// if the file can't be opened the beacons simply stay on stderr.
|
||||
application::diag::set_log_path(app_data_dir.join("logs").join("idea.log"));
|
||||
application::diag!("[startup] IdeA launched; diagnostics log armed");
|
||||
let app_state = AppState::build(app_data_dir);
|
||||
|
||||
// Wire the domain event bus → Tauri events relay.
|
||||
@ -157,8 +163,11 @@ pub fn run() {
|
||||
commands::dismiss_embedder_suggestion,
|
||||
commands::create_agent,
|
||||
commands::list_agents,
|
||||
commands::get_project_work_state,
|
||||
commands::read_conversation_page,
|
||||
commands::list_live_agents,
|
||||
commands::attach_live_agent,
|
||||
commands::stop_live_agent,
|
||||
commands::read_agent_context,
|
||||
commands::update_agent_context,
|
||||
commands::delete_agent,
|
||||
|
||||
@ -508,8 +508,14 @@ mod tests {
|
||||
|
||||
let out = String::from_utf8(cli_out).unwrap();
|
||||
// Both request responses arrived; the notification produced none.
|
||||
assert!(out.contains("\"id\":1"), "missing initialize response: {out}");
|
||||
assert!(out.contains("\"id\":2"), "missing tools/list response: {out}");
|
||||
assert!(
|
||||
out.contains("\"id\":1"),
|
||||
"missing initialize response: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("\"id\":2"),
|
||||
"missing tools/list response: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// stdin EOF with no request ⇒ relay returns `Ok` (code 0), having still sent
|
||||
|
||||
@ -87,13 +87,29 @@ impl McpEndpoint {
|
||||
/// per-user runtime dir (`$XDG_RUNTIME_DIR`, falling back to `$TMPDIR`, then
|
||||
/// `/tmp`). Kept short so the full socket path stays well under the ~108-byte
|
||||
/// `sockaddr_un` limit (`/run/user/<uid>/idea-mcp/<32 hex>.sock` ≈ 50 bytes).
|
||||
///
|
||||
/// The candidate bases are tried **in priority order**, returning the first whose
|
||||
/// `idea-mcp` subdir exists or can be created. A set-but-unusable `$XDG_RUNTIME_DIR`
|
||||
/// (e.g. a sandbox/CI where it points at a non-existent or read-only path) must not
|
||||
/// dead-end the loopback bind: without this fallback the socket silently never
|
||||
/// binds and inter-agent delegation dies. Deterministic for a given environment, so
|
||||
/// the bind side and any `socket_path()` reader always agree on the same directory.
|
||||
#[cfg(unix)]
|
||||
fn unix_runtime_dir() -> PathBuf {
|
||||
let base = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("TMPDIR").map(PathBuf::from))
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
base.join("idea-mcp")
|
||||
let candidates = [
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
|
||||
std::env::var_os("TMPDIR").map(PathBuf::from),
|
||||
Some(PathBuf::from("/tmp")),
|
||||
];
|
||||
for base in candidates.into_iter().flatten() {
|
||||
let dir = base.join("idea-mcp");
|
||||
if dir.is_dir() || std::fs::create_dir_all(&dir).is_ok() {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
// Last resort: keep the historical `/tmp` target even if creation just failed
|
||||
// (the bind will surface the real error rather than silently picking nowhere).
|
||||
PathBuf::from("/tmp").join("idea-mcp")
|
||||
}
|
||||
|
||||
/// **Single source of truth.** Computes the deterministic loopback endpoint of a
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "IdeA",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"identifier": "app.idea.ide",
|
||||
"build": {
|
||||
"frontendDist": "../../frontend/dist",
|
||||
|
||||
@ -9,7 +9,9 @@ use app_tauri_lib::dto::{
|
||||
};
|
||||
use application::AppError;
|
||||
use application::{
|
||||
CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput,
|
||||
AgentTicketState, AgentWorkState, CreateAgentOutput, InspectConversationOutput,
|
||||
LaunchAgentOutput, ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
|
||||
TicketWorkSource, TicketWorkStatus,
|
||||
};
|
||||
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
|
||||
use domain::ports::ConversationDetails;
|
||||
@ -197,6 +199,187 @@ fn live_agent_list_dto_serialises_camelcase_array() {
|
||||
assert!(arr[0].get("node_id").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_work_state_dto_serialises_live_and_busy_camelcase() {
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
|
||||
let node = NodeId::from_uuid(Uuid::from_u128(13));
|
||||
let session = SessionId::from_uuid(Uuid::from_u128(14));
|
||||
let ticket = domain::TicketId::from_uuid(Uuid::from_u128(15));
|
||||
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
|
||||
agents: vec![AgentWorkState {
|
||||
agent_id: agent,
|
||||
name: "Worker".to_owned(),
|
||||
profile_id: profile,
|
||||
live: Some(LiveWorkSession {
|
||||
node_id: node,
|
||||
session_id: session,
|
||||
kind: LiveSessionKind::Structured,
|
||||
}),
|
||||
busy: domain::AgentBusyState::Busy {
|
||||
ticket,
|
||||
since_ms: 1_234,
|
||||
},
|
||||
tickets: vec![],
|
||||
}],
|
||||
conversations: vec![],
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["agents"][0]["agentId"], agent.to_string());
|
||||
assert_eq!(v["agents"][0]["profileId"], profile.to_string());
|
||||
assert_eq!(v["agents"][0]["live"]["nodeId"], node.to_string());
|
||||
assert_eq!(v["agents"][0]["live"]["sessionId"], session.to_string());
|
||||
assert_eq!(v["agents"][0]["live"]["kind"], "structured");
|
||||
assert_eq!(v["agents"][0]["busy"]["state"], "busy");
|
||||
assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string());
|
||||
assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234);
|
||||
assert_eq!(v["agents"][0]["tickets"], json!([]));
|
||||
assert!(v["agents"][0].get("agent_id").is_none());
|
||||
assert!(v["agents"][0]["live"].get("session_id").is_none());
|
||||
assert!(v["agents"][0]["busy"].get("since_ms").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_work_state_dto_serialises_tickets_camelcase() {
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
|
||||
let requester = AgentId::from_uuid(Uuid::from_u128(20));
|
||||
let head_ticket = domain::TicketId::from_uuid(Uuid::from_u128(30));
|
||||
let next_ticket = domain::TicketId::from_uuid(Uuid::from_u128(31));
|
||||
let conversation = domain::ConversationId::from_uuid(Uuid::from_u128(40));
|
||||
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
|
||||
agents: vec![AgentWorkState {
|
||||
agent_id: agent,
|
||||
name: "Worker".to_owned(),
|
||||
profile_id: profile,
|
||||
live: None,
|
||||
busy: domain::AgentBusyState::Idle,
|
||||
tickets: vec![
|
||||
AgentTicketState {
|
||||
ticket_id: head_ticket,
|
||||
conversation_id: conversation,
|
||||
position: 0,
|
||||
status: TicketWorkStatus::InProgress,
|
||||
source: TicketWorkSource::Agent {
|
||||
agent_id: requester,
|
||||
},
|
||||
requester_label: "Main".to_owned(),
|
||||
task_preview: "Analyser le module".to_owned(),
|
||||
task_len: 842,
|
||||
},
|
||||
AgentTicketState {
|
||||
ticket_id: next_ticket,
|
||||
conversation_id: conversation,
|
||||
position: 1,
|
||||
status: TicketWorkStatus::Queued,
|
||||
source: TicketWorkSource::Human,
|
||||
requester_label: "User".to_owned(),
|
||||
task_preview: "Vérifier".to_owned(),
|
||||
task_len: 8,
|
||||
},
|
||||
],
|
||||
}],
|
||||
conversations: vec![],
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
let head = &v["agents"][0]["tickets"][0];
|
||||
assert_eq!(head["ticketId"], head_ticket.to_string());
|
||||
assert_eq!(head["conversationId"], conversation.to_string());
|
||||
assert_eq!(head["position"], 0);
|
||||
assert_eq!(head["status"], "inProgress");
|
||||
assert_eq!(head["source"]["kind"], "agent");
|
||||
assert_eq!(head["source"]["agentId"], requester.to_string());
|
||||
assert_eq!(head["requesterLabel"], "Main");
|
||||
assert_eq!(head["taskPreview"], "Analyser le module");
|
||||
assert_eq!(head["taskLen"], 842);
|
||||
// snake_case must not leak.
|
||||
assert!(head.get("ticket_id").is_none());
|
||||
assert!(head.get("task_preview").is_none());
|
||||
assert!(head["source"].get("agent_id").is_none());
|
||||
|
||||
let next = &v["agents"][0]["tickets"][1];
|
||||
assert_eq!(next["status"], "queued");
|
||||
assert_eq!(next["source"]["kind"], "human");
|
||||
assert!(next["source"].get("agentId").is_none());
|
||||
assert_eq!(next["requesterLabel"], "User");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_work_state_dto_serialises_conversations_camelcase() {
|
||||
use application::{
|
||||
ConversationPreviewStatus, ConversationTurnWorkPreview, ConversationWorkSummary,
|
||||
};
|
||||
use domain::{ConversationId, TurnId, TurnRole};
|
||||
|
||||
let requester = AgentId::from_uuid(Uuid::from_u128(20));
|
||||
let ready_conv = ConversationId::from_uuid(Uuid::from_u128(40));
|
||||
let missing_conv = ConversationId::from_uuid(Uuid::from_u128(41));
|
||||
let up_to = TurnId::from_uuid(Uuid::from_u128(50));
|
||||
|
||||
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
|
||||
agents: vec![],
|
||||
conversations: vec![
|
||||
ConversationWorkSummary {
|
||||
conversation_id: ready_conv,
|
||||
status: ConversationPreviewStatus::Ready,
|
||||
objective_preview: Some("Livrer le lot C".to_owned()),
|
||||
summary_preview: Some("Résumé du fil".to_owned()),
|
||||
summary_len: 1024,
|
||||
up_to: Some(up_to),
|
||||
recent_turns: vec![],
|
||||
},
|
||||
ConversationWorkSummary {
|
||||
conversation_id: missing_conv,
|
||||
status: ConversationPreviewStatus::Missing,
|
||||
objective_preview: None,
|
||||
summary_preview: None,
|
||||
summary_len: 0,
|
||||
up_to: None,
|
||||
recent_turns: vec![ConversationTurnWorkPreview {
|
||||
role: TurnRole::Prompt,
|
||||
source: TicketWorkSource::Agent {
|
||||
agent_id: requester,
|
||||
},
|
||||
at_ms: 1_700,
|
||||
text_preview: "Analyse le module".to_owned(),
|
||||
text_len: 17,
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
let ready = &v["conversations"][0];
|
||||
assert_eq!(ready["conversationId"], ready_conv.to_string());
|
||||
assert_eq!(ready["status"], "ready");
|
||||
assert_eq!(ready["objectivePreview"], "Livrer le lot C");
|
||||
assert_eq!(ready["summaryPreview"], "Résumé du fil");
|
||||
assert_eq!(ready["summaryLen"], 1024);
|
||||
assert_eq!(ready["upTo"], up_to.to_string());
|
||||
assert_eq!(ready["recentTurns"], json!([]));
|
||||
// No snake_case leak.
|
||||
assert!(ready.get("conversation_id").is_none());
|
||||
assert!(ready.get("summary_preview").is_none());
|
||||
assert!(ready.get("up_to").is_none());
|
||||
|
||||
let missing = &v["conversations"][1];
|
||||
assert_eq!(missing["status"], "missing");
|
||||
// Absent previews surface as null (predictable, not omitted).
|
||||
assert!(missing["summaryPreview"].is_null());
|
||||
assert!(missing["upTo"].is_null());
|
||||
let turn = &missing["recentTurns"][0];
|
||||
assert_eq!(turn["role"], "prompt");
|
||||
assert_eq!(turn["source"]["kind"], "agent");
|
||||
assert_eq!(turn["source"]["agentId"], requester.to_string());
|
||||
assert_eq!(turn["atMs"], 1_700);
|
||||
assert_eq!(turn["textPreview"], "Analyse le module");
|
||||
assert_eq!(turn["textLen"], 17);
|
||||
assert!(turn.get("at_ms").is_none());
|
||||
assert!(turn.get("text_preview").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_agent_request_carries_conversation_id_for_resume() {
|
||||
let raw = json!({
|
||||
@ -369,3 +552,82 @@ fn launch_agent_output_propagates_assigned_conversation_id() {
|
||||
"no snake_case leak"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lot D — attach/stop live agent DTOs (controlled actions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn attach_live_agent_request_deserialises_camelcase() {
|
||||
use app_tauri_lib::dto::AttachLiveAgentRequestDto;
|
||||
|
||||
let raw = json!({
|
||||
"projectId": Uuid::from_u128(1).to_string(),
|
||||
"agentId": Uuid::from_u128(2).to_string(),
|
||||
"nodeId": Uuid::from_u128(3).to_string()
|
||||
});
|
||||
let dto: AttachLiveAgentRequestDto = serde_json::from_value(raw).unwrap();
|
||||
assert_eq!(dto.project_id, Uuid::from_u128(1).to_string());
|
||||
assert_eq!(dto.agent_id, Uuid::from_u128(2).to_string());
|
||||
assert_eq!(dto.node_id, Uuid::from_u128(3).to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_live_agent_response_serialises_camelcase_with_kind() {
|
||||
use app_tauri_lib::dto::AttachLiveAgentResponseDto;
|
||||
use application::{AttachLiveAgentOutput, LiveSessionKind};
|
||||
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let node = NodeId::from_uuid(Uuid::from_u128(12));
|
||||
let session = SessionId::from_uuid(Uuid::from_u128(13));
|
||||
let dto = AttachLiveAgentResponseDto::from(AttachLiveAgentOutput {
|
||||
agent_id: agent,
|
||||
node_id: node,
|
||||
session_id: session,
|
||||
kind: LiveSessionKind::Pty,
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["agentId"], agent.to_string());
|
||||
assert_eq!(v["nodeId"], node.to_string());
|
||||
assert_eq!(v["sessionId"], session.to_string());
|
||||
assert_eq!(v["kind"], "pty");
|
||||
// No snake_case leak.
|
||||
assert!(v.get("agent_id").is_none());
|
||||
assert!(v.get("session_id").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_live_agent_request_deserialises_camelcase() {
|
||||
use app_tauri_lib::dto::StopLiveAgentRequestDto;
|
||||
|
||||
let raw = json!({
|
||||
"projectId": Uuid::from_u128(1).to_string(),
|
||||
"agentId": Uuid::from_u128(2).to_string()
|
||||
});
|
||||
let dto: StopLiveAgentRequestDto = serde_json::from_value(raw).unwrap();
|
||||
assert_eq!(dto.project_id, Uuid::from_u128(1).to_string());
|
||||
assert_eq!(dto.agent_id, Uuid::from_u128(2).to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_live_agent_response_serialises_camelcase_with_kind() {
|
||||
use app_tauri_lib::dto::StopLiveAgentResponseDto;
|
||||
use application::{LiveSessionKind, StopLiveAgentOutput};
|
||||
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let session = SessionId::from_uuid(Uuid::from_u128(13));
|
||||
let dto = StopLiveAgentResponseDto::from(StopLiveAgentOutput {
|
||||
agent_id: agent,
|
||||
session_id: session,
|
||||
kind: LiveSessionKind::Structured,
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["agentId"], agent.to_string());
|
||||
assert_eq!(v["sessionId"], session.to_string());
|
||||
assert_eq!(v["kind"], "structured");
|
||||
// Stop response carries no node id (the session is gone).
|
||||
assert!(v.get("nodeId").is_none());
|
||||
assert!(v.get("session_id").is_none());
|
||||
}
|
||||
|
||||
@ -16,9 +16,9 @@ use std::time::Duration;
|
||||
use app_tauri_lib::state::AppState;
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ids::NodeId;
|
||||
use domain::ports::IdGenerator;
|
||||
use domain::AgentId;
|
||||
use infrastructure::UuidGenerator;
|
||||
use domain::ports::IdGenerator;
|
||||
|
||||
fn temp_path(tag: &str) -> PathBuf {
|
||||
let ids = UuidGenerator::new();
|
||||
@ -124,7 +124,10 @@ async fn set_resume_at_resolves_no_cell_for_an_agent_without_a_live_session() {
|
||||
.structured_sessions
|
||||
.node_for_agent(&unknown)
|
||||
.or_else(|| state.terminal_sessions.node_for_agent(&unknown));
|
||||
assert!(resolved.is_none(), "set_resume_at ⇒ NOT_FOUND (aucun armement orphelin)");
|
||||
assert!(
|
||||
resolved.is_none(),
|
||||
"set_resume_at ⇒ NOT_FOUND (aucun armement orphelin)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Parité runtime du filet humain : `confirm_human_resume` (la délégation de
|
||||
@ -156,8 +159,14 @@ async fn confirm_human_resume_arms_a_cancellable_resume_over_the_real_bus() {
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
assert!(saw_rate_limited, "AgentRateLimited relayed on the bus (humain)");
|
||||
assert!(saw_scheduled, "AgentResumeScheduled relayed on the bus (humain)");
|
||||
assert!(
|
||||
saw_rate_limited,
|
||||
"AgentRateLimited relayed on the bus (humain)"
|
||||
);
|
||||
assert!(
|
||||
saw_scheduled,
|
||||
"AgentResumeScheduled relayed on the bus (humain)"
|
||||
);
|
||||
|
||||
// Annulable par la même voie que l'auto.
|
||||
assert!(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "application"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
@ -24,6 +24,11 @@ use domain::profile::{
|
||||
StructuredAdapter,
|
||||
};
|
||||
|
||||
/// Codex's interactive TUI is sensitive to receiving a large pasted block and the
|
||||
/// submit key too close together. Keep the default conservative so delegated
|
||||
/// prompts are actually submitted instead of remaining in the input editor.
|
||||
pub const CODEX_SUBMIT_DELAY_MS: u32 = 350;
|
||||
|
||||
/// A fixed UUID namespace used to derive stable ids for reference profiles.
|
||||
/// (Random-looking but constant; only its stability matters.)
|
||||
const REFERENCE_NAMESPACE: uuid::Uuid = uuid::uuid!("6f9b1d2a-7c34-4e58-9a1b-2c3d4e5f6a7b");
|
||||
@ -82,6 +87,7 @@ pub fn reference_profiles() -> Vec<AgentProfile> {
|
||||
.expect("codex reference profile is valid")
|
||||
.with_structured_adapter(StructuredAdapter::Codex)
|
||||
.with_projector(ProjectorKey::Codex)
|
||||
.with_submit_delay_ms(CODEX_SUBMIT_DELAY_MS)
|
||||
.with_mcp(McpCapability::new(
|
||||
// Codex lit ses serveurs MCP dans `$CODEX_HOME/config.toml`, pas `.mcp.json` :
|
||||
// IdeA écrit ce TOML DANS le run dir et pointe `CODEX_HOME` dessus pour
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,15 +22,19 @@ pub use structured::{
|
||||
drain_with_readiness, drain_with_readiness_outcome, send_blocking, TurnOutcome,
|
||||
};
|
||||
|
||||
pub use catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
|
||||
pub use catalogue::{
|
||||
reference_profile_id, reference_profiles,
|
||||
selectable_reference_profiles, CODEX_SUBMIT_DELAY_MS,
|
||||
};
|
||||
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
||||
pub use lifecycle::{
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
|
||||
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, HandoffProvider,
|
||||
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||
ListAgentsOutput, McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider,
|
||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredSessionDescriptor,
|
||||
UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
InjectedLiveRow, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||
ListAgentsOutput, LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry,
|
||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, LIVE_STATE_INJECT_MAX,
|
||||
};
|
||||
pub use resume::{
|
||||
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
||||
|
||||
@ -35,8 +35,7 @@ use crate::error::AppError;
|
||||
/// `--resume` (via [`domain::ports::SessionPlan::Resume`]) porte déjà tout
|
||||
/// l'historique : ce prompt n'a qu'à **réamorcer** le tour, pas reconstruire le
|
||||
/// contexte. Volontairement neutre et model-agnostique.
|
||||
pub const RESUME_PROMPT: &str =
|
||||
"La limite de session est levée. Reprends là où tu t'étais arrêté.";
|
||||
pub const RESUME_PROMPT: &str = "La limite de session est levée. Reprends là où tu t'étais arrêté.";
|
||||
|
||||
/// Port applicatif de **reprise d'un agent** (frontière implémentée au composition
|
||||
/// root, LS7). Calqué sur les autres traits-passerelles de l'application
|
||||
@ -161,7 +160,13 @@ impl SessionLimitService {
|
||||
conversation_id,
|
||||
} = plan_resume(now, &limit, conversation_id)
|
||||
{
|
||||
self.arm_scheduled(agent_id, fire_at_ms, node_id, conversation_id, Some(resets_at_ms));
|
||||
self.arm_scheduled(
|
||||
agent_id,
|
||||
fire_at_ms,
|
||||
node_id,
|
||||
conversation_id,
|
||||
Some(resets_at_ms),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,9 +204,14 @@ impl SessionLimitService {
|
||||
conversation_id,
|
||||
},
|
||||
);
|
||||
self.armed.lock().expect("session-limit mutex sain").insert(agent_id, id);
|
||||
self.events
|
||||
.publish(DomainEvent::AgentResumeScheduled { agent_id, fire_at_ms });
|
||||
self.armed
|
||||
.lock()
|
||||
.expect("session-limit mutex sain")
|
||||
.insert(agent_id, id);
|
||||
self.events.publish(DomainEvent::AgentResumeScheduled {
|
||||
agent_id,
|
||||
fire_at_ms,
|
||||
});
|
||||
}
|
||||
|
||||
/// **(b) Exécution de la reprise.** Consomme une [`ScheduledTask::ResumeAgent`]
|
||||
@ -255,7 +265,10 @@ impl SessionLimitService {
|
||||
};
|
||||
|
||||
if self.scheduler.cancel(id) {
|
||||
self.armed.lock().expect("session-limit mutex sain").remove(&agent_id);
|
||||
self.armed
|
||||
.lock()
|
||||
.expect("session-limit mutex sain")
|
||||
.remove(&agent_id);
|
||||
self.events
|
||||
.publish(DomainEvent::AgentResumeCancelled { agent_id });
|
||||
true
|
||||
|
||||
@ -14,8 +14,8 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use domain::input::InputMediator;
|
||||
use domain::ids::AgentId;
|
||||
use domain::input::InputMediator;
|
||||
use domain::ports::{AgentSession, AgentSessionError, ReplyEvent};
|
||||
use domain::readiness::{ReadinessPolicy, ReadinessSignal};
|
||||
|
||||
@ -178,16 +178,15 @@ async fn drain_bounded_events(
|
||||
on_signal: impl FnMut(ReadinessSignal),
|
||||
) -> Result<TurnOutcome, AgentSessionError> {
|
||||
match timeout {
|
||||
Some(dur) => match tokio::time::timeout(
|
||||
dur,
|
||||
drain_to_final(session, prompt, on_event, on_signal),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
// La session **reste vivante** : on ne `shutdown` rien ici (§17.1).
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
},
|
||||
Some(dur) => {
|
||||
match tokio::time::timeout(dur, drain_to_final(session, prompt, on_event, on_signal))
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
// La session **reste vivante** : on ne `shutdown` rien ici (§17.1).
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
}
|
||||
}
|
||||
None => drain_to_final(session, prompt, on_event, on_signal).await,
|
||||
}
|
||||
}
|
||||
@ -302,11 +301,10 @@ mod tests {
|
||||
AgentBusyState::Idle
|
||||
}
|
||||
fn bind_handle(&self, _agent: AgentId, _handle: PtyHandle) {}
|
||||
fn bind_handle_with_prompt(
|
||||
fn bind_handle_with_submit(
|
||||
&self,
|
||||
_agent: AgentId,
|
||||
_handle: PtyHandle,
|
||||
_pattern: Option<String>,
|
||||
_submit: SubmitConfig,
|
||||
) {
|
||||
}
|
||||
@ -317,7 +315,9 @@ mod tests {
|
||||
let session = FakeSession {
|
||||
events: vec![
|
||||
ReplyEvent::TextDelta { text: "a".into() },
|
||||
ReplyEvent::ToolActivity { label: "lit".into() },
|
||||
ReplyEvent::ToolActivity {
|
||||
label: "lit".into(),
|
||||
},
|
||||
ReplyEvent::Heartbeat,
|
||||
ReplyEvent::Final {
|
||||
content: "fini".into(),
|
||||
|
||||
@ -13,6 +13,13 @@
|
||||
//! the PTY or `app-tauri` (that is P6b). It is fully testable with in-memory fakes
|
||||
//! of the three ports.
|
||||
|
||||
mod paginate;
|
||||
mod record;
|
||||
mod rotate;
|
||||
|
||||
pub use paginate::{
|
||||
ConversationArchiveProvider, ReadConversationPage, ReadConversationPageInput, TurnPage,
|
||||
TurnSource, TurnView,
|
||||
};
|
||||
pub use record::RecordTurn;
|
||||
pub use rotate::{RotateConversationLog, RotateConversationLogInput};
|
||||
|
||||
148
crates/application/src/conversation/paginate.rs
Normal file
148
crates/application/src/conversation/paginate.rs
Normal file
@ -0,0 +1,148 @@
|
||||
//! [`ReadConversationPage`] — lecture **humaine** paginée d'une conversation (lot LS6).
|
||||
//!
|
||||
//! Lecture archive-aware (segments d'archive + actif) exposée comme un **DTO humain
|
||||
//! riche** : contrairement au handoff (résumé borné) ou au read-model work-state
|
||||
//! (aperçus tronqués), la page humaine porte le **texte complet, non borné** de chaque
|
||||
//! tour — c'est la vue « transcript » destinée à l'utilisateur (le React arrive en LS7).
|
||||
//!
|
||||
//! Hors chemin chaud : composé du seul port [`ConversationArchive`] (via un provider par
|
||||
//! root), jamais d'`append`/de rotation ici.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::project::ProjectPath;
|
||||
use domain::{AgentId, ConversationArchive, ConversationId, PageCursor, TurnId, TurnRole};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// Fournit le [`ConversationArchive`] **lié au project root** courant (lot LS6),
|
||||
/// calqué sur [`crate::HandoffProvider`]/[`crate::RecordTurnProvider`].
|
||||
///
|
||||
/// Les use cases d'archivage/pagination ([`crate::RotateConversationLog`],
|
||||
/// [`ReadConversationPage`]) sont **uniques** pour tous les projets, alors que les logs
|
||||
/// sont **par project root** (`<root>/.ideai/conversations/`) et l'adapter `Fs*` fixe sa
|
||||
/// racine à la construction. Ce port matérialise un archive ciblant le **bon** dossier à
|
||||
/// chaque appel. `None` ⇒ pas d'archivage/pagination (best-effort absent). Implémenté
|
||||
/// dans `app-tauri` (seul détenteur des adapters `Fs*`).
|
||||
pub trait ConversationArchiveProvider: Send + Sync {
|
||||
/// Construit le [`ConversationArchive`] dont la persistance cible `root`, ou `None`.
|
||||
fn conversation_archive_for(&self, root: &ProjectPath) -> Option<Arc<dyn ConversationArchive>>;
|
||||
}
|
||||
|
||||
/// Origine d'un tour, pour la vue humaine paginée (miroir de [`domain::input::InputSource`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TurnSource {
|
||||
/// L'opérateur humain.
|
||||
Human,
|
||||
/// Un autre agent (délégation via `idea_ask_agent`).
|
||||
Agent {
|
||||
/// L'agent à l'origine du tour.
|
||||
agent_id: AgentId,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<domain::input::InputSource> for TurnSource {
|
||||
fn from(source: domain::input::InputSource) -> Self {
|
||||
match source {
|
||||
domain::input::InputSource::Human => Self::Human,
|
||||
domain::input::InputSource::Agent { agent_id } => Self::Agent { agent_id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Un tour projeté pour la vue humaine (lot LS6) — **texte complet, non borné**.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TurnView {
|
||||
/// Identifiant stable du tour (sert aussi d'ancre de pagination).
|
||||
pub id: TurnId,
|
||||
/// Horodatage (epoch millisecondes).
|
||||
pub at_ms: u64,
|
||||
/// Nature du tour (invite, réponse, activité outillée).
|
||||
pub role: TurnRole,
|
||||
/// Origine (humain ou agent délégant).
|
||||
pub source: TurnSource,
|
||||
/// Le **texte intégral** du tour (jamais tronqué — c'est la vue transcript).
|
||||
pub text: String,
|
||||
/// Longueur (en caractères) du texte, pour l'UI.
|
||||
pub text_len: usize,
|
||||
}
|
||||
|
||||
impl From<domain::ConversationTurn> for TurnView {
|
||||
fn from(turn: domain::ConversationTurn) -> Self {
|
||||
let text_len = turn.text.chars().count();
|
||||
Self {
|
||||
id: turn.id,
|
||||
at_ms: turn.at_ms,
|
||||
role: turn.role,
|
||||
source: turn.source.into(),
|
||||
text: turn.text,
|
||||
text_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Une page de tours pour la vue humaine (lot LS6).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TurnPage {
|
||||
/// Les tours de la page, **du plus ancien au plus récent**.
|
||||
pub turns: Vec<TurnView>,
|
||||
/// `true` s'il existe d'autres tours au-delà de la page dans le sens de progression.
|
||||
pub has_more: bool,
|
||||
/// L'id du **dernier** tour de la page (ancre pour la requête suivante) ; `None` si
|
||||
/// la page est vide.
|
||||
pub next_anchor: Option<TurnId>,
|
||||
}
|
||||
|
||||
/// Entrée de [`ReadConversationPage::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadConversationPageInput {
|
||||
/// Le project root dont dériver l'archive.
|
||||
pub project_root: ProjectPath,
|
||||
/// La conversation (paire) à lire.
|
||||
pub conversation: ConversationId,
|
||||
/// Le curseur (ancre + sens) de pagination.
|
||||
pub cursor: PageCursor,
|
||||
/// La taille de page demandée (`0` ⇒ défaut ; clampée par le domaine).
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// Use case de lecture humaine paginée (lot LS6) — consomme le seul
|
||||
/// [`ConversationArchive`] (résolu par root via le provider).
|
||||
pub struct ReadConversationPage {
|
||||
archives: Arc<dyn ConversationArchiveProvider>,
|
||||
}
|
||||
|
||||
impl ReadConversationPage {
|
||||
/// Construit le use case à partir du provider d'archive par root.
|
||||
#[must_use]
|
||||
pub fn new(archives: Arc<dyn ConversationArchiveProvider>) -> Self {
|
||||
Self { archives }
|
||||
}
|
||||
|
||||
/// Lit une page de `conversation`, mappée vers le DTO humain riche.
|
||||
///
|
||||
/// Provider d'archive absent pour ce root ⇒ page **vide** (best-effort, jamais une
|
||||
/// erreur dure). Le texte de chaque tour est renvoyé **intégralement**.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] si la lecture du log échoue.
|
||||
pub async fn execute(&self, input: ReadConversationPageInput) -> Result<TurnPage, AppError> {
|
||||
let Some(archive) = self.archives.conversation_archive_for(&input.project_root) else {
|
||||
return Ok(TurnPage {
|
||||
turns: Vec::new(),
|
||||
has_more: false,
|
||||
next_anchor: None,
|
||||
});
|
||||
};
|
||||
let slice = archive
|
||||
.page(input.conversation, input.cursor, input.limit)
|
||||
.await?;
|
||||
let next_anchor = slice.turns.last().map(|t| t.id);
|
||||
let turns = slice.turns.into_iter().map(TurnView::from).collect();
|
||||
Ok(TurnPage {
|
||||
turns,
|
||||
has_more: slice.has_more,
|
||||
next_anchor,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,10 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{ConversationId, ConversationLog, ConversationTurn, HandoffStore, HandoffSummarizer};
|
||||
use domain::{
|
||||
bound_handoff_summary, ConversationId, ConversationLog, ConversationTurn, HandoffStore,
|
||||
HandoffSummarizer, HANDOFF_SUMMARY_MAX_CHARS,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@ -63,6 +66,9 @@ impl RecordTurn {
|
||||
/// 1. **append** the turn to the canonical log (source of truth) ;
|
||||
/// 2. **load** the previous handoff (`None` on a first checkpoint) ;
|
||||
/// 3. **fold** `prev` with the single new turn (incremental, best-effort) ;
|
||||
/// 3b. **bound** the folded handoff's summary to [`HANDOFF_SUMMARY_MAX_CHARS`] — a
|
||||
/// **universal cap independent of the summarizer** (heuristic *or* a future LLM):
|
||||
/// idempotent, never rejecting, so it never blocks the checkpoint (LS5) ;
|
||||
/// 4. **save** the advanced handoff.
|
||||
///
|
||||
/// The append happens **first** so the durable source of truth is never behind
|
||||
@ -92,6 +98,13 @@ impl RecordTurn {
|
||||
.fold(prev, std::slice::from_ref(&turn))
|
||||
.await;
|
||||
|
||||
// 3b. **Universal bound** (LS5): clamp the resume summary to
|
||||
// `HANDOFF_SUMMARY_MAX_CHARS`, *independently of the summarizer*. Whatever
|
||||
// produced the handoff — the heuristic or a future LLM summarizer — the
|
||||
// persisted (and later injected) summary is size-bounded. Idempotent and
|
||||
// never rejecting, so it can never block the checkpoint.
|
||||
let handoff = bound_handoff_summary(handoff, HANDOFF_SUMMARY_MAX_CHARS);
|
||||
|
||||
// 4. Persist the advanced handoff (overwrites the previous resume point).
|
||||
self.handoffs.save(conversation, handoff).await?;
|
||||
|
||||
|
||||
95
crates/application/src/conversation/rotate.rs
Normal file
95
crates/application/src/conversation/rotate.rs
Normal file
@ -0,0 +1,95 @@
|
||||
//! [`RotateConversationLog`] — rotation **best-effort** du log d'une conversation (lot LS6).
|
||||
//!
|
||||
//! Déclenchée **hors chemin chaud** (à la reprise/ouverture d'un fil, jamais par un
|
||||
//! `append`), elle borne le segment actif en archivant sa tête froide. L'invariant pivot
|
||||
//! (INV-LS6) est garanti par le **plancher** : `keep_from = up_to` du handoff courant ⇒
|
||||
//! aucun tour `≥ up_to` n'est jamais archivé, donc `read(since = up_to)` (fold incrémental)
|
||||
//! et la reprise restent corrects.
|
||||
//!
|
||||
//! Best-effort strict : pas de handoff (ou `up_to` nil) ⇒ skip ; toute erreur store est
|
||||
//! typée et **avalée par l'appelant** (le câblage app-tauri), jamais propagée en échec de
|
||||
//! reprise.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::project::ProjectPath;
|
||||
use domain::{rotation_plan, ConversationId, RotationDecision, RotationThresholds};
|
||||
|
||||
use crate::conversation::ConversationArchiveProvider;
|
||||
use crate::error::AppError;
|
||||
use crate::HandoffProvider;
|
||||
|
||||
/// Entrée de [`RotateConversationLog::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RotateConversationLogInput {
|
||||
/// Le project root dont dériver l'archive et le handoff.
|
||||
pub project_root: ProjectPath,
|
||||
/// La conversation (paire) à roter.
|
||||
pub conversation: ConversationId,
|
||||
}
|
||||
|
||||
/// Use case de rotation du log (lot LS6) — consomme un [`ConversationArchive`] et le
|
||||
/// [`HandoffStore`] (pour lire `floor = up_to`), tous deux résolus par root.
|
||||
///
|
||||
/// [`ConversationArchive`]: domain::ConversationArchive
|
||||
/// [`HandoffStore`]: domain::HandoffStore
|
||||
pub struct RotateConversationLog {
|
||||
archives: Arc<dyn ConversationArchiveProvider>,
|
||||
handoffs: Arc<dyn HandoffProvider>,
|
||||
thresholds: RotationThresholds,
|
||||
}
|
||||
|
||||
impl RotateConversationLog {
|
||||
/// Construit le use case à partir des providers par root (seuils par défaut).
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
archives: Arc<dyn ConversationArchiveProvider>,
|
||||
handoffs: Arc<dyn HandoffProvider>,
|
||||
) -> Self {
|
||||
Self {
|
||||
archives,
|
||||
handoffs,
|
||||
thresholds: RotationThresholds::defaults(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applique la politique de rotation à `conversation`, si nécessaire.
|
||||
///
|
||||
/// Étapes : résoudre l'archive + le handoff store du root ; lire le plancher
|
||||
/// `floor = up_to` (pas de handoff ⇒ `None` ⇒ skip) ; lire les `stats` bon marché du
|
||||
/// segment actif ; décider via [`rotation_plan`] ; archiver
|
||||
/// (`rotate(keep_from = up_to)`) seulement si la décision est `Archive`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] si la lecture du handoff/stats ou la rotation échoue. L'appelant
|
||||
/// (câblage) **avale** cette erreur : la rotation ne doit jamais casser une reprise.
|
||||
pub async fn execute(&self, input: RotateConversationLogInput) -> Result<(), AppError> {
|
||||
// Sans provider d'archive/handoff câblé pour ce root ⇒ rien à faire (best-effort).
|
||||
let Some(archive) = self.archives.conversation_archive_for(&input.project_root) else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(handoff_store) = self.handoffs.handoff_store_for(&input.project_root) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Plancher = `up_to` du handoff courant (pas de handoff ⇒ None ⇒ skip plus bas).
|
||||
let floor = handoff_store
|
||||
.load(input.conversation)
|
||||
.await?
|
||||
.map(|handoff| handoff.up_to);
|
||||
|
||||
let stats = archive.stats(input.conversation).await?;
|
||||
match rotation_plan(
|
||||
stats.active_turns,
|
||||
stats.active_bytes,
|
||||
floor,
|
||||
self.thresholds,
|
||||
) {
|
||||
RotationDecision::Skip => Ok(()),
|
||||
RotationDecision::Archive { keep_from } => archive
|
||||
.rotate(input.conversation, keep_from)
|
||||
.await
|
||||
.map_err(AppError::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
74
crates/application/src/diag.rs
Normal file
74
crates/application/src/diag.rs
Normal file
@ -0,0 +1,74 @@
|
||||
//! Lightweight, dependency-free diagnostics sink for the orchestrator rendezvous.
|
||||
//!
|
||||
//! The orchestrator already emits best-effort traces through `eprintln!` (see
|
||||
//! [`crate::orchestrator`]). When IdeA is launched by **clicking the AppImage**,
|
||||
//! that stderr is discarded — so an inter-agent block (an `ask` that never gets
|
||||
//! its `idea_reply`) leaves no retrievable trace. This module mirrors those
|
||||
//! traces to a **persistent log file** the user can hand back for diagnosis,
|
||||
//! while keeping the exact same "zero new dependency, never breaks the
|
||||
//! rendezvous" discipline: every write is best-effort and infallible.
|
||||
//!
|
||||
//! - [`set_log_path`] is called once by the composition root (`app-tauri`) with
|
||||
//! `<app-data>/logs/idea.log`. Until then (and in tests) writes go to stderr
|
||||
//! only.
|
||||
//! - [`diag`] (and the [`diag!`] macro) timestamps a line with epoch-millis — the
|
||||
//! same clock as the conversation logs' `atMs` — and appends it to the file,
|
||||
//! always also echoing to stderr so a terminal launch and the test suite still
|
||||
//! see it.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// The resolved log file path, set once at startup. `None` ⇒ stderr-only.
|
||||
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// Serialises concurrent appends so interleaved rendezvous beacons stay on their
|
||||
/// own lines (the orchestrator handles many connections in parallel).
|
||||
static WRITE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Points the diagnostics sink at `path`, creating its parent directory.
|
||||
///
|
||||
/// Idempotent and infallible: a second call (or a failure to create the
|
||||
/// directory) is ignored — diagnostics must never break startup. Called by the
|
||||
/// composition root once the app-data directory is known.
|
||||
pub fn set_log_path(path: PathBuf) {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = LOG_PATH.set(path);
|
||||
}
|
||||
|
||||
/// Current epoch-millis (same clock as the conversation logs' `atMs`).
|
||||
fn now_ms() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Appends one timestamped diagnostic line. Best-effort and infallible: a missing
|
||||
/// path or an I/O error is swallowed (the line still reaches stderr). Never call
|
||||
/// in a hot loop — these are lifecycle beacons, not a metrics stream.
|
||||
pub fn diag(msg: impl std::fmt::Display) {
|
||||
let line = format!("{} {msg}", now_ms());
|
||||
// Always echo to stderr: keeps a terminal launch and the test suite working,
|
||||
// and preserves the pre-existing `eprintln!` behaviour for these beacons.
|
||||
eprintln!("{line}");
|
||||
if let Some(path) = LOG_PATH.get() {
|
||||
let _guard = WRITE_LOCK.lock();
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = writeln!(f, "{line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `diag!("...", ...)` — formats then forwards to [`diag`]. Mirrors `eprintln!`.
|
||||
#[macro_export]
|
||||
macro_rules! diag {
|
||||
($($arg:tt)*) => {
|
||||
$crate::diag::diag(format!($($arg)*))
|
||||
};
|
||||
}
|
||||
@ -57,6 +57,33 @@ pub enum AppError {
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// The delegation target finished its turn and **returned to its prompt without
|
||||
/// calling `idea_reply`**, so the rendezvous could produce no answer. Surfaced
|
||||
/// promptly (after a short grace window) instead of blocking the caller until the
|
||||
/// long safety timeout. **Retryable**: the caller may re-ask, ideally reminding the
|
||||
/// target to render its result via `idea_reply` (never plain text). Carries the
|
||||
/// target's display name.
|
||||
#[error(
|
||||
"agent {0} returned to its prompt without calling idea_reply (no answer was \
|
||||
rendered); retry the request and ensure the agent replies via idea_reply"
|
||||
)]
|
||||
TargetReturnedNoReply(String),
|
||||
|
||||
/// The delegation target is **still actively working** (its transcript kept
|
||||
/// growing across liveness probes) but the rendezvous **absolute ceiling** was
|
||||
/// reached, so the synchronous wait was abandoned. **Distinct** from
|
||||
/// [`Self::TargetReturnedNoReply`]: the target is *not* silent, so a blind retry
|
||||
/// would stack a second heavy turn on a target already mid-task. **Not** a mute
|
||||
/// timeout — the caller should check the target's in-progress work/branch (it may
|
||||
/// still finish and call `idea_reply`) before re-soliciting lightly. Carries the
|
||||
/// target's display name.
|
||||
#[error(
|
||||
"target {0} is still actively working but the rendezvous ceiling was reached; \
|
||||
do not retry blindly — check the target's in-progress work/branch (it may still \
|
||||
finish and call idea_reply), then re-solicit lightly if needed"
|
||||
)]
|
||||
TargetCeilingActive(String),
|
||||
|
||||
/// An unexpected internal error.
|
||||
#[error("internal error: {0}")]
|
||||
Internal(String),
|
||||
@ -76,6 +103,8 @@ impl AppError {
|
||||
Self::Git(_) => "GIT",
|
||||
Self::Remote(_) => "REMOTE",
|
||||
Self::AgentAlreadyRunning { .. } => "AGENT_ALREADY_RUNNING",
|
||||
Self::TargetReturnedNoReply(_) => "TARGET_RETURNED_NO_REPLY",
|
||||
Self::TargetCeilingActive(_) => "RENDEZVOUS_CEILING_ACTIVE",
|
||||
Self::Internal(_) => "INTERNAL",
|
||||
}
|
||||
}
|
||||
@ -160,3 +189,31 @@ impl From<AgentSessionError> for AppError {
|
||||
Self::Process(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// La cible-active-au-plafond porte un code **stable et distinct** du no-reply
|
||||
/// (l'appelant peut brancher sans parser le message) et un message « ne retente
|
||||
/// pas à l'aveugle » différent du retour-au-prompt-sans-reply.
|
||||
#[test]
|
||||
fn ceiling_active_code_is_stable_and_distinct() {
|
||||
let ceiling = AppError::TargetCeilingActive("architect".to_owned());
|
||||
assert_eq!(ceiling.code(), "RENDEZVOUS_CEILING_ACTIVE");
|
||||
|
||||
let no_reply = AppError::TargetReturnedNoReply("architect".to_owned());
|
||||
assert_eq!(no_reply.code(), "TARGET_RETURNED_NO_REPLY");
|
||||
|
||||
// Deux issues TYPÉES distinctes (le cœur du cadrage Architect) : un plafond
|
||||
// atteint sur cible active n'est jamais confondu avec un silence/no-reply.
|
||||
assert_ne!(ceiling.code(), no_reply.code());
|
||||
// Et distinct d'un timeout muet (`Process` via AgentSessionError::Timeout).
|
||||
assert_ne!(
|
||||
ceiling.code(),
|
||||
AppError::from(AgentSessionError::Timeout).code()
|
||||
);
|
||||
// Le message guide explicitement vers « ne pas retenter à l'aveugle ».
|
||||
assert!(ceiling.to_string().contains("do not retry blindly"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,6 +297,15 @@ pub struct SetActiveLayoutInput {
|
||||
pub layout_id: LayoutId,
|
||||
}
|
||||
|
||||
/// Output of [`SetActiveLayout::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetActiveLayoutOutput {
|
||||
/// The id of the layout that was **actually** made active. Equals the
|
||||
/// requested id when it exists; otherwise the unchanged current active id
|
||||
/// (self-healing fallback). Authoritative for the frontend (invariant I4).
|
||||
pub active_id: LayoutId,
|
||||
}
|
||||
|
||||
/// Switches the active layout of a project.
|
||||
pub struct SetActiveLayout {
|
||||
store: Arc<dyn ProjectStore>,
|
||||
@ -317,20 +326,30 @@ impl SetActiveLayout {
|
||||
|
||||
/// Sets the active layout.
|
||||
///
|
||||
/// A stale requested id (e.g. an `activeId` left over after git overwrote
|
||||
/// `layouts.json`) must **not** freeze the workspace: instead of hard-
|
||||
/// erroring, it degrades silently to the current active layout (invariant
|
||||
/// I3). The returned [`SetActiveLayoutOutput::active_id`] is the id that was
|
||||
/// actually activated and is authoritative for the frontend (invariant I4).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::NotFound`] if the project or layout is unknown.
|
||||
pub async fn execute(&self, input: SetActiveLayoutInput) -> Result<(), AppError> {
|
||||
/// - [`AppError::FileSystem`] on persistence failure,
|
||||
/// - [`AppError::Store`] on registry I/O failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: SetActiveLayoutInput,
|
||||
) -> Result<SetActiveLayoutOutput, AppError> {
|
||||
let project = self.store.load_project(input.project_id).await?;
|
||||
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
|
||||
if doc.find(input.layout_id).is_none() {
|
||||
return Err(AppError::NotFound(format!("layout {}", input.layout_id)));
|
||||
}
|
||||
doc.active_id = input.layout_id;
|
||||
// Self-heal: keep the requested id when valid, else fall back to the
|
||||
// (always-valid, I2) current active id rather than erroring.
|
||||
let active_id = doc.resolve_existing_id(Some(input.layout_id));
|
||||
doc.active_id = active_id;
|
||||
|
||||
persist_doc(self.fs.as_ref(), &project, &doc).await?;
|
||||
self.events.publish(DomainEvent::LayoutChanged {
|
||||
project_id: input.project_id,
|
||||
});
|
||||
Ok(())
|
||||
Ok(SetActiveLayoutOutput { active_id })
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ mod usecases;
|
||||
pub use management::{
|
||||
CreateLayout, CreateLayoutInput, CreateLayoutOutput, DeleteLayout, DeleteLayoutInput,
|
||||
DeleteLayoutOutput, LayoutInfo, ListLayouts, ListLayoutsInput, ListLayoutsOutput, RenameLayout,
|
||||
RenameLayoutInput, SetActiveLayout, SetActiveLayoutInput,
|
||||
RenameLayoutInput, SetActiveLayout, SetActiveLayoutInput, SetActiveLayoutOutput,
|
||||
};
|
||||
pub use reconcile::{ReconcileLayouts, ReconcileLayoutsInput, ReconcileLayoutsOutput};
|
||||
pub use snapshot::{
|
||||
|
||||
@ -77,6 +77,22 @@ impl LayoutsDoc {
|
||||
id.unwrap_or(self.active_id)
|
||||
}
|
||||
|
||||
/// Resolves a requested view id to a **valid** layout id, never failing.
|
||||
///
|
||||
/// Returns `id` when it is present in `layouts`; otherwise falls back to
|
||||
/// `active_id` (guaranteed valid by invariant I2 / [`Self::is_valid`]).
|
||||
/// A `None` request (the current view) also resolves to `active_id`. This
|
||||
/// is the self-healing path: a stale `active_id` written by an external tool
|
||||
/// (e.g. git overwriting `layouts.json`) degrades silently instead of
|
||||
/// hard-erroring and freezing the workspace (invariant I3).
|
||||
#[must_use]
|
||||
pub fn resolve_existing_id(&self, id: Option<LayoutId>) -> LayoutId {
|
||||
match id {
|
||||
Some(id) if self.find(id).is_some() => id,
|
||||
_ => self.active_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds a layout by id.
|
||||
#[must_use]
|
||||
pub fn find(&self, id: LayoutId) -> Option<&NamedLayout> {
|
||||
@ -177,3 +193,55 @@ pub async fn resolve_doc(fs: &dyn FileSystem, project: &Project) -> Result<Layou
|
||||
persist_doc(fs, project, &doc).await?;
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Builds a doc with two layouts; the first one is active.
|
||||
fn two_layout_doc() -> (LayoutsDoc, LayoutId, LayoutId) {
|
||||
let active = LayoutId::new_random();
|
||||
let other = LayoutId::new_random();
|
||||
let doc = LayoutsDoc {
|
||||
version: LAYOUTS_VERSION,
|
||||
active_id: active,
|
||||
layouts: vec![
|
||||
NamedLayout {
|
||||
id: active,
|
||||
name: "Default".to_owned(),
|
||||
kind: LayoutKind::Terminal,
|
||||
tree: default_tree(),
|
||||
},
|
||||
NamedLayout {
|
||||
id: other,
|
||||
name: "Second".to_owned(),
|
||||
kind: LayoutKind::Terminal,
|
||||
tree: default_tree(),
|
||||
},
|
||||
],
|
||||
};
|
||||
(doc, active, other)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_existing_id_returns_present_id() {
|
||||
let (doc, _active, other) = two_layout_doc();
|
||||
// A present, non-active id is returned verbatim.
|
||||
assert_eq!(doc.resolve_existing_id(Some(other)), other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_existing_id_falls_back_to_active_when_absent() {
|
||||
let (doc, active, _other) = two_layout_doc();
|
||||
// A stale id absent from `layouts` degrades to the valid active id.
|
||||
let stale = LayoutId::new_random();
|
||||
assert_eq!(doc.resolve_existing_id(Some(stale)), active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_existing_id_none_resolves_to_active() {
|
||||
let (doc, active, _other) = two_layout_doc();
|
||||
// The "current view" request resolves to the active id.
|
||||
assert_eq!(doc.resolve_existing_id(None), active);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +169,10 @@ impl LoadLayout {
|
||||
pub async fn execute(&self, input: LoadLayoutInput) -> Result<LoadLayoutOutput, AppError> {
|
||||
let project = self.store.load_project(input.project_id).await?;
|
||||
let doc = resolve_doc(self.fs.as_ref(), &project).await?;
|
||||
let id = doc.resolve_id(input.layout_id);
|
||||
// Self-healing (I3): a stale requested id degrades to the valid active
|
||||
// id instead of hard-erroring. `find` is now infallible in practice;
|
||||
// the `ok_or` is kept as a purely defensive guard.
|
||||
let id = doc.resolve_existing_id(input.layout_id);
|
||||
let named = doc
|
||||
.find(id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("layout {id}")))?;
|
||||
@ -229,7 +232,10 @@ impl MutateLayout {
|
||||
pub async fn execute(&self, input: MutateLayoutInput) -> Result<MutateLayoutOutput, AppError> {
|
||||
let project = self.store.load_project(input.project_id).await?;
|
||||
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
|
||||
let id = doc.resolve_id(input.layout_id);
|
||||
// Self-healing (I3): a stale requested id degrades to the valid active
|
||||
// id instead of hard-erroring. `find_mut` is now infallible in
|
||||
// practice; the `ok_or` is kept as a purely defensive guard.
|
||||
let id = doc.resolve_existing_id(input.layout_id);
|
||||
|
||||
let named = doc
|
||||
.find_mut(id)
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
pub mod agent;
|
||||
pub mod conversation;
|
||||
pub mod diag;
|
||||
pub mod embedder;
|
||||
pub mod error;
|
||||
pub mod git;
|
||||
@ -27,25 +28,29 @@ pub mod skill;
|
||||
pub mod template;
|
||||
pub mod terminal;
|
||||
pub mod window;
|
||||
pub mod workstate;
|
||||
|
||||
pub use agent::{
|
||||
drain_with_readiness, drain_with_readiness_outcome, reference_profile_id, reference_profiles,
|
||||
selectable_reference_profiles, send_blocking, AgentResumer, ChangeAgentProfile,
|
||||
ChangeAgentProfileInput, ChangeAgentProfileOutput,
|
||||
ConfigureProfiles, ConfigureProfilesInput, ConfigureProfilesOutput, CreateAgentFromScratch,
|
||||
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, DeleteProfile,
|
||||
DeleteProfileInput, DetectProfiles, DetectProfilesInput, DetectProfilesOutput, FirstRunState,
|
||||
FirstRunStateOutput, HandoffProvider, InspectConversation, InspectConversationInput,
|
||||
InspectConversationOutput, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents,
|
||||
ListAgentsInput, ListAgentsOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
||||
ListResumableAgentsInput, ListResumableAgentsOutput, McpRuntime, PermissionProjectorRegistry,
|
||||
ProfileAvailability,
|
||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||
ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveProfile, SaveProfileInput,
|
||||
SaveProfileOutput, SessionLimitService, StructuredSessionDescriptor, TurnOutcome,
|
||||
UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, RESUME_PROMPT,
|
||||
ChangeAgentProfileInput, ChangeAgentProfileOutput, ConfigureProfiles, ConfigureProfilesInput,
|
||||
ConfigureProfilesOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||
DeleteAgent, DeleteAgentInput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
||||
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, HandoffProvider,
|
||||
InjectedLiveRow, InspectConversation, InspectConversationInput, InspectConversationOutput,
|
||||
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||
ListAgentsOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
||||
ListResumableAgentsInput, ListResumableAgentsOutput, LiveStateLeanProvider, McpRuntime,
|
||||
PermissionProjectorRegistry, ProfileAvailability, ProviderSessionProvider, ReadAgentContext,
|
||||
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
||||
ResumableAgent, SaveProfile, SaveProfileInput, SaveProfileOutput, SessionLimitService,
|
||||
StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||
};
|
||||
pub use conversation::{
|
||||
ConversationArchiveProvider, ReadConversationPage, ReadConversationPageInput, RecordTurn,
|
||||
RotateConversationLog, RotateConversationLogInput, TurnPage, TurnSource, TurnView,
|
||||
};
|
||||
pub use conversation::RecordTurn;
|
||||
pub use embedder::{
|
||||
CheckEmbedderSuggestion, CheckEmbedderSuggestionInput, CheckEmbedderSuggestionOutput,
|
||||
DeleteEmbedderProfile, DeleteEmbedderProfileInput, DescribeEmbedderEngines, DismissChoice,
|
||||
@ -67,18 +72,23 @@ pub use layout::{
|
||||
ListLayoutsInput, ListLayoutsOutput, LoadLayout, LoadLayoutInput, LoadLayoutOutput,
|
||||
MutateLayout, MutateLayoutInput, MutateLayoutOutput, NamedLayout, ReconcileLayouts,
|
||||
ReconcileLayoutsInput, ReconcileLayoutsOutput, RenameLayout, RenameLayoutInput,
|
||||
SetActiveLayout, SetActiveLayoutInput, SnapshotRunningAgents, SnapshotRunningAgentsInput,
|
||||
SnapshotRunningAgentsOutput, LAYOUTS_FILE,
|
||||
SetActiveLayout, SetActiveLayoutInput, SetActiveLayoutOutput, SnapshotRunningAgents,
|
||||
SnapshotRunningAgentsInput, SnapshotRunningAgentsOutput, LAYOUTS_FILE,
|
||||
};
|
||||
pub use memory::{
|
||||
CreateMemory, CreateMemoryInput, CreateMemoryOutput, DeleteMemory, DeleteMemoryInput,
|
||||
GetMemory, GetMemoryInput, GetMemoryOutput, ListMemories, ListMemoriesInput,
|
||||
ListMemoriesOutput, ReadMemoryIndex, ReadMemoryIndexInput, ReadMemoryIndexOutput, RecallMemory,
|
||||
RecallMemoryInput, RecallMemoryOutput, ResolveMemoryLinks, ResolveMemoryLinksInput,
|
||||
ResolveMemoryLinksOutput, UpdateMemory, UpdateMemoryInput, UpdateMemoryOutput,
|
||||
GetMemory, GetMemoryInput, GetMemoryOutput, HarvestMemoryFromTurn, ListMemories,
|
||||
ListMemoriesInput, ListMemoriesOutput, MemoryHarvestOutcome, ReadMemoryIndex,
|
||||
ReadMemoryIndexInput, ReadMemoryIndexOutput, RecallMemory, RecallMemoryInput,
|
||||
RecallMemoryOutput, ResolveMemoryLinks, ResolveMemoryLinksInput, ResolveMemoryLinksOutput,
|
||||
UpdateMemory, UpdateMemoryInput, UpdateMemoryOutput,
|
||||
};
|
||||
pub use orchestrator::{
|
||||
McpRuntimeProvider, OrchestratorOutcome, OrchestratorService, RecordTurnProvider,
|
||||
resolve_rendezvous_ceiling, resolve_rendezvous_window, run_inactivity_watchdog,
|
||||
AskLivenessProbe, ContextGuardUseCases, LiveStateProvider, LiveStateReadProvider,
|
||||
McpRuntimeProvider, OrchestratorOutcome, OrchestratorService, ProposeContext, ReadContext,
|
||||
ReadMemory, RecordTurnProvider, WatchdogOutcome, WriteMemory, DEFAULT_RENDEZVOUS_CEILING,
|
||||
DEFAULT_RENDEZVOUS_WINDOW,
|
||||
};
|
||||
pub use permission::{
|
||||
GetProjectPermissions, GetProjectPermissionsInput, GetProjectPermissionsOutput,
|
||||
@ -96,9 +106,9 @@ pub use project::{
|
||||
pub use remote::{ConnectRemote, ConnectRemoteInput, ConnectRemoteOutput};
|
||||
pub use skill::{
|
||||
AssignSkillToAgent, AssignSkillToAgentInput, CreateSkill, CreateSkillInput, CreateSkillOutput,
|
||||
DeleteSkill, DeleteSkillInput, ListSkills, ListSkillsInput, ListSkillsOutput,
|
||||
UnassignSkillFromAgent, UnassignSkillFromAgentInput, UpdateSkill, UpdateSkillInput,
|
||||
UpdateSkillOutput,
|
||||
DeleteSkill, DeleteSkillInput, ListSkills, ListSkillsInput, ListSkillsOutput, ReadSkill,
|
||||
ReadSkillInput, UnassignSkillFromAgent, UnassignSkillFromAgentInput, UpdateSkill,
|
||||
UpdateSkillInput, UpdateSkillOutput,
|
||||
};
|
||||
pub use template::{
|
||||
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
|
||||
@ -109,8 +119,18 @@ pub use template::{
|
||||
UpdateTemplateOutput,
|
||||
};
|
||||
pub use terminal::{
|
||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessions,
|
||||
OpenTerminal, OpenTerminalInput, OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput,
|
||||
StructuredSessions, TerminalSessions, WriteToTerminal, WriteToTerminalInput,
|
||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessionKind,
|
||||
LiveSessionSnapshot, LiveSessions, OpenTerminal, OpenTerminalInput, OpenTerminalOutput,
|
||||
ResizeTerminal, ResizeTerminalInput, StructuredSessions, TerminalSessions, WriteToTerminal,
|
||||
WriteToTerminalInput,
|
||||
};
|
||||
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
||||
pub use workstate::{
|
||||
AgentTicketState, AgentWorkState, AttachLiveAgent, AttachLiveAgentInput, AttachLiveAgentOutput,
|
||||
ConversationLogProvider, ConversationPreviewStatus, ConversationTurnWorkPreview,
|
||||
ConversationWorkSummary, GetLiveStateLean, GetProjectWorkState, GetProjectWorkStateInput,
|
||||
LeanLiveEntry, LeanLiveState, LiveWorkSession, ProjectWorkState, ReconcileLiveState,
|
||||
ReconcileLiveStateInput, StopLiveAgent, StopLiveAgentInput, StopLiveAgentOutput,
|
||||
TicketWorkSource, TicketWorkStatus, UpdateLiveState, UpdateLiveStateInput,
|
||||
LIVE_STATE_MAX_ENTRIES, LIVE_STATE_TTL_MS,
|
||||
};
|
||||
|
||||
114
crates/application/src/memory/harvest.rs
Normal file
114
crates/application/src/memory/harvest.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! Auto-memory harvest use case (Lot E1).
|
||||
//!
|
||||
//! [`HarvestMemoryFromTurn`] turns the text of a finished agent turn into persisted
|
||||
//! memory notes when it embeds ` ```idea-memory ` directives (parsed by the pure
|
||||
//! [`domain::memory_harvest`] module). It is **best-effort strict**: it only acts on
|
||||
//! a `Response` turn, parses **only the current turn's text** (never the full log),
|
||||
//! and never fails — parse/store errors are collected into the returned
|
||||
//! [`MemoryHarvestOutcome`] so the orchestrator can drop them without ever turning a
|
||||
//! successful reply/ticket/handoff into an error.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::conversation_log::TurnRole;
|
||||
use domain::ports::{EventBus, MemoryStore};
|
||||
use domain::{
|
||||
parse_memory_directives, DomainEvent, MarkdownDoc, Memory, MemoryFrontmatter,
|
||||
MemoryHarvestDirective, MemorySlug, ProjectPath,
|
||||
};
|
||||
|
||||
/// The outcome of harvesting one turn: a report the caller may log or ignore.
|
||||
///
|
||||
/// `found` is the number of `idea-memory` directives the agent emitted; `saved`
|
||||
/// the slugs successfully persisted; `skipped` how many were dropped (parse or
|
||||
/// store failures); `errors` the human-readable reasons (parse + store).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct MemoryHarvestOutcome {
|
||||
/// Number of `idea-memory` directives detected in the turn text.
|
||||
pub found: usize,
|
||||
/// Slugs of the notes that were saved (and announced).
|
||||
pub saved: Vec<MemorySlug>,
|
||||
/// Number of directives dropped (invalid, over-limit, or store failure).
|
||||
pub skipped: usize,
|
||||
/// One message per dropped directive (parse + persistence reasons).
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Validates and persists the memory directives embedded in a finished turn.
|
||||
///
|
||||
/// Talks only to ports ([`MemoryStore`], [`EventBus`]); announces
|
||||
/// [`DomainEvent::MemorySaved`] per saved note like the manual CRUD use cases.
|
||||
pub struct HarvestMemoryFromTurn {
|
||||
memories: Arc<dyn MemoryStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl HarvestMemoryFromTurn {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(memories: Arc<dyn MemoryStore>, events: Arc<dyn EventBus>) -> Self {
|
||||
Self { memories, events }
|
||||
}
|
||||
|
||||
/// Harvests `text` (the current turn's content) into `root`'s memory store.
|
||||
///
|
||||
/// Best-effort: returns a [`MemoryHarvestOutcome`] and **never** an error. A
|
||||
/// non-`Response` turn is a no-op (empty outcome) — prompts and tool activity
|
||||
/// never carry harvest directives.
|
||||
pub async fn harvest(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
role: TurnRole,
|
||||
text: &str,
|
||||
) -> MemoryHarvestOutcome {
|
||||
if role != TurnRole::Response {
|
||||
return MemoryHarvestOutcome::default();
|
||||
}
|
||||
let report = parse_memory_directives(text);
|
||||
let mut outcome = MemoryHarvestOutcome {
|
||||
found: report.found,
|
||||
// Parse-level skips are already final; store-level skips are added below.
|
||||
skipped: report.errors.len(),
|
||||
errors: report.errors,
|
||||
saved: Vec::new(),
|
||||
};
|
||||
for directive in report.directives {
|
||||
match self.persist(root, directive).await {
|
||||
Ok(slug) => outcome.saved.push(slug),
|
||||
Err(err) => {
|
||||
outcome.skipped += 1;
|
||||
outcome.errors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Persists one validated directive and announces it, mapping the (today
|
||||
/// title-less) [`Memory`] entity from the directive. Returns the saved slug or
|
||||
/// a message on a build/store failure (never panics).
|
||||
async fn persist(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
directive: MemoryHarvestDirective,
|
||||
) -> Result<MemorySlug, String> {
|
||||
let slug = directive.slug.clone();
|
||||
// The persisted `Memory` has no title field today; the directive's `title`
|
||||
// is intentionally not stored separately (the index derives its title from
|
||||
// the slug). See the Lot E1 report's écart note.
|
||||
let frontmatter = MemoryFrontmatter {
|
||||
name: slug.clone(),
|
||||
description: directive.description,
|
||||
r#type: directive.r#type,
|
||||
};
|
||||
let memory = Memory::new(frontmatter, MarkdownDoc::new(directive.body.as_str()))
|
||||
.map_err(|e| format!("invalid memory {slug}: {e}"))?;
|
||||
self.memories
|
||||
.save(root, &memory)
|
||||
.await
|
||||
.map_err(|e| format!("store failed for {slug}: {e}"))?;
|
||||
self.events
|
||||
.publish(DomainEvent::MemorySaved { slug: slug.clone() });
|
||||
Ok(slug)
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,11 @@
|
||||
//! [`UpdateMemory`], [`DeleteMemory`]) announce a [`domain::DomainEvent`]; the
|
||||
//! read use cases emit nothing.
|
||||
|
||||
mod harvest;
|
||||
mod usecases;
|
||||
|
||||
pub use harvest::{HarvestMemoryFromTurn, MemoryHarvestOutcome};
|
||||
|
||||
pub use usecases::{
|
||||
CreateMemory, CreateMemoryInput, CreateMemoryOutput, DeleteMemory, DeleteMemoryInput,
|
||||
GetMemory, GetMemoryInput, GetMemoryOutput, ListMemories, ListMemoriesInput,
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::fileguard::{FileGuard, GuardError, GuardedResource};
|
||||
use domain::fileguard::{may_write_directly, FileGuard, GuardError, GuardedResource};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::memory::{Memory, MemoryFrontmatter, MemorySlug, MemoryType};
|
||||
use domain::ports::{AgentContextStore, Clock, FileSystem, MemoryStore, RemotePath};
|
||||
@ -135,9 +135,10 @@ impl ReadContext {
|
||||
/// Proposes new content for an IdeA-owned context under the [`FileGuard`].
|
||||
///
|
||||
/// For an **agent** context: a direct write under an exclusive write-lease. For the
|
||||
/// **global** project context by a non-orchestrator: the guard returns
|
||||
/// [`GuardError::Forbidden`], which this use case turns into a *materialised proposal*
|
||||
/// (a file under `.ideai/proposals/`) — never an overwrite of the live context.
|
||||
/// **global** project context by a non-orchestrator: the use case asks the domain
|
||||
/// policy whether the requester may write directly; otherwise it materialises a
|
||||
/// proposal (a file under `.ideai/proposals/`) — never an overwrite of the live
|
||||
/// context.
|
||||
pub struct ProposeContext {
|
||||
guard: Arc<dyn FileGuard>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
@ -214,24 +215,23 @@ impl ProposeContext {
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
None => {
|
||||
// Global project context: single-writer. Try to acquire the write
|
||||
// lease; Forbidden ⇒ materialise a proposal instead of overwriting.
|
||||
match self
|
||||
.guard
|
||||
.acquire_write(requester, GuardedResource::ProjectContext)
|
||||
.await
|
||||
{
|
||||
Ok(_lease) => {
|
||||
let path = join_root(&project, PROJECT_CONTEXT_FILE);
|
||||
self.fs.write(&path, content.as_bytes()).await?;
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
Err(GuardError::Forbidden) => {
|
||||
let path = self.file_proposal(&project, requester, &content).await?;
|
||||
Ok(ProposeOutcome::Proposed { path })
|
||||
}
|
||||
Err(other) => Err(map_guard_err(other)),
|
||||
// Global project context: single-writer. Authorization stays in the
|
||||
// domain policy; the guard only serialises the eventual direct write.
|
||||
let manifest = self.contexts.load_manifest(&project).await?;
|
||||
let designation = manifest.orchestrator_designation();
|
||||
let resource = GuardedResource::ProjectContext;
|
||||
if !may_write_directly(requester, &resource, &designation) {
|
||||
let path = self.file_proposal(&project, requester, &content).await?;
|
||||
return Ok(ProposeOutcome::Proposed { path });
|
||||
}
|
||||
let _lease = self
|
||||
.guard
|
||||
.acquire_write(requester, resource)
|
||||
.await
|
||||
.map_err(map_guard_err)?;
|
||||
let path = join_root(&project, PROJECT_CONTEXT_FILE);
|
||||
self.fs.write(&path, content.as_bytes()).await?;
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -392,7 +392,7 @@ mod tests {
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{AgentManifest, ManifestEntry};
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::fileguard::{may_write_directly, ReadLease, WriteLease};
|
||||
use domain::fileguard::{ReadLease, WriteLease};
|
||||
use domain::ports::{FsError, MemoryError, StoreError};
|
||||
use domain::project::ProjectPath;
|
||||
use domain::{ProfileId, ProjectId, RemoteRef};
|
||||
@ -443,12 +443,9 @@ mod tests {
|
||||
}
|
||||
async fn acquire_write(
|
||||
&self,
|
||||
who: ConversationParty,
|
||||
_who: ConversationParty,
|
||||
res: GuardedResource,
|
||||
) -> Result<WriteLease, GuardError> {
|
||||
if !may_write_directly(who, &res) {
|
||||
return Err(GuardError::Forbidden);
|
||||
}
|
||||
let lock = self.lock_for(&res);
|
||||
Ok(WriteLease::new(Box::new(lock.write_owned().await)))
|
||||
}
|
||||
@ -612,6 +609,7 @@ mod tests {
|
||||
Arc::new(FakeContexts {
|
||||
manifest: AgentManifest {
|
||||
version: 1,
|
||||
orchestrator: None,
|
||||
entries: vec![ManifestEntry {
|
||||
agent_id: agent,
|
||||
name: name.to_owned(),
|
||||
|
||||
@ -5,12 +5,18 @@
|
||||
//! ever spawning a process itself. See [`service::OrchestratorService`].
|
||||
|
||||
mod context_guard;
|
||||
pub mod rendezvous;
|
||||
mod service;
|
||||
|
||||
pub use context_guard::{
|
||||
ProposeContext, ProposeContextInput, ProposeOutcome, ReadContext, ReadContextInput, ReadMemory,
|
||||
ReadMemoryInput, WriteMemory, WriteMemoryInput,
|
||||
};
|
||||
pub use rendezvous::{
|
||||
resolve_rendezvous_ceiling, resolve_rendezvous_window, run_inactivity_watchdog,
|
||||
WatchdogOutcome, DEFAULT_RENDEZVOUS_CEILING, DEFAULT_RENDEZVOUS_WINDOW,
|
||||
};
|
||||
pub use service::{
|
||||
AskLivenessProbe, ContextGuardUseCases, LiveStateProvider, LiveStateReadProvider,
|
||||
McpRuntimeProvider, OrchestratorOutcome, OrchestratorService, RecordTurnProvider,
|
||||
};
|
||||
|
||||
294
crates/application/src/orchestrator/rendezvous.rs
Normal file
294
crates/application/src/orchestrator/rendezvous.rs
Normal file
@ -0,0 +1,294 @@
|
||||
//! Inactivity watchdog for the synchronous inter-agent rendezvous
|
||||
//! (`idea_ask_agent` ⇄ `idea_reply`).
|
||||
//!
|
||||
//! # Why this exists (root cause)
|
||||
//!
|
||||
//! A delegation target can legitimately work for a **very long time in a single
|
||||
//! turn** (implement + `cargo test`/`clippy`), without ever returning to its prompt
|
||||
//! nor emitting an intermediate signal the application can race against: the AI CLI
|
||||
//! turn ([`domain::ports::AgentSession::send`]) only resolves **once the whole turn
|
||||
//! is done** — its `ReplyEvent`s arrive as one batch at the end, not incrementally.
|
||||
//! A **flat absolute timeout** around that wait therefore cuts a target that is
|
||||
//! manifestly alive (its transcript keeps growing) at the cap, surfacing a generic,
|
||||
//! non-actionable timeout.
|
||||
//!
|
||||
//! The fix is to bound the wait with an **inactivity window** re-armed on an observed
|
||||
//! **sign of life**, under an **absolute ceiling**, with **distinct typed outcomes**:
|
||||
//!
|
||||
//! - progress observed under the ceiling ⇒ re-arm the window (no timeout);
|
||||
//! - progress observed but the absolute ceiling is reached ⇒ a distinct
|
||||
//! [`WatchdogOutcome::CeilingActive`] (the target is *active*, not silent — a blind
|
||||
//! retry would stack a second heavy turn), never a mute timeout;
|
||||
//! - genuine silence (no progress across a whole window) ⇒
|
||||
//! [`WatchdogOutcome::NoReply`], exactly the previous timeout semantics.
|
||||
//!
|
||||
//! # Sign of life
|
||||
//!
|
||||
//! Liveness cannot come from the event stream (it is batched at end-of-turn, see
|
||||
//! above), so it is supplied by an injected **monotonic activity probe** (in practice
|
||||
//! the cumulative byte size of the target's transcript, which grows mid-turn). The
|
||||
//! probe is a pure closure here — the algorithm stays free of infrastructure and is
|
||||
//! unit-testable with a synthetic future + probe.
|
||||
//!
|
||||
//! # DRY
|
||||
//!
|
||||
//! This is the **single** implementation of that algorithm. The application layer
|
||||
//! (`OrchestratorService`) governs the *delegated turn* with it (the seam that
|
||||
//! actually cuts a long turn), and the infrastructure MCP adapter re-uses the very
|
||||
//! same function as its outer safety net — no second, diverging copy.
|
||||
|
||||
use std::future::Future;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Verdict of [`run_inactivity_watchdog`] over an arbitrary `dispatch` future,
|
||||
/// decoupled from any concrete service/adapter so the algorithm is unit-testable
|
||||
/// with a synthetic future + probe.
|
||||
#[derive(Debug)]
|
||||
pub enum WatchdogOutcome<T> {
|
||||
/// The dispatch resolved before the watchdog tripped — carries its value.
|
||||
Resolved(T),
|
||||
/// Genuine silence (no observable progress across a window), or the flat-window
|
||||
/// fallback when no probe is wired ⇒ the **retryable** no-reply / timeout verdict.
|
||||
NoReply,
|
||||
/// The absolute ceiling was reached **while still observing progress** ⇒ the
|
||||
/// distinct, **non-retryable-blind** "target active, ceiling reached" verdict.
|
||||
CeilingActive,
|
||||
}
|
||||
|
||||
/// Races `dispatch` against an inactivity `window`, re-arming the window each time the
|
||||
/// target shows **progress** (the monotonic token from `probe` strictly increased, or
|
||||
/// became observable for the first time) until the dispatch resolves or the absolute
|
||||
/// `ceiling` (measured from `started`) is reached.
|
||||
///
|
||||
/// - dispatch resolves first ⇒ [`WatchdogOutcome::Resolved`];
|
||||
/// - window elapses with **no progress** (token unchanged/regressed/lost, or
|
||||
/// `has_probe` is false ⇒ flat-window fallback) ⇒ [`WatchdogOutcome::NoReply`];
|
||||
/// - window elapses **with progress** but `started.elapsed() >= ceiling` ⇒
|
||||
/// [`WatchdogOutcome::CeilingActive`]; otherwise the window is re-armed.
|
||||
///
|
||||
/// `has_probe` distinguishes "no probe wired at all" (flat fallback: never progress,
|
||||
/// = the previous flat-timeout behaviour ⇒ zero regression) from "probe wired but
|
||||
/// currently returns `None`" (cannot prove liveness ⇒ treated as no progress).
|
||||
///
|
||||
/// `on_extended` / `on_expired` / `on_ceiling` are best-effort observation hooks
|
||||
/// (diagnostics beacons); pass no-ops to ignore them.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn run_inactivity_watchdog<T, F, Fut>(
|
||||
dispatch: impl Future<Output = T>,
|
||||
window: Duration,
|
||||
ceiling: Duration,
|
||||
started: Instant,
|
||||
has_probe: bool,
|
||||
mut probe: F,
|
||||
mut on_extended: impl FnMut(Duration),
|
||||
mut on_expired: impl FnMut(Duration),
|
||||
mut on_ceiling: impl FnMut(Duration),
|
||||
) -> WatchdogOutcome<T>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Option<u64>>,
|
||||
{
|
||||
tokio::pin!(dispatch);
|
||||
// Baseline token before the first window; progress is judged relative to it.
|
||||
let mut last_token = if has_probe { probe().await } else { None };
|
||||
loop {
|
||||
match tokio::time::timeout(window, &mut dispatch).await {
|
||||
Ok(result) => return WatchdogOutcome::Resolved(result),
|
||||
Err(_elapsed) => {
|
||||
let elapsed = started.elapsed();
|
||||
let now = if has_probe { probe().await } else { None };
|
||||
// Progress iff a probe is wired AND the token strictly advanced (or
|
||||
// became observable for the first time). No probe ⇒ never progress
|
||||
// (flat-window fallback). Token lost ⇒ no proof of life ⇒ no progress.
|
||||
let progressed = match (has_probe, last_token, now) {
|
||||
(false, _, _) => false,
|
||||
(true, Some(prev), Some(cur)) => cur > prev,
|
||||
(true, None, Some(_)) => true,
|
||||
(true, _, None) => false,
|
||||
};
|
||||
if progressed {
|
||||
if elapsed >= ceiling {
|
||||
on_ceiling(elapsed);
|
||||
return WatchdogOutcome::CeilingActive;
|
||||
}
|
||||
on_extended(elapsed);
|
||||
last_token = now;
|
||||
continue;
|
||||
}
|
||||
on_expired(elapsed);
|
||||
return WatchdogOutcome::NoReply;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default inactivity window (silence budget) of the inter-agent rendezvous: the size
|
||||
/// of **one** no-progress probe window, NOT an absolute cap. 600 s, matching the
|
||||
/// historical flat cap so a genuinely silent target still expires in the same delay.
|
||||
pub const DEFAULT_RENDEZVOUS_WINDOW: Duration = Duration::from_secs(600);
|
||||
|
||||
/// Default absolute ceiling of the inter-agent rendezvous: the extending inactivity
|
||||
/// window never parks a call past this even against a perpetually-busy target.
|
||||
/// Generous (4 h) so a heavy-but-real single turn finishes well within it.
|
||||
pub const DEFAULT_RENDEZVOUS_CEILING: Duration = Duration::from_secs(4 * 60 * 60);
|
||||
|
||||
/// Resolves the effective rendezvous **inactivity window** from an optional override in
|
||||
/// **milliseconds**: `Some(ms>0)` ⇒ that window, else the [`DEFAULT_RENDEZVOUS_WINDOW`].
|
||||
/// `Some(0)` is treated as "no override" so a misconfigured zero can never collapse the
|
||||
/// window to an instant expiry. Pure and unit-testable without wiring.
|
||||
#[must_use]
|
||||
pub fn resolve_rendezvous_window(override_ms: Option<u32>) -> Duration {
|
||||
match override_ms {
|
||||
Some(ms) if ms > 0 => Duration::from_millis(u64::from(ms)),
|
||||
_ => DEFAULT_RENDEZVOUS_WINDOW,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the effective rendezvous **absolute ceiling** from an optional override in
|
||||
/// **milliseconds**: `Some(ms>0)` ⇒ that ceiling, else [`DEFAULT_RENDEZVOUS_CEILING`].
|
||||
/// `Some(0)` is treated as "no override". Pure and unit-testable without wiring.
|
||||
#[must_use]
|
||||
pub fn resolve_rendezvous_ceiling(override_ms: Option<u32>) -> Duration {
|
||||
match override_ms {
|
||||
Some(ms) if ms > 0 => Duration::from_millis(u64::from(ms)),
|
||||
_ => DEFAULT_RENDEZVOUS_CEILING,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A `dispatch` future that never resolves — stands in for a target busy in one
|
||||
/// long turn (or wedged) that has not yet produced a reply.
|
||||
async fn never() -> Result<u32, u32> {
|
||||
std::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
const WINDOW: Duration = Duration::from_millis(30);
|
||||
|
||||
fn noop(_: Duration) {}
|
||||
|
||||
/// (c) Vrai silence : un probe est câblé mais son token n'avance jamais ⇒ le
|
||||
/// watchdog expire en ~une fenêtre avec le verdict no-reply (pas d'extension),
|
||||
/// soit exactement l'ancien timeout plat.
|
||||
#[tokio::test]
|
||||
async fn silent_target_expires_no_reply() {
|
||||
let started = Instant::now();
|
||||
let probe = || async { Some(42_u64) };
|
||||
let outcome = run_inactivity_watchdog(
|
||||
never(),
|
||||
WINDOW,
|
||||
Duration::from_secs(3600),
|
||||
started,
|
||||
true,
|
||||
probe,
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(outcome, WatchdogOutcome::NoReply));
|
||||
assert!(started.elapsed() < Duration::from_secs(1));
|
||||
}
|
||||
|
||||
/// (d) Non-régression du fallback fenêtre plate : sans probe câblé, la première
|
||||
/// expiration de fenêtre est un no-reply (= ancien timeout plat).
|
||||
#[tokio::test]
|
||||
async fn no_probe_falls_back_to_flat_window() {
|
||||
let started = Instant::now();
|
||||
let probe = || async { None };
|
||||
let outcome = run_inactivity_watchdog(
|
||||
never(),
|
||||
WINDOW,
|
||||
Duration::from_secs(3600),
|
||||
started,
|
||||
false,
|
||||
probe,
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(outcome, WatchdogOutcome::NoReply));
|
||||
}
|
||||
|
||||
/// (a) Cible qui progresse AU-DELÀ de l'ancien plafond : le probe avance à chaque
|
||||
/// tick (extension) bien au-delà d'une fenêtre, puis la dispatch se résout ⇒ AUCUN
|
||||
/// timeout prématuré, le résultat est rendu. C'est le défaut central corrigé.
|
||||
#[tokio::test]
|
||||
async fn progressing_target_extends_then_resolves() {
|
||||
let started = Instant::now();
|
||||
let tick = Arc::new(AtomicU64::new(0));
|
||||
let tick2 = Arc::clone(&tick);
|
||||
let probe = move || {
|
||||
let tick = Arc::clone(&tick2);
|
||||
async move { Some(tick.fetch_add(1, Ordering::SeqCst) + 1) }
|
||||
};
|
||||
// Résout après ~plusieurs fenêtres (110 ms ≫ 30 ms) : sans réarmement, un
|
||||
// timeout plat de 30 ms aurait coupé. Le watchdog doit étendre et rendre 7.
|
||||
let dispatch = async {
|
||||
tokio::time::sleep(Duration::from_millis(110)).await;
|
||||
Ok::<u32, u32>(7)
|
||||
};
|
||||
let outcome = run_inactivity_watchdog(
|
||||
dispatch,
|
||||
WINDOW,
|
||||
Duration::from_secs(3600),
|
||||
started,
|
||||
true,
|
||||
probe,
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
)
|
||||
.await;
|
||||
match outcome {
|
||||
WatchdogOutcome::Resolved(Ok(v)) => assert_eq!(v, 7),
|
||||
_ => panic!("expected Resolved(Ok(7)) — la fenêtre aurait dû être réarmée"),
|
||||
}
|
||||
}
|
||||
|
||||
/// (b) Plafond absolu atteint malgré progrès : le probe avance toujours (cible
|
||||
/// active) mais le plafond est minuscule ⇒ verdict DISTINCT CeilingActive (pas un
|
||||
/// faux no-reply muet), et l'appel termine (jamais parqué à l'infini).
|
||||
#[tokio::test]
|
||||
async fn progressing_target_hits_ceiling_distinctly() {
|
||||
let started = Instant::now();
|
||||
let tick = Arc::new(AtomicU64::new(0));
|
||||
let tick2 = Arc::clone(&tick);
|
||||
let probe = move || {
|
||||
let tick = Arc::clone(&tick2);
|
||||
async move { Some(tick.fetch_add(1, Ordering::SeqCst) + 1) }
|
||||
};
|
||||
let outcome = run_inactivity_watchdog(
|
||||
never(),
|
||||
WINDOW,
|
||||
Duration::from_millis(45),
|
||||
started,
|
||||
true,
|
||||
probe,
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(outcome, WatchdogOutcome::CeilingActive));
|
||||
assert!(started.elapsed() < Duration::from_secs(1));
|
||||
}
|
||||
|
||||
/// Les résolveurs honorent un override et retombent sur leurs défauts finis ;
|
||||
/// `Some(0)` ⇒ "pas d'override" (jamais d'effondrement instantané).
|
||||
#[test]
|
||||
fn resolvers_apply_override_else_finite_default() {
|
||||
assert_eq!(resolve_rendezvous_window(Some(1234)), Duration::from_millis(1234));
|
||||
assert_eq!(resolve_rendezvous_window(Some(0)), DEFAULT_RENDEZVOUS_WINDOW);
|
||||
assert_eq!(resolve_rendezvous_window(None), DEFAULT_RENDEZVOUS_WINDOW);
|
||||
assert_eq!(resolve_rendezvous_ceiling(Some(9999)), Duration::from_millis(9999));
|
||||
assert_eq!(resolve_rendezvous_ceiling(Some(0)), DEFAULT_RENDEZVOUS_CEILING);
|
||||
assert_eq!(resolve_rendezvous_ceiling(None), DEFAULT_RENDEZVOUS_CEILING);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ mod usecases;
|
||||
|
||||
pub use usecases::{
|
||||
AssignSkillToAgent, AssignSkillToAgentInput, CreateSkill, CreateSkillInput, CreateSkillOutput,
|
||||
DeleteSkill, DeleteSkillInput, ListSkills, ListSkillsInput, ListSkillsOutput,
|
||||
UnassignSkillFromAgent, UnassignSkillFromAgentInput, UpdateSkill, UpdateSkillInput,
|
||||
UpdateSkillOutput,
|
||||
DeleteSkill, DeleteSkillInput, ListSkills, ListSkillsInput, ListSkillsOutput, ReadSkill,
|
||||
ReadSkillInput, UnassignSkillFromAgent, UnassignSkillFromAgentInput, UpdateSkill,
|
||||
UpdateSkillInput, UpdateSkillOutput,
|
||||
};
|
||||
|
||||
@ -25,6 +25,10 @@ use crate::error::AppError;
|
||||
pub struct CreateSkillInput {
|
||||
/// Display name (also the `.md` stem on disk).
|
||||
pub name: String,
|
||||
/// Optional one-line affordance description (feature « skills à la MCP »).
|
||||
/// `None`/empty ⇒ the skill falls back to the first line of its body when
|
||||
/// surfaced (see [`domain::Skill::effective_description`]).
|
||||
pub description: Option<String>,
|
||||
/// Initial Markdown body.
|
||||
pub content: String,
|
||||
/// Scope the skill is created in (selects its backing store).
|
||||
@ -61,7 +65,8 @@ impl CreateSkill {
|
||||
pub async fn execute(&self, input: CreateSkillInput) -> Result<CreateSkillOutput, AppError> {
|
||||
let id = SkillId::from_uuid(self.ids.new_uuid());
|
||||
let skill = Skill::new(id, input.name, MarkdownDoc::new(input.content), input.scope)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?
|
||||
.with_description(input.description);
|
||||
self.skills.save(&skill, &input.project_root).await?;
|
||||
Ok(CreateSkillOutput { skill })
|
||||
}
|
||||
@ -204,6 +209,77 @@ impl DeleteSkill {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReadSkill
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`ReadSkill::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadSkillInput {
|
||||
/// Skill display name to resolve (case-insensitive).
|
||||
pub name: String,
|
||||
/// Active project root (used for the [`SkillScope::Project`] lookup).
|
||||
pub project_root: ProjectPath,
|
||||
}
|
||||
|
||||
/// Reads a skill's Markdown body **by name** — the application side of the MCP
|
||||
/// `idea_skill_read` tool (feature « skills à la MCP »).
|
||||
///
|
||||
/// Resolution follows the affordance contract: **project scope first, then
|
||||
/// global** (a project skill shadows a global one of the same name). It composes
|
||||
/// the **existing** [`SkillStore`] port only — no new port. Read-only.
|
||||
pub struct ReadSkill {
|
||||
skills: Arc<dyn SkillStore>,
|
||||
}
|
||||
|
||||
impl ReadSkill {
|
||||
/// Builds the use case from the existing skill store port.
|
||||
#[must_use]
|
||||
pub fn new(skills: Arc<dyn SkillStore>) -> Self {
|
||||
Self { skills }
|
||||
}
|
||||
|
||||
/// Resolves `name` in `scope`, returning the single match.
|
||||
///
|
||||
/// `Ok(None)` ⇒ no skill by that name in this scope; `Err(Invalid)` ⇒ the
|
||||
/// name is **ambiguous** (more than one skill shares it in this scope).
|
||||
async fn resolve_in(
|
||||
&self,
|
||||
scope: SkillScope,
|
||||
input: &ReadSkillInput,
|
||||
) -> Result<Option<Skill>, AppError> {
|
||||
let all = self.skills.list(scope, &input.project_root).await?;
|
||||
let mut matches = all
|
||||
.into_iter()
|
||||
.filter(|s| s.name.eq_ignore_ascii_case(&input.name));
|
||||
match (matches.next(), matches.next()) {
|
||||
(None, _) => Ok(None),
|
||||
(Some(skill), None) => Ok(Some(skill)),
|
||||
(Some(_), Some(_)) => Err(AppError::Invalid(format!(
|
||||
"skill name `{}` is ambiguous in {scope:?} scope (several skills share it)",
|
||||
input.name
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the skill by name and returns its Markdown body.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::Invalid`] if the name is ambiguous within a scope,
|
||||
/// - [`AppError::NotFound`] if no skill carries that name in either scope,
|
||||
/// - [`AppError::Store`] on a store failure.
|
||||
pub async fn execute(&self, input: ReadSkillInput) -> Result<MarkdownDoc, AppError> {
|
||||
// Project scope shadows global: try it first, then fall back to global.
|
||||
if let Some(skill) = self.resolve_in(SkillScope::Project, &input).await? {
|
||||
return Ok(skill.content_md);
|
||||
}
|
||||
if let Some(skill) = self.resolve_in(SkillScope::Global, &input).await? {
|
||||
return Ok(skill.content_md);
|
||||
}
|
||||
Err(AppError::NotFound(format!("skill `{}`", input.name)))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AssignSkillToAgent / UnassignSkillFromAgent
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -349,3 +425,185 @@ impl UnassignSkillFromAgent {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{SkillStore, StoreError};
|
||||
|
||||
/// In-memory [`SkillStore`] fake: skills are bucketed by scope, ignoring the
|
||||
/// project root (the [`ReadSkill`] resolution logic is root-agnostic — it only
|
||||
/// switches scope). Enough to exercise project-first resolution, shadowing,
|
||||
/// ambiguity and absence without touching disk.
|
||||
#[derive(Default)]
|
||||
struct FakeSkillStore {
|
||||
global: Mutex<Vec<Skill>>,
|
||||
project: Mutex<Vec<Skill>>,
|
||||
}
|
||||
|
||||
impl FakeSkillStore {
|
||||
fn bucket(&self, scope: SkillScope) -> &Mutex<Vec<Skill>> {
|
||||
match scope {
|
||||
SkillScope::Global => &self.global,
|
||||
SkillScope::Project => &self.project,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&self, skill: Skill) {
|
||||
self.bucket(skill.scope).lock().unwrap().push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SkillStore for FakeSkillStore {
|
||||
async fn list(
|
||||
&self,
|
||||
scope: SkillScope,
|
||||
_root: &ProjectPath,
|
||||
) -> Result<Vec<Skill>, StoreError> {
|
||||
Ok(self.bucket(scope).lock().unwrap().clone())
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
scope: SkillScope,
|
||||
_root: &ProjectPath,
|
||||
id: SkillId,
|
||||
) -> Result<Skill, StoreError> {
|
||||
self.bucket(scope)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|s| s.id == id)
|
||||
.cloned()
|
||||
.ok_or(StoreError::NotFound)
|
||||
}
|
||||
async fn save(&self, skill: &Skill, _root: &ProjectPath) -> Result<(), StoreError> {
|
||||
self.push(skill.clone());
|
||||
Ok(())
|
||||
}
|
||||
async fn delete(
|
||||
&self,
|
||||
_scope: SkillScope,
|
||||
_root: &ProjectPath,
|
||||
_id: SkillId,
|
||||
) -> Result<(), StoreError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn root() -> ProjectPath {
|
||||
ProjectPath::new("/proj").unwrap()
|
||||
}
|
||||
|
||||
fn skill(id: u128, name: &str, body: &str, scope: SkillScope) -> Skill {
|
||||
Skill::new(
|
||||
SkillId::from_uuid(uuid::Uuid::from_u128(id)),
|
||||
name,
|
||||
MarkdownDoc::new(body),
|
||||
scope,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn read_skill_uc(store: Arc<FakeSkillStore>) -> ReadSkill {
|
||||
ReadSkill::new(store)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_resolves_project_scope() {
|
||||
// (a) skill present in the project scope ⇒ its body is returned.
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
store.push(skill(1, "deploy", "PROJECT_BODY", SkillScope::Project));
|
||||
|
||||
let body = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "deploy".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(body.as_str(), "PROJECT_BODY");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_falls_back_to_global_scope() {
|
||||
// (b) absent from project but present globally ⇒ resolved via global.
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
store.push(skill(1, "deploy", "GLOBAL_BODY", SkillScope::Global));
|
||||
|
||||
let body = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "deploy".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(body.as_str(), "GLOBAL_BODY");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_project_shadows_global() {
|
||||
// (c) same name in both scopes ⇒ project wins.
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
store.push(skill(1, "deploy", "GLOBAL_BODY", SkillScope::Global));
|
||||
store.push(skill(2, "deploy", "PROJECT_BODY", SkillScope::Project));
|
||||
|
||||
let body = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "deploy".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(body.as_str(), "PROJECT_BODY");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_is_case_insensitive() {
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
store.push(skill(1, "Deploy", "BODY", SkillScope::Project));
|
||||
|
||||
let body = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "deploy".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(body.as_str(), "BODY");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_unknown_is_not_found() {
|
||||
// (d) unknown in both scopes ⇒ NotFound.
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
let err = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "ghost".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, AppError::NotFound(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_skill_ambiguous_name_is_invalid() {
|
||||
// (e) two skills share a name within a scope ⇒ Invalid (ambiguous).
|
||||
let store = Arc::new(FakeSkillStore::default());
|
||||
store.push(skill(1, "deploy", "ONE", SkillScope::Project));
|
||||
store.push(skill(2, "Deploy", "TWO", SkillScope::Project));
|
||||
|
||||
let err = read_skill_uc(store)
|
||||
.execute(ReadSkillInput {
|
||||
name: "deploy".to_owned(),
|
||||
project_root: root(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, AppError::Invalid(_)), "got {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user