Lot B1 du chantier server/client mode : création de la crate `backend`
qui héberge le cœur commun (endpoint MCP, outils OpenAI) indépendant de
Tauri, préalable au futur serveur web + PTY WebSocket.
- crates/backend : nouvelle crate (lib.rs, mcp_endpoint.rs, openai_tools.rs).
- crates/app-tauri : câblage sur la crate backend (state.rs, mcp_bridge.rs,
Cargo.toml, tests/orchestrator_wiring.rs).
- Cargo.toml / Cargo.lock racine : ajout de la crate au workspace.
Validé : cargo check --workspace vert, tests backend/app-tauri verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le runner de tâches de fond détectait la fin d'un process via l'EOF du drain de
sortie PTY, couplant le cycle de vie du process au flux d'output. Un output
encore ouvert (ou drainé ailleurs) pouvait masquer ou retarder la détection de
fin.
Ajoute wait/try_wait au port figé PtyPort :
- wait : bloquant, rend un ExitStatus idempotent ;
- try_wait : non bloquant, rend Option<ExitStatus> ;
- exit status mémorisé pour garder wait/try_wait/kill cohérents.
Implémentation dans PortablePtyAdapter (état d'exit mémorisé). Le
CommandBackgroundRunner détecte désormais la fin via pty.wait (select sur
cancel/deadline) au lieu de l'EOF du drain. Tous les fakes PtyPort du workspace
sont complétés en conséquence.
Le tee live UI reste hors périmètre (traité en #58). Aucun breaking IPC/front.
Tests : infrastructure/tests/background_task_runner.rs (nouveau) + pty_adapter.rs
verts, non-régression application/app-tauri OK (hors échecs réseau du sandbox).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Un profil structuré rencontrant une factory de session non câblée retombait
silencieusement sur pty.spawn (fallthrough du `if let`), masquant une erreur de
configuration au lieu de la signaler.
Remplace ce fallthrough par une intention explicite :
- nouvel enum StructuredRoutingMode { HumanPtyFallback, RequireStructured } sur
LaunchAgent, posé via le builder with_structured_routing_mode ;
- le `if let` devient un `match` explicite (agent/lifecycle.rs) ; en mode
RequireStructured, un profil structuré sans factory câblée retourne
AppError::Process("structured profile requires structured session factory")
au lieu de tomber sur pty.spawn.
Composition root (app-tauri/src/state.rs) : launcher humain = HumanPtyFallback,
launcher orchestrateur = RequireStructured, wake background rebranché sur
orchestrator_launch_agent.
Tests : agent_lifecycle.rs (4 branches de routage) + non-régression
agent_wake/structured_launch_d3. Crate application verte.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stretch B2/F2 de #54, par-dessus le MVP déjà mergé (B1/F1).
Backend : le port de téléchargement HF publie une progression débouncée
(bytes reçus / total, pourcentage) via le stream de statut du serveur
modèle, avec gestion du total inconnu (pas de faux %), du cache hit,
de l'annulation et du timeout.
Frontend : l'overlay plein-cellule de préparation du serveur affiche la
progression réelle (barre, %, octets, source) en mappant le fil de
statut, avec la règle « pas de faux % » quand le total est inconnu.
Tests : application + infrastructure (téléchargement débouncé, cancel,
timeout, cache hit, total inconnu) et vitest (overlay + formatage pur).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sur le rendez-vous délégué (idea_ask_agent), quand la cible touchait sa
limite de session, l'événement AgentRateLimited n'était pas émis : l'écart
backend documenté (ticket #7/F2) laissait la surface UI sans signal de
limite pour la cible déléguée.
Le service orchestrateur relaie désormais la limite de la cible vers le
service de limite de session, fermant l'écart en cohérence avec le chemin
direct (#30).
Couverture QA (sortie réelle) : orchestrator_service 63 passed,
session_limit_service 15 passed, session_limit_t4 7 passed,
cargo test -p application 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Partie backend. Les fenêtres/panneaux détachés étaient restaurés avec un
project_id figé au moment du détachement, si bien qu'ils restaient collés à
un projet mort ou incohérent après redémarrage. Ils sont désormais restaurés
en mode panel-only, sans project_id figé, et suivent le projet en focus de la
fenêtre principale via un event focused-project exposé par la couche fenêtre.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L'assistant IA d'édition de ticket éditait les fichiers du ticket en direct,
hors de tout contrôle. Il passe désormais par les tools MCP idea_ticket_* :
préparation d'un environnement structuré dédié et policy d'enforcement scopée
au ticket courant, de sorte que l'assistant ne peut agir que sur son ticket
via la surface MCP plutôt que sur le système de fichiers.
Couvert par de nouveaux tests QA (mcp_server, assistant_context_store).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persiste l'état des fenêtres (layout/position) et le restaure au
relancement de l'application. Découpage hexagonal :
- domaine : modèle et port d'état des fenêtres (layout, ports)
- application : use cases de persistance/restauration
- infrastructure : store window_state (adapter de persistance)
- présentation : câblage app-tauri (state, commands, lib)
Couvert par des tests ciblés domaine/application/infrastructure.
Depend de #39 (fermeture des fenêtres auxiliaires).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le first-run wizard restait figé : `CliAgentRuntime::detect` construisait un
runtime tokio courant-thread via `futures_block_on` pour piloter un
`ProcessSpawner` async. Appelé depuis le runtime async de Tauri, ce
`block_on` imbriqué panique ; la commande IPC ne répond alors jamais, la
promesse `detectProfiles` reste pendante et `busy` ne redescend plus.
Ce commit s'attaque à la cause côté backend :
- `AgentRuntime::detect` devient `async fn` (`#[async_trait]`) : le port cesse
de mentir sur sa nature. Il pilote un spawner async, il est async. Le
`futures_block_on` disparaît, et avec lui le runtime imbriqué.
- La sonde CLI est bornée par `tokio::time::timeout(DETECTION_TIMEOUT)` :
un binaire qui ne rend jamais la main dégrade la détection en `Err`, il
ne gèle plus l'appelant.
- Les profils `StructuredAdapter::OpenAiCompatible` n'ont pas de CLI à
spawner : les sonder revenait à tester un binaire inexistant. Ils sont
désormais sondés par un GET HTTP sur l'endpoint `/models` dérivé de
`chat_http.endpoint`, borné par le même timeout.
`DetectProfiles` séquence les `await` sur les candidats. Les fakes
`AgentRuntime` des crates application/infrastructure/app-tauri suivent la
nouvelle signature.
Tests: cargo test -p domain (241), -p application (81), -p infrastructure
(269), -p app-tauri (63+15) — verts. `rg futures_block_on crates` sans match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute la suppression complète d'un ticket (#6).
Backend Rust :
- port IssueStore::delete (NotFound si absent) + event IssueDeleted{freed_sprint}
- FsIssueStore::delete : supprime .ideai/tickets/<N>/ et l'index sous lock
- use case DeleteIssue + adaptations des tests sprint/ticket_assistant au port
- commande Tauri ticket_delete + câblage events/state/lib
Frontend :
- gateway delete (ports, adapter ticket + mock, domain)
- useTicketDetail : retrait de la liste et fermeture du détail via event issueDeleted
- intégration TicketDetail + tests
Tests verts : application/issue_usecases (6), infrastructure/issue_store (7),
app-tauri --lib (56), frontend vitest (503), npm run build (exit 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ticket #10 — introduction du modèle de sprints côté backend.
- Domaine : nouvel agrégat Sprint (sprint.rs), IDs, événements et invariants ;
rattachement des issues à un sprint (issue.rs) et ports associés.
- Application : use-cases sprints (application/src/sprints) + erreurs dédiées.
- Infrastructure : store de sprints (infrastructure/src/sprints.rs), adaptation
du store d'issues et exposition MCP via orchestrator/mcp/tickets.rs.
- app-tauri : commandes, state et events pour piloter les sprints depuis l'UI.
Tests domaine/application/infra/app-tauri verts (sprint_usecases, sprint_store,
issue_store, mcp_server).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T1 — Wake automatique du propriétaire à la complétion.
La complétion d'une tâche de fond est désormais livrée à l'agent
propriétaire dès que la session accepte l'envoi (wake.rs :
mark_completion_delivered au send accepté), via un drain de flux dédié
(structured.rs : drain_reply_stream_with_readiness). L'inbox médiée
enfile l'item sans démarrer de tour ni marquer l'agent busy
(input/mod.rs : enqueue FIFO silencieux). Régression couverte
(tests/agent_wake.rs, tests input/mod.rs).
T3 — Tâches de fond projetées dans le read-model du panneau Work.
AgentWorkState porte désormais background_tasks
(VO AgentBackgroundTaskState), alimenté par un builder best-effort
with_background_tasks(store) : union list_open_for_agent + dispatch des
completions non livrées par owner_agent_id, erreur store => Vec vide
(aucune régression live/busy/tickets). DTO Tauri backgroundTasks et
wiring du BackgroundTaskStore côté state.rs. Le frontend, déjà câblé,
affiche Cancel/Retry (mapping queued/waiting -> pending, tri sur
updatedAtMs). Borne V1 : une tâche terminale déjà livrée n'est plus
énumérable (Retry limité à la fenêtre non livrée).
Tests : cargo build --workspace OK ; cargo test -p application /
-p app-tauri / -p infrastructure verts ; frontend build + vitest verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Intègre le système de tickets/issues V1 (validé QA vert de bout en bout,
mémoire tickets-v1-e2e-validated-qa) dans la branche des tâches de fond.
Conflits résolus : app-tauri/state.rs, domain/events.rs, domain/lib.rs, domain/ports.rs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Livre le lot backend B8 des tâches de fond :
- infrastructure : runner de commandes concret (CommandBackgroundRunner)
sur le port BackgroundTaskRunner, tail borné (bounded_tail/BoundedTail),
éclatement du module background_task en sous-modules (mod/runner/tail,
sink extrait de l'ancien background_task.rs).
- application : nouveau module background exposant les cas d'usage
SpawnBackgroundCommand, CancelBackgroundTask, RetryBackgroundTask et le
port BackgroundCommandArchive.
- domain : refactor point-2 de l'arbitrage Architect — sortie du trait
BackgroundCommandArchive de la couche domaine vers application.
- app-tauri : câblage runtime (commands, dto, state, lib) des commandes
spawn/cancel/retry et de la boucle de complétion sink fermée en
composition root.
Build workspace + tests application/infrastructure verts (QA).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Branche le store de tâches de fond dans le runtime : store per-root
routé par project_id, AppReconcileBackgroundTasks au boot,
AppWakeSessionProvider et boucle ready→MediatedInbox→AgentWakeService,
avec .with_background_tasks(store, clock) au builder OrchestratorService.
open_project appelle reconcile_background_tasks au démarrage.
Aligne aussi 4 tests MCP périmés de l'ancien protocole : idea_reply
retiré (erreur JSON-RPC -32601), idea_ask_agent exposé (13 tools),
rendez-vous capture le Final inline. cargo test -p app-tauri --lib
= 52 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La conversation inter-agent (idea_ask_agent) doit rester strictement
headless : la cible répond via une session structured capturée par IdeA,
sans jamais écrire dans le PTY d'une cellule visible ni fermer le terminal
que l'utilisateur observe.
- lifecycle: flag `allow_structured_alongside_pty` — ouvre une session
structured pour la délégation sans fermer le PTY visible (coexistence).
- orchestrator/service: `ensure_structured_session` ne ferme plus le PTY
visible ; mapping typé de l'erreur no-reply ; coexistence PTY/structured.
- infrastructure/input: garantit zéro `DelegationReady` et zéro write PTY
pour une délégation headless, même quand l'entrée est `front_owned`.
- app-tauri (commands/state): câblage du flag de coexistence.
- tests: fixtures portées vers le modèle structured/headless, assertions
cibles mises à jour (aucun #[ignore] ajouté, aucun test retiré).
Validé réel : cargo build OK ; orchestrator_service 60/0, agent_lifecycle
62/0, structured_launch_d3 22/0, infrastructure input 35/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redessine la borne de fin de tour du rendez-vous `idea_ask_agent` ⇄ `idea_reply` :
au lieu d'un timeout plat (qui coupait à 600 s un seul long tour de la cible, cf.
T7), la borne devient une **fenêtre d'inactivité réarmée** à chaque progrès observé
de la cible, sous un **plafond absolu** (défaut 4 h, réglable via
`IDEA_ASK_RENDEZVOUS_CEILING_MS`).
- Sonde d'activité (`transcript_activity_token`, inspector) : jeton monotone =
octets cumulés des `.jsonl` de la cible. Croît même pendant un seul long tour
sans `turn_duration` ⇒ détecte « vivant et au travail » mi-tour. Best-effort,
sans effet de bord ; folder absent/illisible ⇒ « pas de progrès ».
- Watchdog (`run_inactivity_watchdog`, nouveau module `orchestrator/rendezvous`) :
fenêtre réarmable + plafond, fallback timeout plat si aucune sonde (zéro régression).
- Issue typée distincte `TargetCeilingActive` (code `RENDEZVOUS_CEILING_ACTIVE`) :
une cible **active** stoppée au plafond n'est jamais confondue avec un
`TargetReturnedNoReply` (silence) ; le message guide « ne pas retenter à l'aveugle ».
- Câblage composition-root (`state.rs`) : sonde résolue nom→AgentId→run-dir transcript,
branchée sur le service et sur l'McpServer (`AskActivityProbe`, `with_ask_ceiling`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Au redémarrage, les agents qui étaient `working` lors de la fermeture restaient
figés en statut fantôme `working` alors qu'aucun tour n'est plus en cours (bug
reproductible en live : QA et DevBackend eux-mêmes sont restés `working` fantôme
après leurs tâches). On réconcilie l'état persistant à l'ouverture du projet :
les statuts orphelins sont ramenés à la cible `idle`.
- domain/live_state.rs : const STALE_AT_RESTART_MARKER + `reconcile_orphans` (+ tests).
- application/workstate/reconcile.rs (nouveau) : use case ReconcileLiveState (pas de
nouveau port), best-effort, no-op si store vide (+ tests, dont empty_store_is_a_noop_at_boot).
- application/workstate/mod.rs + lib.rs : module + re-export.
- app-tauri/state.rs : provider par-root + champ AppState + wiring.
- app-tauri/commands.rs : hook best-effort dans open_project.
QA VERT par commande réelle : cargo test --workspace 1624 passed / 0 failed,
cargo clippy --workspace --all-targets 0 erreur sur le code ajouté.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remédiation du wedge persistant après échec live du fix Finding A (77e62e5).
Détecte la fin de tour d'un agent sollicité qui n'a pas appelé idea_reply et
débloque l'agent demandeur au lieu de le laisser en attente indéfinie.
Ajoute le suivi de tour côté inspector Claude (claude_turn_watcher) et la
résolution des chemins de session (claude_paths), câblés dans le rendez-vous
idea_ask_agent ⇄ idea_reply.
Build workspace vert, suite complète verte, zéro warning.
Backstop NON prouvé levé en live : merge develop interdit tant que la levée
du wedge n'est pas validée en conditions réelles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Branche la mise à jour automatique du live-state sur le cycle de
délégation de l'orchestrateur, en best-effort non bloquant.
- Trait `LiveStateProvider` (résolution par root) + champ `Option` dans
l'`OrchestratorService`, injecté via le builder `with_live_state` ;
`None` ⇒ aucun effet (zéro régression).
- Transitions dérivées du cycle : `ask` → entrée `Working`,
`reply` → `Done` + `last_delegation`.
- Helpers best-effort : un échec de mise à jour du live-state ne
transforme jamais un succès de délégation en erreur.
- `AppLiveStateProvider` + wiring côté app-tauri (provider par root).
cargo test -p application : 0 échec ; --test orchestrator_service : 55/0
(dont 4 nouveaux tests LS3) ; cargo fmt --all --check : exit 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QA avait mis à jour le contrat 11→12 dans infrastructure/tests/mcp_server.rs
mais une seconde assertion codée en dur subsistait dans le test duplex de
state.rs. Ajoute idea_skill_read à la liste attendue et passe le compte à 12.
Conséquence directe de T4 (skill-awareness).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface les skills assignés à un agent « à la MCP » : description sur l'entité
Skill (+ effective_description fallback), section « # Skills disponibles » haute
altitude dans le convention-file (mode MCP), outil read-only idea_skill_read
(résolution projet→global), use case ReadSkill (port SkillStore existant), et
câblage au composition root. Dump legacy du corps complet conservé en mode
sans-MCP (zéro régression). Rétro-compat index.json (serde default).
Tests : domain+application+infrastructure 1212 passed / 0 failed (23 ajoutés).
Reste : T6 (champ description front) + T7 (e2e après rebuild AppImage).
NB topologie : commit réalisé par l'orchestrateur car l'agent Git était
injoignable (bug de livraison cold-start) ; à faire relire/rebaser par Git.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Câble la récolte automatique de mémoire de bout en bout, sous contrôle
explicite, du domaine jusqu'à l'UI :
- domain : modèle `memory_harvest` (candidats de récolte, décision) +
exports `lib.rs`.
- application : use case `memory/harvest` branché dans `memory/mod`,
`lib.rs`, le cycle de vie d'agent (`agent/lifecycle`) et
l'orchestrateur (`orchestrator/service`).
- app-tauri : wiring runtime dans `state`.
- frontend : DTO `domain/index` + hook `useMemory`.
Couverture : domain `memory_harvest` (14), application `memory_harvest`
(5) et `orchestrator_service` (récolte, 4), plus patch test-only du mock
gateway `workState` (`mock.test.ts`) et UI `memory.test`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute un module d'actions contrôlées (`workstate/actions.rs`) côté application
et l'expose via des commandes Tauri : DTO camelCase, câblage commands/state/lib.
Les actions valident leurs invariants avant d'agir sur le read-model.
- application : use cases d'actions contrôlées + intégration au work-state.
- app-tauri : commandes dédiées, DTO et enregistrement dans le handler.
Tests verts : application workstate_actions (7), workstate (21),
app-tauri dto_agents (25), list_live_agents_r0b (5), cargo check OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Étend le read-model work-state pour exposer un résumé par conversation
(participants, dernier message/activité, compteurs) câblé jusqu'au DTO
camelCase de l'état app-tauri.
- application : assemblage des résumés de conversation dans le work-state.
- app-tauri : DTO camelCase des résumés + exposition dans l'état.
Tests verts : application workstate (21), app-tauri dto_agents (21),
aucun warning Rust.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute un port de lecture ségrégué `AgentQueueSnapshot` (ISP) distinct du
`AgentMailbox` mutant : il expose `queue_for(agent)` qui renvoie des
`QueuedTicketSnapshot` clonés, ordonnés FIFO, avec position recalculée
(0 = tête). Observer la file ne la mute jamais ; le one-shot reply sender
reste dans l'adaptateur.
- domain : value object `QueuedTicketSnapshot` + trait `AgentQueueSnapshot`
(object-safe, partagé en `Arc<dyn …>`).
- infrastructure : `InMemoryMailbox` implémente la vue lecture en plus de la
vue mutation ; positions recalculées à chaque appel.
- application : le read-model work-state liste les délégations en attente via
ce port, troncature de l'aperçu de tâche en conservant la longueur d'origine.
- app-tauri : DTO `camelCase` des tickets en file câblé dans l'état.
Tests verts : domain mailbox (6), infrastructure mailbox (13),
application workstate (12), app-tauri dto_agents (20).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduit le module application `workstate` (modèle de read-model live + snapshots
des conversations/délégations en cours) et l'expose via la couche terminal
(exports mod/registry). Câble la surface Tauri : DTO, commande et state pour
exposer le live-state au frontend (lib + state + commands), avec tests.
QA verte. Réserve environnementale non bloquante : tests loopback socket Unix
réels non exécutables en sandbox (UnixListener::bind PermissionDenied),
alternatives avec skips vertes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
É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>
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>
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>
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>
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>
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>
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>
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>