Le tool-calling local ne fonctionnait jamais via Ollama. Refonte du
support local d'OpenCode autour de llama.cpp: profil, catalogue,
matérialisation de la config OpenCode et surface first-run alignés sur
llama-server (backend + frontend).
QA vert (commandes réelles): domain 244, application 81+64, infra 263
(10 échecs = bind-port sandbox identiques sur develop, non-régression),
frontend 574, tsc propre. Réserve E2E live non bloquante faute de
llama-server joignable.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs
(List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés,
environnement local détecté, stratégies compilées). UI EmbedderSettings.
- LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la
mémoire dépasse le budget de recall sans embedder configuré (event
EmbedderSuggested, anti-spam 1×/session, « ne plus demander »).
- Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les
agents/profils au lancement, avant la persona. UI ProjectContextPanel.
Tests : backend workspace vert (0 échec) ; frontend 306/306.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Un agent ne peut tourner que dans une seule cellule à la fois. La garde dans
LaunchAgent refuse le spawn si l'agent est déjà vivant dans un autre node
(AGENT_ALREADY_RUNNING) ; idempotent sur le même node ; le chemin resume
(agent mort) reste inchangé. Le node_id est désormais plombé jusqu'au use case.
Corrige le reset asymétrique d'une cellule au changement d'onglet : deux leaves
partageant le même agent id rendaient session_for_agent/is_agent_live/stop_agent
ambigus (cible arbitraire). Le churn reset/reattach déclenchait aussi les accents
mélangés (FIFO intact, non touché).
- snapshot agentWasRunning calculé par node (is_node_live) et non par agent
- commande list_live_agents + live_agents()/node_for_agent()/is_node_live()
- UI : dropdown grise les agents déjà placés ailleurs ; 2e cellule en doublon
affiche « disponible » au lieu d'une relance fantôme
Tests : cargo test (application + app-tauri) vert ; tsc + vitest vert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
À l'activation d'un agent, LaunchAgent compose désormais une section
« # Mémoire projet » dans le convention file généré (CLAUDE.md/AGENTS.md…),
au même titre que les skills assignés (§14.2). Les agents lisent ainsi la
mémoire projet sans aucun flag ni mécanisme propre à une CLI.
- LaunchAgent reçoit le port MemoryRecall (Arc<dyn MemoryRecall>), résout le
rappel (budget AGENT_MEMORY_RECALL_BUDGET=2048, requête = persona de l'agent)
en best-effort : mémoire vide/absente ou erreur ⇒ section omise, le launch
n'est jamais bloqué (comme un SkillRef dangling).
- compose_convention_file gagne un argument `memory: &[MemoryIndexEntry]`
(reste pure) ; section injectée seulement pour la stratégie conventionFile.
- state.rs : une seule instance NaiveMemoryRecall partagée (recall + LaunchAgent).
Tests: 57 binaires verts. compose_convention_file (vide ⇒ inchangé, ordre,
cohabitation skills) + intégration LaunchAgent (section présente / absente /
best-effort sur erreur / pas d'injection en stratégie env).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).
- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
(persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
(statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
avant le Resume auto, jamais sur le chemin reattach
fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents
fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigating (layout/tab switch) tore the xterm view down and called
handle.close(), killing the backend PTY and cutting off running AIs. Now
the view's cleanup only detaches; only an explicit user action kills a PTY.
Backend:
- PortablePtyAdapter: per-session scrollback ring buffer (~100KB, most
recent) + re-subscribable fan-out broadcast replacing the single-take
output_rx. Reader thread feeds both the ring buffer and current
subscribers; on EOF it closes subscribers (streams end) while keeping
scrollback for late re-attach.
- PtyPort: new scrollback() method; subscribe_output is now re-subscribable
(all impls + test fakes updated).
- reattach_terminal IPC command: returns scrollback and re-wires a fresh
output channel on the live session without re-spawning.
- CloseRequested hook kills all live PTYs cleanly on app shutdown.
- TerminalSessions::handles() to enumerate live sessions at shutdown.
Frontend:
- TerminalHandle.detach(); TerminalGateway/AgentGateway.reattach() + mocks.
- TerminalView cleanup detaches (never close); on mount it re-attaches to a
persisted session (repainting scrollback) instead of opening a new PTY.
- LayoutGrid persists the cell's session id via setSession; AgentsPanel
tracks per-agent session ids — both drive reattach-vs-open.
Tests: ring buffer bounds to 100KB keeping newest bytes; scrollback retained;
re-subscription delivers post-reattach output; TerminalView detaches (not
closes) on unmount and reattaches with a known session; mock detach/reattach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ARCHITECTURE §14.1: an agent's PTY cwd is now its own
`<project_root>/.ideai/run/<agent-id>/` directory, never the project root, so
N agents of the same profile no longer collide on a single conventional file
(CLAUDE.md/AGENTS.md/...).
- profile: cwd_template is now "{agentRunDir}" (built-in catalogue + docs).
- runtime: resolve_cwd substitutes {agentRunDir} (legacy {projectRoot} kept).
- LaunchAgent: computes + creates the run dir via FileSystem::create_dir_all,
passes it as the cwd base to the pure prepare_invocation. Contract chosen:
pass run_dir as the `cwd` argument (no PreparedContext change) — keeps
prepare_invocation pure, I/O stays in the use case.
- convention file is generated by IdeA inside the run dir via a pure
compose_convention_file(project_root, agent_md): absolute project-root header
+ agent persona (extensible for skills, §14.2).
- .gitignore: ignore .ideai/run/.
- run-dir cleanup left as a TODO (FileSystem port exposes no delete).
Tests: anti-collision (2 agents -> 2 distinct cwd, 2 distinct convention files,
none at root), run-dir creation order, composed convention file; pure unit
tests for agent_run_dir + compose_convention_file; runtime {agentRunDir}
substitution. cargo test --workspace + clippy -D warnings green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>