## Lot D — actions contrôlées depuis Work

### Objectif produit

Rendre le panneau `Work` opérable sans changer sa nature : il reste un tableau de bord live/conversations, mais l’utilisateur peut agir sur l’état déjà visible.

Périmètre minimal prioritaire :
- retrouver ou afficher la cellule d’un agent live ;
- rattacher une session déjà live à une cellule existante et déterminée ;
- arrêter explicitement un agent live ;
- voir davantage de la preview conversationnelle Lot C ;
- copier le résumé affiché.

Exclus :
- pas de lancement d’un agent neuf depuis Work ;
- pas de création automatique de cellule/layout ;
- pas de cancel/requeue/priorisation FIFO ;
- pas de modification de handoff, mémoire automatique ou Lot E ;
- pas de lecture/exposition brute de `log.jsonl` ;
- pas de transcript complet.

### Actions à livrer

Priorité 1 — `Open` / `Go to cell`
- Si `agent.live.nodeId` correspond à une cellule visible dans le layout actif : focus/scroll/highlight de cette cellule.
- Aucun appel backend. Aucun spawn.
- Libellé UI : `Open` ou `Go to cell` selon la place.

Priorité 2 — `Attach`
- Si l’agent est live mais sa cellule hôte n’est plus visible, rattacher la session à une cellule existante et non ambiguë.
- Cible autorisée pour ce lot : une cellule visible déjà pinnée sur le même agent et sans session, ou une cellule explicitement sélectionnée par l’utilisateur dans le layout.
- Si aucune cible déterministe : action désactivée avec message court du type `Pin this agent to a cell first`.
- Ne jamais créer une nouvelle session. Ne jamais appeler `launch_agent`.

Priorité 3 — `Stop`
- Arrête la session live de l’agent après confirmation.
- Fonctionne pour PTY et structured.
- Ne supprime pas l’agent, ne supprime pas le handoff, ne vide pas `conversationId`.
- Après succès : refresh Work + layout/live state via events existants ou refresh explicite.

Priorité 4 — `View conversation`
- Expansion inline dans Work sur la base de `ProjectWorkState.conversations[]`.
- Montre objectif, summary preview, et les `recentTurns` déjà bornés par Lot C.
- Pas de nouveau endpoint de transcript.

Priorité 5 — `Copy summary`
- Frontend-only via clipboard.
- Copie uniquement le texte déjà exposé par Lot C : objectif + summary preview + recent turns bornés si summary absent.
- Désactivé si aucune preview exploitable.

### Backend

Réutiliser :
- `GetProjectWorkState` : source unique du panneau, inchangé fonctionnellement ;
- `LiveSessions` / `TerminalSessions` / `StructuredSessions` : source de vérité live ;
- `LayoutGateway.mutateLayout` avec `setSession` / `setCellAgent` ;
- `close_terminal` et `close_agent_session` comme primitives existantes ;
- `attach_live_agent` existant, mais à durcir.

Contrats backend recommandés :

1. Faire évoluer `attach_live_agent` en vrai use case applicatif `AttachLiveAgent`.

Entrée :
```rust
pub struct AttachLiveAgentInput {
    pub project: Project,
    pub agent_id: AgentId,
    pub node_id: NodeId,
}
```

Sortie :
```rust
pub struct AttachLiveAgentOutput {
    pub agent_id: AgentId,
    pub node_id: NodeId,
    pub session_id: SessionId,
    pub kind: LiveSessionKind,
}
```

Comportement :
- chercher la session live dans `TerminalSessions`, sinon `StructuredSessions` ;
- rebind `node_id` sans spawn ;
- retourner `NOT_FOUND` si l’agent n’est plus live ;
- idempotent si déjà attaché à cette cellule ;
- ne touche pas `ConversationLog`, `HandoffStore`, `ProviderSessionStore`.

2. Ajouter `StopLiveAgent` plutôt que faire deviner le registre au frontend.

Entrée :
```rust
pub struct StopLiveAgentInput {
    pub project: Project,
    pub agent_id: AgentId,
}
```

Sortie :
```rust
pub struct StopLiveAgentOutput {
    pub agent_id: AgentId,
    pub session_id: SessionId,
    pub kind: LiveSessionKind,
}
```

Comportement :
- résoudre la session live par agent ;
- si PTY : déléguer à `CloseTerminal` / `PtyPort.kill` ;
- si structured : `StructuredSessions::remove` puis `AgentSession::shutdown()` ;
- publier `DomainEvent::AgentExited { agent_id, code }` ou l’équivalent déjà relayé ;
- no-op contrôlé ou `NOT_FOUND` si déjà arrêté. Préférence : `NOT_FOUND` côté API, frontend l’interprète comme refresh.

Pourquoi pas seulement `close_terminal(sessionId)` : `WorkState.live.kind` existe, et le modèle a deux registries. Une commande par agent garde la cohérence single-agent côté backend.

### Tauri / DTO

Nouvelles/évolutions :

```ts
attach_live_agent({ request: { projectId, agentId, nodeId } })
  -> { agentId, nodeId, sessionId, kind }

stop_live_agent({ request: { projectId, agentId } })
  -> { agentId, sessionId, kind }
```

`LiveAgentDto` peut être réutilisé si on lui ajoute/garantit `kind`; sinon créer un DTO dédié. Préférence : enrichir `LiveAgent` additivement avec `kind: "pty" | "structured"`, cohérent avec `LiveWorkSession.kind`.

Pas de commande `get_conversation_log`, `open_conversation`, `copy_summary`.

### Frontend

Ports UI :
- `AgentGateway.attachLiveAgent(projectId, agentId, nodeId): Promise<LiveAgent>` doit retourner `kind` ;
- ajouter `AgentGateway.stopLiveAgent(projectId, agentId): Promise<StoppedLiveAgent>` ;
- ne pas faire appeler `close_terminal` directement par `ProjectWorkStatePanel` pour stopper un agent ;
- `LayoutGateway.loadLayout/mutateLayout` réutilisés pour trouver/attacher la cellule cible.

Coordination UI recommandée :
- extraire le helper de focus cellule (`goToCell`) de `LayoutGrid` vers `features/layout/focusCell.ts` ;
- ajouter un petit hook `useWorkActions(projectId, activeLayoutId?)` consommé par `ProjectWorkStatePanel` ;
- le hook sait charger le layout actif, trouver les feuilles, vérifier si `live.nodeId` est visible, et appeler `attachLiveAgent + mutateLayout(setCellAgent/setSession)` si une cible déterministe existe.

### Sécurité / cohérence

Règles obligatoires :
- `Open` et `Attach` ne spawnent jamais. Interdit d’appeler `launchAgent` depuis Work dans Lot D.
- `Attach` ne doit pas écraser une cellule qui héberge déjà une session différente, sauf remplacement explicite futur hors lot.
- `Stop` demande confirmation ; c’est une action destructive sur le process, mais pas sur les fichiers.
- `Stop` ne supprime ni agent, ni tickets, ni conversation summary, ni handoff.
- `Stop` doit libérer la live registry pour préserver l’invariant “un agent = une session live max”.
- Si un ticket est `inProgress`, afficher dans la confirmation que le tour en cours sera interrompu ; ne pas tenter de résoudre le ticket dans ce lot.
- Pas de permission filesystem nouvelle. Les seules mutations durables sont les mutations layout déjà existantes (`setSession`/`setCellAgent`) lors d’un attach.

### UI minimale

Dans chaque ligne agent :
- bouton `Open` si live ;
- bouton `Attach` si live mais cellule non visible et une cible existe ;
- bouton `Stop` si live ;
- état désactivé explicite si action impossible.

Dans chaque ticket/summary :
- bouton disclosure `View` pour ouvrir/fermer les détails Lot C ;
- bouton `Copy` à côté du summary, pas sur toute la ligne ;
- garder l’UI dense : pas de drawer global, pas de modal conversation, pas de rendu Markdown lourd.

Feedback :
- erreurs action en inline dans le panneau ;
- après `Stop`/`Attach`, refresh `getProjectWorkState` et layout actif ;
- `Copy` affiche un état bref `Copied`.

### Tests attendus

Application Rust :
- `AttachLiveAgent` rebind PTY sans changer `session_id` ;
- `AttachLiveAgent` rebind structured sans changer `session_id` ;
- attach agent absent => `NOT_FOUND` ;
- attach même node => idempotent ;
- `StopLiveAgent` arrête PTY et retire la session live ;
- `StopLiveAgent` arrête structured via `shutdown()` et retire la session live ;
- stop absent => `NOT_FOUND` ;
- stop ne touche pas handoff/log/provider sessions.

Tauri DTO/tests :
- request camelCase pour `attach_live_agent` et `stop_live_agent` ;
- response sérialise `kind`;
- erreurs `INVALID` ids et `NOT_FOUND` live session.

Frontend tests :
- `Open` focus une cellule visible et n’appelle aucun launch ;
- `Attach` appelle `attachLiveAgent` puis `mutateLayout(setSession)` sur une cible déterministe ;
- `Attach` désactivé sans cible ;
- `Stop` demande confirmation puis appelle `stopLiveAgent`, refresh Work ;
- `Stop` sur agent busy affiche l’avertissement ;
- `View conversation` n’affiche que les champs Lot C ;
- `Copy summary` copie uniquement la preview exposée, pas de log brut ;
- pas de régression des refresh events Lot A/B/C.

Commandes :

```bash
cargo test -p application terminal_usecases structured_registry workstate
cargo test -p app-tauri dto_agents list_live_agents_r0b
npm --prefix frontend test -- workstate
npm --prefix frontend test -- LayoutGrid singletonAgent agentAlreadyRunning
```

Puis en validation large :

```bash
cargo test
npm --prefix frontend test
```

### Décision de cadrage

Lot D doit être “actions sur état existant”, pas “nouveau centre de contrôle”. Le seul ajout backend vraiment justifié est de faire porter `attach/stop` par des contrats agent-level qui respectent PTY + structured et l’invariant single-agent. La conversation reste une preview bornée issue de Lot C ; le log brut et la mémoire automatique restent hors périmètre.


Après Lots Work A/B/C/D mergés dans `develop` @ `c1e99d1`, l'utilisateur demande d'enchaîner sur tout ce qu'il reste. Détermine le prochain chantier prioritaire hors Work et cadre un lot implémentable.

Contexte livré:
- A: read-model/panneau live/busy.
- B: tickets FIFO par agent.
- C: summaries conversationnels best-effort.
- D: actions contrôlées Open/Attach/Stop/View/Copy.

Restes connus mémoire projet:
- persistance conversationnelle produit/cross-profile plus profonde,
- handoff/canonical log incrémental,
- mise à jour automatique mémoire/contexte pendant la vie d'un agent,
- docs architecture potentiellement obsolètes,
- intégration MCP réelle/AppImage/e2e.

Merci de choisir le prochain lot prioritaire le plus utile et cadrer précisément:
- objectif produit,
- inclus/exclus,
- frontières domaine/application/infra/Tauri/frontend si applicable,
- tests attendus,
- ordre de délégation recommandé.

Ne code pas.


## Prochain chantier prioritaire : Lot E1 — Auto-memory contrôlée aux checkpoints

### Choix

Le prochain lot hors Work le plus utile est **la capitalisation automatique en mémoire projet**, mais en première marche contrôlée : écrire/mettre à jour la mémoire uniquement quand un agent émet une directive mémoire explicite dans sa réponse de fin de tour.

Pourquoi ce lot maintenant :
- Work A-D donne la visibilité et les actions sur les conversations.
- Le log/handoff incrémental et le cross-profile sont déjà très avancés dans le code/tests (`RecordTurn`, `FsConversationLog`, `FsHandoffStore`, provider sessions, swap préservant le pair id).
- La mémoire existe déjà : CRUD, `FsMemoryStore`, recall, injection au lancement, MCP `idea_memory_read/write`.
- Le vrai trou produit restant est que l’utilisateur ne doit pas avoir à transformer lui-même les apprentissages d’un agent en mémoire durable.

Ce lot est volontairement **déterministe et safe** : pas d’extraction LLM libre depuis tout le log, pas d’écriture depuis un résumé ambigu, pas de pollution massive.

### Objectif Produit

Quand un agent termine un tour et indique explicitement une information à mémoriser, IdeA la persiste automatiquement dans `.ideai/memory/` sans action utilisateur.

L’agent peut donc écrire, dans sa réponse finale, une directive structurée du genre :

```markdown
```idea-memory
slug: architecture-workstate-actions
title: Work panel actions
type: project
description: Work actions are Open/Attach/Stop/View/Copy, without spawning new sessions.
---
The Work panel may focus or attach existing sessions, but must not launch a new agent from Work.
```
```
```

IdeA détecte ce bloc au checkpoint, le valide, puis crée ou remplace la note mémoire correspondante via les ports existants.

### Inclus

- Parser déterministe de blocs `idea-memory` dans les réponses agent.
- Un use case applicatif `HarvestMemoryFromTurn` branché après un `Response` turn réussi.
- Écriture via le store mémoire existant, sous garde fichier quand applicable.
- Publication `MemorySaved` existante pour que l’UI puisse se rafraîchir.
- Injection d’une courte consigne dans le contexte agent : quand une décision durable est prise, émettre un bloc `idea-memory`.
- Best-effort strict : une directive invalide ou une erreur mémoire ne fait jamais échouer la réponse, le ticket, ni le handoff.

### Exclus

- Pas d’extraction automatique libre depuis tout `log.jsonl`.
- Pas de summarizer LLM.
- Pas de création de “suggestions” à valider dans une nouvelle persistance.
- Pas de refonte du MemoryPanel.
- Pas d’auto-modification du contexte global `CLAUDE.md`.
- Pas de mélange avec l’e2e MCP/AppImage.
- Pas de changement des règles Work A-D.

### Frontières Hexagonales

**Domaine**

Ajouter des value objects purs, sans I/O :

```rust
pub struct MemoryHarvestDirective {
    pub slug: MemorySlug,
    pub title: String,
    pub description: String,
    pub r#type: MemoryType,
    pub body: MarkdownDoc,
}

pub struct MemoryHarvestReport {
    pub found: usize,
    pub saved: usize,
    pub skipped: usize,
}
```

Le domaine porte les invariants : slug valide, body non vide, type autorisé. Pas de `tokio`, pas de FS.

**Application**

Nouveau use case :

```rust
pub struct HarvestMemoryFromTurn {
    memories: Arc<dyn MemoryStore>,
    events: Arc<dyn EventBus>,
    // optionnel si on veut passer par le garde existant :
    // guard: Arc<dyn FileGuard>,
}

pub struct HarvestMemoryFromTurnInput {
    pub project: Project,
    pub conversation: ConversationId,
    pub agent_id: AgentId,
    pub turn: ConversationTurn,
}
```

Règles :
- ne traiter que `TurnRole::Response` ;
- parser uniquement le texte du tour courant ;
- pour chaque directive valide, construire un `Memory` et appeler `MemoryStore::save(project.root, &memory)` ;
- publier `DomainEvent::MemorySaved { slug }` ;
- retourner un rapport, mais ne jamais propager l’erreur vers l’orchestration live si appelé en mode checkpoint.

Branchement :
- dans le chemin où `RecordTurn` est déjà appelé pour la réponse d’un `ask` réussi ;
- après append/handoff, car le log canonique reste prioritaire ;
- si harvest échoue, log diagnostic seulement.

**Infrastructure**

Aucun nouveau stockage.

Réutiliser :
- `FsMemoryStore` ;
- `FsConversationLog` / `FsHandoffStore` restent inchangés ;
- `FileGuard` si le wiring courant permet de passer par `WriteMemory`, sinon utiliser directement `MemoryStore` mais garder le lot borné aux écritures IdeA internes.

**Tauri**

Composition root :
- construire `HarvestMemoryFromTurn` avec le `memory_store_port` et `event_bus` existants ;
- l’injecter dans l’orchestrateur à côté de `RecordTurnProvider`.

Pas de nouvelle commande Tauri obligatoire.

**Frontend**

Minimal :
- `useMemory` doit écouter `MemorySaved` / `MemoryDeleted` / `MemoryIndexRebuilt` et rafraîchir l’index si le panneau mémoire est monté.
- Aucun nouvel écran.
- Optionnel : afficher une ligne discrète dans MemoryPanel quand l’index change, mais pas nécessaire pour E1.

### Format Directive Recommandé

Bloc fenced unique ou multiple :

```markdown
```idea-memory
slug: kebab-case-slug
title: Human title
type: project|reference|feedback|user
description: One-line recall hook
---
Markdown body to persist.
```
```

Contraintes :
- `slug` obligatoire ;
- `title` obligatoire ;
- `description` obligatoire et bornée ;
- `body` obligatoire ;
- taille max par bloc, par exemple `8 KiB` ;
- max blocs par réponse, par exemple `3` ;
- bloc invalide ignoré individuellement, sans bloquer les autres.

### Sécurité et Qualité Mémoire

- Une réponse ne peut pas écrire hors `.ideai/memory` car elle passe par `MemorySlug` + `MemoryStore`.
- Pas de chemins arbitraires.
- Pas de suppression automatique.
- Remplacement idempotent par slug : une même décision peut être raffinée.
- Les agents doivent recevoir la consigne : n’émettre un bloc mémoire que pour des décisions durables, conventions, préférences utilisateur ou faits projet stables.
- Ne pas mémoriser les tâches temporaires, logs d’exécution, tickets, erreurs transitoires.

### Tests Attendues

**Domaine / parser**

- parse un bloc valide ;
- parse plusieurs blocs ;
- refuse slug invalide ;
- refuse body vide ;
- borne taille et nombre de blocs ;
- ignore les fences non `idea-memory` ;
- conserve le Markdown du body sans corruption.

**Application**

- `HarvestMemoryFromTurn` ignore `Prompt` et `ToolActivity` ;
- une `Response` valide crée/remplace une note mémoire ;
- publie `MemorySaved` par note sauvegardée ;
- directives partiellement invalides : sauvegarde les valides, skip les invalides ;
- erreur `MemoryStore` convertie en rapport/diagnostic sans casser l’appelant checkpoint ;
- pas de lecture du `log.jsonl` complet.

**Orchestrateur**

- un `idea_ask_agent` réussi avec réponse contenant un bloc `idea-memory` écrit la mémoire après reply ;
- la réponse rendue au demandeur ne contient pas d’erreur même si la mémoire échoue ;
- `RecordTurn` continue de produire Prompt + Response comme avant ;
- pas de harvest sur timeout/cancel/réponse absente.

**Tauri / state**

- composition root câble le harvester ;
- événement `MemorySaved` relayé comme avant.

**Frontend**

- `MemoryPanel` se rafraîchit sur `MemorySaved` ;
- pas de refresh sur événements sans rapport ;
- l’éditeur manuel continue de fonctionner.

Commandes ciblées :

```bash
cargo test -p domain memory_harvest
cargo test -p application memory_harvest orchestrator_service
cargo test -p app-tauri state
npm --prefix frontend test -- memory
```

Validation large :

```bash
cargo test
npm --prefix frontend test
```

### Ordre de Délégation Recommandé

1. **Architecture** : valider le format `idea-memory`, les invariants et le point de branchement exact après `RecordTurn`.
2. **DevBackend** : domaine parser + use case `HarvestMemoryFromTurn` + wiring orchestrateur/state.
3. **DevFrontend** : refresh MemoryPanel sur événements mémoire, sans nouvelle UI lourde.
4. **QA** : tests ciblés parser/application/orchestrateur/frontend, puis régression Work A-D.
5. **Git** : revue diff, vérification que le lot ne touche pas MCP/AppImage ni les docs hors corrections nécessaires.

### Suite Après E1

E1 prépare deux lots futurs, sans les mélanger :
- **E2** : extraction assistée depuis handoff/log avec stratégie substituable, probablement LLM optionnel ou heuristique plus riche.
- **MCP/AppImage e2e** : validation réelle des outils IdeA dans un agent lancé depuis l’application packagée.

Décision : faire E1 maintenant. C’est le plus petit lot qui transforme les conversations observables en apprentissage durable, tout en respectant les frontières existantes et sans risquer une mémoire automatique bruitée.