stale
Le backend renvoie l'id actif autoritaire et le
frontend l'adopte pour éviter de rejouer un layout
disparu après overwrite externe.
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>
`encode_cwd` ne remplaçait que `/` et `\` par `-`, laissant le `.` intact. Or
Claude Code aplatit le cwd via `replace(/[^a-zA-Z0-9]/g, '-')` : tout caractère
non alphanumérique devient `-`, y compris le `.`. Pour un run dir isolé
`…/IdeA/.ideai/run/<uuid>`, le `/.` doit collapser en double dash
(`…-IdeA--ideai-run-<uuid>`). L'ancienne version calculait `…-IdeA-.ideai-run-…`,
un dossier inexistant sur disque : le turn-watcher voyait un répertoire vide
(baseline 0, conversation <none>) et ne déclenchait jamais `turn_ended`, ce qui
wedgeait le backstop no-reply du rendez-vous inter-agents.
Validé EN LIVE : demandeur libéré ~grâce 2s (au lieu du timeout 600s), preuves
dans idea.log. Test de non-régression `encode_cwd_encodes_dot_in_run_dir_to_double_dash`
+ maj test Windows (`C:\Users\me` -> `C--Users-me`).
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>
Domaine pur du live-state des agents : `LiveState`/`LiveEntry`/`WorkStatus`.
- Fusion en keyed last-writer-wins : une entrée par clé, la plus récente
écrase l'ancienne (ordonnancement déterministe).
- Invariants de bornes anti-dump : bornes douces (troncature) + rejet dur
au-delà des limites, pour empêcher un agent de noyer le live-state.
- `prune` pour borner la rétention.
- Port `LiveStateStore` (lecture/écriture), sans dépendance d'infra.
9 nouveaux tests live_state (cargo test -p domain : 208 passed / 0 failed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enrichit `compose_convention_file` pour que TOUT agent neuf, de tout
projet, soit briefé sur l'ensemble des capacités IdeA — plus seulement la
délégation. Briefing à haute altitude, télégraphique, injecté à chaque
lancement (sobriété token : capacité exposée, jamais le contenu).
Les 3 capacités sont décrites sur les 2 surfaces, strictement cloisonnées :
- MCP : via les outils `idea_context_read`/`_propose`/`idea_update_context`
(contexte projet single-writer, proposition globale enregistrée pour
validation, pas auto-appliquée), `idea_memory_read`/`_write` (mémoire
durable partagée), `idea_skill_read`/`idea_create_skill` (skills).
- Sans MCP : mêmes concepts via les seuls fichiers `.ideai/` (CONTEXT.md,
memory/ + MEMORY.md, skills `.md`), aucun nom d'outil `idea_*` ne fuit.
But : un agent ne réinvente pas son propre contexte/mémoire/workflow alors
qu'IdeA les fournit déjà.
5 tests dédiés (briefing inconditionnel même à vide, honnêteté single-writer
du contexte, cloisonnement des surfaces, ordre brief-avant-persona). 1 assert
existant adapté (`..._without_skills_omits_section` : `idea_skill_read` est
désormais toujours évoqué par le brief ; l'intention reste gardée par
l'assert sur l'absence de la section `# Skills disponibles`).
`cargo test -p application` = 0 failed.
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>
Au démarrage à froid, lorsque les évènements de cycle de vie du portail
d'écriture arrivent dans l'ordre inverse (libération observée avant
l'acquisition correspondante), le latch busy restait coincé et bloquait
définitivement la médiation d'entrée. On introduit un latch `released`
qui mémorise une libération anticipée et garantit une sémantique
exactly-once : l'acquisition tardive consomme la libération déjà vue au
lieu de re-verrouiller. Couvert par 2 tests de régression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduit le seam TurnResolution : sur prompt-ready, après un délai de grâce,
la délégation en attente est résolue de façon typée plutôt que de rester bloquée
indéfiniment (jusquà 24h). Couvre domain (mailbox), infrastructure (mailbox/input)
et application (orchestrator/error), avec tests étendus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute des logs diag! sans changement sémantique le long du canal de
délégation inter-agents (application/orchestrator, infrastructure
input, mailbox, mcp server & tools) pour diagnostiquer les blocages
récurrents de idea_ask_agent. Couvert par les tests existants étendus
(mcp_server, mailbox, input, tools, orchestrator_service).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`unix_runtime_dir` essaie désormais les bases candidates dans l'ordre de
priorité ($XDG_RUNTIME_DIR → $TMPDIR → /tmp) et retient la première dont
le sous-dossier `idea-mcp` existe ou peut être créé. Un
$XDG_RUNTIME_DIR positionné mais inutilisable (sandbox/CI pointant un
chemin inexistant ou en lecture seule) ne doit plus dead-end le bind
loopback : sans ce fallback le socket ne se liait jamais en silence et la
délégation inter-agents mourait. Déterministe pour un environnement donné,
donc le côté bind et tout lecteur de `socket_path()` s'accordent sur le
même répertoire.
Indépendant du Lot E1.
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>
Durcit le portail d'écriture de délégation et son acheminement bout-en-bout
(commande Tauri → orchestrateur → file d'entrée infrastructure → portail
frontend), avec une journalisation du submit pour diagnostiquer les cas où la
délégation n'était pas remise. Couvre le portail d'écriture côté front
(useWritePortal) avec ses tests.
QA : checks application/front/infrastructure/app-tauri verts. Les tests loopback
socket Unix réels ne sont pas exécutables en sandbox (UnixListener::bind →
PermissionDenied) ; alternatives avec skips vertes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
compose_convention_file émet désormais, dans le bloc d'orchestration (avant le
contexte projet et la persona), un court paragraphe expliquant qu'un skill
assigné est du contexte opérationnel — ni commande magique, ni sous-tâche
fournisseur — et oriente la capitalisation de workflows réutilisables vers la
surface d'orchestration active : `idea_create_skill` en profil MCP, le protocole
fichier `skill.create` sinon.
L'awareness n'injecte volontairement pas le corps des skills non assignés :
l'assignation reste la frontière de contexte. Tests de composition (présence,
ordre root → orchestration → contexte → persona, branche MCP vs fichier) verts.
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>
Bump des 4 crates du workspace (domain, infrastructure, application,
app-tauri), de tauri.conf.json, du package.json frontend et des entrées
correspondantes de Cargo.lock. Prépare l'intégration de develop dans main.
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>
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>
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>
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>
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>
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>
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>
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>
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>
É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>
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>
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>
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>
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>
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>
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>