Commit Graph

336 Commits

Author SHA1 Message Date
181727d851 fix(orchestrator): câble le ContextGuard au composition root
Le composition root (app-tauri/state.rs) n'appelait pas
.with_context_guard(...) : les outils MCP de contexte/mémoire
(idea_context_read/propose, idea_memory_read/write) n'étaient
donc pas réellement branchés au runtime, alors que le use case
existait côté application.

- application: re-export public de ContextGuardUseCases (orchestrator/mod.rs)
  et de ContextGuardUseCases, ReadContext, ProposeContext, ReadMemory,
  WriteMemory (lib.rs), pour que app-tauri puisse les câbler.
- app-tauri: branchement .with_context_guard(...) au composition root
  + 3 tests de non-régression (round-trip memory read/write, context read,
  symétrie d'erreur typée) dans mcp_serve_peer_tests.

Tests QA verts : cargo test -p app-tauri (0 failed), build 0 warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:36:22 +02:00
6969dc7988 chore(wip): état runtime .ideai (conversations, layouts, mémoire, skills)
Snapshot de l'état runtime accumulé sur develop : logs/handoffs de
conversations, layouts, notes mémoire (dont git-owns-commit-merge-decisions)
et catalogue de skills.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:14:01 +02:00
ba56be6951 chore(release): passe la version à 0.2.0
Bump des manifestes (crates Rust + frontend) et des lockfiles vers
0.2.0 en préparation de la release. Ajoute /node_modules à .gitignore
(tooling installé à la racine, jamais versionné).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:13:53 +02:00
d7041c53ce merge(session-limits): intégration de la feature limites de session (3 niveaux)
Intègre feature/agent-session-limits dans develop. Surface complète :
- Niveau 1 : tap de détection sur agent_send (limites remontées par la CLI).
- Niveau 2 : détecteur déclaratif au lancement (parser regex confiné) +
  reprise automatique planifiée (TokioScheduler) et annulable.
- Niveau 3 : filet humain (set_resume_at / saisie d'heure) quand l'heure
  n'est pas exploitable automatiquement.
Backend (domain/application/app-tauri) + front (badge, compte à rebours,
formulaire de reprise) ; suites Rust et front vertes.

Commits : LS2..LS8 (a1755e53f3504e).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:45:39 +02:00
3f3504efa3 fix(test): corrige le compteur de gateways du mock (13 → 14)
Le test asseyait « thirteen gateways » alors que la gateway permission
(présente depuis eca2ba9) en porte le total à 14. Compteur périmé sans
rapport avec session-limits, corrigé isolément avant le merge d'intégration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:45:00 +02:00
5d9dd32c29 feat(session-limits): LS8-front — filet humain niveau 3 (saisie d'heure de reprise)
UI permettant à l'humain de renseigner l'heure de reprise quand le
niveau 2 détecte une limite sans heure exploitable.

- ports/index.ts : setResumeAt(agentId, resetsAtMs) sur InputGateway.
- adapters/input.ts : setResumeAt → invoke("set_resume_at").
- adapters/mock/index.ts : MockInputGateway.setResumeAt (resumeArmings[]).
- features/agents/useAgents.ts : action setResumeAt (sans mutation optimiste).
- features/agents/AgentLimitBadge.tsx : formulaire de saisie d'heure sur
  l'état suspected sans heure + helper pur timeInputToEpochMs (TODO LS7 retiré).
- features/agents/AgentsPanel.tsx : câblage onSetResumeAt.

Tests AgentLimitBadge.test.tsx mis à jour au nouveau contrat + couverture LS8 ;
typecheck propre, suite agents verte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:44:54 +02:00
c480d2820a feat(session-limits): LS8-backend — filet humain niveau 3 (set_resume_at)
Permet à l'humain de confirmer/forcer l'heure de reprise quand le
niveau 2 a détecté une limite sans heure exploitable.

- application/agent/session_limit.rs : refactor privé arm_scheduled
  (param resets_at_ms brut) partagé par on_rate_limited + nouvelle
  confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms)
  (source Human, réutilise la branche Scheduled, annulable).
- app-tauri/commands.rs : commande set_resume_at(agent_id, resets_at_ms)
  (résout node_id via node_for_agent + conversation_id best-effort,
  NOT_FOUND si pas de cellule vivante).
- app-tauri/lib.rs : set_resume_at enregistrée après cancel_resume.

Réutilise les événements existants (AgentRateLimited + AgentResumeScheduled),
aucun nouvel événement. Tests : +6 session_limit_service, +2 wiring, verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:44:45 +02:00
4fad0423e7 feat(session-limits): LS7-front — UI limites de session (badge + compte à rebours + filet humain)
Expose la surface produit des limites de session côté React/TS,
au-dessus du câblage backend (9df5923).

- domain/index.ts : 5 variantes ajoutées au union DomainEvent
  (agentRateLimited / ResumeScheduled / ResumeCancelled / Resumed /
  RateLimitSuspected).
- ports/index.ts : cancelResume(agentId) ajouté à InputGateway.
- adapters/input.ts : TauriInputGateway.cancelResume → invoke("cancel_resume").
- adapters/mock/index.ts : MockInputGateway.cancelResume
  (cancelledResumes / cancelResumeResult).
- features/agents/useAgents.ts : état limitByAgent + action cancelResume.
- features/agents/AgentLimitBadge.tsx (nouveau) : badge + compte à rebours
  + bouton Annuler + helpers purs.
- features/agents/AgentsPanel.tsx : câblage du badge.

Tests : useAgentsLimits.test.tsx (13) + AgentLimitBadge.test.tsx (11),
suite agents 63 tests verts, tsc --noEmit propre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:06:56 +02:00
9df592389c feat(session-limits): LS7 — câblage backend app-tauri (taps niveaux 1&2 + reprise annulable)
Branche le SessionLimitService dans l'application Tauri et expose la
surface de reprise/annulation au front.

- application/agent/lifecycle.rs : LaunchAgentOutput.profile exposé
  (None sur réattache/idempotent, Some sur lancement effectif).
- application/terminal/registry.rs : StructuredSessions::meta_for_session()
  (lookup agent/node par SessionId pour le tap niveau 1).
- app-tauri/state.rs : ResumeContext(s), AppAgentResumer (impl du port
  AgentResumer au-dessus de LaunchAgent), instanciation + câblage du
  SessionLimitService (TokioScheduler + drain des réveils) dans AppState::build.
- app-tauri/commands.rs : taps niveau 1 (agent_send) et niveau 2
  (launch_agent, parser regex confiné), alimentation de resume_contexts,
  commande cancel_resume.
- app-tauri/lib.rs : enregistrement de cancel_resume dans le handler.
- app-tauri/Cargo.toml : dépendance async-trait.

Tests : session_limit_wiring.rs (2 tests de composition) + meta_for_session
dans structured_registry_d1.rs ; fixtures dto_agents/dto_chat ajustées
(profile: None). Tout vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:56:08 +02:00
ea94e756e2 feat(session-limits): LS6 — câblage des événements de limite vers le front
Relaie les 5 nouvelles variantes DomainEvent (AgentRateLimited, ResumeScheduled,
ResumeCancelled, Resumed, RateLimitSuspected) via leurs DTO miroirs et bras From
dans events.rs, et propage ReplyEvent::RateLimited en chunk côté chat.rs pour que
le front soit informé des suspensions/reprises de session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:02:29 +02:00
98bfcf4f22 feat(session-limits): LS5 — détecteur niveau 2 déclaratif + parsing temps partagé
Ajoute un détecteur niveau 2 (RateLimitParser, regex confiné à l'infra) qui
repère les mentions de limite de session dans la sortie textuelle, et factorise
le parsing d'heures dans un module pur (timeparse) partagé entre les niveaux 1
et 2. Le détecteur Claude niveau 1 est refactoré vers timeparse (~-121 lignes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:02:24 +02:00
9000b4d09f feat(session-limits): LS4 — service application + réconciliation T4
Orchestre la détection et la reprise au niveau application :
- session_limit.rs (nouveau) : SessionLimitService + port AgentResumer +
  const RESUME_PROMPT.
- structured.rs : réconciliation T4 — enum TurnOutcome +
  drain_with_readiness_outcome ; signatures historiques préservées.
- agent/mod.rs + lib.rs : modules et re-exports.

Tests QA (nouveaux) : tests/session_limit_service.rs (9) +
tests/session_limit_t4.rs (7).
`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.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:55:26 +02:00
253310bb3e feat(session-limits): LS3 — port Scheduler + adapter TokioScheduler
Introduit l'abstraction de planification pour la reprise différée à la
levée d'une limite de session :
- domaine : trait Scheduler + enum ScheduledTask (ports.rs), ScheduleId
  via typed_id! (ids.rs), re-exports (lib.rs).
- infra : TokioScheduler (scheduler/mod.rs, nouveau) + pub mod scheduler
  et re-export (lib.rs).

Tests QA inline (#[cfg(test)]) : 7 tests scheduler (3× sans flaky).
`cargo test -p infrastructure` = 195 passed / 0 failed ; domaine + infra
builds 0 warning ; LS1/LS2 toujours verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:07:48 +02:00
a1755e51bc feat(session-limits): LS2 — adapter Claude niveau 1 (infra)
Câble la détection de limite de session dans l'adapter Claude :
- claude.rs : parse_event émet ReplyEvent::RateLimited ; nouvelle fonction
  pure parse_reset_ms + helpers ; parseur ISO maison ; doccomments T4.
- mod.rs : 26 nouveaux tests QA (#[cfg(test)]) + 2 tests existants alignés
  sur le nouveau contrat.
- conformance.rs : RateLimited ajouté aux événements non terminaux autorisés.

`cargo test -p infrastructure` = 188 passed / 0 failed, zéro régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:44:53 +02:00
0bf1eb3b11 feat(session-limits): LS1 — couche domaine (détection + plan de reprise)
Pose les briques pures du domaine pour la gestion des limites de session
des agents (état en mémoire, aucun schéma de persistance modifié) :
- session_limit.rs (nouveau) : SessionLimit, ResumePlan, RateLimitSource,
  plan_resume (calcul du plan de reprise annulable).
- ports.rs : variante ReplyEvent::RateLimited.
- readiness.rs : variante ReadinessSignal::RateLimited + classify.
- profile.rs : RateLimitPattern + champ + builder.
- events.rs : 5 variantes DomainEvent pour le cycle de vie limite/reprise.
- lib.rs : module + re-exports.

Tests QA inline (#[cfg(test)]) : 24 tests dédiés.
`cargo test -p domain` = 165 passed / 0 failed, zéro régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:33:57 +02:00
fa5b826df5 docs(session-limits): cadrage Architect — gestion des limites de session des agents
Pose le cadrage de la feature « Gestion des limites de session des agents »
avant tout code (lots LS1→LS8) :
- ARCHITECTURE.md §21 : détection hiérarchique des limites de session +
  reprise auto annulable (domaine → adapter Claude → port Scheduler →
  service → parser regex → filet humain → app-tauri → frontend) ; état en
  mémoire uniquement, aucun schéma de persistance modifié.
- .ideai/memory/session-limit-handling-design.md : design validé.
- .ideai/memory/MEMORY.md : pointeur vers le design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:25:05 +02:00
401c18ad3c chore(repo): retire les gitlinks de worktree .claude/ committés par accident
Deux entrées gitlink (mode 160000) pointant vers des commits de worktrees
agent locaux avaient été versionnées par erreur. Elles sont déjà couvertes
par .gitignore (.claude/worktrees/) ; on les sort simplement du suivi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:01:19 +02:00
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