Commit Graph

319 Commits

Author SHA1 Message Date
a04fbb7e1c merge(develop): intègre permissions/sandbox OS + pont Codex inter-agents
Intègre dans develop le travail terminé et testé porté par
wip/p8c-checkpoint-before-codex :
- enforcement OS des permissions (Landlock) bout-en-bout (LP4-0→LP4-4)
- pont Codex inter-agents + readiness/heartbeat
- layouts UI responsive + setup agent Git
Tests crates cœur (domain/application/infrastructure) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:56:34 +02:00
69304b0f6b chore(wip): état runtime .ideai (agents.json, layouts.json)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:56:06 +02:00
64ab3835c7 docs(agents): introduit l'agent Git (contexte + intégration au cycle)
Ajoute le contexte de l'agent Git (.ideai/agents/git.md) garant du dépôt
git local (commits, branches, merges/rebases) et l'inscrit dans le cycle
de dev de CLAUDE.md (§2.4 + étape « décide de la branche » / « merge
éventuel feature/* → develop »). Formalise le modèle main ← develop ←
feature/*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:56:06 +02:00
00bda6a988 chore(wip): état runtime .ideai (conversations, agents, layouts)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:39:07 +02:00
31b636037d fix(ui): layouts responsive pour panneau agents et bandeau d'onglets sidebar
AgentsPanel : formulaire de création et items d'agent passent en colonne,
les libellés/états longs (running in IdeA, profil) wrappent/tronquent au lieu
de déborder. ProjectsView : les onglets de la sidebar wrappent sur plusieurs
lignes pour rester tous visibles, le bouton collapse reste épinglé en haut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:39:03 +02:00
40bce5c8bf feat(sandbox): Landlock no-op sous Posture::Allow (allow-by-default)
Un allowlist Landlock ne peut que retirer des accès (deny-by-default). Sous
une fallback Posture::Allow (allow-by-default), enforcer les grants
verrouillerait tout hors du project root — binaire CLI, libs et ~/.<cli> —
provoquant le 'failed to open terminal'. On n'enforce donc rien sous Allow ;
seuls les Deny explicites font foi (advisory via la projection LP3).
Couvre permissions.json persisté avec fallback allow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:38:57 +02:00
ab9162f686 docs(memory): consigne l'etat du systeme de permissions/sandbox + risque residuel $HOME
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:51:57 +02:00
6236cd727b feat(permissions): LP4-4 — enforcement Landlock sur le chemin structuré
Étend l'enforcement OS au chemin structuré (sessions Claude/Codex mode JSON),
jusqu'ici seulement advisory. Approche validée par l'Architecte : transposer la
technique du PTY plutôt qu'un pre_exec (rejeté — landlock alloue, deadlock malloc
post-fork en process multithreadé).

Mécanique (cfg(target_os=linux)) : run_turn_sandboxed/drain_sandboxed exécutent
enforce(plan) sur un thread jetable AVANT le spawn std, puis std::process::spawn
depuis ce thread ; l'enfant hérite le domaine Landlock via les credentials de la
tâche (garanti à travers fork/clone/execve, y compris posix_spawn — pas de pre_exec
nécessaire, forbid(unsafe_code) préservé). Fail-closed sur Err d'enforce (aucun
child). Timeout sous sandbox : oneshot killer + tokio::time::timeout → kill → EOF →
reap (pas de zombie/thread bloqué). Chemin non-sandboxé (plan None / pas d'enforcer /
non-Linux) = drain async tokio inchangé.

Contrat : SpawnLine.sandbox ; AgentSessionFactory::start(.., sandbox) ;
StructuredSessionFactory::with_sandbox_enforcer (jumeau du PTY) ; plan calculé en
step 5d de lifecycle relayé à launch_structured ; default_enforcer() injecté au
composition root.

Tests : 7 invariants e2e (parité, companion négatif, fail-closed, no-op natif,
confinement de l'irréversibilité entre tours, timeout, resume préservé) — zéro
token (sh/FakeCli). 80 suites vertes, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:47:33 +02:00
7e01ac60cb feat(permissions): LP4-3 — activation bout-en-bout de la sandbox OS au runtime
Composition root : PortablePtyAdapter::new() injecte désormais default_enforcer()
(Landlock sur Linux / Noop ailleurs) via with_sandbox_enforcer, dans state.rs et
dans LocalHost (remote/mod.rs).

Launch path : LaunchAgent::execute peuple spec.sandbox via le plan pur
compile_sandbox_plan(effective_permissions, SandboxContext{project_root, run_dir}),
en réutilisant l'EffectivePermissions déjà résolu pour la projection advisory LP3.
Invariant produit conservé : eff == None ⇒ plan None ⇒ comportement natif, aucune
projection OS.

Portée : seul le chemin PTY brut est OS-enforcé ; le chemin structuré (Claude/Codex
mode JSON) porte le plan mais ne l'enforce pas encore (lot ultérieur).

Tests : ajout d'un test d'intégration bout-en-bout (pty/mod.rs, sandbox_e2e_tests)
prouvant qu'un enfant spawné sous un plan RW restreint ne peut PAS écrire hors-grant
(écriture bloquée par le kernel) alors que l'écriture dans le grant passe, plus un
test compagnon anti faux-positif (sans plan ⇒ pas de sandbox). Skip propre si le
kernel n'a pas Landlock. domain+infrastructure+application+app-tauri verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:08:40 +02:00
17ca65ed0f feat(permissions): LP4-1/LP4-2 — enforcer Landlock OS + consommation PTY du plan
LP4-1 : adaptateurs SandboxEnforcer concrets (LandlockSandbox Linux via le LSM
Landlock, NoopSandbox ailleurs) + default_enforcer() cfg-sélectionné, prêts à
être injectés au composition root (LP4-3). Aucun changement de comportement
runtime tant que rien n'est câblé : SpawnSpec.sandbox reste None.

LP4-2 : PortablePtyAdapter consomme SpawnSpec.sandbox via spawn_command_sandboxed
(thread jetable restreint par enforce() puis fork → le domaine Landlock est hérité
par l'enfant à travers execve ; les autres threads d'IdeA ne sont jamais sandboxés).
Builder additif with_sandbox_enforcer ; fail-closed si enforce() échoue.

Tests : domain + infrastructure verts (dont les tests Landlock réels de fencing
read-only/write-only et la confine-au-thread).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:00:47 +02:00
b05d04ab7a feat(permissions): LP4-0 — fondations domaine de l'enforcement OS (pur)
Fondations pures (zéro I/O, zéro dépendance landlock, aucun câblage
runtime — SpawnSpec.sandbox posé mais jamais lu ⇒ zéro régression) de la
voie « airtight » des permissions, complément de la voie projection LP3.

- domain/sandbox.rs : SandboxPlan/PathGrant/PathAccess (RO|RW|EXEC),
  SandboxContext, SandboxKind/Status/Error, port SandboxEnforcer, et la
  fonction pure compile_sandbox_plan(EffectivePermissions → plan OS).
- domain/permission.rs : render_permission_summary (bloc Markdown injecté
  plus tard ; mentionne explicitement fichiers OS-enforced vs commandes
  advisory).
- domain/ports.rs : SpawnSpec.sandbox: Option<SandboxPlan> (None ⇒ natif),
  propagé à tous les sites de construction.

Sémantique de compile_sandbox_plan :
- Invariant produit : eff == None ⇒ None (rien posé ⇒ CLI 100 % native).
- Borne Landlock : seules les capabilities fichier produisent des grants
  (Read→RO, Write/Delete→RW) ; ExecuteBash reste advisory (non verrouillable
  par chemin).
- Deny-wins PAR CLASSE D'ACCÈS (RO/RW), fail-closed intra-classe : un Deny
  ne ferme que les grants de sa propre classe (un Deny Write n'ampute pas un
  Allow Read). Choix retenu pour maximiser l'autonomie des agents : on
  respecte exactement la politique pré-renseignée sans sur-restreindre, donc
  moins de blocages qui forceraient l'agent à redemander l'utilisateur.
- Globs réduits à leur préfixe statique ; grant abandonné si une barrière de
  même classe chevauche (égal/ancêtre/descendant) — sous-approximation
  conservatrice (un sandbox additif ne peut pas carver un deny sous-arbre).

Tests : 16 tests purs sur sandbox + 3 sur render_permission_summary,
cargo test --workspace 100 % vert, 0 ignored.

Reste LP4 : LP4-1 adapter LandlockSandbox + pre_exec PTY (fail-open+warning
sauf posture Deny), LP4-2 câblage application, LP4-3 composition root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:37:34 +02:00
27597eb64e feat(permissions): voie projection CLI (LP0→LP3) + checkpoint Codex/input
Jalon vert regroupant deux chantiers entrelacés dans le working tree,
indissociables au niveau fichier mais tous deux verts (cargo test
--workspace + tests frontend permissions au vert).

Permissions — voie « projection CLI » (advisory), complète :
- LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve
  deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs).
- LP1 store : FsPermissionStore (.ideai/permissions.json).
- LP2 use cases : Get/Update project, Update agent override, Resolve.
- LP3 projecteurs Claude/Codex (settings.local.json / config.toml),
  câblage launch-path + PermissionProjectorRegistry, nettoyage des
  fichiers Replace orphelins au swap de profil (LP3-4), composition root
  + commandes Tauri, UI PermissionsPanel (projet + override agent).
- ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap).

Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS
airtight (Landlock fichiers) + résumé de permissions injecté.

Inclut aussi le chantier Codex/input/sessions structurées en cours
(McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les
mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:39:18 +02:00
46492506e1 fix(orchestrator): garde RAII contre une cible coincée Busy après une délégation interrompue
Une délégation `idea_ask_agent` interrompue/annulée côté demandeur (futur
`ask_agent` *dropped*) laissait la cible en `Busy` à vie : sur un drop, aucune
branche du `select!` ne s'exécute, donc `mark_idle` n'était jamais appelé. Toute
délégation suivante restait en file derrière ce tour fantôme et n'était jamais
livrée au PTY (symptôme : « DevFrontend/QA ne répondent plus », pont MCP pourtant
ESTAB).

Ajoute un garde RAII `BusyTurnGuard` posé juste après l'enqueue sur les deux
chemins (`ask` PTY et `ask_structured`) : son `Drop` fait `cancel_head` +
`mark_idle` quoi qu'il arrive (erreur, timeout, drop), sauf `disarm()` sur la
branche succès. Indispensable d'être un garde et non un `mark_idle` dans les
branches : le cas réel est un futur droppé. Retire les `cancel_head` redondants
des branches erreur/timeout (positionnel/idempotent).

`sweep_stalled` reste advisory (n'appelle jamais `mark_idle`) — non concerné.

Tests: dropped_ask_future_frees_busy_target, second_delegation_delivered_after_dropped_ask,
cancelled_ask_marks_target_idle + 2 tests du garde. cargo test --workspace vert (80 suites).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:15:01 +02:00
aa2f67ae89 fix(orchestrator): ne pas câbler with_structured sur l'orchestrateur (régression 0f8ba38)
Régression introduite par 0f8ba38 : le re-câblage de `.with_structured(...)`
sur `OrchestratorService` (pour activer `drain_with_readiness`, readiness lot 1)
faisait emprunter à `ask_agent` la branche structurée `ensure_structured_session`,
qui ne peut JAMAIS aboutir : depuis le lot B-2 (« Option 1 Terminal + MCP »,
eca2ba9) la fabrique structurée est décâblée de `LaunchAgent`, donc aucun agent
n'a de session structurée — tous tournent en PTY brut et la délégation passe par
les outils MCP. Symptôme : `idea_ask_agent` échouait systématiquement avec
« aucune session structurée vivante après lancement » à chaque relance d'IdeA.

Non détecté par les tests : les e2e loopback câblent leur propre service
structuré complet, jamais le composition root réel (gap tests ≠ composition).

Fix : retirer le `.with_structured` du composition root pour que
`self.structured = None` et que `ask_agent` retombe sur le chemin PTY+MCP
fonctionnel (état pré-0f8ba38). `drain_with_readiness` reste dormant tant que
la voie structurée n'est pas réactivée au composition root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:45:39 +02:00
0f8ba38d51 feat(agents): pont Codex inter-agents + readiness/heartbeat lot 1
Deux chantiers livrés au vert (workspace entier : domain+application+
infrastructure 42 + app-tauri --lib 128, 0 échec).

## Codex inter-agents
- domaine: McpConfigStrategy::TomlConfigHome { target, home_env } +
  toml_config_home(...); AgentProfile::materializes_idea_bridge()
  (whitelist Claude/ConfigFile + Codex/TomlConfigHome); McpServerWiring
  + encodeur TOML.
- application: lifecycle apply_mcp_config bras TomlConfigHome (écrit
  {runDir}/<target>, pousse (home_env, parent) dans spec.env);
  guard_mcp_bridge_supported ré-exprimée via materializes_idea_bridge();
  catalogue Codex porte toml_config_home(".codex/config.toml","CODEX_HOME").
- app-tauri: is_codex_mcp_profile, migrate_codex_run_dir,
  mcp_server_entry_toml.
- tests: matrice domaine TomlConfigHome + round-trip dual Claude/Codex
  sur loopback réel (fakes, zéro token).

## Readiness/heartbeat lot 1
- domaine: readiness.rs — ReadinessPolicy::classify (Final => TurnEnded),
  variantes ReplyEvent::Heartbeat / ToolActivity.
- application: drain_with_readiness consulte la policy et appelle
  mark_idle sur le signal déterministe; branché dans ask_agent.
  Corrige la cause racine: une cible qui ne renvoie qu'un Final (sans
  idea_reply) débloque désormais sa file Busy.
- infrastructure: adapters de session émettent Heartbeat/ToolActivity.
- tests: drain_with_readiness_lot1 (points QA 5 & 6) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:28:44 +02:00
fdcf16c387 chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations
C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le
support de la délégation inter-agents pour les profils Codex.

Le round-trip inter-agent question/réponse est couvert sans tokens par
les tests loopback existants (state::mcp_e2e_loopback_tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:42:53 +02:00
4509f0db9d feat(persistence): P8d — swap cross-profile préserve l'id de paire + handoff
Capstone du chantier handoff : un agent change de moteur (Claude↔Codex) en
gardant la continuité du travail.

- clean_conversation → invalidate_engine_link : préserve conversation_id (id de
  paire stable) et n'efface que engine_session_id (lien moteur étranger) ;
  renvoie l'id de paire préservé
- relaunch_if_live relance avec l'id de paire (repli for_pair si session de
  fond) ⇒ handoff P7 réinjecté dans le nouveau moteur, resume P8c routé via
  providers.json[nouveau provider] (vide ⇒ SessionPlan::None : l'ancien
  resumable n'est jamais repassé ; fidélité par le handoff)
- tests : 4 cas swap (préservation id de paire, engine_session_id vidé, handoff
  réinjecté, pas de --resume de l'ancien moteur, repli for_pair) ;
  change_agent_profile 12 verts, agrégat 829 passed 0 failed

Chantier persistance conversationnelle + handoff cross-profile (P1→P8d) complet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:52:50 +02:00
d87b8f6ed2 feat(persistence): P8c — routage --resume moteur via providers.json (structuré)
Referme la régression latente de P8a : pour un profil structuré, le resumable
passé au moteur vient de providers.json[provider] (vrai id Claude/Codex), jamais
de l'id de paire désormais porté par la cellule.

- resolve_session_plan devient async + prend root ; branche structurée câblée :
  get(pair, provider_key) ⇒ Resume{engine_id}, sinon None ; repli gracieux sur
  l'ancien comportement si le store n'est pas câblé (zéro régression tests) ;
  branche non structurée strictement inchangée
- tests : 6 cas (claude/codex Resume{engine} avec assert_ne! vs id de paire,
  discrimination de clé provider, store vide/cellule neuve/non-uuid ⇒ None) ;
  structured_launch_d3 22 verts, domain+application+app-tauri sans régression

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:42:35 +02:00
9b053216e3 feat(persistence): P8b — écriture providers.json (resumable par provider)
Au lancement structuré, le resumable exposé par le moteur est rangé dans
providers.json sous (id de paire, provider) — best-effort, n'altère jamais le
lancement.

- domaine : StructuredAdapter::provider_key() ⇒ "claude"/"codex" (clé stable
  par famille, lisible, indépendante de l'uuid d'instance)
- application : port ProviderSessionProvider (root par appel) +
  with_provider_session_provider ; helper persist_provider_session dans
  launch_structured (skip si provider/adapter/id moteur absents ou set KO)
- app-tauri : AppProviderSessionProvider (FsProviderSessionStore sur le root)
- tests : 6 cas (nominal claude/codex, no-op sans provider/sans id moteur,
  best-effort, mapping provider_key) ; domain+application verts

Lecture du store pour --resume = P8c.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:36:19 +02:00
09cc8f0902 test(persistence): P8a — déterminisme registre + cohérence de clé end-to-end
Scelle la cohérence corrigée en P8a :
- infra (5) : resolve stable à travers 2 instances de registre (restart),
  resolve.id == for_pair == uuid agent, commutativité, distinction des paires
- application (2) : round-trip réel « handoff sauvé sous la clé resolve ↔
  rechargé au relancement de la cellule portant cette clé » + contre-épreuve
  (handoff d'une autre paire ⇒ pas de reprise)

Clarifie : une cellule neuve (conversation_id=None) ne charge pas de handoff ;
la reprise opère à la réouverture. domain/infra/application verts, zéro
régression. Code de prod non modifié.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:27:09 +02:00
e583b2b49f feat(persistence): P8a — cohérence de clé conversation (id de paire stable)
Corrige l'incohérence P6/P7 : la cellule et la persistance partagent désormais
le MÊME id de paire IdeA, déterministe et stable au redémarrage — la clé sous
laquelle P6b sauve log/handoff == celle sous laquelle P7 les charge.

- domaine : ConversationId::for_pair(a,b) pur/déterministe (User↔Agent = uuid
  agent ; Agent↔Agent = XOR commutatif) ; LeafCell gagne engine_session_id
  (cache resumable moteur, additif serde default)
- infrastructure : InMemoryConversationRegistry::resolve utilise for_pair au
  lieu de new_random() → id recalculable sans état, identique après restart
- application : launch_structured persiste l'id de paire sur la cellule (et l'id
  moteur sur engine_session_id) ; cellule neuve ⇒ dérive for_pair(User,agent) ;
  resolve_conversation repli factorisé sur for_pair ; withers layout clone-and-
  mutate (préservent les champs additifs)
- app-tauri : engine_session_id propagé dans les DTO

Suites domain/infra/application/app-tauri vertes (assertion structured_launch_d3
recodée sur le contrat). Test déterminisme/restart dédié à ajouter au retour de
quota agent (binôme Test interrompu par limite de session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:10:50 +02:00
19ba77824f docs(architecture): §19.7 cohérence des ids + découpage P8a→P8d
Cadrage du swap cross-profile et correction de l'incohérence de clé P6/P7 :
LeafCell.conversation_id = id de paire IdeA (clé log/handoff, stable au swap),
resumable moteur isolé par provider dans providers.json (+ cache
engine_session_id). §19.6 : P8 éclaté en P8a (prioritaire/bloquant) → P8d.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:47:57 +02:00
c1411d3b69 feat(persistence): P7 — reprise par injection du handoff au lancement
Au (re)lancement, l'agent retrouve sur quoi il travaillait : si la cellule a une
conversation_id avec un handoff, son objectif + résumé sont injectés en dernière
section du convention file (# Reprise de la conversation), best-effort et
provider-indépendant.

- application : port HandoffProvider (root par appel, comme MemoryRecall) ;
  LaunchAgent.with_handoff_provider + resolve_handoff (parse uuid + load,
  jamais bloquant) ; section rendue dans compose_convention_file après la
  mémoire projet
- app-tauri : AppHandoffProvider (FsHandoffStore sur le root du projet) câblé
- tests : 4 purs (objectif présent/None/blanc/absent) + 5 intégration
  (happy path, provider absent, conversation_id None, uuid invalide, sans
  handoff ⇒ lancement OK sans section) ; application 311 + app-tauri 204 verts

Volet --resume/providers.json = P8 (swap cross-profile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:42:19 +02:00
0bf7a5c43c feat(persistence): P6b — câblage live du checkpoint conversationnel (best-effort)
ask_agent persiste désormais chaque paire dans son conversationId : tour Prompt
à l'enqueue, tour Response au succès — best-effort (un échec de persistance ne
dégrade jamais la délégation).

- application : port RecordTurnProvider (matérialise un RecordTurn sur le bon
  project root — OrchestratorService est mono-instance multi-projets, le log est
  par root) ; wither with_record_turn(provider, Clock) ; helper
  record_turn_best_effort ; horodatage via domain::ports::Clock (pas d'horloge
  infra dans application)
- app-tauri : AppRecordTurnProvider (Fs* sur le root du projet) câblé au
  composition root avec le SystemClock partagé
- tests : 5 cas (paire Prompt→Response, fil A↔B vs User↔B, no-op sans provider,
  ask Ok même si record échoue) ; orchestrator_service 40, application +
  app-tauri verts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:31:13 +02:00
2a5873dcf0 feat(persistence): P6a — use case RecordTurn (checkpoint fin de tour, isolé)
Logique du checkpoint conversationnel isolée et testable, sans câblage live :
append du tour au log canonique → load handoff → fold incrémental (tour neuf
seul) → save. Mapping StoreError→AppError via From existant. Debounce repoussé.

- application/src/conversation/{mod,record}.rs : RecordTurn::record
- tests : 6 cas (incrémentalité prouvée via summarizer-espion, propagation
  d'erreur sans effet de bord, isolation par conversation) ; suite verte

Câblage sur les seams réels (reply/ask) + composition root = P6b.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:20:05 +02:00
f3046f3dd8 feat(persistence): P5 — ProviderSessionStore (resumable_id par provider)
Range le resumable_id du moteur par (conversation, provider) dans
providers.json, support de reprise non exclusif (le handoff reste la source de
fidélité au swap cross-profile) — §19.2/§19.3.

- domaine : port ProviderSessionStore (get/set par provider)
- infra : FsProviderSessionStore — providers.json map plate, set en
  read-modify-write atomique (tmp+rename) sérialisé par conversation
  (coexistence multi-providers garantie), get absent ⇒ None, corrompu ⇒
  StoreError::Serialization

Tests : 6 cas P5 (coexistence claude+codex, 12 set concurrents sans perte,
corruption, isolation) ; cible conversation_log 30 verts, suites complètes
sans régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:14:34 +02:00
75e4f57a71 feat(persistence): couche conversationnelle — cadrage §18/§19 + briques P1→P4
Resync ARCHITECTURE.md (état livré + cadrage persistance/handoff) et premières
briques de la couche de persistance conversationnelle (log canonique par paire
+ handoff incrémental), indépendante du provider — prépare reprise fiable et
handoff cross-profile Claude↔Codex.

ARCHITECTURE.md
- §14.3.2/§17 : M5 marqué livré, verrou « ouvert » périmé, §17 réconcilié
  (vue = terminal de sortie, pas d'UI chat) ; §18 état livré 2026-06-12 ;
  §19 cadrage persistance/handoff (log par paire + handoff, 10 lots P1→P10)

Domaine (conversation_log.rs, pur)
- P1 : ConversationTurn / TurnId / TurnRole + port ConversationLog
- P3 : Handoff + port HandoffStore
- P4 : port HandoffSummarizer (async, seam OCP pour adapter LLM futur)

Infrastructure (conversation_log/)
- P2 : FsConversationLog — JSONL append-only par paire, sync_all (durabilité
  crash), skip ligne corrompue, fichier absent ⇒ vide
- P3 : FsHandoffStore — handoff.md front-matter, write atomique tmp+rename
- P4 : HeuristicHandoffSummarizer — incrémental, zéro modèle/I/O, fenêtre WINDOW

Tests : domaine 12 + infra 24 (conversation_log) verts, suites complètes sans
régression. Cycle dev/test : le binôme a débusqué et corrigé un bug de
durabilité (append sans flush) au passage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:09:35 +02:00
eca2ba95c4 feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP
Coeur inter-agents consolidé et surface front réalignée sur la décision
"terminal natif PTY, pas d'UI chat" (Option 1).

Domaine
- nouveaux modules conversation, mailbox, input, fileguard (ports + types)
- orchestrator/profile/events étendus (conversation par paire, FIFO)

Application / Infrastructure
- orchestrator/service + context_guard : sérialisation FIFO par agent,
  garde RW mémoire/contexte, dispatch ask/reply
- adapters in-memory conversation / mailbox / input / fileguard
- registry session + lifecycle agent durcis (1 agent = 1 session vivante)
- outils MCP idea_* alignés sur le nouveau dispatch

Frontend
- MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA,
  terminal = vue sortie inchangée
- suppression de la vue chat structurée (AgentChatView) — abandonnée
- adapter input + ports mis à jour

Divers
- .ideai/ : mémoire projet + briefs de cadrage versionnés ;
  requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA)

Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 07:33:04 +02:00
5f45c22941 fix(agent): ne câbler le PTY qu'en lancement brut, pas structuré — §17.4/§17.6
Un launch structuré route vers une AgentSession enregistrée dans
StructuredSessions ; son id n'est jamais un PTY vivant. Câbler le bridge PTY
appelait subscribe_output avec un id inconnu de l'adapter → NotFound
("pty handle not found"), faisant échouer tout lancement d'agent structuré.
On ne pompe les octets que sur le chemin PTY brut (structured: None).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:15:36 +02:00
2f20fdbab4 fix(agent): retrait --ask-for-approval invalide pour codex exec 0.137 — §17.9 D3
`codex exec` 0.137 ne connaît pas `--ask-for-approval` (`error: unexpected
argument`) ; ce flag appartient à la commande interactive `codex`, pas à la
sous-commande `exec` déjà non-interactive. Sa présence cassait toute réponse
d'un agent Codex (flux structuré terminé sans Final), bloquant la conversation
inter-agents. argv désormais verrouillé exact dans les tests (16/16 verts).

Découvert et corrigé pendant la validation réelle de la messagerie inter-agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:15:36 +02:00
cf89b3b9a5 feat(agent): bind transport S-MCP — outils idea_* vivants de bout en bout (M5a-e) — §14.3.1
Dernier kilomètre de l'orchestration native : une CLI MCP réellement lancée
joint le serveur MCP du projet et ses outils idea_* aboutissent au vrai dispatch.

- M5a endpoint loopback par projet (interprocess UDS/named pipe, source unique
  mcp_endpoint, cleanup au close) — zéro port réseau, AppImage/SSH-safe.
- M5b sous-commande `idea mcp-server` : pont stdio↔loopback headless (avant init
  Tauri), handshake {"project","requester"}, endpoint-absent borné, EOF propre.
- M5c McpServerHandle boucle accept + serve_peer par pair ; requester réel
  propagé jusqu'à OrchestratorRequestProcessed (fin du "mcp" figé) ; isolation
  des pairs ; terminaison propre.
- M5d apply_mcp_config écrit la déclaration réelle (current_exe + --endpoint
  mcp_endpoint + --project simple-uuid + --requester) ; McpRuntime injecté comme
  donnée (application ne dépend pas de app-tauri).
- M5e smoke e2e sur vrai loopback : list/ask inline, cible PTY → erreur typée,
  JSON malformé → pas de panic, requester propagé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:09:41 +02:00
6ca519b815 feat(agent): robustesse routage ask — fix registre session + concurrence (R0+A0) — §14.3/§15
Stabilise le routage de `ask` avant le bind transport MCP (v5). Invariant
« 1 agent = 1 employé » durci ; un agent traite un tour à la fois.

- R0a garde LaunchAgent : lève AgentAlreadyRunning pour un lancement neuf
  ciblant un agent déjà vivant sur un autre node (PTY + structuré) ; rebind
  seulement même-node ou réattache explicite (conversation_id). Idem spawn_agent.
- R0b list_live_agents agrège PTY + structuré (LiveSessions) + dédup.
- R0c réconciliation des layouts.json à doublons à l'ouverture (host déterministe,
  idempotent) — corrige « une cellule reset au retour d'onglet ».
- R0d UI : option agent désactivée si vivant ailleurs + « aller à la cellule »,
  mapping AGENT_ALREADY_RUNNING.
- A0 sérialisation FIFO des tours par agent_id dans ask_agent (verrou tokio par
  agent ; agents différents en parallèle ; timeout tour 300s, cap attente 600s).

Cadrage : .ideai/briefs/orchestration-v5-transport-bind-cadrage.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:34:57 +02:00
37e72747d3 feat(agent): orchestration v3 — surface MCP model-agnostic (M0→M4) — §14.3
Expose l'orchestration IdeA comme serveur MCP par-dessus le même
OrchestratorService::dispatch, avec repli fichier .ideai/requests pour les
CLI sans MCP. v3 réduite à la surface MCP : la messagerie inter-agents et la
corrélation requête↔réponse étaient déjà résolues par §17 (send_blocking).

- M0 capacité MCP sur le profil (McpCapability/McpConfigStrategy/McpTransport)
- M1 injection conf MCP au LaunchAgent + prose adaptée selon la surface
- M2 serveur/adapter MCP (JSON-RPC 2.0 maison ; outils idea_*) + ListAgents
- M3 câblage par projet (registre mcp_servers jumeau du watcher)
- M4 observabilité UI : OrchestratorRequestProcessed.source = file|mcp + badge

Trois portes d'entrée (fichier, MCP, UI) → un seul dispatch ; aucun nouveau
port applicatif ; MCP confiné à l'adapter infra. Tous lots verts (cycle §3).
Cadrage : .ideai/briefs/orchestration-v3-cadrage.md ; ARCHITECTURE.md §14.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:53:31 +02:00
97daf3fae5 feat(agent): surfacer reply dans le writer wire .response.json (D6b) — §17
Ferme le canal fichier de la messagerie inter-agents : un agent qui délègue
via le protocole .ideai/requests reçoit désormais le CONTENU de la réponse,
plus seulement l'ACK de cycle de vie.

- OrchestratorResponse gagne reply: Option<String> (skip_serializing_if
  None, camelCase). success() porte le reply ; failure() => None.
- dispatch_file passe out.reply (de l'OrchestratorOutcome D6) à success.

Champ additif : un .response.json legacy sans reply reste valide ; une
commande non-ask (agent.run, spawn_agent...) => clé reply absente du JSON.

Tests (QA) : +4 verts — ask => reply présent ET detail coexistent ;
non-ask => clé absente (garde anti-always-green sur l'absence littérale) ;
round-trip legacy ; failure => absent. cargo test --workspace : 824 passed.

Orchestration structurée bouclée : in-process (D6) + protocole fichier (D6b).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:04:13 +02:00
6de4e5a6e0 feat(agent): menu de profils restreint Claude/Codex + retrait custom (D7) — §17
Dernier lot de §17 : seuls les profils pilotables en mode structuré sont
proposés à la sélection/création.

- domain : AgentProfile::is_selectable() = structured_adapter.is_some(),
  source unique de vérité du prédicat.
- infrastructure : AgentSessionFactory::supports délègue à is_selectable
  (supports et is_selectable ne peuvent plus diverger).
- application : selectable_reference_profiles() = reference_profiles()
  filtré ; ReferenceProfiles et FirstRunState exposent la liste filtrée
  (Claude/Codex). reference_profiles() brut reste à 4 (data intacte) ⇒
  un agent Gemini/Aider/custom legacy déjà configuré continue de tourner.
- frontend : bloc AddCustomProfile retiré du wizard first-run, action
  addCustom retirée du viewmodel ; wizard n'affiche que la liste filtrée.

Tests (QA) : is_selectable (table de vérité), cohérence stricte
is_selectable<->supports, liste exposée=2 / data brute=4, garde
anti-régression Vitest sur l'absence du bloc custom — validées par
mutation. cargo test --workspace : 820 passed. npx vitest run : 344 passed.

§17 COMPLET (D0->D7). Suivi restant : D6b (surfacer reply dans le writer
wire .response.json pour la délégation par protocole fichier).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:57:51 +02:00
dd1194abe8 feat(agent): messagerie inter-agents via send_blocking (D6) — §17
Comble le trou historique : un agent qui en interroge un autre reçoit enfin
le CONTENU de la réponse, plus seulement un ACK de cycle de vie.

- domain : OrchestratorCommand::AskAgent { target, task } + action wire
  agent.message (target+task requis) ; DomainEvent::AgentReplied
  { agent_id, reply_len } (bus I/O-free, le contenu remonte par l'outcome).
- application : OrchestratorOutcome gagne reply: Option<String>. Méthode
  ask_agent (§17.4) — cible structurée vivante ⇒ send_blocking direct ;
  cible morte ⇒ LaunchAgent structuré puis send ; agent vivant en PTY ou
  profil sans structured_adapter ⇒ Invalid (non adressable, jamais d'ACK
  trompeur) ; agent inconnu ⇒ NotFound ; service non câblé ⇒ Invalid.
  Timeout (300s) ⇒ erreur typée SANS tuer la session. Succès ⇒ publie
  AgentReplied + reply: Some(content). Injection additive via builders
  with_structured / with_events (call sites legacy intacts).
- app-tauri : state câble with_structured/with_events ; DTO AgentReplied.

Tests (QA) : +14 verts (11 app + 3 domaine), invariants validés par
mutation test (reply!=None, timeout ne shutdown pas). cargo test
--workspace : 817 passed, 0 failed.

Suivi (D6b) : surfacer reply dans le writer wire .response.json
(infrastructure/orchestrator) pour la délégation par protocole fichier.
Reste D7 : menu restreint Claude/Codex + retrait custom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:44:53 +02:00
5059f37890 feat(agent): vue chat frontend + routage cellKind (D5) — §17
Le frontend consomme l'exécution structurée livrée en D4 :
- AgentChatView : jumeau chat de TerminalView. Accumule les textDelta du
  tour courant, badges toolActivity, fige sur final (le contenu final
  remplace les deltas, pas de double rendu). Saisie Enter/Shift+Enter.
  Au montage : reattach (rejoue le scrollback) ou launch ; au démontage :
  détache seulement (jamais de close). Vue pure pilotée par le port.
- LayoutGrid/LeafView : routage cellKind — AgentChatView si "chat",
  TerminalView sinon (chemin PTY inchangé). Cache cellKindBySession pour
  router correctement après ré-attache/navigation.
- Port AgentGateway : sendPrompt / reattachChat / closeAgentSession ;
  cellKind sur TerminalHandle. Adapter Tauri (invoke + Channel<ReplyChunk>)
  et mock (MockChatSession, _setChatAgents) étendus.
- Types TS mirrors : CellKind, ReplyChunk, ReattachChatDto + cellKind sur
  TerminalSession.

Tests (QA) : 21 Vitest verts — routage chat/pty (non-régression terminal),
accumulation/non-doublon (garde anti-always-green delta!=final), ré-attache
sans re-spawn, envoi Enter vs Shift+Enter, badges toolActivity, mock stream.
npx vitest run : 345 passed, 0 failed. npm run build vert.

Reste D6 (messagerie inter-agents via send_blocking) et D7 (menu restreint
Claude/Codex + retrait custom) pour clore §17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:30:20 +02:00
f4d5727a69 feat(agent): commandes Tauri + ChatBridge streaming (D4) — §17
Expose l'exécution structurée à l'UI (jumeau du chemin PTY) :
- ChatBridge (chat.rs) : pompe ReplyStream -> Channel<ReplyChunk>,
  generation-tracked (anti-double-pompe) ; scrollback de conversation
  côté transport (le port AgentSession est sans mémoire), purgé à la
  fermeture, préservé à la ré-attache.
- Commandes agent_send / reattach_agent_chat / close_agent_session.
- DTO : ReplyChunk (textDelta/toolActivity/final, camelCase tagué kind),
  ReattachChatDto, CellKind {pty,chat} + champ cellKind dérivé sur
  TerminalSessionDto (chat ssi LaunchAgentOutput.structured = Some).
- Wiring composition root : StructuredSessions + ChatBridge dans AppState,
  via la factory déjà injectée (aucun new d'adapter — règle D §17.8).
- From<AgentSessionError> for AppError (mappe sur PROCESS).

Tests (QA) : 35 unitaires verts — generation supersede validé par mutation
test, séquence deltas*+Final, reattach sans re-spawn, teardown shutdown+
unregister, DTO cellKind + round-trip ReplyChunk, non-régression PTY.
cargo test --workspace : 803 passed, 0 failed.

Reste D5 : AgentChatView (frontend) + routage cellKind dans LayoutGrid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:15:08 +02:00
050afa7d24 fix(test): champ structured manquant dans LaunchAgentOutput (build D3 cassé)
Le commit D3 (56913b9) a ajouté le champ structured: Option<...> à
LaunchAgentOutput mais a laissé deux constructions de test sans ce champ,
cassant la compilation du crate de test app-tauri (E0063). Ajoute
structured: None aux deux. Suite workspace de nouveau verte (775/0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:56:44 +02:00
56913b9053 feat(agent): routage LaunchAgent structuré vs PTY + réconciliation A/B (D3) — §17
- LaunchAgent route sur profile.structured_adapter : Some ⇒ AgentSession via
  factory + enregistrement StructuredSessions (aucun pty.spawn) ; None ⇒ PTY
  inchangé. Dépendances structurées injectées par builders additifs
  with_structured (signatures publiques inchangées ⇒ A/B legacy verts).
- LaunchAgentOutput étendu d'un champ optionnel structured (non cassant).
- A (ChangeAgentProfile) : kill polymorphe — session structurée ⇒ shutdown(),
  PTY ⇒ kill ; détection sur les deux registres.
- B : resume_supported vrai pour profil structuré ; resolve_session_plan ⇒
  Resume{conversation_id} (Claude --resume / Codex exec resume).
- Codex : build_spawn_line porte --sandbox workspace-write --ask-for-approval
  never (autonomie d'écriture ; à terme piloté par les permissions).

Tests : structured_launch 8 + codex flags 2 ; application/infrastructure 0 échec.

Reste D4 : ChatBridge (Channel) + commandes agent_send/reattach + StructuredSessions
dans AppState + DTO cellKind/ReplyChunk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:48:25 +02:00
f104682477 fix(agent): adapters Claude/Codex au format CLI réel (spikes S1/S2 résolus)
Formats vérifiés sur les vraies CLI le 2026-06-09 :
- Claude (claude -p … --output-format stream-json --verbose ; reprise --resume
  <session_id>) : parse system/init (session_id), ignore rate_limit_event +
  types inconnus, assistant.content[] ITÉRÉ (fix multi-blocs), result→Final.
  Flag --verbose ajouté à build_spawn_line.
- Codex (codex exec --json --skip-git-repo-check ; reprise codex exec resume
  <thread_id>) : thread.started→conversation_id, item.completed/agent_message
  →Final, autres items→ToolActivity, turn.*/inconnu ignorés.

parse_event émet désormais Vec<ReplyEvent> (multi-blocs) ; drain aplatit.
Scripts fake CLI passés aux lignes réelles. Tests : session 46 + intégration,
workspace vert.

À vérifier à l'intégration (D3) : reprise Codex (ordre d'args), et lancement
Codex avec --sandbox workspace-write + approbation non bloquante (hors parsing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:34:03 +02:00
751d94dd89 feat(agent): adapters structurés Claude/Codex + fake CLI + conformité (D2) — §17
infrastructure/src/session/ : machinerie de process générique (paramétrable par
la commande = seam d'injection du fake CLI), adapters ClaudeSdkSession/
CodexExecSession avec parsing ISOLÉ par adapter (parse_event), factory
StructuredSessionFactory (routage par structured_adapter), FakeCli scriptable +
harnais de conformité Liskov assert_agent_session_contract.

Incarnation « un run par tour » (send relance claude -p / --resume <id>,
continuité via conversation_id — colle au pivot reprise B).

Tests : 41 contre le FAKE CLI (jamais le vrai claude/codex), workspace vert.

Points en attente des spikes S1/S2 (format réel) — n'impactent que parse_event :
- mapping JSON→ReplyEvent Claude (S1) et Codex (S2) sur schémas SUPPOSÉS ;
- Claude multi-blocs : parse_event ne garde que le 1er bloc (à corriger si Claude
  émet plusieurs blocs/message — confirmer S1) ;
- flux sans Final : permissif côté adapter, l'erreur est gérée par send_blocking
  (consommateur). À reconfirmer côté UI streaming (D4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:11:38 +02:00
5e10b5eb42 feat(agent): fondation exécution structurée des agents IA (D0+D1) — §17
Pivot orchestration : agents IA pilotés via leur mode programmatique/JSON
(capture déterministe), au lieu du TUI brut + self-report. §16 (idea/MCP)
marquée remplacée comme voie principale.

- D0 (domaine) : port AgentSession + AgentSessionFactory, types ReplyEvent
  /ReplyStream/AgentSessionError, champ AgentProfile.structured_adapter
  (Option<StructuredAdapter{Claude,Codex}>, skip si None ⇒ zéro régression),
  catalogue Claude/Codex annotés.
- D1 (application) : registre StructuredSessions (jumeau de TerminalSessions),
  agrégateur LiveSessions{pty,structured} derrière LiveAgentRegistry (vivant si
  PTY OU structuré, surface du trait inchangée), helper send_blocking (draine le
  ReplyStream jusqu'au Final, Timeout sans tuer la session).

Tests : domaine 16+2 ; application registre 11 + send_blocking 9 ; workspace 0 échec.
A/B intacts. Aucun adapter concret (D2), pas de Tauri/front.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:54:48 +02:00
7375f706da feat(agent): reprise des sessions au redémarrage (B2) — commande + ResumeProjectPanel
- Tauri : commande list_resumable_agents (projectId) + ResumableAgentListDto
  { resumable } / ResumableAgentDto (camelCase, conversationId omis si None),
  câblage state.rs (réutilise stores + ProfileStore, aucun nouveau port).
- Front : gateway listResumableAgents, ResumeProjectPanel monté à l'ouverture
  de projet (opt-in, FR) : Reprendre (launch_agent nodeId+conversationId),
  Nouvelle conversation (setCellConversation(null) puis launch), Ignorer,
  Tout reprendre/ignorer. resumeSupported=false ⇒ « relance à neuf ».
- doc : §15.2 coquille agents→resumable (alignée use case/back/front).

Tests : app-tauri dto 9/9 ; vitest 324 (+10) ; workspace Rust 0 échec. 0 régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:04:41 +02:00
b82e3e1a40 feat(agent): UI hot-swap de profil (A2) — commande Tauri + sélecteur + dialog
- Tauri : commande change_agent_profile + ChangeAgentProfileRequestDto/Dto
  (camelCase, relaunchedSession omis si None), câblage state.rs par composition.
- Front : gateway changeAgentProfile (adapters Tauri+mock), sélecteur de profil
  par agent, dialog de confirmation FR (« Changer le moteur abandonne l'historique
  de conversation… »), refresh sur event agentProfileChanged.

Tests : app-tauri dto 5/5 ; vitest 314/314 (mapping, payload, dialog gating,
libellé FR, refresh). 0 régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:07:35 +02:00
2433e173a1 feat(agent): backend hot-swap profil (A0+A1) + inventaire reprise session (B1) — L15
Cadrage Architecture §15 (figé) : « agent = entité à session persistante ».

- A0 (domaine) : Agent::with_profile, LayoutTree::leaf, event AgentProfileChanged
  (+ DTO miroir DomainEventDto camelCase).
- A1 (application) : use case ChangeAgentProfile — no-op si profil identique,
  mutation manifeste, nettoyage conversation_id/agent_was_running sur layouts
  persistés, swap à chaud (kill PTY + relance même cellule via composition de
  LaunchAgent), event AgentProfileChanged. Décision : repartir à neuf (on garde
  .md + mémoire, on jette l'historique de conversation).
- B1 (application) : use case ListResumableAgents (lecture seule) — inventaire des
  cellules was_running||conversation_id, resume_supported selon profil, best-effort.

Aucun nouveau port/adapter (composition de l'existant). Hexagonal strict.
Tests : domaine 11 + app-tauri dto + ChangeAgentProfile 9 + ListResumableAgents 8,
suite application complète verte (0 régression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:55:44 +02:00
62bd5130fb chore(agents): contextes DevBackend, DevFrontend et QA pour le chantier « agent = entité »
Personas durables alignés sur la méthode (cycle dev↔QA), l'architecture
hexagonale réelle (crates/dossiers/commandes) et la roadmap A+B+C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:55:29 +02:00
785e9935fd feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs
  (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés,
  environnement local détecté, stratégies compilées). UI EmbedderSettings.
- LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la
  mémoire dépasse le budget de recall sans embedder configuré (event
  EmbedderSuggested, anti-spam 1×/session, « ne plus demander »).
- Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les
  agents/profils au lancement, avant la persona. UI ProjectContextPanel.

Tests : backend workspace vert (0 échec) ; frontend 306/306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:24:51 +02:00
32398827fb feat(memory): embedders vectoriels réels HTTP + ONNX derrière features (LOT C1)
Remplace les StubEmbedder pour les stratégies localServer/api/localOnnx par de
vrais moteurs, chacun derrière une feature cargo off-by-default — la posture
fondatrice « rien d'imposé, zéro dépendance » (défaut none → rappel naïf) reste
byte-for-byte inchangée.

C1a (feature vector-http, reqwest rustls optional):
- HttpEmbedder couvrant localServer (Ollama/llama.cpp) et api (OpenAI/Voyage…),
  payload OpenAI-compatible /v1/embeddings, ordre restauré par index, bearer
  token lu via env var (jamais en clair), timeout client 30s.
- detect_ollama() pour la détection de l'existant (C3).

C1b (feature vector-onnx, fastembed v5 optional):
- OnnxEmbedder en-process (e5-small, dim 384), init paresseuse + spawn_blocking,
  cache modèle sous <app_data>/embedders/onnx — aucun download au build ni au
  first-run, uniquement à la demande au 1er embed.
- Catalogue RECOMMENDED_ONNX_MODELS + ONNX_CACHE_SUBDIR + onnx_model_is_cached
  exposés (sans feature) pour la config (C2) et la popup (C3).

embedder_from_profile(profile, onnx_cache_dir) dispatche feature-gated ; sans la
feature, retombe sur StubEmbedder (Unsupported) → fallback naïf via
AdaptiveMemoryRecall. Composition root (build_memory_recall) propage le cache dir.

Tests: 10 HTTP + 6 ONNX (dont 2 #[ignore] download réel) + 26 vectoriels, verts
en défaut, --features vector-http et --features vector-onnx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:00:40 +02:00
b39c11a64d fix(agents): enforcer l'invariant « 1 session vivante par agent » (singleton)
Un agent ne peut tourner que dans une seule cellule à la fois. La garde dans
LaunchAgent refuse le spawn si l'agent est déjà vivant dans un autre node
(AGENT_ALREADY_RUNNING) ; idempotent sur le même node ; le chemin resume
(agent mort) reste inchangé. Le node_id est désormais plombé jusqu'au use case.

Corrige le reset asymétrique d'une cellule au changement d'onglet : deux leaves
partageant le même agent id rendaient session_for_agent/is_agent_live/stop_agent
ambigus (cible arbitraire). Le churn reset/reattach déclenchait aussi les accents
mélangés (FIFO intact, non touché).

- snapshot agentWasRunning calculé par node (is_node_live) et non par agent
- commande list_live_agents + live_agents()/node_for_agent()/is_node_live()
- UI : dropdown grise les agents déjà placés ailleurs ; 2e cellule en doublon
  affiche « disponible » au lieu d'une relance fantôme

Tests : cargo test (application + app-tauri) vert ; tsc + vitest vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:43:48 +02:00