From cc7d99a63ebfe27116b0cb32bda9f888b647ce23 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 20 Jun 2026 18:52:16 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(workstate):=20snapshot=20des=20d=C3=A9?= =?UTF-8?q?l=C3=A9gations=20en=20file=20par=20agent=20(Lot=20B=20backend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`). - 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 --- crates/app-tauri/src/dto.rs | 93 +++++++++- crates/app-tauri/src/state.rs | 5 + crates/app-tauri/tests/dto_agents.rs | 72 +++++++- crates/application/src/lib.rs | 4 +- crates/application/src/workstate/mod.rs | 119 +++++++++++- crates/application/tests/workstate.rs | 222 ++++++++++++++++++++++- crates/domain/src/lib.rs | 5 +- crates/domain/src/mailbox.rs | 59 ++++++ crates/infrastructure/src/mailbox/mod.rs | 128 ++++++++++++- 9 files changed, 692 insertions(+), 15 deletions(-) diff --git a/crates/app-tauri/src/dto.rs b/crates/app-tauri/src/dto.rs index db026a2..1bf8c52 100644 --- a/crates/app-tauri/src/dto.rs +++ b/crates/app-tauri/src/dto.rs @@ -9,8 +9,9 @@ use serde::{Deserialize, Serialize}; use application::{ - AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport, - LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, ProjectWorkState, + AgentTicketState, AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, + HealthInput, HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, + ProjectWorkState, TicketWorkSource, TicketWorkStatus, }; use domain::{AgentBusyState, Project, ProjectId}; @@ -1503,6 +1504,87 @@ pub struct LiveWorkSessionDto { pub kind: LiveWorkSessionKindDto, } +/// Derived processing status of a queued ticket. +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TicketWorkStatusDto { + /// The agent's current busy turn is running this ticket. + InProgress, + /// Waiting behind the head / the agent is idle. + Queued, +} + +impl From for TicketWorkStatusDto { + fn from(status: TicketWorkStatus) -> Self { + match status { + TicketWorkStatus::InProgress => Self::InProgress, + TicketWorkStatus::Queued => Self::Queued, + } + } +} + +/// Origin of a queued ticket (human operator or a delegating agent). +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase", tag = "kind")] +pub enum TicketWorkSourceDto { + /// The human operator. + Human, + /// Another agent delegating via `idea_ask_agent`. + #[serde(rename_all = "camelCase")] + Agent { + /// The delegating agent id. + agent_id: String, + }, +} + +impl From for TicketWorkSourceDto { + fn from(source: TicketWorkSource) -> Self { + match source { + TicketWorkSource::Human => Self::Human, + TicketWorkSource::Agent { agent_id } => Self::Agent { + agent_id: agent_id.to_string(), + }, + } + } +} + +/// One queued/in-progress delegation ticket for an agent. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentTicketStateDto { + /// Stable id of the queued ticket. + pub ticket_id: String, + /// Conversation thread this task enters. + pub conversation_id: String, + /// FIFO position at snapshot time (`0` = head). + pub position: u32, + /// Derived status (in-progress vs queued). + pub status: TicketWorkStatusDto, + /// Origin of the ticket. + pub source: TicketWorkSourceDto, + /// Display label of the requester. + pub requester_label: String, + /// Bounded excerpt of the task. + pub task_preview: String, + /// Character length of the original (un-truncated) task. + pub task_len: usize, +} + +impl From for AgentTicketStateDto { + fn from(ticket: AgentTicketState) -> Self { + Self { + ticket_id: ticket.ticket_id.to_string(), + conversation_id: ticket.conversation_id.to_string(), + position: ticket.position, + status: ticket.status.into(), + source: ticket.source.into(), + requester_label: ticket.requester_label, + task_preview: ticket.task_preview, + task_len: ticket.task_len, + } + } +} + /// One manifest agent's current live/busy state. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -1517,6 +1599,8 @@ pub struct AgentWorkStateDto { pub live: Option, /// Current mediated-input busy state. pub busy: AgentBusyState, + /// Pending/in-progress delegation tickets, in FIFO order. + pub tickets: Vec, } /// Project-level read model for conversation/delegation UX. @@ -1543,6 +1627,11 @@ impl From for ProjectWorkStateDto { kind: live.kind.into(), }), busy: agent.busy, + tickets: agent + .tickets + .into_iter() + .map(AgentTicketStateDto::from) + .collect(), }) .collect(), } diff --git a/crates/app-tauri/src/state.rs b/crates/app-tauri/src/state.rs index 59625d9..1c89bbe 100644 --- a/crates/app-tauri/src/state.rs +++ b/crates/app-tauri/src/state.rs @@ -993,6 +993,10 @@ impl AppState { // partagé pour `resolve`/`resolve_ticket`/`cancel_head` côté orchestrateur. let inmemory_mailbox = Arc::new(InMemoryMailbox::new()); let mailbox = Arc::clone(&inmemory_mailbox) as Arc; + // Same concrete mailbox, second (read-only) port view for the work-state + // read model: lists pending tickets without touching the mutating surface. + let queue_snapshot = + Arc::clone(&inmemory_mailbox) as Arc; let mediated_inbox = Arc::new( MediatedInbox::with_pty( Arc::clone(&inmemory_mailbox), @@ -1031,6 +1035,7 @@ impl AppState { Arc::clone(&contexts_port), Arc::clone(&live_sessions), Arc::clone(&input_mediator), + queue_snapshot, )); // --- Limites de session des agents (ARCHITECTURE §21, LS7) --- diff --git a/crates/app-tauri/tests/dto_agents.rs b/crates/app-tauri/tests/dto_agents.rs index 71d9d39..80b6c12 100644 --- a/crates/app-tauri/tests/dto_agents.rs +++ b/crates/app-tauri/tests/dto_agents.rs @@ -9,8 +9,9 @@ use app_tauri_lib::dto::{ }; use application::AppError; use application::{ - AgentWorkState, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, - ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState, + AgentTicketState, AgentWorkState, CreateAgentOutput, InspectConversationOutput, + LaunchAgentOutput, ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState, + TicketWorkSource, TicketWorkStatus, }; use domain::ids::{AgentId, NodeId, ProfileId, SessionId}; use domain::ports::ConversationDetails; @@ -219,6 +220,7 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() { ticket, since_ms: 1_234, }, + tickets: vec![], }], }); @@ -231,11 +233,77 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() { assert_eq!(v["agents"][0]["busy"]["state"], "busy"); assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string()); assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234); + assert_eq!(v["agents"][0]["tickets"], json!([])); assert!(v["agents"][0].get("agent_id").is_none()); assert!(v["agents"][0]["live"].get("session_id").is_none()); assert!(v["agents"][0]["busy"].get("since_ms").is_none()); } +#[test] +fn project_work_state_dto_serialises_tickets_camelcase() { + let agent = AgentId::from_uuid(Uuid::from_u128(11)); + let profile = ProfileId::from_uuid(Uuid::from_u128(12)); + let requester = AgentId::from_uuid(Uuid::from_u128(20)); + let head_ticket = domain::TicketId::from_uuid(Uuid::from_u128(30)); + let next_ticket = domain::TicketId::from_uuid(Uuid::from_u128(31)); + let conversation = domain::ConversationId::from_uuid(Uuid::from_u128(40)); + let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState { + agents: vec![AgentWorkState { + agent_id: agent, + name: "Worker".to_owned(), + profile_id: profile, + live: None, + busy: domain::AgentBusyState::Idle, + tickets: vec![ + AgentTicketState { + ticket_id: head_ticket, + conversation_id: conversation, + position: 0, + status: TicketWorkStatus::InProgress, + source: TicketWorkSource::Agent { + agent_id: requester, + }, + requester_label: "Main".to_owned(), + task_preview: "Analyser le module".to_owned(), + task_len: 842, + }, + AgentTicketState { + ticket_id: next_ticket, + conversation_id: conversation, + position: 1, + status: TicketWorkStatus::Queued, + source: TicketWorkSource::Human, + requester_label: "User".to_owned(), + task_preview: "Vérifier".to_owned(), + task_len: 8, + }, + ], + }], + }); + + let v = serde_json::to_value(&dto).unwrap(); + let head = &v["agents"][0]["tickets"][0]; + assert_eq!(head["ticketId"], head_ticket.to_string()); + assert_eq!(head["conversationId"], conversation.to_string()); + assert_eq!(head["position"], 0); + assert_eq!(head["status"], "inProgress"); + assert_eq!(head["source"]["kind"], "agent"); + assert_eq!(head["source"]["agentId"], requester.to_string()); + assert_eq!(head["requesterLabel"], "Main"); + assert_eq!(head["taskPreview"], "Analyser le module"); + assert_eq!(head["taskLen"], 842); + // snake_case must not leak. + assert!(head.get("ticket_id").is_none()); + assert!(head.get("task_preview").is_none()); + assert!(head["source"].get("agent_id").is_none()); + + let next = &v["agents"][0]["tickets"][1]; + assert_eq!(next["status"], "queued"); + assert_eq!(next["source"]["kind"], "human"); + assert!(next["source"].get("agentId").is_none()); + assert_eq!(next["requesterLabel"], "User"); +} + #[test] fn launch_agent_request_carries_conversation_id_for_resume() { let raw = json!({ diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 9a50497..50e0cb6 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -119,6 +119,6 @@ pub use terminal::{ }; pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput}; pub use workstate::{ - AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput, LiveWorkSession, - ProjectWorkState, + AgentTicketState, AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput, + LiveWorkSession, ProjectWorkState, TicketWorkSource, TicketWorkStatus, }; diff --git a/crates/application/src/workstate/mod.rs b/crates/application/src/workstate/mod.rs index 897c0a8..b1eebb9 100644 --- a/crates/application/src/workstate/mod.rs +++ b/crates/application/src/workstate/mod.rs @@ -8,13 +8,22 @@ use std::collections::HashMap; use std::sync::Arc; -use domain::input::AgentBusyState; +use domain::input::{AgentBusyState, InputSource}; use domain::ports::AgentContextStore; -use domain::{AgentId, InputMediator, NodeId, ProfileId, Project, SessionId}; +use domain::{ + AgentId, AgentQueueSnapshot, ConversationId, InputMediator, NodeId, ProfileId, Project, + QueuedTicketSnapshot, SessionId, TicketId, +}; use crate::error::AppError; use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions}; +/// Maximum length (in characters) of a derived [`AgentTicketState::task_preview`]. +/// +/// The full task can be a very long prompt; the panel only needs a scannable +/// excerpt, with [`AgentTicketState::task_len`] signalling there is more. +const TASK_PREVIEW_MAX_CHARS: usize = 160; + /// Input for [`GetProjectWorkState::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GetProjectWorkStateInput { @@ -42,6 +51,64 @@ pub struct AgentWorkState { pub live: Option, /// Current FIFO/busy state. pub busy: AgentBusyState, + /// Pending/in-progress delegation tickets, in FIFO order. + pub tickets: Vec, +} + +/// One queued delegation ticket, projected for the work-state read model. +/// +/// A read-only, presentation-oriented view of a [`QueuedTicketSnapshot`]: the +/// status is **derived** by crossing the FIFO snapshot with the agent's busy state +/// (never stored), and `task_preview` is a bounded excerpt of the full task (the +/// full text is never sent to the panel; `task_len` signals there is more). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentTicketState { + /// Stable id of the queued ticket. + pub ticket_id: TicketId, + /// Conversation thread this task enters. + pub conversation_id: ConversationId, + /// FIFO position at snapshot time (`0` = head). + pub position: u32, + /// Derived status (in-progress = the agent's current busy ticket). + pub status: TicketWorkStatus, + /// Origin of the ticket (human operator or a delegating agent). + pub source: TicketWorkSource, + /// Display label of the requester. + pub requester_label: String, + /// Bounded, whitespace-normalised excerpt of the task. + pub task_preview: String, + /// Character length of the **original** (un-truncated) task. + pub task_len: usize, +} + +/// Derived processing status of a queued ticket. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TicketWorkStatus { + /// The agent's current busy turn is running this ticket. + InProgress, + /// Waiting behind the head / the agent is idle. + Queued, +} + +/// Origin of a queued ticket, mirroring [`InputSource`] for the read model. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TicketWorkSource { + /// The human operator. + Human, + /// Another agent delegating via `idea_ask_agent`. + Agent { + /// The delegating agent. + agent_id: AgentId, + }, +} + +impl From for TicketWorkSource { + fn from(source: InputSource) -> Self { + match source { + InputSource::Human => Self::Human, + InputSource::Agent { agent_id } => Self::Agent { agent_id }, + } + } } /// Live session coordinates exposed by the work-state read model. @@ -60,6 +127,7 @@ pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, + queue: Arc, } impl GetProjectWorkState { @@ -69,11 +137,13 @@ impl GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, + queue: Arc, ) -> Self { Self { contexts, live, input, + queue, } } @@ -102,12 +172,23 @@ impl GetProjectWorkState { session_id: snapshot.session_id, kind: snapshot.kind, }); + // Tickets are crossed with the busy state: only an agent absent from + // the manifest is dropped (this loop only visits manifest entries), so + // the manifest boundary is naturally preserved. + let busy_ticket = self.input.busy_state(agent.id).ticket(); + let tickets = self + .queue + .queue_for(agent.id) + .into_iter() + .map(|snapshot| ticket_state(snapshot, busy_ticket)) + .collect(); Ok(AgentWorkState { agent_id: agent.id, name: agent.name, profile_id: agent.profile_id, live, busy: self.input.busy_state(agent.id), + tickets, }) }) .collect::, AppError>>()?; @@ -115,6 +196,40 @@ impl GetProjectWorkState { } } +/// Projects one FIFO snapshot into a read-model ticket, deriving status from the +/// agent's current busy ticket (the one running its turn = `InProgress`). +fn ticket_state(snapshot: QueuedTicketSnapshot, busy_ticket: Option) -> AgentTicketState { + let status = if busy_ticket == Some(snapshot.id) { + TicketWorkStatus::InProgress + } else { + TicketWorkStatus::Queued + }; + AgentTicketState { + ticket_id: snapshot.id, + conversation_id: snapshot.conversation, + position: snapshot.position, + status, + source: snapshot.source.into(), + requester_label: snapshot.requester, + task_preview: task_preview(&snapshot.task), + task_len: snapshot.task.chars().count(), + } +} + +/// Builds a bounded, whitespace-normalised excerpt of a task for the UI panel. +/// +/// Trims, collapses any run of whitespace to a single space, then truncates to +/// [`TASK_PREVIEW_MAX_CHARS`] characters (never mid-codepoint). The original length +/// is reported separately as [`AgentTicketState::task_len`]. +fn task_preview(task: &str) -> String { + let normalised = task.split_whitespace().collect::>().join(" "); + if normalised.chars().count() <= TASK_PREVIEW_MAX_CHARS { + normalised + } else { + normalised.chars().take(TASK_PREVIEW_MAX_CHARS).collect() + } +} + fn live_by_agent(snapshots: Vec) -> HashMap { let mut out = HashMap::new(); for snapshot in snapshots { diff --git a/crates/application/tests/workstate.rs b/crates/application/tests/workstate.rs index 0be9502..cd8c5da 100644 --- a/crates/application/tests/workstate.rs +++ b/crates/application/tests/workstate.rs @@ -9,16 +9,18 @@ use async_trait::async_trait; use application::{ GetProjectWorkState, GetProjectWorkStateInput, LiveSessionKind, LiveSessions, - StructuredSessions, TerminalSessions, + StructuredSessions, TerminalSessions, TicketWorkSource, TicketWorkStatus, +}; +use domain::mailbox::{ + AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket, }; -use domain::mailbox::{MailboxError, PendingReply, Ticket}; use domain::ports::{ AgentContextStore, AgentSession, AgentSessionError, PtyHandle, ReplyStream, StoreError, }; use domain::{ - Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, InputMediator, ManifestEntry, - MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath, PtySize, RemoteRef, SessionId, - SessionKind, TerminalSession, + Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, ConversationId, InputMediator, + InputSource, ManifestEntry, MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath, + PtySize, RemoteRef, SessionId, SessionKind, TerminalSession, TicketId, }; use uuid::Uuid; @@ -138,6 +140,45 @@ impl InputMediator for FakeInput { } } +#[derive(Default)] +struct FakeQueue { + queues: Mutex>>, +} + +impl FakeQueue { + fn set(&self, agent: AgentId, tickets: Vec) { + self.queues.lock().unwrap().insert(agent, tickets); + } +} + +impl AgentQueueSnapshot for FakeQueue { + fn queue_for(&self, agent: AgentId) -> Vec { + self.queues + .lock() + .unwrap() + .get(&agent) + .cloned() + .unwrap_or_default() + } +} + +fn snapshot( + id: u128, + position: u32, + source: InputSource, + requester: &str, + task: &str, +) -> QueuedTicketSnapshot { + QueuedTicketSnapshot { + id: TicketId::from_uuid(Uuid::from_u128(id)), + source, + conversation: ConversationId::from_uuid(Uuid::from_u128(id + 1000)), + requester: requester.to_owned(), + task: task.to_owned(), + position, + } +} + struct FakeSession { id: SessionId, } @@ -188,6 +229,7 @@ struct Fixture { pty: Arc, structured: Arc, input: Arc, + queue: Arc, project: Project, } @@ -197,18 +239,22 @@ fn fixture(agents: &[Agent]) -> Fixture { let live = Arc::new(LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured))); let input = Arc::new(FakeInput::default()); let input_port = Arc::clone(&input) as Arc; + let queue = Arc::new(FakeQueue::default()); + let queue_port = Arc::clone(&queue) as Arc; let usecase = GetProjectWorkState::new( Arc::new(FakeContexts { manifest: manifest(agents), }), live, input_port, + queue_port, ); Fixture { usecase, pty, structured, input, + queue, project: project(), } } @@ -305,3 +351,169 @@ async fn workstate_ignores_live_agents_absent_from_manifest() { assert_eq!(out.agents[0].agent_id, a.id); assert_eq!(out.agents[0].live, None); } + +#[tokio::test] +async fn workstate_agent_without_queue_has_no_tickets() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + assert!(out.agents[0].tickets.is_empty()); +} + +#[tokio::test] +async fn workstate_lists_two_tickets_in_fifo_order() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + let from = aid(20); + f.queue.set( + a.id, + vec![ + snapshot(1, 0, InputSource::agent(from), "Main", "first"), + snapshot(2, 1, InputSource::agent(from), "Main", "second"), + ], + ); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + let tickets = &out.agents[0].tickets; + assert_eq!(tickets.len(), 2); + assert_eq!(tickets[0].ticket_id, ticket_id(1)); + assert_eq!(tickets[0].position, 0); + assert_eq!(tickets[1].ticket_id, ticket_id(2)); + assert_eq!(tickets[1].position, 1); +} + +#[tokio::test] +async fn workstate_marks_busy_head_in_progress_and_rest_queued() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + let from = aid(20); + f.queue.set( + a.id, + vec![ + snapshot(1, 0, InputSource::agent(from), "Main", "first"), + snapshot(2, 1, InputSource::agent(from), "Main", "second"), + ], + ); + f.input.set_busy( + a.id, + AgentBusyState::Busy { + ticket: ticket_id(1), + since_ms: 5, + }, + ); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + let tickets = &out.agents[0].tickets; + assert_eq!(tickets[0].status, TicketWorkStatus::InProgress); + assert_eq!(tickets[1].status, TicketWorkStatus::Queued); +} + +#[tokio::test] +async fn workstate_marks_all_queued_when_agent_idle() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + let from = aid(20); + f.queue.set( + a.id, + vec![snapshot(1, 0, InputSource::agent(from), "Main", "first")], + ); + // Agent left Idle (default): even the head is only queued, not in-progress. + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + assert_eq!(out.agents[0].tickets[0].status, TicketWorkStatus::Queued); +} + +#[tokio::test] +async fn workstate_ignores_queue_for_agent_absent_from_manifest() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + // A queue exists for an agent that is not in the manifest: it must not surface. + f.queue.set( + aid(999), + vec![snapshot(1, 0, InputSource::Human, "User", "ghost")], + ); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + assert_eq!(out.agents.len(), 1); + assert!(out.agents[0].tickets.is_empty()); +} + +#[tokio::test] +async fn workstate_truncates_task_preview_and_keeps_original_len() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + // 400 chars with runs of whitespace to normalise; well over the 160 cap. + let long_task = format!("start{}end", " x ".repeat(80)); + let original_len = long_task.chars().count(); + f.queue.set( + a.id, + vec![snapshot(1, 0, InputSource::Human, "User", &long_task)], + ); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + let ticket = &out.agents[0].tickets[0]; + assert_eq!(ticket.task_preview.chars().count(), 160); + assert!(!ticket.task_preview.contains(" "), "whitespace normalised"); + assert_eq!(ticket.task_len, original_len); + assert!(ticket.task_len > 160); +} + +#[tokio::test] +async fn workstate_maps_human_and_agent_ticket_sources() { + let a = agent(10, "alpha"); + let f = fixture(std::slice::from_ref(&a)); + let from = aid(20); + f.queue.set( + a.id, + vec![ + snapshot(1, 0, InputSource::Human, "User", "from human"), + snapshot(2, 1, InputSource::agent(from), "Main", "from agent"), + ], + ); + + let out = f + .usecase + .execute(GetProjectWorkStateInput { project: f.project }) + .await + .unwrap(); + + let tickets = &out.agents[0].tickets; + assert_eq!(tickets[0].source, TicketWorkSource::Human); + assert_eq!(tickets[0].requester_label, "User"); + assert_eq!( + tickets[1].source, + TicketWorkSource::Agent { agent_id: from } + ); + assert_eq!(tickets[1].requester_label, "Main"); +} diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 4cbc8db..996a63f 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -82,7 +82,10 @@ pub use profile::{ McpServerWiring, RateLimitPattern, SessionStrategy, }; -pub use mailbox::{AgentMailbox, MailboxError, PendingReply, Ticket, TicketId}; +pub use mailbox::{ + AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket, + TicketId, +}; pub use conversation::{ Conversation, ConversationError, ConversationId, ConversationParty, ConversationRegistry, diff --git a/crates/domain/src/mailbox.rs b/crates/domain/src/mailbox.rs index 4eeb70a..92f813f 100644 --- a/crates/domain/src/mailbox.rs +++ b/crates/domain/src/mailbox.rs @@ -33,6 +33,47 @@ use crate::conversation::ConversationId; use crate::ids::AgentId; use crate::input::InputSource; +/// A read-only, cloned view of one queued [`Ticket`] in a target agent's FIFO. +/// +/// Carries **only the data** of a ticket (never the one-shot reply sender, which +/// stays inside the adapter), plus its `position` in the FIFO at snapshot time +/// (`0` = head). Pure value object used by the [`AgentQueueSnapshot`] read port so +/// the work-state read model can list pending delegations without touching the +/// mutating [`AgentMailbox`] surface (ISP). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QueuedTicketSnapshot { + /// Stable id of the queued ticket. + pub id: TicketId, + /// The origin of the input (Human or a delegating Agent). + pub source: InputSource, + /// The conversation thread this task enters. + pub conversation: ConversationId, + /// Display name (or id) of the requester. + pub requester: String, + /// The full task/message text (preview/truncation is the caller's concern). + pub task: String, + /// Position in the FIFO at snapshot time (`0` = head, recomputed each call). + pub position: u32, +} + +/// Read-only inspection port over the per-agent delegation FIFO. +/// +/// Segregated from the mutating [`AgentMailbox`] (Interface Segregation): the +/// work-state read model depends on this port to *observe* the queue, never on +/// `enqueue`/`resolve`/`cancel`. Implementations return **cloned** snapshots in +/// FIFO order with recomputed positions; observing the queue must never mutate it. +/// +/// Object-safe (`&self`) so the application layer holds it as +/// `Arc`; one concrete `InMemoryMailbox` can be shared as +/// both an [`AgentMailbox`] (mutation) and an [`AgentQueueSnapshot`] (read) view. +pub trait AgentQueueSnapshot: Send + Sync { + /// Returns a cloned, FIFO-ordered snapshot of `agent`'s queued tickets. + /// + /// An agent with no queue yields an empty `Vec`. Positions are recomputed from + /// the current order (`0` = head). Pure read: the queue is left untouched. + fn queue_for(&self, agent: AgentId) -> Vec; +} + /// Identifies one queued [`Ticket`] within a target agent's mailbox. /// /// Newtype around [`uuid::Uuid`]; minted by the adapter on `enqueue`. It is **never @@ -310,4 +351,22 @@ mod tests { let a = AgentId::from_uuid(uuid::Uuid::from_u128(1)); assert_ne!(MailboxError::NoPendingRequest(a), MailboxError::Cancelled); } + + #[test] + fn queued_ticket_snapshot_carries_ticket_data_and_position() { + let conv = ConversationId::from_uuid(uuid::Uuid::from_u128(3)); + let from = AgentId::from_uuid(uuid::Uuid::from_u128(4)); + let snap = QueuedTicketSnapshot { + id: TicketId::from_uuid(uuid::Uuid::from_u128(8)), + source: InputSource::agent(from), + conversation: conv, + requester: "Main".to_owned(), + task: "delegate".to_owned(), + position: 2, + }; + assert_eq!(snap.source.as_agent(), Some(from)); + assert_eq!(snap.conversation, conv); + assert_eq!(snap.position, 2); + assert_eq!(snap.task, "delegate"); + } } diff --git a/crates/infrastructure/src/mailbox/mod.rs b/crates/infrastructure/src/mailbox/mod.rs index dba9095..93b262b 100644 --- a/crates/infrastructure/src/mailbox/mod.rs +++ b/crates/infrastructure/src/mailbox/mod.rs @@ -25,7 +25,10 @@ use std::sync::Mutex; use tokio::sync::oneshot; use domain::ids::AgentId; -use domain::mailbox::{AgentMailbox, MailboxError, PendingReply, Ticket, TicketId}; +use domain::mailbox::{ + AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket, + TicketId, +}; /// One queued request plus the sender that resolves its awaiting [`PendingReply`]. struct Slot { @@ -160,6 +163,35 @@ impl AgentMailbox for InMemoryMailbox { } } +impl AgentQueueSnapshot for InMemoryMailbox { + fn queue_for(&self, agent: AgentId) -> Vec { + // Pure read: clone each ticket's *data* (never the `oneshot::Sender`) under + // the lock, recomputing the FIFO position from the current order (0 = head). + // The queue is observed, not mutated. + let queues = self + .queues + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + queues + .get(&agent) + .map(|queue| { + queue + .iter() + .enumerate() + .map(|(idx, slot)| QueuedTicketSnapshot { + id: slot.ticket.id, + source: slot.ticket.source, + conversation: slot.ticket.conversation, + requester: slot.ticket.requester.clone(), + task: slot.ticket.task.clone(), + position: u32::try_from(idx).unwrap_or(u32::MAX), + }) + .collect() + }) + .unwrap_or_default() + } +} + #[cfg(test)] mod tests { use super::*; @@ -270,4 +302,98 @@ mod tests { Some(TicketId::from_uuid(uuid::Uuid::from_u128(10))) ); } + + fn tid(n: u128) -> TicketId { + TicketId::from_uuid(uuid::Uuid::from_u128(n)) + } + + #[test] + fn snapshot_of_empty_queue_is_empty() { + let mb = InMemoryMailbox::new(); + assert!(mb.queue_for(agent(1)).is_empty()); + } + + #[test] + fn snapshot_preserves_fifo_order_and_positions() { + let mb = InMemoryMailbox::new(); + let a = agent(1); + let _p1 = mb.enqueue(a, ticket(10, "first")); + let _p2 = mb.enqueue(a, ticket(11, "second")); + + let snap = mb.queue_for(a); + assert_eq!(snap.len(), 2); + assert_eq!(snap[0].id, tid(10)); + assert_eq!(snap[0].position, 0); + assert_eq!(snap[0].task, "first"); + assert_eq!(snap[1].id, tid(11)); + assert_eq!(snap[1].position, 1); + assert_eq!(snap[1].task, "second"); + } + + #[test] + fn snapshot_carries_ticket_metadata() { + use domain::conversation::ConversationId; + use domain::input::InputSource; + + let mb = InMemoryMailbox::new(); + let a = agent(1); + let from = agent(2); + let conv = ConversationId::from_uuid(uuid::Uuid::from_u128(42)); + let _p = mb.enqueue( + a, + Ticket::from_agent(tid(10), from, conv, "Main", "delegate task"), + ); + + let snap = mb.queue_for(a); + assert_eq!(snap.len(), 1); + assert_eq!(snap[0].source, InputSource::agent(from)); + assert_eq!(snap[0].conversation, conv); + assert_eq!(snap[0].requester, "Main"); + assert_eq!(snap[0].task, "delegate task"); + } + + #[test] + fn snapshot_is_read_only() { + let mb = InMemoryMailbox::new(); + let a = agent(1); + let _p1 = mb.enqueue(a, ticket(10, "first")); + let _p2 = mb.enqueue(a, ticket(11, "second")); + + let _ = mb.queue_for(a); + let _ = mb.queue_for(a); + + // Observing the queue did not mutate it. + assert_eq!(mb.pending(&a), 2); + assert_eq!(mb.head_ticket(&a), Some(tid(10))); + } + + #[test] + fn snapshot_updates_after_resolve_ticket() { + let mb = InMemoryMailbox::new(); + let a = agent(1); + let _p1 = mb.enqueue(a, ticket(10, "first")); + let _p2 = mb.enqueue(a, ticket(11, "second")); + + mb.resolve_ticket(a, tid(10), "done".to_owned()).unwrap(); + + let snap = mb.queue_for(a); + assert_eq!(snap.len(), 1); + assert_eq!(snap[0].id, tid(11)); + assert_eq!(snap[0].position, 0, "remaining ticket becomes head"); + } + + #[test] + fn snapshot_updates_after_cancel_head() { + let mb = InMemoryMailbox::new(); + let a = agent(1); + let _p1 = mb.enqueue(a, ticket(10, "head")); + let _p2 = mb.enqueue(a, ticket(11, "next")); + + mb.cancel_head(a, tid(10)); + + let snap = mb.queue_for(a); + assert_eq!(snap.len(), 1); + assert_eq!(snap[0].id, tid(11)); + assert_eq!(snap[0].position, 0, "next ticket recomputed to position 0"); + } } From c60060494d85a527486f098727de7c7cb72a3e65 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 20 Jun 2026 18:52:23 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat(workstate):=20UI=20des=20d=C3=A9l?= =?UTF-8?q?=C3=A9gations=20en=20file=20par=20agent=20(Lot=20B=20frontend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Affiche les tickets en attente dans `ProjectWorkStatePanel` à partir du snapshot de file exposé par le read-model : type de domaine et adaptateur mock alignés sur le DTO `camelCase`, panneau enrichi (requester, aperçu de tâche, position FIFO), hook de lecture mis à jour. Tests verts : workstate.test.tsx + projects.test.tsx (2 fichiers / 17), `tsc --noEmit` OK. Co-Authored-By: Claude Opus 4.8 --- frontend/src/adapters/mock/index.ts | 24 ++- frontend/src/domain/index.ts | 21 +++ .../workstate/ProjectWorkStatePanel.tsx | 140 +++++++++++----- .../features/workstate/useProjectWorkState.ts | 1 + .../src/features/workstate/workstate.test.tsx | 155 +++++++++++++++++- 5 files changed, 300 insertions(+), 41 deletions(-) diff --git a/frontend/src/adapters/mock/index.ts b/frontend/src/adapters/mock/index.ts index 65a3b89..41ba72f 100644 --- a/frontend/src/adapters/mock/index.ts +++ b/frontend/src/adapters/mock/index.ts @@ -1654,8 +1654,11 @@ export class MockWorkStateGateway implements WorkStateGateway { private states = new Map(); /** Seeds the read-model returned for a project (deterministic tests/dev). */ - _setProjectWorkState(projectId: string, state: ProjectWorkState): void { - this.states.set(projectId, structuredClone(state)); + _setProjectWorkState( + projectId: string, + state: ProjectWorkState | LegacyProjectWorkState, + ): void { + this.states.set(projectId, normalizeProjectWorkState(state)); } async getProjectWorkState(projectId: string): Promise { @@ -1663,6 +1666,23 @@ export class MockWorkStateGateway implements WorkStateGateway { } } +type LegacyProjectWorkState = { + agents: Array & { + tickets?: ProjectWorkState["agents"][number]["tickets"]; + }>; +}; + +function normalizeProjectWorkState( + state: ProjectWorkState | LegacyProjectWorkState, +): ProjectWorkState { + return { + agents: state.agents.map((agent) => ({ + ...structuredClone(agent), + tickets: structuredClone(agent.tickets ?? []), + })), + }; +} + function mostRestrictive( project?: PermissionSet["fallback"], agent?: PermissionSet["fallback"], diff --git a/frontend/src/domain/index.ts b/frontend/src/domain/index.ts index 06c9adb..06cb66a 100644 --- a/frontend/src/domain/index.ts +++ b/frontend/src/domain/index.ts @@ -149,6 +149,26 @@ export type WorkBusyState = | { state: "idle" } | { state: "busy"; ticket: string; sinceMs: number }; +/** FIFO status for an input/ticket currently tracked by the backend queue. */ +export type TicketWorkStatus = "inProgress" | "queued"; + +/** Who produced an input/ticket in the work-state read-model. */ +export type TicketWorkSource = + | { kind: "human" } + | { kind: "agent"; agentId: string }; + +/** One queued or in-progress input/ticket for an agent. */ +export interface AgentTicketState { + ticketId: string; + conversationId: string; + position: number; + status: TicketWorkStatus; + source: TicketWorkSource; + requesterLabel: string; + taskPreview: string; + taskLen: number; +} + /** One agent row in the project work-state read-model. */ export interface AgentWorkState { agentId: string; @@ -156,6 +176,7 @@ export interface AgentWorkState { profileId: string; live?: LiveWorkSession; busy: WorkBusyState; + tickets: AgentTicketState[]; } /** Minimal read-only live-state surface for a project. */ diff --git a/frontend/src/features/workstate/ProjectWorkStatePanel.tsx b/frontend/src/features/workstate/ProjectWorkStatePanel.tsx index 53629d9..969c606 100644 --- a/frontend/src/features/workstate/ProjectWorkStatePanel.tsx +++ b/frontend/src/features/workstate/ProjectWorkStatePanel.tsx @@ -1,9 +1,9 @@ /** * Read-only project work-state panel: one row per agent, showing live/offline - * and idle/busy state from the backend read-model. + * and idle/busy state from the backend read-model, plus the current input queue. */ -import type { AgentWorkState } from "@/domain"; +import type { AgentTicketState, AgentWorkState } from "@/domain"; import { Button, Panel, Spinner, cn } from "@/shared"; import { useProjectWorkState } from "./useProjectWorkState"; @@ -15,48 +15,112 @@ function shortTicket(ticket: string): string { return ticket.length <= 8 ? ticket : ticket.slice(0, 8); } +function formatStatus(status: AgentTicketState["status"]): string { + return status === "inProgress" ? "In progress" : "Queued"; +} + +function requesterLabel(ticket: AgentTicketState): string { + if (ticket.source.kind === "human") return "Human"; + const id = shortTicket(ticket.source.agentId); + return ticket.requesterLabel.trim() + ? `${ticket.requesterLabel} (${id})` + : `Agent ${id}`; +} + +function TicketRow({ ticket }: { ticket: AgentTicketState }) { + const truncated = ticket.taskLen > ticket.taskPreview.length; + return ( +
  • + + #{ticket.position + 1} {formatStatus(ticket.status)} + + + {requesterLabel(ticket)} + · + {ticket.taskPreview} + {truncated && ( + + {" "} + +{ticket.taskLen - ticket.taskPreview.length} + + )} + + + {shortTicket(ticket.ticketId)} + +
  • + ); +} + function AgentRow({ agent }: { agent: AgentWorkState }) { const live = agent.live !== undefined; const busy = agent.busy.state === "busy"; + const tickets = [...agent.tickets].sort((a, b) => a.position - b.position); return ( -
  • - - - {agent.name} +
  • +
    + + + {agent.name} + + {agent.profileId} - {agent.profileId} - - - - {live ? "Live" : "Offline"} - - - {busy ? "Busy" : "Idle"} - - {agent.busy.state === "busy" && ( - + - {shortTicket(agent.busy.ticket)} - - )} - + {live ? "Live" : "Offline"} + + + {busy ? "Busy" : "Idle"} + + {agent.busy.state === "busy" && ( + + {shortTicket(agent.busy.ticket)} + + )} + +
    + {tickets.length > 0 && ( +
      + {tickets.map((ticket) => ( + + ))} +
    + )}
  • ); } diff --git a/frontend/src/features/workstate/useProjectWorkState.ts b/frontend/src/features/workstate/useProjectWorkState.ts index 035e663..4c0ffd9 100644 --- a/frontend/src/features/workstate/useProjectWorkState.ts +++ b/frontend/src/features/workstate/useProjectWorkState.ts @@ -55,6 +55,7 @@ export function useProjectWorkState(projectId: string): ProjectWorkStateViewMode event.type === "agentLaunched" || event.type === "agentExited" || event.type === "agentBusyChanged" || + event.type === "delegationReady" || event.type === "orchestratorRequestProcessed" ) { void refresh(); diff --git a/frontend/src/features/workstate/workstate.test.tsx b/frontend/src/features/workstate/workstate.test.tsx index 20dec69..003dc27 100644 --- a/frontend/src/features/workstate/workstate.test.tsx +++ b/frontend/src/features/workstate/workstate.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor, within } from "@testing-library/react"; import { DIProvider } from "@/app/di"; import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock"; @@ -49,6 +49,7 @@ describe("ProjectWorkStatePanel", () => { expect(await screen.findByText("Planner")).toBeTruthy(); expect(screen.getByText("Offline")).toBeTruthy(); expect(screen.getByText("Idle")).toBeTruthy(); + expect(screen.queryByLabelText("Planner tickets")).toBeNull(); }); it("renders a live busy agent with a short ticket", async () => { @@ -81,6 +82,116 @@ describe("ProjectWorkStatePanel", () => { expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy(); }); + it("renders an in-progress human ticket", async () => { + const workState = new MockWorkStateGateway(); + workState._setProjectWorkState(PROJECT_ID, { + agents: [ + { + agentId: "agent-4", + name: "Implementer", + profileId: "codex", + busy: { + state: "busy", + ticket: "ticket-in-progress-abcdef", + sinceMs: 789, + }, + tickets: [ + { + ticketId: "ticket-in-progress-abcdef", + conversationId: "conversation-a", + position: 0, + status: "inProgress", + source: { kind: "human" }, + requesterLabel: "Anthony", + taskPreview: "Wire the workstate queue UI", + taskLen: 36, + }, + ], + }, + ], + }); + + renderPanel(workState); + + const list = await screen.findByLabelText("Implementer tickets"); + expect(within(list).getByText("#1 In progress")).toBeTruthy(); + expect(within(list).getByText("Human")).toBeTruthy(); + expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy(); + expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy(); + expect(within(list).getByLabelText("9 more characters")).toBeTruthy(); + }); + + it("renders two tickets in FIFO order", async () => { + const workState = new MockWorkStateGateway(); + workState._setProjectWorkState(PROJECT_ID, { + agents: [ + { + agentId: "agent-5", + name: "Reviewer", + profileId: "claude", + busy: { state: "idle" }, + tickets: [ + { + ticketId: "ticket-second-abcdef", + conversationId: "conversation-b", + position: 1, + status: "queued", + source: { kind: "agent", agentId: "agent-main-123456" }, + requesterLabel: "Main", + taskPreview: "Then review the result", + taskLen: 22, + }, + { + ticketId: "ticket-first-abcdef", + conversationId: "conversation-a", + position: 0, + status: "inProgress", + source: { kind: "agent", agentId: "agent-qa-123456" }, + requesterLabel: "QA", + taskPreview: "Check behavior", + taskLen: 14, + }, + ], + }, + ], + }); + + renderPanel(workState); + + const list = await screen.findByLabelText("Reviewer tickets"); + expect(within(list).getByText("QA (agent-qa)")).toBeTruthy(); + expect(within(list).getByText("Main (agent-ma)")).toBeTruthy(); + expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan( + list.textContent?.indexOf("#2 Queued") ?? -1, + ); + }); + + it("normalizes mock work state without tickets", async () => { + const workState = new MockWorkStateGateway(); + workState._setProjectWorkState(PROJECT_ID, { + agents: [ + { + agentId: "agent-6", + name: "Legacy", + profileId: "gemini", + busy: { state: "idle" }, + }, + ], + }); + + await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({ + agents: [ + { + agentId: "agent-6", + name: "Legacy", + profileId: "gemini", + busy: { state: "idle" }, + tickets: [], + }, + ], + }); + }); + it("refreshes when a relevant domain event fires", async () => { const workState = new MockWorkStateGateway(); const system = new MockSystemGateway(); @@ -108,4 +219,46 @@ describe("ProjectWorkStatePanel", () => { expect(await screen.findByText("Responder")).toBeTruthy(); await waitFor(() => expect(spy).toHaveBeenCalled()); }); + + it("refreshes when delegationReady fires", async () => { + const workState = new MockWorkStateGateway(); + const system = new MockSystemGateway(); + renderPanel(workState, system); + + await screen.findByText("No agent work state."); + const spy = vi.spyOn(workState, "getProjectWorkState"); + workState._setProjectWorkState(PROJECT_ID, { + agents: [ + { + agentId: "agent-7", + name: "Delegatee", + profileId: "codex", + busy: { state: "busy", ticket: "ticket-ready-123456", sinceMs: 123 }, + tickets: [ + { + ticketId: "ticket-ready-123456", + conversationId: "conversation-ready", + position: 0, + status: "inProgress", + source: { kind: "agent", agentId: "agent-main-123456" }, + requesterLabel: "Main", + taskPreview: "Handle this delegated task", + taskLen: 26, + }, + ], + }, + ], + }); + + system.emit({ + type: "delegationReady", + agentId: "agent-7", + ticket: "ticket-ready-123456", + text: "Handle this delegated task", + }); + + expect(await screen.findByText("Delegatee")).toBeTruthy(); + expect(screen.getByText("#1 In progress")).toBeTruthy(); + await waitFor(() => expect(spy).toHaveBeenCalled()); + }); }); From 5cb99fd353464146be3f54954aedbddba3b2e522 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 20 Jun 2026 18:52:27 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore(wip):=20=C3=A9tat=20runtime=20.ideai?= =?UTF-8?q?=20(flux=20conversation=20live,=20layouts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../handoff.md | 8 +++--- .../log.jsonl | 2 ++ .../handoff.md | 6 +++-- .../log.jsonl | 2 ++ .../handoff.md | 8 +++--- .../log.jsonl | 2 ++ .../handoff.md | 10 +++---- .../log.jsonl | 3 +++ .ideai/layouts.json | 27 +++++++++++++++---- 9 files changed, 48 insertions(+), 20 deletions(-) diff --git a/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/handoff.md b/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/handoff.md index c2db80d..a8153dc 100644 --- a/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/handoff.md +++ b/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/handoff.md @@ -1,11 +1,9 @@ --- -upTo: 39f05602-a660-4632-accc-42d4887ce1ed +upTo: b916af4a-6f94-4777-92b1-c5f89f898cde objective: Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont --- **Objectif :** Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont -- **Response:** ✅ LS4 — Tests couche application « limites de session » : TOUT VERT, zéro régression. Commande : `cargo test -p application` Résultat : **tous les binaires de test verts, 0 failed** sur l'ensemble du crate. 16 nouveaux tests LS4 ajoutés, répartis en 2 fichiers d'intégration (style des tests applicatifs existants, 100 % fakes des ports). Confirmation explicite NON-RÉGRESSION : les suites préexistantes citées restent vertes — - `drain_with_readiness_lot1.rs` : **7/7** ✓ - `send_blocking_d1.rs` : **9/9** ✓ - (et tout le reste du crate : project/agent_lifecycle/orchestrator/structured_launch/etc., 0 échec). === Nouveau fichier `tests/session_limit_service.rs` (9 tests) — fakes Clock fixe / Scheduler enregistreur-contrôlable / EventBus espion / AgentResumer espion-contrôlable === (a) détection→planification : - `on_rate_limited_future_arms_and_emits_in_order` : Some(reset futur) ⇒ EXACTEMENT 1 `arm(fire_at_ms==reset, ResumeAgent{agent,node,conv})` + events `AgentRateLimited` PUIS `AgentResumeScheduled` dans cet ordre. - `on_rate_limited_past_reset_clamps_fire_at_to_now` : reset passé ⇒ `fire_at_ms==now` (clamp anti-passé) ; l'event RateLimited garde l'heure brute passée, ResumeScheduled porte le now clampé. - `on_rate_limited_without_reset_is_human_fallback_no_arm` : None ⇒ AUCUN arm + events `AgentRateLimited{None}` puis `AgentRateLimitSuspected{None}`. - `on_rate_limited_twice_same_agent_dedups_cancelling_previous` : 2 signaux même agent ⇒ l'ancien ScheduleId est cancel-é avant réarmement, et AUCUN `AgentResumeCancelled` émis (dédoublonnage interne silencieux, §21.10-4). (b) exécution : - `execute_resume_calls_resumer_with_prompt_and_emits_resumed` : resume(agent,node,conv, prompt==RESUME_PROMPT vérifié) + `AgentResumed` publié + entrée retirée (cancel_resume ultérieur ⇒ false). - `execute_resume_propagates_error_without_emitting_resumed` : Resumer Err ⇒ erreur propagée ET `AgentResumed` NON publié. (c) annulation : - `cancel_resume_after_arm_returns_true_and_emits_cancelled` : cancel renvoyant true ⇒ true + bon ScheduleId passé + `AgentResumeCancelled` publié. - `cancel_resume_without_arm_is_false_no_event` : sans armement ⇒ false, Scheduler non sollicité, aucun event. - `cancel_resume_when_scheduler_already_fired_is_false_no_event` : Scheduler renvoyant false (« déjà tiré ») ⇒ false ET PAS d'`AgentResumeCancelled` (contrat anti-course important). === Nouveau fichier `tests/session_limit_t4.rs` (7 tests) — réconciliation §21.2-T4, fakes AgentSession + InputMediator === - `outcome_rate_limited_some_without_final_is_graceful` : `[RateLimited{Some(t)}]` ⇒ `Ok(TurnOutcome::RateLimited{Some(t)})` (PAS d'Err) + vérifie qu'un RateLimited ne marque PAS Idle. - `outcome_rate_limited_none_without_final_is_graceful` : `[RateLimited{None}]` ⇒ `Ok(RateLimited{None})`. - `outcome_rate_limited_then_final_is_completed` : `[.., RateLimited, Final]` ⇒ `Completed(contenu)` (Final l'emporte) + Idle marqué. - `outcome_truncated_stream_without_final_or_ratelimit_is_io_error` : `[TextDelta]` seul ⇒ `Err(Io)` INCHANGÉ (non-régression critique du flux tronqué). - non-régression signatures historiques : `drain_with_readiness_rate_limited_is_io_error`, `send_blocking_rate_limited_is_io_error` (limite ⇒ Io), `drain_with_readiness_nominal_still_completes` (cas nominal Final intact). Aucun bug détecté : le code DevBackend est conforme au cadrage §21.5 sur tous les axes (ordre des events, clamp via plan_resume, dédoublonnage silencieux, prompt constant, propagation d'erreur sans event, contrat anti-course du cancel). Je n'ai modifié AUCUNE ligne de prod (uniquement ajouté 2 fichiers de tests). Feature LS4 validée côté tests application. Prêt pour la suite du cycle (Git : merge éventuel). -- **Prompt:** Tests LS5 — parser regex niveau 2 (infra) : `RateLimitParser` (crates/infrastructure/src/ratelimit/mod.rs) + le module d'heure partagé `timeparse` (crates/infrastructure/src/timeparse.rs). Branche feature/agent-session-limits. DevBackend a livré (cargo build -p infrastructure OK, 0 warning ; tests LS2 toujours verts). Tout est pur (now_ms injecté), testable sans I/O. Écris et exécute les tests dans le style existant. RateLimitParser (new + detect + applies) : - `new` sur regex INVALIDE → None (jamais de panique) ; - pattern qui matche SANS reset_capture → `Some(SessionLimit{ resets_at_ms: None, source: Pattern, detected_at_ms == now_ms })` ; - capture nommée `(?P...)` + time_format ABSOLU : `epoch_s` (secondes→×1000), `epoch_ms` (tel quel), `iso8601`/`rfc3339` (`...Z` → ms attendus) → resets_at_ms corrects ; - time_format RELATIF (`relative_s`, capture « 600 », now=T) → `Some(resets_at_ms == T + 600_000)` ; - time_format MURAL (`wall`, capture « 3pm ») : now correspondant à 10h du jour → 15h AUJOURD'HUI (même jour UTC) ; now correspondant à 16h → 15h DEMAIN (passage de minuit, +24h). Choisis des now_ms calculés proprement (epoch connu) et calcule l'attendu à la main ; - pattern NE matche PAS → detect → None ; - pattern matche mais capture absente/valeur pourrie/non parsable → `Some(SessionLimit{ resets_at_ms: None })` (détection utile sans heure) ; - vérifie que `source == RateLimitSource::Pattern` dans tous les cas détectés ; - compilation du regex faite une seule fois (à new) — au minimum vérifie que detect peut être appelé plusieurs fois sans souci. applies(profile) : - profil avec structured_adapter (structuré) → false (même s'il a un rate_limit_pattern) ; - profil PTY (sans structured_adapter) AVEC rate_limit_pattern → true ; - profil PTY SANS rate_limit_pattern → false. timeparse (fonctions réexportées) : - `days_from_civil` : 1970-01-01 → 0 ; une année bissextile (2024-02-29) cohérente ; - `parse_wall_clock` : « 3pm » → (15,0,0) ; « 15:00:30 » → (15,0,30) ; format invalide → None ; - `wall_clock_to_ms` : même jour vs lendemain selon now ; - `parse_absolute_ms` : epoch s/ms/ISO cohérents (recoupe avec LS2 si utile). - NON-RÉGRESSION : confirme que les tests LS2 (parse_reset_ms/parse_event dans session/mod.rs) restent verts après le refactor qui a déplacé les helpers vers timeparse. Exécute `cargo test -p infrastructure`. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que LS2/LS3 restent verts, et en cas d'échec rapport CLAIR (attendu vs obtenu) pour DevBackend. EN PLUS (petit nettoyage que DevBackend t'a signalé) : tes tests scheduler LS3 (crates/infrastructure/src/scheduler/mod.rs, ~lignes 253/285) déclenchent 2 warnings `irrefutable if let` car ScheduledTask est mono-variante aujourd'hui. Corrige ces 2 warnings dans le code de TEST (ex. déstructuration directe au lieu de `if let`) pour garder la suite sans warning — c'est du test, tu peux le modifier. Ne touche à aucun code de prod ; signale tout vrai bug sans le corriger. - **Response:** ✅ LS5 — Tests parser regex niveau 2 (`RateLimitParser`) + module `timeparse` : TOUT VERT, 0 warning, zéro régression. Commande : `cargo test -p infrastructure` Résultat lib : **221 passed; 0 failed; 0 ignored** (195 antérieurs + **26 nouveaux LS5**). Tous les fichiers d'intégration + doctests verts. **Build de test : 0 warning** (vérifié `--no-run | grep -c warning` = 0). Confirmation explicite NON-RÉGRESSION : - **LS2** (`session::tests::parse_reset_ms*` / `parse_event*` / `send_emits_rate*`) : tous verts APRÈS le refactor qui a déplacé les helpers vers `timeparse` ✓ - **LS3** (`scheduler::tests::*`) : 7/7 verts ✓ (+ les 2 warnings `irrefutable if let` corrigés — voir plus bas) === `ratelimit/mod.rs` — 15 tests (in-file `#[cfg(test)] mod tests`) === new + detect : - `new_returns_none_on_invalid_regex` : regex invalide `"rate limit ("` ⇒ None (jamais de panique). - `detect_returns_none_when_pattern_does_not_match` : pas de match ⇒ None. - `detect_match_without_reset_capture_has_no_time` : match sans reset_capture ⇒ `SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}`. - formats ABSOLUS : `detect_epoch_seconds_format` (×1000), `detect_epoch_millis_format` (tel quel), `detect_iso8601_format` (`2023-11-14T22:13:20Z`→1_700_000_000_000). - format RELATIF : `detect_relative_seconds_format_uses_now` (capture « 600 », now=T ⇒ T+600_000). - format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : `detect_wall_clock_same_day_when_future` (now=10h, « 3pm » ⇒ 15h même jour) ; `detect_wall_clock_next_day_when_past` (now=16h ⇒ 15h DEMAIN, +24h). - capture inexploitable ⇒ détection sans heure : `detect_match_with_missing_capture_group_has_no_time`, `detect_match_with_unparsable_value_has_no_time` (⇒ `resets_at_ms:None`). - `detect_can_be_called_multiple_times` : regex compilé une seule fois, detect appelé 3× sans souci. - `source == RateLimitSource::Pattern` asserté dans tous les cas détectés. applies(profile) : - `applies_false_for_structured_profile_even_with_pattern` (structuré ⇒ false, niveau 1 prend la main) ; - `applies_true_for_pty_profile_with_pattern` (PTY + pattern ⇒ true) ; - `applies_false_for_pty_profile_without_pattern` (PTY sans pattern ⇒ false). === `timeparse.rs` — 11 tests === - `days_from_civil_epoch_is_zero` (1970-01-01→0) ; `days_from_civil_leap_day_2024` (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars). - `parse_wall_clock_variants` (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; `parse_wall_clock_rejects_invalid` (texte, 25:00, 10:75, trop de composantes ⇒ None). - `wall_clock_to_ms_*` : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent). - `parse_absolute_ms_*` : epoch s/ms, ISO8601, garbage→None (recoupe LS2). - `parse_rfc3339_offsets_convert_to_utc` : +01:00 et −01:00 ⇒ même instant UTC. === Nettoyage LS3 demandé === Les 2 warnings `irrefutable if let` (scheduler/mod.rs ~253/285, `ScheduledTask` mono-variante) sont corrigés : remplacés par une **déstructuration directe** `let ScheduledTask::ResumeAgent { conversation_id, .. } = task;` (code de TEST uniquement). Suite désormais 0 warning. Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection `applies`, factorisation `timeparse` sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3). Feature LS5 validée côté tests infra. Prêt pour la suite du cycle. - **Prompt:** Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche `feature/agent-session-limits`. DevFrontend vient d'implémenter (typecheck `tsc --noEmit` propre, 39 tests agents existants toujours verts, non commité). Périmètre à couvrir (frontend/) : 1. `src/features/agents/useAgents.ts` — nouvel état `limitByAgent: Record` (`{ limitedUntil?, resumeFireAt?, suspected? }`) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement `onDomainEvent`. À tester (via le mock gateway qui émet des events) : - `agentRateLimited` {agentId, resetsAtMs} → entrée `{ limitedUntil: resetsAtMs, suspected: false }`. - `agentResumeScheduled` {agentId, fireAtMs} → arme `resumeFireAt`. - `agentResumeCancelled` {agentId} → retire `resumeFireAt`, reste limité. - `agentResumed` {agentId} → supprime l'entrée (état effacé). - `agentRateLimitSuspected` {agentId, resetsAtMs?} → `{ ..., suspected: true }`, y compris le cas SANS `resetsAtMs` (heure inconnue). - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel `input.cancelResume` (mock `cancelledResumes` / `cancelResumeResult`). - Action `cancelResume(agentId)` exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via `cancelResumeResult`). 2. `src/features/agents/AgentLimitBadge.tsx` — helpers purs exportés `formatResetTime(epochMs)` (→ HH:MM) et `formatCountdown(remainingMs)` (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand `resumeFireAt` est armé. 3. Adapter mock `src/adapters/mock/index.ts` — `MockInputGateway.cancelResume` (`cancelledResumes`, `cancelResumeResult`) : couvre via les tests du hook. Suis le style des tests existants (cf. `singletonAgent.test.tsx`, `mock.test.ts`, les tests agents). Exécute la suite (`npm test` ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien. - **Response:** QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité. ## Fichiers de test ajoutés (2) - `frontend/src/features/agents/useAgentsLimits.test.tsx` (13 tests) — état `limitByAgent` du hook via le `MockSystemGateway` qui émet les events + `MockInputGateway`. - `frontend/src/features/agents/AgentLimitBadge.test.tsx` (11 tests) — helpers purs + rendu du badge. ## Commandes exactes lancées (cwd = frontend/) 1. `npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsx` → **Test Files 2 passed (2) · Tests 24 passed (24)** 2. `npx vitest run src/features/agents/` (suite agents complète, régression) → **Test Files 4 passed (4) · Tests 63 passed (63)** (39 existants + 24 nouveaux, 0 régression) ## Couverture livrée useAgents (§21), tous via events réels : - `agentRateLimited{resetsAtMs}` → `{limitedUntil, resumeFireAt:undefined, suspected:false}` ✓ - `agentResumeScheduled{fireAtMs}` → arme `resumeFireAt` par-dessus l'état limité ✓ - `agentResumeCancelled` → retire `resumeFireAt`, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓ - `agentResumed` → entrée supprimée (`agentId in map === false`) ✓ - `agentRateLimitSuspected` AVEC et SANS `resetsAtMs` → `suspected:true`, `limitedUntil` undefined dans le cas sans heure ✓ - séquence réaliste rateLimited→scheduled→`cancelResume(action)` : retrait optimiste du countdown + agent toujours limité + `input.cancelledResumes === [agentId]` + verdict ✓ - action `cancelResume` : verdict `true` ET `false` (via `cancelResumeResult`), + appel du port même sans reprise armée ✓ - deux agents foldés indépendamment ✓ AgentLimitBadge : - `formatCountdown` : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), ≥60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") ✓ - `formatResetTime` : HH:MM sans secondes (même minute +30s = même libellé), minuit stable ✓ - rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue — reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure ✓ - bouton "Annuler la reprise" : appelle `onCancelResume` au clic quand `resumeFireAt` armé, countdown rendu, bouton désactivé si `busy` ✓ MockInputGateway.cancelResume (`cancelledResumes`/`cancelResumeResult`) : couvert via les tests du hook. ## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front) `src/adapters/mock/mock.test.ts:16` échoue (1 test) : il assert « thirteen gateways » mais le `permission` gateway porte le compte à 14. Vérifié : la gateway `permission` existe déjà dans HEAD committé (eca2ba9) — l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite `adapters/mock` 100% verte. @@ -23,4 +21,6 @@ objective: Tâche : ajouter UN test fonctionnel anti-régression de la communica - **Prompt:** Suite au rouge loopback, DevBackend a diagnostiqué une contrainte sandbox: un smoke Rust minimal `UnixListener::bind` échoue `PermissionDenied` dans `/tmp` et `/run/user/1000`, donc les tests nécessitant un vrai socket Unix ne peuvent pas passer dans cet environnement. Il a aussi exécuté avec succès les alternatives: `cargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together` => OK 9 passed, 4 filtered; `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` => OK 39 passed, 8 filtered. Merci de confirmer le verdict QA final: accepter comme vert avec réserve environnementale, ou maintenir KO; liste les commandes vertes et la réserve exacte. Ne modifie pas le code. - **Response:** Verdict QA final : vert accepté avec réserve environnementale. Je ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert. Commandes vertes déjà validées QA sur ce périmètre : ```text cargo test -p application compose_convention_file --no-fail-fast ``` Résultat : `15 passed; 0 failed; 28 filtered out`. ```text cargo test -p application ``` Résultat : `43 passed; 0 failed` pour les unit tests application, tous les tests d’intégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`. ```text cargo test -p infrastructure input ``` Résultat : `35 passed; 0 failed; 187 filtered out`. ```text cargo test -p application --test orchestrator_service ``` Résultat : `45 passed; 0 failed` avec le même warning existant `writes`. ```text cd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx ``` Résultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx ``` Résultat : `3 passed`, `24 tests passed`. Même warnings Vite. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. ```text cargo check -p app-tauri ``` Résultat : succès, `Finished dev profile`. Commandes de confirmation après diagnostic sandbox, relancées par QA : ```text cargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together ``` Résultat réel : ```text running 9 tests ... test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat réel : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Réserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding d’un vrai socket Unix loopback : - `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`. - `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`. Ces tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je n’ai modifié aucun fichier. - **Prompt:** QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work. Merci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum: - `cargo test -p application workstate` - `cargo test -p application` - `cargo check -p app-tauri` - tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Retourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire. -- **Response:** Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels. Périmètre relu : - Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`. - Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète. - Ordre manifeste conservé par itération sur `manifest.entries`. - Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis. - Snapshot live typé ajouté sans changer `live_agents()` existant. - Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`. - Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants. Commandes exécutées et résultats : ```text cargo fmt -p application -p app-tauri -- --check ``` Résultat : OK, aucune sortie. ```text cargo test -p application workstate ``` Résultat : ```text running 5 tests test workstate_attaches_live_pty_session_to_manifest_agent ... ok test workstate_includes_busy_state_from_input_mediator ... ok test workstate_attaches_live_structured_session_to_manifest_agent ... ok test workstate_ignores_live_agents_absent_from_manifest ... ok test workstate_lists_manifest_agents_idle_without_live_sessions ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Warning existant observé : ```text warning: method `writes` is never used --> crates/application/tests/orchestrator_service.rs:1642:8 ``` ```text cargo test -p application ``` Résultat : ```text running 43 tests ... test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ... Running tests/workstate.rs running 5 tests ... test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests application running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Même warning existant `CapturingFs::writes` unused. ```text cargo check -p app-tauri ``` Résultat : ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s ``` ```text cargo test -p app-tauri --test dto_agents ``` Résultat : ```text running 19 tests ... test project_work_state_dto_serialises_live_and_busy_camelcase ... ok ... test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --test list_live_agents_r0b ``` Résultat : ```text running 5 tests test no_sessions_yields_empty_list ... ok test both_kinds_live_listed_without_duplicates ... ok test pty_live_agent_is_listed ... ok test same_agent_in_both_registries_is_deduplicated ... ok test structured_live_agent_is_listed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Les 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A. ```text cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` Résultat : ```text Test Files 2 passed (2) Tests 13 passed (13) ``` Warnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. Risques restants : - Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas d’historique conversationnel, pas de lecture log/handoff. - Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem. - Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot. Aucune correction requise. Je n’ai modifié aucun fichier. \ No newline at end of file +- **Response:** Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels. Périmètre relu : - Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`. - Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète. - Ordre manifeste conservé par itération sur `manifest.entries`. - Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis. - Snapshot live typé ajouté sans changer `live_agents()` existant. - Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`. - Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants. Commandes exécutées et résultats : ```text cargo fmt -p application -p app-tauri -- --check ``` Résultat : OK, aucune sortie. ```text cargo test -p application workstate ``` Résultat : ```text running 5 tests test workstate_attaches_live_pty_session_to_manifest_agent ... ok test workstate_includes_busy_state_from_input_mediator ... ok test workstate_attaches_live_structured_session_to_manifest_agent ... ok test workstate_ignores_live_agents_absent_from_manifest ... ok test workstate_lists_manifest_agents_idle_without_live_sessions ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Warning existant observé : ```text warning: method `writes` is never used --> crates/application/tests/orchestrator_service.rs:1642:8 ``` ```text cargo test -p application ``` Résultat : ```text running 43 tests ... test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ... Running tests/workstate.rs running 5 tests ... test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests application running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Même warning existant `CapturingFs::writes` unused. ```text cargo check -p app-tauri ``` Résultat : ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s ``` ```text cargo test -p app-tauri --test dto_agents ``` Résultat : ```text running 19 tests ... test project_work_state_dto_serialises_live_and_busy_camelcase ... ok ... test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --test list_live_agents_r0b ``` Résultat : ```text running 5 tests test no_sessions_yields_empty_list ... ok test both_kinds_live_listed_without_duplicates ... ok test pty_live_agent_is_listed ... ok test same_agent_in_both_registries_is_deduplicated ... ok test structured_live_agent_is_listed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Les 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A. ```text cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` Résultat : ```text Test Files 2 passed (2) Tests 13 passed (13) ``` Warnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. Risques restants : - Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas d’historique conversationnel, pas de lecture log/handoff. - Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem. - Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot. Aucune correction requise. Je n’ai modifié aucun fichier. +- **Prompt:** Valide le Lot B complet `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`. Périmètre à vérifier: - Backend/Tauri: port read-only `AgentQueueSnapshot`, snapshots FIFO d'`InMemoryMailbox`, `GetProjectWorkState.agents[].tickets`, DTO camelCase, wiring `AppState`. - Frontend: types `tickets`, mock normalisé, panneau Work affiche tickets FIFO inProgress/queued, refresh sur `delegationReady`. - Les tickets human et agent sont inclus; l'UI ne doit pas les présenter tous comme délégations agent. Commandes minimales demandées (adapte si le repo impose un cwd): - `cargo fmt --all -- --check` - `cargo test -p infrastructure mailbox --lib` - `cargo test -p application --test workstate` - `cargo test -p app-tauri --test dto_agents` - `cargo check -p app-tauri` - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Tu peux ajouter des commandes pertinentes, mais ne committe pas et ne modifie pas les fichiers applicatifs sauf tests QA indispensables. Ignore le dirty runtime `.ideai/*` s'il est seulement live-session. Retour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, donne la sortie d'erreur exploitable. +- **Response:** Verdict: VERT pour le Lot B `workstate delegation/queue snapshot` sur `feature/workstate-delegation-queue`. Je n’ai modifié aucun fichier. Dirty ignoré côté runtime `.ideai/*`; les fichiers applicatifs/test du lot étaient déjà modifiés dans le worktree. Commandes exécutées et résultats réels: 1. `cargo fmt --all -- --check` - Exit 0, aucune sortie. 2. `cargo test -p infrastructure mailbox --lib` - Exit 0. - `running 13 tests` - `test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 215 filtered out; finished in 0.00s` - Couvre notamment `snapshot_preserves_fifo_order_and_positions`, `snapshot_carries_ticket_metadata`, `snapshot_is_read_only`, updates après cancel/resolve. 3. `cargo test -p application --test workstate` - Exit 0. - `running 12 tests` - `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `workstate_lists_two_tickets_in_fifo_order`, `workstate_marks_busy_head_in_progress_and_rest_queued`, `workstate_marks_all_queued_when_agent_idle`, `workstate_maps_human_and_agent_ticket_sources`, queue absente/agent hors manifest. 4. `cargo test -p app-tauri --test dto_agents` - Exit 0. - `running 20 tests` - `test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `project_work_state_dto_serialises_tickets_camelcase`: `tickets`, `inProgress`, source `agent.agentId`, source `human`, absence de snake_case. 5. `cargo check -p app-tauri` - Exit 0. - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.59s` 6. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Exit 0. - Warnings Vite non bloquants: options `esbuild` dépréciées/ignorées au profit de `oxc`. - `Test Files 2 passed (2)` - `Tests 17 passed (17)` - `Duration 2.02s` 7. `cd frontend && npx tsc --noEmit` - Exit 0, aucune sortie. 8. Commande QA ajoutée, car `crates/domain/src/mailbox.rs` est touché par le lot: `cargo test -p domain mailbox --lib` - Exit 0. - `running 6 tests` - `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 170 filtered out; finished in 0.00s` Vérifications de périmètre: - Backend/Tauri: - Port read-only présent: `domain::mailbox::AgentQueueSnapshot` dans `crates/domain/src/mailbox.rs:69`. - `InMemoryMailbox` implémente la snapshot read-only dans `crates/infrastructure/src/mailbox/mod.rs:166`; tests FIFO/métadonnées/read-only verts. - `GetProjectWorkState` consomme `Arc` dans `crates/application/src/workstate/mod.rs:130` et mappe `agents[].tickets`; tests application verts. - DTO `tickets` camelCase dans `crates/app-tauri/src/dto.rs:1602` et test camelCase dédié vert. - Wiring `AppState`: même `InMemoryMailbox` exposé comme `AgentMailbox` et `AgentQueueSnapshot` dans `crates/app-tauri/src/state.rs:994-999`, puis injecté dans `GetProjectWorkState::new` vers `state.rs:1034-1038`. - Frontend: - Types `tickets` présents dans `frontend/src/domain/index.ts:153-179`. - Mock normalisé avec fallback `tickets: []` dans `frontend/src/adapters/mock/index.ts:1670-1684`. - Panneau Work trie les tickets par `position` avant affichage dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:65`. - UI distingue source humaine et agent: `Human` pour `source.kind === "human"`, sinon `label (agent-id-prefix)` dans `ProjectWorkStatePanel.tsx:21-27`; test `renders an in-progress human ticket` et test FIFO agent verts. - Refresh sur `delegationReady` couvert par `frontend/src/features/workstate/workstate.test.tsx:223` et vert. Anomalies: aucune anomalie bloquante détectée. Les seuls messages observés sont les warnings Vite sur la config `esbuild` dépréciée, sans échec de test ni impact Lot B constaté. \ No newline at end of file diff --git a/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/log.jsonl b/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/log.jsonl index 66cc1d3..75245cd 100644 --- a/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/log.jsonl +++ b/.ideai/conversations/08336578-5b47-09d2-2f41-5cc483f101f4/log.jsonl @@ -62,3 +62,5 @@ {"id":"aab45df1-f0ac-4eca-850b-c89b688c3211","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781945801522,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA final : vert accepté avec réserve environnementale.\n\nJe ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert.\n\nCommandes vertes déjà validées QA sur ce périmètre :\n\n```text\ncargo test -p application compose_convention_file --no-fail-fast\n```\nRésultat : `15 passed; 0 failed; 28 filtered out`.\n\n```text\ncargo test -p application\n```\nRésultat : `43 passed; 0 failed` pour les unit tests application, tous les tests d’intégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`.\n\n```text\ncargo test -p infrastructure input\n```\nRésultat : `35 passed; 0 failed; 187 filtered out`.\n\n```text\ncargo test -p application --test orchestrator_service\n```\nRésultat : `45 passed; 0 failed` avec le même warning existant `writes`.\n\n```text\ncd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `3 passed`, `24 tests passed`. Même warnings Vite.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\n```text\ncargo check -p app-tauri\n```\nRésultat : succès, `Finished dev profile`.\n\nCommandes de confirmation après diagnostic sandbox, relancées par QA :\n\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat réel :\n```text\nrunning 9 tests\n...\ntest result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat réel :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\n\nRéserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding d’un vrai socket Unix loopback :\n- `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`.\n- `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`.\n\nCes tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je n’ai modifié aucun fichier."} {"id":"ea85a3be-0cf0-4ad7-b8a7-7344426844b7","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971470719,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work.\n\nMerci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum:\n- `cargo test -p application workstate`\n- `cargo test -p application`\n- `cargo check -p app-tauri`\n- tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\nRetourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire."} {"id":"39f05602-a660-4632-accc-42d4887ce1ed","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971551021,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels.\n\nPérimètre relu :\n- Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`.\n- Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète.\n- Ordre manifeste conservé par itération sur `manifest.entries`.\n- Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis.\n- Snapshot live typé ajouté sans changer `live_agents()` existant.\n- Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`.\n- Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants.\n\nCommandes exécutées et résultats :\n\n```text\ncargo fmt -p application -p app-tauri -- --check\n```\nRésultat : OK, aucune sortie.\n\n```text\ncargo test -p application workstate\n```\nRésultat :\n```text\nrunning 5 tests\ntest workstate_attaches_live_pty_session_to_manifest_agent ... ok\ntest workstate_includes_busy_state_from_input_mediator ... ok\ntest workstate_attaches_live_structured_session_to_manifest_agent ... ok\ntest workstate_ignores_live_agents_absent_from_manifest ... ok\ntest workstate_lists_manifest_agents_idle_without_live_sessions ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nWarning existant observé :\n```text\nwarning: method `writes` is never used\n --> crates/application/tests/orchestrator_service.rs:1642:8\n```\n\n```text\ncargo test -p application\n```\nRésultat :\n```text\nrunning 43 tests\n...\ntest result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n...\nRunning tests/workstate.rs\nrunning 5 tests\n...\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n\nDoc-tests application\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nMême warning existant `CapturingFs::writes` unused.\n\n```text\ncargo check -p app-tauri\n```\nRésultat :\n```text\nFinished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s\n```\n\n```text\ncargo test -p app-tauri --test dto_agents\n```\nRésultat :\n```text\nrunning 19 tests\n...\ntest project_work_state_dto_serialises_live_and_busy_camelcase ... ok\n...\ntest result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --test list_live_agents_r0b\n```\nRésultat :\n```text\nrunning 5 tests\ntest no_sessions_yields_empty_list ... ok\ntest both_kinds_live_listed_without_duplicates ... ok\ntest pty_live_agent_is_listed ... ok\ntest same_agent_in_both_registries_is_deduplicated ... ok\ntest structured_live_agent_is_listed ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\nLes 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A.\n\n```text\ncd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx\n```\nRésultat :\n```text\nTest Files 2 passed (2)\nTests 13 passed (13)\n```\nWarnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\nRisques restants :\n- Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas d’historique conversationnel, pas de lecture log/handoff.\n- Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem.\n- Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot.\n\nAucune correction requise. Je n’ai modifié aucun fichier."} +{"id":"81d3494b-a647-40ea-909d-7b4649094e81","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781974100258,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Valide le Lot B complet `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`.\n\nPérimètre à vérifier:\n- Backend/Tauri: port read-only `AgentQueueSnapshot`, snapshots FIFO d'`InMemoryMailbox`, `GetProjectWorkState.agents[].tickets`, DTO camelCase, wiring `AppState`.\n- Frontend: types `tickets`, mock normalisé, panneau Work affiche tickets FIFO inProgress/queued, refresh sur `delegationReady`.\n- Les tickets human et agent sont inclus; l'UI ne doit pas les présenter tous comme délégations agent.\n\nCommandes minimales demandées (adapte si le repo impose un cwd):\n- `cargo fmt --all -- --check`\n- `cargo test -p infrastructure mailbox --lib`\n- `cargo test -p application --test workstate`\n- `cargo test -p app-tauri --test dto_agents`\n- `cargo check -p app-tauri`\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\n\nTu peux ajouter des commandes pertinentes, mais ne committe pas et ne modifie pas les fichiers applicatifs sauf tests QA indispensables. Ignore le dirty runtime `.ideai/*` s'il est seulement live-session.\n\nRetour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, donne la sortie d'erreur exploitable."} +{"id":"b916af4a-6f94-4777-92b1-c5f89f898cde","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781974271224,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict: VERT pour le Lot B `workstate delegation/queue snapshot` sur `feature/workstate-delegation-queue`.\n\nJe n’ai modifié aucun fichier. Dirty ignoré côté runtime `.ideai/*`; les fichiers applicatifs/test du lot étaient déjà modifiés dans le worktree.\n\nCommandes exécutées et résultats réels:\n\n1. `cargo fmt --all -- --check`\n - Exit 0, aucune sortie.\n\n2. `cargo test -p infrastructure mailbox --lib`\n - Exit 0.\n - `running 13 tests`\n - `test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 215 filtered out; finished in 0.00s`\n - Couvre notamment `snapshot_preserves_fifo_order_and_positions`, `snapshot_carries_ticket_metadata`, `snapshot_is_read_only`, updates après cancel/resolve.\n\n3. `cargo test -p application --test workstate`\n - Exit 0.\n - `running 12 tests`\n - `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s`\n - Couvre `workstate_lists_two_tickets_in_fifo_order`, `workstate_marks_busy_head_in_progress_and_rest_queued`, `workstate_marks_all_queued_when_agent_idle`, `workstate_maps_human_and_agent_ticket_sources`, queue absente/agent hors manifest.\n\n4. `cargo test -p app-tauri --test dto_agents`\n - Exit 0.\n - `running 20 tests`\n - `test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s`\n - Couvre `project_work_state_dto_serialises_tickets_camelcase`: `tickets`, `inProgress`, source `agent.agentId`, source `human`, absence de snake_case.\n\n5. `cargo check -p app-tauri`\n - Exit 0.\n - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.59s`\n\n6. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Exit 0.\n - Warnings Vite non bloquants: options `esbuild` dépréciées/ignorées au profit de `oxc`.\n - `Test Files 2 passed (2)`\n - `Tests 17 passed (17)`\n - `Duration 2.02s`\n\n7. `cd frontend && npx tsc --noEmit`\n - Exit 0, aucune sortie.\n\n8. Commande QA ajoutée, car `crates/domain/src/mailbox.rs` est touché par le lot: `cargo test -p domain mailbox --lib`\n - Exit 0.\n - `running 6 tests`\n - `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 170 filtered out; finished in 0.00s`\n\nVérifications de périmètre:\n\n- Backend/Tauri:\n - Port read-only présent: `domain::mailbox::AgentQueueSnapshot` dans `crates/domain/src/mailbox.rs:69`.\n - `InMemoryMailbox` implémente la snapshot read-only dans `crates/infrastructure/src/mailbox/mod.rs:166`; tests FIFO/métadonnées/read-only verts.\n - `GetProjectWorkState` consomme `Arc` dans `crates/application/src/workstate/mod.rs:130` et mappe `agents[].tickets`; tests application verts.\n - DTO `tickets` camelCase dans `crates/app-tauri/src/dto.rs:1602` et test camelCase dédié vert.\n - Wiring `AppState`: même `InMemoryMailbox` exposé comme `AgentMailbox` et `AgentQueueSnapshot` dans `crates/app-tauri/src/state.rs:994-999`, puis injecté dans `GetProjectWorkState::new` vers `state.rs:1034-1038`.\n\n- Frontend:\n - Types `tickets` présents dans `frontend/src/domain/index.ts:153-179`.\n - Mock normalisé avec fallback `tickets: []` dans `frontend/src/adapters/mock/index.ts:1670-1684`.\n - Panneau Work trie les tickets par `position` avant affichage dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:65`.\n - UI distingue source humaine et agent: `Human` pour `source.kind === \"human\"`, sinon `label (agent-id-prefix)` dans `ProjectWorkStatePanel.tsx:21-27`; test `renders an in-progress human ticket` et test FIFO agent verts.\n - Refresh sur `delegationReady` couvert par `frontend/src/features/workstate/workstate.test.tsx:223` et vert.\n\nAnomalies: aucune anomalie bloquante détectée. Les seuls messages observés sont les warnings Vite sur la config `esbuild` dépréciée, sans échec de test ni impact Lot B constaté."} diff --git a/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/handoff.md b/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/handoff.md index 32f1443..d12cdec 100644 --- a/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/handoff.md +++ b/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/handoff.md @@ -1,5 +1,5 @@ --- -upTo: a0b9d44f-2761-4652-b2f3-28222dbc4c62 +upTo: 7d70c87a-e7b5-468a-90c7-41decf1185bc objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e --- **Objectif :** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e @@ -17,4 +17,6 @@ objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu r - **Prompt:** Deux livrables distincts sur `feature/agent-session-limits` (Git en fera 2 commits séparés — ne commit pas toi-même). ═══ LIVRABLE 1 — LS8-front : formulaire de saisie d'heure (filet humain niveau 3) ═══ Le backend expose maintenant une commande Tauri `set_resume_at(agentId: string, resetsAtMs: number) -> void` (argument nu `{ agentId, resetsAtMs }`, comme `cancel_resume`). Elle arme la MÊME reprise annulable que l'auto et réémet `agentResumeScheduled` — donc une fois appelée, ton badge bascule TOUT SEUL de « heure inconnue » vers l'état nominal « limité jusqu'à HH:MM » + compte à rebours + bouton Annuler (déjà câblés en LS7). Aucun nouvel événement à consommer. À FAIRE : 1. Port : ajoute `setResumeAt(agentId: string, resetsAtMs: number): Promise` à `InputGateway` (`src/ports/index.ts`), à côté de `cancelResume`. 2. Adapter Tauri (`src/adapters/input.ts`) : `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })`. 3. Mock (`src/adapters/mock/index.ts`) : `MockInputGateway.setResumeAt` enregistre dans un tableau (ex. `resumeArmings: { agentId, resetsAtMs }[]`) pour les tests ; suis le patron de `cancelledResumes`. 4. Hook `useAgents` : expose une action `setResumeAt(agentId, resetsAtMs)` qui délègue au port (pas de mutation optimiste nécessaire — l'event `agentResumeScheduled` rebasculera l'état). 5. UI `AgentLimitBadge.tsx` : sur l'état SUSPECTED SANS heure (`suspected === true` && pas de `limitedUntil`/`resumeFireAt`), remplace le TODO/placeholder par un petit formulaire : un input heure (type `time` ou `datetime-local`) + bouton « Programmer la reprise ». À la soumission : convertir la saisie en epoch-ms (si input `time` seul = aujourd'hui à HH:MM ; si déjà passé dans la journée, le backend clampe à maintenant ⇒ reprise immédiate, donc pas besoin de validation stricte côté front, mais documente le comportement). Appeler l'action `setResumeAt(agentId, epochMs)`. Garde un helper pur exporté pour la conversion (ex. `timeInputToEpochMs(value, now)`) pour la testabilité. Style cohérent avec le reste du badge ; désactive pendant `busy`. 6. Retire le `TODO(LS-front)` posé en LS7 (le filet est maintenant complet). ═══ LIVRABLE 2 — fix test préexistant (hygiène, commit séparé) ═══ `src/adapters/mock/mock.test.ts:16` asserte « thirteen gateways » mais il y en a 14 (la gateway `permission` existe déjà dans HEAD). Mets le compteur à jour (14, et le libellé texte si présent) pour que `npx vitest run src/adapters/mock/` repasse vert. NE change RIEN d'autre dans ce fichier ; vérifie juste que le compte réel est bien 14 avant de figer le nombre. Lance `npx vitest run src/features/agents/ src/adapters/mock/` + `tsc --noEmit` et RENDS-MOI via idea_reply : fichiers modifiés/créés par livrable, le helper de conversion exact, le résultat des deux commandes (typecheck + tests), et le nombre réel de gateways constaté. Structure pour testabilité (logique dans le hook + helpers purs). NE COMMIT PAS — Git tranche, en 2 commits séparés (LS8-front / fix-test). - **Response:** Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés). ## Résultats des commandes - `npx tsc --noEmit` : **propre** (0 erreur). - `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**. ## Nombre réel de gateways constaté : **14** Le `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés. ## Helper de conversion exact (pur, exporté, testé) ```ts // AgentLimitBadge.tsx export function timeInputToEpochMs(value: string, now: number): number | null { const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim()); if (!match) return null; const hours = Number(match[1]); const minutes = Number(match[2]); if (hours > 23 || minutes > 59) return null; const d = new Date(now); d.setHours(hours, minutes, 0, 0); return d.getTime(); } ``` Comportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée). ═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══ Fichiers modifiés : 1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise` ajouté à `InputGateway`, à côté de `cancelResume`. 2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })` (args nus, comme cancel_resume). 3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`). 4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant). 5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type="time"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**. 6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge. 7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)* ═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══ Fichier modifié : - `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `"permission"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier. ## Point de friction / note Petit ajustement non listé mais nécessaire : le `
    ` portait initialement le même `aria-label="schedule resume"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `"resume time form"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle. Le filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`). - **Prompt:** Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter. Contrat backend disponible: - commande Tauri `get_project_work_state(projectId)`. - DTO `ProjectWorkStateDto { agents }` camelCase. - agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: "pty"|"structured" }`, `busy` taggé `{ state: "idle" }` ou `{ state: "busy", ticket, sinceMs }`. Périmètre frontend strict: - Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - Ajouter port `WorkStateGateway` et adapter Tauri `workState`. - Ajouter mock gateway. - Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`. - Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh. - Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern. - Pas d'actions attach/stop/voir conversation dans ce lot. Tests attendus: - panel empty state. - agent idle/offline. - agent live + busy avec ticket court. - refresh déclenché par event si implémenté. - onglet Work visible/rendu dans ProjectsView si raisonnable. Exécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques. -- **Response:** Lot A frontend implémenté, sans commit. Fichiers modifiés/ajoutés frontend: - `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`. - `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`. - `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`. - `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock. - `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`. - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court. - `frontend/src/features/workstate/index.ts`: export feature. - `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event. - `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`. - `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu. Commandes exécutées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Résultat: OK, 2 fichiers passés, 13 tests passés. - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`. - `cd frontend && npx tsc --noEmit` - Résultat: OK, aucune erreur. Risques / limites: - Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent. - UI strictement read-only comme demandé: pas d’attach/stop/voir conversation. - Je n’ai pas lancé tout le `vitest` global, seulement le ciblé demandé. - Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés. \ No newline at end of file +- **Response:** Lot A frontend implémenté, sans commit. Fichiers modifiés/ajoutés frontend: - `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`. - `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`. - `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`. - `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock. - `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`. - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court. - `frontend/src/features/workstate/index.ts`: export feature. - `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event. - `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`. - `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu. Commandes exécutées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Résultat: OK, 2 fichiers passés, 13 tests passés. - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`. - `cd frontend && npx tsc --noEmit` - Résultat: OK, aucune erreur. Risques / limites: - Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent. - UI strictement read-only comme demandé: pas d’attach/stop/voir conversation. - Je n’ai pas lancé tout le `vitest` global, seulement le ciblé demandé. - Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés. +- **Prompt:** Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend. Contexte backend disponible: - `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase. - Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`"inProgress" | "queued"`), `source` (`{ kind: "human" } | { kind: "agent"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`. - Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement "tickets"/"inputs" ou gérer Human/Agent sans prétendre que tout est délégation agent. À faire: 1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`). 2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire. 3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO: - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant, - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent), - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace, - agent sans ticket: ne pas ajouter de bruit visuel excessif. 4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi. 5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets. Contraintes: - Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff. - Ne committe pas. - Ignore les fichiers runtime `.ideai/*` dirty. Vérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage. +- **Response:** Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué. Fichiers modifiés: - `frontend/src/domain/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/workstate.test.tsx` Choix UI: - Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket. - Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`. - Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible. - `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`. - `ticketId` court en monospace avec `title` complet. Détails techniques: - Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`. - `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`. - `useProjectWorkState` rafraîchit maintenant aussi sur l’événement domaine `delegationReady`, via l’infrastructure `SystemGateway.onDomainEvent` déjà existante. Vérifications réelles: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`. - `cd frontend && npx tsc --noEmit` : OK, aucune sortie d’erreur. Blocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé. \ No newline at end of file diff --git a/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/log.jsonl b/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/log.jsonl index 4a2bc75..3254982 100644 --- a/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/log.jsonl +++ b/.ideai/conversations/09b15ec3-ce2f-0af2-2903-ef699df9de41/log.jsonl @@ -12,3 +12,5 @@ {"id":"50c3e999-c6a3-4d8b-aef3-b6c273ed9afc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781677152871,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés).\n\n## Résultats des commandes\n- `npx tsc --noEmit` : **propre** (0 erreur).\n- `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**.\n\n## Nombre réel de gateways constaté : **14**\nLe `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés.\n\n## Helper de conversion exact (pur, exporté, testé)\n```ts\n// AgentLimitBadge.tsx\nexport function timeInputToEpochMs(value: string, now: number): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(value.trim());\n if (!match) return null;\n const hours = Number(match[1]);\n const minutes = Number(match[2]);\n if (hours > 23 || minutes > 59) return null;\n const d = new Date(now);\n d.setHours(hours, minutes, 0, 0);\n return d.getTime();\n}\n```\nComportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée).\n\n═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══\nFichiers modifiés :\n1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise` ajouté à `InputGateway`, à côté de `cancelResume`.\n2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke(\"set_resume_at\", { agentId, resetsAtMs })` (args nus, comme cancel_resume).\n3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`).\n4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant).\n5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type=\"time\"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**.\n6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge.\n7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)*\n\n═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══\nFichier modifié :\n- `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `\"permission\"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier.\n\n## Point de friction / note\nPetit ajustement non listé mais nécessaire : le `` portait initialement le même `aria-label=\"schedule resume\"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `\"resume time form\"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle.\n\nLe filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`)."} {"id":"b84fc73b-74fc-465f-a9b2-e4b79651e0f0","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971246800,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter.\n\nContrat backend disponible:\n- commande Tauri `get_project_work_state(projectId)`.\n- DTO `ProjectWorkStateDto { agents }` camelCase.\n- agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: \"pty\"|\"structured\" }`, `busy` taggé `{ state: \"idle\" }` ou `{ state: \"busy\", ticket, sinceMs }`.\n\nPérimètre frontend strict:\n- Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- Ajouter port `WorkStateGateway` et adapter Tauri `workState`.\n- Ajouter mock gateway.\n- Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`.\n- Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh.\n- Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern.\n- Pas d'actions attach/stop/voir conversation dans ce lot.\n\nTests attendus:\n- panel empty state.\n- agent idle/offline.\n- agent live + busy avec ticket court.\n- refresh déclenché par event si implémenté.\n- onglet Work visible/rendu dans ProjectsView si raisonnable.\n\nExécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."} {"id":"a0b9d44f-2761-4652-b2f3-28222dbc4c62","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971459114,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot A frontend implémenté, sans commit.\n\nFichiers modifiés/ajoutés frontend:\n- `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`.\n- `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`.\n- `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`.\n- `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock.\n- `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`.\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court.\n- `frontend/src/features/workstate/index.ts`: export feature.\n- `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event.\n- `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`.\n- `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu.\n\nCommandes exécutées:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Résultat: OK, 2 fichiers passés, 13 tests passés.\n - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`.\n- `cd frontend && npx tsc --noEmit`\n - Résultat: OK, aucune erreur.\n\nRisques / limites:\n- Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent.\n- UI strictement read-only comme demandé: pas d’attach/stop/voir conversation.\n- Je n’ai pas lancé tout le `vitest` global, seulement le ciblé demandé.\n- Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés."} +{"id":"01871912-13d6-42dd-b4df-497f0a1a9703","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781973915788,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend.\n\nContexte backend disponible:\n- `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase.\n- Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`\"inProgress\" | \"queued\"`), `source` (`{ kind: \"human\" } | { kind: \"agent\"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`.\n- Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement \"tickets\"/\"inputs\" ou gérer Human/Agent sans prétendre que tout est délégation agent.\n\nÀ faire:\n1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`).\n2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire.\n3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO:\n - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant,\n - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent),\n - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace,\n - agent sans ticket: ne pas ajouter de bruit visuel excessif.\n4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi.\n5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets.\n\nContraintes:\n- Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff.\n- Ne committe pas.\n- Ignore les fichiers runtime `.ideai/*` dirty.\n\nVérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage."} +{"id":"7d70c87a-e7b5-468a-90c7-41decf1185bc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781974090422,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué.\n\nFichiers modifiés:\n- `frontend/src/domain/index.ts`\n- `frontend/src/adapters/mock/index.ts`\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`\n- `frontend/src/features/workstate/useProjectWorkState.ts`\n- `frontend/src/features/workstate/workstate.test.tsx`\n\nChoix UI:\n- Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket.\n- Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`.\n- Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible.\n- `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`.\n- `ticketId` court en monospace avec `title` complet.\n\nDétails techniques:\n- Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`.\n- `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`.\n- `useProjectWorkState` rafraîchit maintenant aussi sur l’événement domaine `delegationReady`, via l’infrastructure `SystemGateway.onDomainEvent` déjà existante.\n\nVérifications réelles:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`.\n- `cd frontend && npx tsc --noEmit` : OK, aucune sortie d’erreur.\n\nBlocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé."} diff --git a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md index fb1bb2c..541fbc6 100644 --- a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md +++ b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md @@ -1,11 +1,9 @@ --- -upTo: 95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e +upTo: 53773ad0-5bb6-4e24-8456-d7ab17be3db0 objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est --- **Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est -- **Response:** Base remise au propre après le rebuild AppImage. Aucune action sortante, aucune suppression. ## Inspection Les modifications observées (3 affichées + 2 autres détectées) sont **100 % de l'état runtime/mémoire `.ideai/`**, zéro ligne de code source : - `.ideai/conversations/6bc594e8…/handoff.md` + `log.jsonl` : dérive de conversation. - `.ideai/layouts.json` : état de layout UI. - `.ideai/memory/MEMORY.md` : +1 ligne d'index. - `.ideai/memory/checkpoint-orchestrator-designation-appimage-build.md` (non suivi) : ton checkpoint de build. Vérifié : l'artefact `target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage` est **git-ignored (hors git)** → je n'y touche pas, comme demandé. ## Décision & action Pur runtime → **un seul commit `chore(wip)` sur `develop`**, directement (ce n'est pas une feature, pas de cycle Architect→Dev→QA requis), pour repartir d'une base propre sans rien laisser dirty ni perdre ton checkpoint : - `9c71a5b` **chore(wip): état runtime .ideai post-build AppImage 0.3.0** (conversation 6bc594e8 handoff+log, layouts, checkpoint de build + son index MEMORY.md). ## État final - Branche courante : **`develop`** @ `9c71a5b` (en avance de 9 sur `origin/develop`, non poussé). - `main` inchangé @ `29232dd`. - Worktree : **propre (0 dirty)**. `develop` est de nouveau une base saine pour le prochain chantier. Quand tu me l'annonces, je crée une `feature/* dédiée depuis develop`. Rappel : décision toujours en attente sur le ménage des branches `feature/agent-session-limits` (intégrée, supprimable), `feature/agent-skills`, `feature/agent-skill-awareness`, `fix/cold-start-delivery-race`. -- **Prompt:** Préparation du prochain chantier après fermeture de `orchestrator-designation`. Contexte: - Branche courante propre: `develop` @ `9c71a5b`, en avance de 9 sur `origin/develop`, aucun push. - `orchestrator-designation` est intégré et sa branche supprimée. - L'utilisateur a donné autonomie pour terminer les chantiers restants. Branches anciennes à statuer selon tes rappels: - `feature/agent-session-limits`: tu l'as qualifiée comme intégrée/supprimable. - `feature/agent-skills`: 1 commit non intégré. - `feature/agent-skill-awareness`: 5 commits non intégrés. - `fix/cold-start-delivery-race`: 3 commits non intégrés, probable sous-ensemble de skill-awareness. Ta mission Git: 1. Inspecte ces branches et leur relation à `develop`. 2. Prends la décision git la plus saine en autonomie: supprimer les branches déjà intégrées, conserver celles qui représentent un chantier réel, identifier les doublons/sous-ensembles. 3. Ne push pas. Pas d'action sortante. 4. Si une branche non intégrée doit être reprise comme prochain chantier, dis à Main laquelle et pourquoi. Si elle doit passer par Architect/Dev/QA avant merge, indique-le. 5. Rends compte des actions effectuées et de l'état final. Réponds via `idea_reply`. - **Response:** Ménage des branches fait + cadrage du prochain chantier. Aucune action sortante, aucune suppression risquée. ## Action exécutée - **Supprimé `feature/agent-session-limits`** (`git branch -d`, refus auto si non mergée) : 0 commit propre, entièrement intégrée dans develop. Suppression sûre. ## Analyse des 3 branches restantes (relations git vérifiées) - **`feature/agent-skill-awareness`** — base `8452333`, 5 commits propres, NON intégrée. Le chantier le plus riche et récent : `ab34363` (manifeste de skills + outil MCP `idea_skill_read`), `e93a2c1` (fix cold-start delivery), `1a10d67` (test compteur outils MCP 11→12), `566bff4` (brief « capacités IdeA »), `5be8987` (runtime). **À conserver.** - **`feature/agent-skills`** — base `9736c42`, 1 commit `ef101db` (skills domain/usecases/FS store, L12), NON intégrée, **divergente** de skill-awareness (pas ancêtre). ⚠️ Elle touche **les mêmes fichiers** que skill-awareness (`domain/src/skill.rs`, `application/src/skill/*`, `infrastructure/src/store/skill.rs`) → ce sont très probablement **deux itérations concurrentes de la même couche skill**, pas des compléments. **Conservée**, mais l'arbitrage « laquelle fait foi » est une décision d'**architecture, pas git**. - **`fix/cold-start-delivery-race`** — base `6969dc7` (ligne main : elle traîne les commits de release 0.1.0/0.2.0). Son **seul contenu réel** est `9590eac` (fix cold-start), dont le **jumeau logique `e93a2c1` est déjà dans skill-awareness** (message identique). → **doublon superseded**. `git cherry` confirme qu'aucun de ces patchs n'est encore dans develop. ## Décisions / recommandations 1. **Prochain chantier = `feature/agent-skill-awareness`.** Pourquoi : c'est le travail le plus avancé et non intégré, et il englobe le fix cold-start de la branche `fix/cold-start-delivery-race`. 2. **Il DOIT passer par le cycle avant tout merge**, dans cet ordre : - **Architect d'abord** : trancher le chevauchement skill-awareness ⟷ `feature/agent-skills` (couche skill implémentée sur les mêmes fichiers). Décider qui est la source de vérité (probablement skill-awareness subsume agent-skills) avant de figer quoi que ce soit. - **Dev/QA** : revalider vert, en particulier `1a10d67` (compteur d'outils MCP, qui a déjà bougé 11→12 puis 12→13 dans state.rs côté develop) et le fix cold-start. 3. ⚠️ **Le rebase de skill-awareness sur develop VA conflicter** : la branche modifie `crates/app-tauri/src/state.rs` et `crates/infrastructure/src/input/mod.rs`, tous deux lourdement réécrits par orchestrator-designation déjà mergé dans develop. Ce n'est **pas un rebase aveugle** : je le ferai avec l'appui de Dev pour résoudre les conflits, puis QA revalide avant mon merge `--no-ff`. 4. **`fix/cold-start-delivery-race` : je recommande sa suppression** (force `-D`, car non mergée), son contenu unique étant préservé dans skill-awareness et le reste n'étant que des commits de release déjà sur main. Je ne l'ai PAS forcée en autonomie (force-delete = action plus difficile à annuler). **Donne-moi le feu vert et je la supprime**, idéalement une fois skill-awareness intégré dans develop. ## État final - Branche courante : **`develop`** @ `9c71a5b` (+9 vs origin, non poussé). - Branches : `develop`, `main` (@ `29232dd`), `feature/agent-skill-awareness`, `feature/agent-skills`, `fix/cold-start-delivery-race`. - Worktree : **propre**. Dis-moi : (a) je lance le prochain chantier sur skill-awareness — confie d'abord à Architect l'arbitrage du chevauchement avec agent-skills ; (b) feu vert ou non pour supprimer `fix/cold-start-delivery-race`. - **Prompt:** Main reprend le cycle après cadrage Architect. État connu: repo `/home/anthony/Documents/Projects/IdeA`, branche actuelle `develop`, `develop...origin/develop [ahead 9]`, dirty: `.ideai/conversations/.../handoff.md`, `.ideai/conversations/.../log.jsonl`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, nouveau `.ideai/memory/checkpoint-blocked-until-appimage-030-restart.md`. Architect recommande de ne pas merger la vieille branche `feature/agent-skill-awareness`, mais de créer une branche neuve depuis `develop`, par ex. `feature/agent-skill-awareness-v2`. Merci de décider et effectuer uniquement les actions Git/locales nécessaires: gestion de branche, éventuellement commit/ignore/stash des fichiers runtime si tu le juges nécessaire selon ton rôle. Pas de push. Retourne: branche active, actions effectuées, état `git status --short --branch`, et consignes pour DevBackend/DevFrontend/QA. - **Response:** Décision Git appliquée (local uniquement, aucun push). **Branche active : `feature/agent-skill-awareness-v2`** (créée depuis `develop`, working tree propre). **Actions effectuées :** 1. Les 7 fichiers dirty étaient tous de l'état runtime `.ideai/` (conversations handoff+log, layouts.json, MEMORY.md, nouveau checkpoint-blocked-until-appimage-030-restart.md). Conformément à mon rôle (séparer code de feature et état runtime), je les ai committés sur `develop` : - `c... ` chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint blocage AppImage 0.3.0) 2. Création de la branche neuve `feature/agent-skill-awareness-v2` depuis `develop` (tip = ce commit). J'ai suivi la reco Architect : la vieille `feature/agent-skill-awareness` (5be8987) n'est PAS mergée et reste en place comme archive — repart d'une base develop propre, pas de dette d'historique divergent (develop avait 7 commits d'avance / la feature 5 d'écart). **`git status --short --branch` :** ``` ## feature/agent-skill-awareness-v2 ``` (working tree propre) **Contexte branches :** - `develop` : +9 puis +1 (le commit runtime) vs origin → désormais ahead 10, non poussé. - `feature/agent-skill-awareness` : intacte (5be8987), abandonnée, conservée comme archive. - `feature/agent-skill-awareness-v2` : NOUVELLE base de travail. **Consignes pour DevBackend / DevFrontend / QA :** - Travaillez tous sur **`feature/agent-skill-awareness-v2`**. Ne touchez pas à `develop` ni à la vieille `feature/agent-skill-awareness`. - Ne committez pas vous-mêmes : signalez-moi (via Main) quand l'implémentation est prête, je fais les commits atomiques (code feature séparé de l'état runtime `.ideai/`). - Rappel checkpoint mémoire : un blocage de validation e2e attendait un restart sur AppImage 0.3.0 — vérifier que l'AppImage qui tourne est bien rebuildée avant de conclure une validation (le binaire qui tourne = AppImage, pas les sources). - Quand QA est vert avec sortie réelle, revenez vers moi : je rebase si besoin puis merge `--no-ff` v2 → develop. @@ -23,4 +21,6 @@ objective: Nouvelle feature cadrée par Architect, prête à démarrer le dével - **Response:** État repo vérifié et stratégie locale appliquée — en tenant compte du travail partiel du ticket précédent. ## Constat (travail antérieur pris en compte) - La branche **`feature/conversation-ux-live-state`** existait déjà (créée au ticket précédent depuis `develop`), j'étais dessus. Pas besoin de la recréer. - Elle était à jour sur `develop` (tip commun `338051e`, develop ancêtre). - Seul dirty : 1 conversation runtime live (`.ideai/conversations/6bc594e8…` handoff + log.jsonl), append-only. ## Actions 1. Runtime live figé **sur `develop`** (sa place : baseline runtime partagée), sans perte : - `6cfa0b0` — chore(wip): état runtime .ideai (flux conversation live). 2. **`feature/conversation-ux-live-state` réalignée** en fast-forward sur `develop` → la feature reste **propre, sans commit runtime parasite**, prête à recevoir le code. ## État final - **Branche courante : `feature/conversation-ux-live-state`** (tip `6cfa0b0`, = `develop`). - Working tree **propre**. - `develop` ahead origin/develop de **17** (local only). Aucune branche obsolète supprimée. **Aucun push.** La branche est prête : DevBackend/DevFrontend peuvent implémenter le chantier « UX conversations/délégations + read-model live-state minimal ». Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge. - **Prompt:** QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet. Changements code attendus: Backend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests. Frontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests. Runtime dirty `.ideai/*` aussi présent suite aux conversations live. Stratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status. - **Response:** Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée. ## Commits (sur la feature) **1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)** - Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs` **2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)** - Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx) **3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)** - Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6) ## Exclus / hors commit Rien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code. ## Décision merge : MERGÉ Lot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct). - Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54. ## État final - **Branche courante : `develop`** (tip `3bfb932`). - `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme). - Working tree **propre**. - Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`). - Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages. Dis-moi quand le Lot B est cadré : je prépare la branche. -- **Prompt:** Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état. \ No newline at end of file +- **Prompt:** Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état. +- **Response:** Repo prêt pour le Lot B « workstate delegation/queue snapshot ». ## Actions 1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte : - `7453181` — chore(wip): état runtime .ideai (flux conversation live). 2. Nouvelle branche feature créée depuis `develop` propre : - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime). ## État final - **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`). - Base : `develop`, historique linéaire, working tree **propre**. - `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.** DevBackend/DevFrontend peuvent implémenter le Lot B. Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge. +- **Prompt:** Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte: - Branche active attendue: `feature/workstate-delegation-queue`. - QA vert: - `cargo fmt --all -- --check` OK - `cargo test -p infrastructure mailbox --lib` OK (13 passed) - `cargo test -p application --test workstate` OK (12 passed) - `cargo test -p app-tauri --test dto_agents` OK (20 passed) - `cargo check -p app-tauri` OK - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests) - `cd frontend && npx tsc --noEmit` OK - `cargo test -p domain mailbox --lib` OK (6 passed) - Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`. - Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code. Décide: 1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire), 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final. \ No newline at end of file diff --git a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl index cb83648..27b5763 100644 --- a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl +++ b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl @@ -47,3 +47,5 @@ {"id":"93ed87f6-8dc6-4ff8-85d9-81aa30d30bed","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971560322,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet.\n\nChangements code attendus:\nBackend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests.\nFrontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests.\nRuntime dirty `.ideai/*` aussi présent suite aux conversations live.\n\nStratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status."} {"id":"d93a5849-41e6-4249-9dbf-9691c760c64b","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971634198,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée.\n\n## Commits (sur la feature)\n**1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)**\n- Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs`\n\n**2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)**\n- Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx)\n\n**3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)**\n- Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6)\n\n## Exclus / hors commit\nRien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code.\n\n## Décision merge : MERGÉ\nLot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct).\n- Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54.\n\n## État final\n- **Branche courante : `develop`** (tip `3bfb932`).\n- `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme).\n- Working tree **propre**.\n- Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`).\n- Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages.\n\nDis-moi quand le Lot B est cadré : je prépare la branche."} {"id":"95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971838757,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état."} +{"id":"9654549c-fd9d-4661-8dc2-e6d2a78eab7a","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971855209,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Repo prêt pour le Lot B « workstate delegation/queue snapshot ».\n\n## Actions\n1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte :\n - `7453181` — chore(wip): état runtime .ideai (flux conversation live).\n2. Nouvelle branche feature créée depuis `develop` propre :\n - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime).\n\n## État final\n- **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`).\n- Base : `develop`, historique linéaire, working tree **propre**.\n- `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.**\n\nDevBackend/DevFrontend peuvent implémenter le Lot B. Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge."} +{"id":"53773ad0-5bb6-4e24-8456-d7ab17be3db0","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974288762,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push.\n\nContexte:\n- Branche active attendue: `feature/workstate-delegation-queue`.\n- QA vert:\n - `cargo fmt --all -- --check` OK\n - `cargo test -p infrastructure mailbox --lib` OK (13 passed)\n - `cargo test -p application --test workstate` OK (12 passed)\n - `cargo test -p app-tauri --test dto_agents` OK (20 passed)\n - `cargo check -p app-tauri` OK\n - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests)\n - `cd frontend && npx tsc --noEmit` OK\n - `cargo test -p domain mailbox --lib` OK (6 passed)\n- Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`.\n- Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`.\n- Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code.\n\nDécide:\n1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire),\n2. merge local éventuel vers `develop`,\n3. état final.\n\nNe push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final."} diff --git a/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/handoff.md b/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/handoff.md index 182f528..8667486 100644 --- a/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/handoff.md +++ b/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/handoff.md @@ -1,12 +1,9 @@ --- -upTo: 5d78a64e-ce73-4fca-b62d-7af302dbac90 +upTo: b0d07b39-565c-4d30-a9ae-0c3721338aeb objective: IMPLÉMENTATION (code de production uniquement — PAS les nouveaux tests, c'est QA qui les écrit ; mais ne casse aucun test existant). Cadrage validé par l'Architect. Objectif : permettre la délégation --- **Objectif :** IMPLÉMENTATION (code de production uniquement — PAS les nouveaux tests, c'est QA qui les écrit ; mais ne casse aucun test existant). Cadrage validé par l'Architect. Objectif : permettre la délégation -- **Prompt:** LS5 — niveau 2 (repli déclaratif) côté INFRA : le parser regex qui détecte une limite de session dans la sortie texte d'un agent PTY/TUI sans adapter structuré. Branche feature/agent-session-limits (LS1→LS4 committés). Respecte ARCHITECTURE.md §21 (T2 notamment). Rappel T2 : le DOMAINE ne porte que la DONNÉE du motif (`domain::profile::RateLimitPattern { pattern, reset_capture, time_format }`, déjà livré en LS1) ; le MOTEUR regex + le parsing d'heure vivent en INFRA. C'est ici qu'on ajoute la dépendance `regex` — UNIQUEMENT au Cargo.toml du crate `infrastructure`, jamais au domaine. À faire : 1. crates/infrastructure/Cargo.toml — ajouter la dépendance `regex` (version cohérente avec l'écosystème du workspace ; regarde Cargo.lock / les versions déjà présentes pour t'aligner). 2. NOUVEAU module crates/infrastructure/src/ratelimit/ (déclaré dans lib.rs) — un `RateLimitParser` (nom à confirmer selon les conventions) qui, à partir d'un `&RateLimitPattern` et d'un fragment de sortie texte (+ l'heure courante `now_ms` injectée, car contrairement à LS2 on PEUT avoir besoin de résoudre une heure murale/relative), produit un `Option` (ou `Option resets_at_ms` que l'appelant emballe — choisis la forme la plus propre et cohérente avec la façon dont LS4 consomme la détection). Comportement : - Compiler le `pattern` regex. Compilation invalide ⇒ pas de détection (None), JAMAIS de panique ni d'erreur fatale (un profil mal configuré par l'utilisateur ne doit pas planter IdeA — robustesse « solide même pour un novice »). Idéalement, compiler paresseusement/une seule fois si tu peux mettre en cache, mais sans sur-ingénierie. - Si le pattern matche le texte ⇒ limite DÉTECTÉE. Si `reset_capture` est renseigné, extraire le groupe de capture (nommé de préférence, ex. (?P...)) et le parser en époche-ms selon `time_format` : * Réutilise le savoir de parsing d'heure que tu as déjà écrit en LS2 (parse_reset_ms / parseur ISO maison) si pertinent et factorisable proprement — sinon garde-le isolé ici. NE duplique pas bêtement : si une fonction de LS2 est réutilisable, expose-la/partage-la proprement (module commun infra) plutôt que copier-coller. * `time_format` guide l'interprétation : heure ABSOLUE (epoch/ISO) → directement en ms ; heure MURALE LOCALE (ex. « resets at 3pm ») → convertir avec now_ms comme référence (aujourd'hui à 15h, et si c'est déjà passé dans la journée → demain ; documente la règle de passage de minuit — spike §21.10-2) ; DÉLAI RELATIF (ex. « retry after 600s ») → now_ms + delta. - Si le pattern matche mais qu'aucune heure exploitable n'en sort ⇒ limite détectée AVEC resets_at_ms = None (⇒ filet humain en aval). C'est volontaire : détecter sans heure reste utile. 3. Garde le parsing PUR et isolé (le moteur regex est un détail, la logique d'extraction/conversion d'heure est testable sans I/O). L'intégration de ce parser dans le flux PTY (brancher la sortie terminal d'un agent non-structuré vers ce parser, puis vers SessionLimitService::on_rate_limited) relève du câblage — précise si tu la fais ici proprement via un port déjà existant, ou si tu la laisses à LS7 ; ne bricole pas un branchement PTY ad hoc. Attention dédoublonnage (§21.10-4) : si un agent a À LA FOIS un adapter structuré (niveau 1) ET un rate_limit_pattern (niveau 2), il ne faut pas détecter deux fois la même limite. Documente comment on évite ça (probablement : niveau 2 ne s'applique QU'aux agents sans adapter structuré — précise la règle de sélection). Vérifie `cargo build -p infrastructure`. Rends : fichiers créés/modifiés, la dépendance regex ajoutée (version), la signature publique du parser, comment tu réutilises (ou non) le parsing d'heure de LS2, la règle anti-double-détection niveau1/niveau2, et ce que tu laisses à LS7 pour le câblage PTY. Garde tout testable par QA (pattern qui matche avec/sans capture, heure absolue/murale/relative, regex invalide → None sans panique, passage de minuit). -- **Response:** LS5 (niveau 2 déclaratif, infra) terminé. `cargo build -p infrastructure` = **OK, 0 warning**. Les 5 tests LS2 restent **verts** après refactor (DRY du parsing d'heure). ## Fichiers **Créés** - `crates/infrastructure/src/ratelimit/mod.rs` — `RateLimitParser` + règle `applies`. - `crates/infrastructure/src/timeparse.rs` — parsing d'heure **pur partagé** (factorisé depuis LS2). **Modifiés** - `crates/infrastructure/Cargo.toml` — dépendance `regex`. - `crates/infrastructure/src/session/claude.rs` — délègue le parsing d'heure à `crate::timeparse` (plus de duplication). - `crates/infrastructure/src/lib.rs` — `pub mod ratelimit; pub mod timeparse;` + `pub use ratelimit::RateLimitParser;`. ## Dépendance ajoutée `regex = "1"` **au seul `Cargo.toml` d'infrastructure** (jamais au domaine, T2). Résout en **1.12.3**, déjà présente transitivement dans `Cargo.lock` (zéro nouvelle version dans l'arbre ; tire `aho-corasick`/`regex-automata`/`regex-syntax` déjà verrouillés). ## Signature publique du parser ```rust // Détecteur (motif compilé UNE fois à la construction). pub struct RateLimitParser { /* regex, reset_capture, time_format */ } impl RateLimitParser { pub fn new(pattern: &RateLimitPattern) -> Option; // None si regex invalide pub fn detect(&self, text: &str, now_ms: i64) -> Option; } // Règle de sélection anti-double-détection (§21.10-4). pub fn applies(profile: &AgentProfile) -> bool; ``` **Forme de retour : `Option`** (et non `Option`), car il faut 3 états : `None` = pas de limite ; `Some(SessionLimit{resets_at_ms: Some})` = limite + heure ; `Some(SessionLimit{resets_at_ms: None})` = **limite détectée sans heure** (filet humain) — un `Option` ne distingue pas les 2 derniers. Le `SessionLimit` est rempli avec `source = RateLimitSource::Pattern`, `detected_at_ms = now_ms`. **Robustesse** : `new` renvoie `None` sur regex invalide (profil pourri ⇒ pas de détecteur, **jamais de panique**). Compilation **une seule fois** (cache dans la struct), pas par fragment. **Stratégies d'heure** (déduites une fois du `time_format`, enum interne `ResetTimeFormat`) : `Auto` (None/inconnu → best-effort absolu epoch/ISO) ; `epoch_s|epoch_seconds|unix_s` ; `epoch_ms|epoch_millis|unix_ms` ; `iso8601|rfc3339|iso` ; `relative_s|relative_seconds|duration_s|retry_after_s` (→ `now+delta`) ; `relative_ms|relative_millis` ; `wall|wall_clock|local|hh:mm` (heure murale « 3pm »/« 15:00 »). Capture par **groupe nommé** en priorité (`(?P…)`), repli sur index décimal. Match sans capture exploitable ⇒ `resets_at_ms: None` (détection utile sans heure). ## Réutilisation du parsing d'heure de LS2 (pas de copier-coller) J'ai **factorisé** les helpers génériques de LS2 (qui vivaient en privé dans `claude.rs`) dans un nouveau module partagé `crate::timeparse` : `int_epoch_to_ms`/`float_epoch_to_ms`, `parse_rfc3339_to_ms` (+ `split_tz`/`split_seconds_frac`), `days_from_civil` (algo Howard Hinnant), `parse_absolute_ms`, `EPOCH_MS_THRESHOLD`. `claude.rs::value_to_epoch_ms` **délègue** maintenant à `timeparse` (seule l'extraction depuis `serde_json::Value` reste côté Claude). Le niveau 2 réutilise les **mêmes** fonctions. **Ajouts** propres à LS5 dans `timeparse` : `wall_clock_to_ms` (heure murale + passage de minuit) et `parse_wall_clock` (« 3pm »/« 15:00:30 » → h,m,s). Tout reste **pur** (`now_ms` injecté), testable sans I/O. **Passage de minuit (spike §21.10-2)** : `wall_clock_to_ms` calcule « aujourd'hui à h:m:s » sur la **journée UTC** contenant `now_ms` ; si c'est ≤ now ⇒ **+24 h** (demain). Limite assumée et documentée : faute de base de fuseaux (dépendance-zéro), la journée de référence est UTC ; le rollover borne l'erreur à « jamais dans le passé ». Un offset de fuseau explicite (champ futur) lèverait la limite. ## Règle anti-double-détection niveau 1 / niveau 2 (§21.10-4) Source **unique** : `ratelimit::applies(profile) == profile.structured_adapter.is_none() && profile.rate_limit_pattern.is_some()`. Le niveau 2 ne s'applique **qu'aux agents sans adapter structuré** (les structurés détectent par le niveau 1 = flux machine, LS2). Le câblage (LS7) **doit** consulter `applies` avant d'instancier un `RateLimitParser` ⇒ jamais deux détections du même épisode. Documenté dans le module. ## Laissé à LS7 (câblage PTY — pas bricolé ici) Je **n'ai pas** branché la sortie PTY → parser → service (intégration runtime, intriquée avec le watcher de prompt / `MediatedInbox` qui observe déjà la sortie PTY, et l'accès au `SessionLimitService`). LS7 devra : 1. à l'activation d'un agent où `ratelimit::applies(profile)` ⇒ construire `RateLimitParser::new(profile.rate_limit_pattern)` ; 2. **armer ce parser sur le même flux de sortie PTY** que le watcher de prompt (réutiliser l'armement existant, §21.6) ; 3. sur `detect(fragment, clock.now_millis()) == Some(limit)` ⇒ appeler `SessionLimitService` pour planifier la reprise. ⚠️ **Point d'attention pour LS7/LS4** : `SessionLimitService::on_rate_limited(...)` (LS4) reconstruit en interne un `SessionLimit` avec `source = Structured`. Le parser niveau 2 produit déjà un `SessionLimit{source: Pattern}`. Pour préserver la traçabilité de source (UI), LS7 aura besoin soit d'un point d'entrée du service acceptant un `SessionLimit`/une `source` pré-construit(e), soit d'un petit amendement de `on_rate_limited` (param `source`). Je **n'ai pas** modifié LS4 (discipline de lot) ; à arbitrer par toi. La donnée existe (le parser la porte), il ne reste qu'à la router. ## Builds - `cargo build -p infrastructure` ✅ 0 warning ; tests LS2 (5) ✅ verts. - `cargo build --workspace` ❌ **app-tauri uniquement** (inchangé : `chat.rs:185` → LS8, `events.rs:261` → LS7). LS5 n'ajoute aucune rupture. - Note non bloquante : 2 warnings `irrefutable if let` dans le **code de TEST** de `scheduler/mod.rs` (253/285, écrit par QA en LS3 ; `ScheduledTask` est mono-variante aujourd'hui) — **hors** code LS5, à corriger côté QA (ou disparaîtra dès qu'une 2ᵉ variante de `ScheduledTask` existera). ## Pour QA (binôme LS5) `RateLimitParser` 100 % testable sans I/O (`now_ms` injecté) : - pattern qui matche **sans** `reset_capture` ⇒ `Some(SessionLimit{resets_at_ms: None, source: Pattern})` ; - avec capture nommée + `time_format` **absolu** (`epoch_s`/`epoch_ms`/`iso8601`) ⇒ ms attendus ; - **relatif** (`relative_s`, ex. capture « 600 », now=T) ⇒ `T + 600_000` ; - **murale** (`wall`, « 3pm ») : si now=10h ⇒ 15h **aujourd'hui** ; si now=16h ⇒ 15h **demain** (passage de minuit) ; - regex **invalide** (`new`) ⇒ `None` (pas de panique) ; - pas de match ⇒ `detect` ⇒ `None` ; - capture présente mais valeur pourrie / groupe absent ⇒ `Some(SessionLimit{resets_at_ms: None})` ; - `applies` : profil structuré ⇒ `false` ; profil PTY + pattern ⇒ `true` ; profil PTY sans pattern ⇒ `false`. - `timeparse` testable directement (réexporté) : `wall_clock_to_ms`, `parse_wall_clock`, `parse_absolute_ms`, `days_from_civil`. -- **Prompt:** LS6 — Câblage des événements de limite de session vers le front (app-tauri). Project root: /home/anthony/Documents/Projects/IdeA. Contexte : LS1 a ajouté au domaine 5 nouvelles variantes `DomainEvent` (AgentRateLimited{agent_id, resets_at_ms: Option}, AgentResumeScheduled{agent_id, fire_at_ms: i64}, AgentResumeCancelled{agent_id}, AgentResumed{agent_id}, AgentRateLimitSuspected{agent_id, resets_at_ms: Option}) et la variante `ReplyEvent::RateLimited{resets_at_ms: Option}`. Le workspace ne compile plus : `app-tauri` a deux `match` non exhaustifs. Travail attendu (faire compiler le workspace, propre et cohérent avec le style existant) : 1. `crates/app-tauri/src/events.rs` : - Ajouter 5 variantes correspondantes à l'enum wire `DomainEventDto` (vers la ligne 28), chacune `#[serde(rename_all = "camelCase")]`, avec les `agent_id` sérialisés en `String` (via `.to_string()`) comme les autres variantes, et les champs temps (`resets_at_ms`/`fire_at_ms`) en `Option`/`i64`. Documenter chaque variante (doc-comment) dans le même esprit que les voisines. - Ajouter les 5 bras correspondants dans `impl From<&DomainEvent> for DomainEventDto` (vers la ligne 259), sur le modèle de `AgentBusyChanged`/`AgentLivenessChanged`. 2. `crates/app-tauri/src/chat.rs` : dans `chunk_from_event` (~ligne 184), traiter `ReplyEvent::RateLimited { .. }`. Sémantique domaine (ports.rs §21.2-T4) : un `RateLimited` est NON terminal et SANS contenu chat, exactement comme `Heartbeat` — il doit donc mapper vers `None` (le badge UI vient du bus `DomainEvent::AgentRateLimited`, pas du flux chat). Aligne le commentaire sur celui de `Heartbeat`. Contraintes : respecte SOLID/hexagonal (le DTO wire reste un miroir pur du domaine, aucune logique). Ne touche QUE app-tauri. À la fin, lance `cargo build --workspace` et `cargo test -p app-tauri` et rapporte la sortie réelle (succès/échec). Si des tests unitaires de relais existent (ex. autour de la ligne 427 d'events.rs pour AgentLivenessChanged), ajoute au moins un test de relais pour AgentRateLimited sur le même modèle. Rends ton résultat via idea_reply avec un résumé des fichiers modifiés et la sortie de compilation/tests. - **Response:** LS6 terminé — workspace recompile, app-tauri vert. **Fichiers modifiés (app-tauri uniquement)** 1. `crates/app-tauri/src/events.rs` - Enum `DomainEventDto` : ajout de 5 variantes miroir (insérées avant `PtyOutput`, respectant l'ordre du domaine), chacune `#[serde(rename_all = "camelCase")]`, `agent_id: String` : - `AgentRateLimited { agent_id, resets_at_ms: Option }` - `AgentResumeScheduled { agent_id, fire_at_ms: i64 }` - `AgentResumeCancelled { agent_id }` - `AgentResumed { agent_id }` - `AgentRateLimitSuspected { agent_id, resets_at_ms: Option }` Les champs `Option` portent `#[serde(skip_serializing_if = "Option::is_none")]` (cohérent avec les voisins `DelegationReady`). Doc-comments dans le même esprit que les variantes existantes (réf. §21). - `impl From<&DomainEvent> for DomainEventDto` : 5 bras correspondants (modèle `AgentBusyChanged`/`AgentLivenessChanged`), `agent_id.to_string()`, champs temps copiés (`*resets_at_ms` / `*fire_at_ms`). DTO = miroir pur, zéro logique. - Test de relais ajouté `rate_limited_relays_to_dto_and_wire` (sur le modèle de `liveness_changed_*`) : vérifie `type == "agentRateLimited"`, `agentId` et `resetsAtMs`. 2. `crates/app-tauri/src/chat.rs` - `chunk_from_event` : ajout du bras `ReplyEvent::RateLimited { .. } => None` (non terminal, sans contenu chat — comme `Heartbeat`, ports §21.2-T4). Commentaire de la fonction étendu pour expliquer que le badge UI vient du bus `DomainEvent::AgentRateLimited`, pas du flux chat. **Sortie compilation/tests (réelle)** - `cargo build --workspace` : `Finished dev profile ... in 16.89s` ✅ - `cargo test -p app-tauri` : tous verts, dont : - `events::tests::rate_limited_relays_to_dto_and_wire ... ok` - `events::tests::liveness_changed_{stalled,alive}_relays_to_dto_and_wire ... ok` - `chunk_from_event_*` (heartbeat/final/text_delta/tool_activity) ... ok - Module events : `test result: ok. 43 passed; 0 failed` - Aucun échec sur l'ensemble des binaires/tests d'intégration d'app-tauri. Contraintes respectées : SOLID/hexagonal (wire DTO = miroir pur du domaine), périmètre limité à app-tauri. - **Prompt:** LS7 — câblage de la feature session-limits au composition root (app-tauri). Project root: /home/anthony/Documents/Projects/IdeA. Périmètre : UNIQUEMENT app-tauri (state.rs, commands.rs, lib.rs) + au besoin exposer un champ sur LaunchAgentOutput. Aucun nouveau port domaine, aucun nouvel adapter infra : c'est du câblage de l'existant. Architect a fourni la carte de câblage suivante ; suis-la fidèlement, dans l'ORDRE D'IMPLÉMENTATION recommandé. Vérifie chaque ancrage (fichier/struct/ligne) toi-même avant d'éditer car les lignes ont pu bouger. === CARTE DE CÂBLAGE ARCHITECT === 1. INSTANCIATION DU SERVICE — dans crates/app-tauri/src/state.rs, AppState::build. Réutiliser clock (SystemClock, Arc) et event_bus partagé (events_port, Arc). Séquence (après construction de launch_agent et project_store, avant le bloc orchestrateur) : a. let (resume_tx, resume_rx) = tokio::sync::mpsc::unbounded_channel::(); b. let scheduler = Arc::new(TokioScheduler::new(resume_tx, Arc::clone(&clock) as Arc)) as Arc; c. let resumer = Arc::new(AppAgentResumer::new(...)) as Arc; d. let session_limit_service = Arc::new(SessionLimitService::new(Arc::clone(&clock) as Arc, scheduler, Arc::clone(&events_port), resumer)); Ajouter champ `pub session_limit_service: Arc` à AppState et le renvoyer dans le littéral final. resume_rx N'entre PAS dans AppState : il est moved dans la tâche de drain spawné dans build (§5). Imports : application::{SessionLimitService, AgentResumer}, domain::ports::{Scheduler, ScheduledTask}, infrastructure::TokioScheduler. 2. PORT AgentResumer → LaunchAgent — nouvel adapter AppAgentResumer dans state.rs, à côté des passerelles AppHandoffProvider / AppProviderSessionProvider / AppRecordTurnProvider (même patron impl application::Trait for AppXxx). impl application::AgentResumer { async fn resume(agent_id, node_id, conversation_id, resume_prompt) -> Result<(),AppError> } recompose un LaunchAgentInput et appelle self.launch_agent.execute(...) (le MÊME Arc que la commande launch_agent). LaunchAgent applique déjà SessionPlan::Resume quand conversation_id présent. ⚠️ POINT DUR : AgentResumer::resume et ScheduledTask::ResumeAgent ne portent PAS de project_id, mais LaunchAgentInput exige Project complet + rows/cols + mcp_runtime. Solution : AppAgentResumer détient un Arc>> (ResumeContext = { project: Project, rows: u16, cols: u16 }) ALIMENTÉ par la commande launch_agent (là où project/rows/cols sont en main) et lu au resume. mcp_runtime recalculé dans resume via crate::mcp_endpoint::{idea_exe_path, mcp_endpoint} (même recette que la commande launch_agent). store_port injecté en repli. Injection du resume_prompt (constante application::RESUME_PROMPT) comme premier tour : pour le chemin PTY natif, réutiliser le médiateur d'entrée / portail d'écriture PTY (MediatedInbox) plutôt qu'un write brut. 3. TAP NIVEAU 1 (structuré) — dans crates/app-tauri/src/commands.rs, fn agent_send, boucle de pump du ReplyStream. AVANT chunk_from_event : `if let ReplyEvent::RateLimited { resets_at_ms } = &event { service.on_rate_limited(agent_id, node_id, conversation_id, *resets_at_ms); }` puis continuer le drain (non terminal). Récup node_id/agent_id : ajouter méthode meta_for_session(&SessionId)->Option<(AgentId,NodeId)> sur StructuredSessions (crates/application/src/terminal/registry.rs, jumeau de live_agents, lookup dans entries). conversation_id : passer None (acceptable LS7). Ce tap est DORMANT en composition B-2 mais à câbler pour forward-compat. Arc::clone(&state.session_limit_service) avant le thread::spawn, move dans le thread. 4. TAP NIVEAU 2 (PTY) — chemin ACTIF — dans commands.rs, fn launch_agent, branche PTY (if output.structured.is_none() + thread::spawn du pump d'octets) : a. Sélection §21.10-4 : appeler infrastructure::ratelimit::applies(&profile) avant d'armer. Besoin : exposer le AgentProfile (ou au minimum le RateLimitPattern) résolu sur LaunchAgentOutput (LaunchAgent::execute le résout déjà en interne — option la plus propre, zéro I/O). b. RateLimitParser::new(&pattern) (Option ⇒ regex invalide = pas de détecteur, jamais de panique), construit une fois par lancement, déplacé dans le thread de pump. c. Dans la boucle for chunk in stream, après send_output : String::from_utf8_lossy(&chunk) puis parser.detect(&text, clock.now_millis()). Sur Some(SessionLimit) ⇒ service.on_rate_limited(agent_id, node_id, conversation_id, limit.resets_at_ms). agent_id/node_id/conversation_id (request.conversation_id) déjà en main dans la commande ⇒ cloner avant thread::spawn. Besoin d'un Arc (réutiliser SystemClock). Anti-double-détection garantie par applies (structured_adapter.is_none()). Fragmentation PTY : best-effort par fragment pour LS7 (note QA). 5. DRAIN DU SCHEDULER — resume_rx drainé dans une tâche détachée spawné DANS AppState::build sur le patron EXACT de sweep_stalled : utiliser tauri::async_runtime::spawn (PAS tokio::spawn — build tourne dans le hook setup sans runtime ambiant). Boucle : while let Some(task) = resume_rx.recv().await { if let Err(e) = service.execute_resume(task).await { /* log best-effort */ } }. service (Arc) et resume_rx moved dans la closure. 6. COMMANDE TAURI cancel_resume — dans commands.rs : #[tauri::command] pub async fn cancel_resume(agent_id: String, state: State<'_, AppState>) -> Result { let id = parse_agent_id(&agent_id)?; Ok(state.session_limit_service.cancel_resume(id)) }. Enregistrer dans crates/app-tauri/src/lib.rs generate_handler![ … ] (à côté des commandes agent, ex. après change_agent_profile). ORDRE : (1)+(5) avec un AgentResumer stub d'abord (valide armement→canal→execute_resume→AgentResumed), puis (6) cancel_resume, puis (4) tap N2 PTY, puis (2) AppAgentResumer réel + registre ResumeContext alimenté par launch_agent, puis (3) tap N1 structuré + meta_for_session. CONFORMITÉ : domaine pur, regex/parse confinés infra, service pur-ports — ne rien violer. Pas de panique sur profil mal configuré / resume sans contexte (repli propre). === FIN CARTE === À la fin : `cargo build --workspace` puis `cargo test --workspace` et rapporte la SORTIE RÉELLE (succès/échec, nombres). Ajoute des tests unitaires côté app-tauri pour ce qui est testable sans Tauri runtime (ex. AppAgentResumer avec un LaunchAgent fake, meta_for_session sur StructuredSessions, la commande cancel_resume si possible). Rends via idea_reply : fichiers modifiés, décisions prises sur les points durs, et la sortie compilation/tests. - **Response:** LS7 terminé — workspace recompile, suite complète verte (83 binaires de test, 0 échec). Câblage suivi fidèlement, dans l'ordre recommandé. ## Fichiers modifiés **app-tauri (cœur du câblage)** - `crates/app-tauri/Cargo.toml` : `async-trait` déplacé/ajouté en `[dependencies]` (il n'était qu'en `[dev-dependencies]` ⇒ l'impl du port async `AgentResumer` côté lib ne compilait pas). - `crates/app-tauri/src/state.rs` : - **(1) Instanciation du service** dans `AppState::build`, après `input_mediator` et avant le bloc orchestrateur : `unbounded_channel::()`, `TokioScheduler` (clock partagé), `AppAgentResumer`, `SessionLimitService::new(clock, scheduler, events_port, resumer)`. Champ `pub session_limit_service: Arc` + `pub resume_contexts: ResumeContexts` ajoutés à `AppState` et au littéral final. - **(2) Port `AgentResumer` → `LaunchAgent`** : nouvel adapter `AppAgentResumer` (passerelle, même patron que `AppHandoffProvider`/`AppProviderSessionProvider`). Recompose un `LaunchAgentInput` à partir du `ResumeContext` lu + recalcule `mcp_runtime` via `mcp_endpoint::{idea_exe_path, mcp_endpoint}`, appelle le **même** `Arc`, puis injecte `RESUME_PROMPT` comme premier tour via le **portail d'entrée** (`InputMediator::enqueue`, pas de write brut, §20). Types `ResumeContext { project, rows, cols }` + alias `ResumeContexts = Arc>>`. - **(5) Drain du scheduler** : tâche détachée `tauri::async_runtime::spawn` (patron exact de `sweep_stalled`) : `while let Some(task) = resume_rx.recv().await { service.execute_resume(task).await }` (log best-effort sur erreur). - `crates/app-tauri/src/commands.rs` : - **(4) Tap niveau 2 (PTY, chemin actif)** dans `launch_agent` : sélection §21.10-4 via `infrastructure::ratelimit::applies(&output.profile)`, `RateLimitParser::new(pattern)` construit une fois, déplacé dans le thread de pump ; dans la boucle, avant `send_output`, `String::from_utf8_lossy` + `parser.detect(text, clock.now_millis())` ⇒ `service.on_rate_limited(agent_id, host_node_id, conversation_id, limit.resets_at_ms)`. Alimente aussi `state.resume_contexts` (projet cloné + rows/cols) à chaque lancement. - **(3) Tap niveau 1 (structuré, dormant mais câblé)** dans `agent_send` : `meta_for_session(&sid)` résout `(agent_id, node_id)` ; sur `ReplyEvent::RateLimited { resets_at_ms }` ⇒ `service.on_rate_limited(.., None, *resets_at_ms)` puis on continue le drain (`conversation_id = None`, acceptable LS7). - **(6) Commande `cancel_resume(agent_id) -> Result`** déléguant à `session_limit_service.cancel_resume`. - `crates/app-tauri/src/lib.rs` : `commands::cancel_resume` enregistré dans `generate_handler!`. **application (sanctionné par la carte)** - `crates/application/src/agent/lifecycle.rs` : champ `pub profile: Option` exposé sur `LaunchAgentOutput` (résolu déjà en interne, zéro I/O) ; `Some(profile.clone())` sur les 2 lancements effectifs (PTY + structuré), `None` sur les 3 réattaches (rebind/idempotent). - `crates/application/src/terminal/registry.rs` : `StructuredSessions::meta_for_session(&SessionId) -> Option<(AgentId, NodeId)>` (jumeau « inverse » de `live_agents`, lookup direct). **Tests** - `crates/app-tauri/tests/session_limit_wiring.rs` (nouveau) : `cancel_resume` no-op propre pour agent inconnu ; `on_rate_limited` arme une reprise annulable et publie `AgentRateLimited`→`AgentResumeScheduled`→(après cancel)`AgentResumeCancelled` sur le **vrai** bus via `AppState::build`. - `crates/application/tests/structured_registry_d1.rs` : test `meta_for_session` (résout agent+cellule, `None` si id inconnu/retiré). - Tests existants `dto_agents.rs` / `dto_chat.rs` mis à jour avec `profile: None` (nouveau champ du struct). ## Décisions sur les points durs - **Profil pour la sélection N2** : exposé en `Option` sur `LaunchAgentOutput` (option « la plus propre » de la carte) plutôt que dupliquer la règle ; app-tauri appelle `infrastructure::ratelimit::applies` ⇒ règle anti-double-détection N1/N2 reste l'unique source infra. `None` sur réattache (best-effort : pas d'armement, le tour n'a pas de nouvelle session). - **`AgentResumer::resume` sans `project_id`** : registre `ResumeContexts` partagé (composition root ↔ commande `launch_agent`) ; alimenté là où `Project`/taille sont en main. Resume « à l'aveugle » (contexte absent) ⇒ `Err(NotFound)` propre ⇒ `AgentResumed` non publié, jamais de panique. - **Injection `RESUME_PROMPT`** : via `InputMediator::enqueue` (portail d'écriture unique, §20), `PendingReply` lâché (reprise ≠ délégation corrélée). - **Horloge du tap N2** : `infrastructure::SystemClock` local au pump (l'`AppState` n'expose pas le clock). - **Limitations notées (best-effort LS7)** : fragmentation PTY (motif coupé entre 2 fragments) ⇒ raté best-effort par fragment ; le ré-abonnement du pump PTY n'est pas recâblé sur la reprise auto hors-commande (l'agent tourne, le front peut réattacher). ## Sortie compilation/tests (réelle) - `cargo build --workspace` : `Finished dev profile ... in 18.04s` ✅ (0 warning sur les crates touchées). - `cargo test --workspace` : **83 binaires `test result: ok`, 0 failed**, 0 erreur. Dont : - `tests/session_limit_wiring.rs` : `cancel_resume_is_a_clean_noop_for_unknown_agent` ok, `on_rate_limited_arms_a_cancellable_resume_over_the_real_bus` ok. - `application` `structured_meta_for_session_resolves_agent_and_node` ok (12 passed). - infrastructure 221 passed, app-tauri (events/pty/orchestrator/dto…) verts. Conformité hexagonale respectée : DTO/wire inchangés côté domaine, regex/parse confinés infra (`applies`/`RateLimitParser`), service pur-ports, aucun nouveau port domaine ni adapter infra (câblage de l'existant uniquement). @@ -23,4 +20,7 @@ objective: IMPLÉMENTATION (code de production uniquement — PAS les nouveaux t - **Prompt:** QA bloque sur `cargo test -p app-tauri --test orchestrator_wiring`: 4 tests loopback socket échouent car socket `/run/user/1000/idea-mcp/.sock` non bindé. Compléments QA: `cargo test -p app-tauri bind_endpoint -- --nocapture` échoue avec `PermissionDenied` sur `lay corpse socket`; `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` échoue car `bind_endpoint returned None`. Merci de diagnostiquer si c'est une contrainte d'environnement/sandbox ou une régression code liée aux changements récents. Si correction applicative/test nécessaire, implémente-la sans toucher aux fichiers runtime `.ideai`. Sinon, fournis une justification précise et une commande de vérification alternative. Ne committe pas. - **Response:** Diagnostic : blocage environnement/sandbox, pas régression applicative liée au MVP `agent-skill-awareness-v2` ni aux changements récents. Constats : - `cargo test -p app-tauri bind_endpoint -- --nocapture` échoue exactement comme QA : `lay corpse socket: Os { code: 1, kind: PermissionDenied, message: "Operation not permitted" }` sur `std::os::unix::net::UnixListener::bind`. - `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` échoue car `bind_endpoint` retourne `None` pour `/run/user/1000/idea-mcp/.sock`. - Même en forçant `XDG_RUNTIME_DIR=/tmp`, les tests loopback échouent : `bind_endpoint returned None for "/tmp/idea-mcp/.sock"`. - Smoke test Rust minimal hors repo, compilé dans `/tmp`, confirme que le sandbox interdit `AF_UNIX` path sockets partout : - `/tmp/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted` - `/run/user/1000/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted` Interprétation : - Les tests qui échouent nécessitent un vrai listener Unix domain socket. L’échec se produit avant la logique IdeA, au niveau syscall de bind socket. - Le code récent `agent-skill-awareness-v2` ne touche pas `app-tauri`, `state.rs`, `mcp_endpoint.rs`, `mcp_bridge.rs`, ni la dérivation/binding des endpoints. - `mcp_endpoint` choisit correctement `$XDG_RUNTIME_DIR` puis `/tmp`; le fait que `/tmp` échoue aussi exclut un simple problème de permissions de `/run/user/1000`. Commandes de vérification exécutées : - `cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue `PermissionDenied`, reproduit QA. - `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue `bind_endpoint returned None`, reproduit QA. - `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue aussi `PermissionDenied`. - `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue aussi `bind_endpoint returned None`. - Smoke Rust minimal avec `UnixListener::bind` dans `/tmp` et `/run/user/1000` => échoue `PermissionDenied` dans les deux cas. Commandes alternatives vertes dans ce sandbox, en excluant uniquement les tests qui nécessitent un vrai socket Unix : ```text cargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together ``` Résultat : OK, 9 passed, 4 filtered out. ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : OK, 39 passed, 8 filtered out. Aucune correction code/test appliquée. Je n’ai pas touché aux fichiers runtime `.ideai`. - **Prompt:** Implémente le Lot A backend/Tauri du chantier `UX conversations/délégations + read-model live-state minimal` sur la branche `feature/conversation-ux-live-state`. Respecte le cadrage Architect: read-only, sans nouvelle persistance durable, pas de lecture log/handoff, pas de FIFO complète. Objectif backend: - Ajouter un read-model `ProjectWorkState` qui liste les agents du manifeste avec leur état live et busy. - Agréger depuis les stores/registres existants: manifeste agents via context store, `LiveSessions`/registries existants, `InputMediator::busy_state`. - Conserver l'ordre manifeste. - Ignorer les agents live absents du manifeste. - Exposer une commande Tauri `get_project_work_state(projectId)`. Contrats recommandés par Architect: - `ProjectWorkState { agents: Vec }` - `AgentWorkState { agent_id, name, profile_id, live: Option, busy }` - `LiveWorkSession { node_id, session_id, kind: Pty|Structured }` si raisonnable; sinon explique pourquoi tu omets `kind`. - Busy DTO camelCase/tag `state`: `idle` ou `busy { ticket, sinceMs }` selon les types existants. Fichiers probables: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, peut-être `crates/application/src/terminal/registry.rs`; Tauri `dto.rs`, `commands.rs`, `state.rs`, `lib.rs`. Tests attendus: - tests application `workstate`: aucun live/busy; live PTY ou structured; busy; agent live hors manifeste ignoré. - tests DTO/commande si conventions existantes. - Exécute au minimum `cargo test -p application workstate`, `cargo check -p app-tauri`, et les tests app-tauri pertinents que tu peux lancer dans ce sandbox. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques. -- **Response:** Lot A backend/Tauri implémenté sur `feature/conversation-ux-live-state`, sans commit. Fichiers modifiés pour le lot : - `crates/application/src/workstate/mod.rs` (nouveau) : read-model `GetProjectWorkState`, `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`. - `crates/application/src/lib.rs` : exports du module workstate et des snapshots live typés. - `crates/application/src/terminal/mod.rs` : exports `LiveSessionKind` / `LiveSessionSnapshot`. - `crates/application/src/terminal/registry.rs` : ajout de `LiveSessionKind`, `LiveSessionSnapshot`, `LiveSessions::live_agent_snapshots()` ; `live_agents()` existant inchangé. - `crates/application/tests/workstate.rs` (nouveau) : tests du read-model. - `crates/app-tauri/src/state.rs` : construction/câblage du use case dans `AppState` avec `AgentContextStore`, `LiveSessions`, `InputMediator`. - `crates/app-tauri/src/commands.rs` : commande `get_project_work_state(projectId)`. - `crates/app-tauri/src/dto.rs` : DTO `ProjectWorkStateDto`, `AgentWorkStateDto`, `LiveWorkSessionDto`, `LiveWorkSessionKindDto` ; `busy` réutilise `domain::AgentBusyState` (`state: idle|busy`, `sinceMs`). - `crates/app-tauri/src/lib.rs` : enregistrement Tauri command. - `crates/app-tauri/tests/dto_agents.rs` : test DTO camelCase/live/busy. Comportement : - Read-only, aucune nouvelle persistance durable. - Aucune lecture log/handoff, aucune FIFO complète exposée. - Agents listés dans l’ordre du manifeste. - Agents live absents du manifeste ignorés. - Live session expose `nodeId`, `sessionId`, `kind: pty|structured`. Commandes exécutées côté DevBackend : - `cargo fmt -p application -p app-tauri` : OK. - `cargo test -p application workstate` : OK, 5 tests workstate passés. - `cargo check -p app-tauri` : OK. - `cargo test -p app-tauri --test dto_agents project_work_state -- --nocapture` : OK, 1 passed. - `cargo test -p app-tauri --test dto_agents` : OK, 19 passed. - `cargo test -p app-tauri --test list_live_agents_r0b` : OK, 5 passed. - `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` : OK, 39 passed, 8 filtered out. - `cargo test -p application` : OK, application complète verte. Warning existant/non lié : `CapturingFs::writes` unused dans `crates/application/tests/orchestrator_service.rs:1642`. Validation QA : vert. QA a relu le périmètre, confirmé read-only / ordre manifeste / absence log-handoff-persistance, et réexécuté les commandes clés avec succès. Même réserve non bloquante que précédemment : les 8 tests app-tauri filtrés exigent un vrai socket Unix loopback, bloqué dans ce sandbox par `UnixListener::bind` `PermissionDenied`; à exécuter hors sandbox permissif socket Unix. Risques / notes : - `kind` est inclus car l’agrégateur sait distinguer PTY vs structured via les deux registres. - Le read-model retourne un instantané live/busy au moment de l’appel ; pas de garantie transactionnelle entre registre live et busy state, ce qui est cohérent avec un read-model live minimal. - `git status` montre aussi des fichiers `.ideai/*` runtime modifiés par l’orchestration/conversation ; je ne les ai pas édités pour ce lot et ils ne font pas partie du livrable. \ No newline at end of file +- **Response:** Lot A backend/Tauri implémenté sur `feature/conversation-ux-live-state`, sans commit. Fichiers modifiés pour le lot : - `crates/application/src/workstate/mod.rs` (nouveau) : read-model `GetProjectWorkState`, `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`. - `crates/application/src/lib.rs` : exports du module workstate et des snapshots live typés. - `crates/application/src/terminal/mod.rs` : exports `LiveSessionKind` / `LiveSessionSnapshot`. - `crates/application/src/terminal/registry.rs` : ajout de `LiveSessionKind`, `LiveSessionSnapshot`, `LiveSessions::live_agent_snapshots()` ; `live_agents()` existant inchangé. - `crates/application/tests/workstate.rs` (nouveau) : tests du read-model. - `crates/app-tauri/src/state.rs` : construction/câblage du use case dans `AppState` avec `AgentContextStore`, `LiveSessions`, `InputMediator`. - `crates/app-tauri/src/commands.rs` : commande `get_project_work_state(projectId)`. - `crates/app-tauri/src/dto.rs` : DTO `ProjectWorkStateDto`, `AgentWorkStateDto`, `LiveWorkSessionDto`, `LiveWorkSessionKindDto` ; `busy` réutilise `domain::AgentBusyState` (`state: idle|busy`, `sinceMs`). - `crates/app-tauri/src/lib.rs` : enregistrement Tauri command. - `crates/app-tauri/tests/dto_agents.rs` : test DTO camelCase/live/busy. Comportement : - Read-only, aucune nouvelle persistance durable. - Aucune lecture log/handoff, aucune FIFO complète exposée. - Agents listés dans l’ordre du manifeste. - Agents live absents du manifeste ignorés. - Live session expose `nodeId`, `sessionId`, `kind: pty|structured`. Commandes exécutées côté DevBackend : - `cargo fmt -p application -p app-tauri` : OK. - `cargo test -p application workstate` : OK, 5 tests workstate passés. - `cargo check -p app-tauri` : OK. - `cargo test -p app-tauri --test dto_agents project_work_state -- --nocapture` : OK, 1 passed. - `cargo test -p app-tauri --test dto_agents` : OK, 19 passed. - `cargo test -p app-tauri --test list_live_agents_r0b` : OK, 5 passed. - `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` : OK, 39 passed, 8 filtered out. - `cargo test -p application` : OK, application complète verte. Warning existant/non lié : `CapturingFs::writes` unused dans `crates/application/tests/orchestrator_service.rs:1642`. Validation QA : vert. QA a relu le périmètre, confirmé read-only / ordre manifeste / absence log-handoff-persistance, et réexécuté les commandes clés avec succès. Même réserve non bloquante que précédemment : les 8 tests app-tauri filtrés exigent un vrai socket Unix loopback, bloqué dans ce sandbox par `UnixListener::bind` `PermissionDenied`; à exécuter hors sandbox permissif socket Unix. Risques / notes : - `kind` est inclus car l’agrégateur sait distinguer PTY vs structured via les deux registres. - Le read-model retourne un instantané live/busy au moment de l’appel ; pas de garantie transactionnelle entre registre live et busy state, ce qui est cohérent avec un read-model live minimal. - `git status` montre aussi des fichiers `.ideai/*` runtime modifiés par l’orchestration/conversation ; je ne les ai pas édités pour ce lot et ils ne font pas partie du livrable. +- **Prompt:** Implémente le Lot B backend/Tauri `workstate delegation/queue snapshot` sur la branche `feature/workstate-delegation-queue`. Respecte le cadrage Architect. Objectif: - Ajouter un port read-only `AgentQueueSnapshot` dans `domain/src/mailbox.rs` avec snapshots clonés des tickets, sans exposer ni cloner les senders. - Implémenter ce port pour `InMemoryMailbox` en conservant l'ordre FIFO et positions recalculées. - Étendre `GetProjectWorkState` pour inclure `agents[].tickets`, en croisant snapshot queue + `InputMediator::busy_state` pour dériver `inProgress` vs `queued`. - Conserver manifest boundary: tickets d'agents absents du manifeste ignorés. - Générer `taskPreview` côté application: trim, espaces normalisés, coupure ~160 caractères; conserver `taskLen` original utile. - Exposer DTO Tauri camelCase dans la commande existante `get_project_work_state`. Décision de périmètre: afficher les tickets `source == Agent` en priorité pour délégations; si tu inclus aussi human, explique et teste le libellé/source. Ne change pas la sémantique enqueue/resolve/cancel. Pas de nouvelle persistance, pas lecture log/handoff, pas événement queue changé. Tests attendus: - infrastructure mailbox snapshot: ordre/metadata/read-only/update after resolve/cancel. - application workstate: queue vide, deux tickets ordonnés, busy first inProgress, idle queued, absent manifest ignored, preview truncation, source mapping. - app-tauri DTO serialization tickets. Commandes: `cargo fmt`, `cargo test -p infrastructure mailbox --lib`, `cargo test -p application --test workstate`, `cargo test -p app-tauri --test dto_agents`, `cargo check -p app-tauri`, et autres pertinentes. Ne committe pas. Ignore les fichiers runtime `.ideai/*` dirty, ne les modifie pas volontairement. +- **Prompt:** Reprise orchestration Main. Je retrouve la branche `feature/workstate-delegation-queue` avec des modifications backend/Tauri non committées pour le Lot B `workstate delegation/queue snapshot` (ticket mentionné par l'utilisateur: dbca904a-2843-4f41-9926-d13b7804c70f). Peux-tu confirmer ton état exact: périmètre implémenté, choix notables (notamment tickets human inclus ou exclus), commandes exécutées et résultats réels, et ce qu'il reste selon toi ? Ne modifie rien, ne committe pas. +- **Response:** État DevBackend — branche `feature/workstate-delegation-queue`, Lot B « workstate delegation/queue snapshot » (ticket dbca904a). **Rien committé, rien modifié pendant ce diagnostic.** ## Périmètre implémenté Extension du read-model live-state (Lot A) pour exposer, **par agent**, ses tickets de délégation en file FIFO. 4 couches touchées (sens hexagonal respecté) : - **domain/mailbox.rs** : nouveau value object `QueuedTicketSnapshot` (données du ticket + `position`, 0 = tête ; **jamais** le `oneshot::Sender`) + nouveau **port lecture seule** `AgentQueueSnapshot` (`queue_for(agent) -> Vec`), **ségrégé (ISP)** du port mutant `AgentMailbox`. Re-export depuis `domain/lib.rs`. - **infrastructure/mailbox/mod.rs** : `impl AgentQueueSnapshot for InMemoryMailbox` — clone les données sous le lock, recalcule la position depuis l'ordre courant, lecture pure (n'altère pas la file). - **application/workstate/mod.rs** : nouveaux `AgentTicketState`, `TicketWorkStatus` (InProgress/Queued), `TicketWorkSource` (Human/Agent), `task_preview` (160 car., espaces normalisés) + `task_len`. `GetProjectWorkState` gagne une dépendance `queue: Arc` ; champ `AgentWorkState.tickets` peuplé. - **app-tauri/dto.rs** : `AgentTicketStateDto`, `TicketWorkStatusDto`, `TicketWorkSourceDto` (camelCase, enum source taggée `kind`), champ `AgentWorkStateDto.tickets`. - **app-tauri/state.rs** : composition root — même `InMemoryMailbox` concret partagé en **deux vues Arc** (`AgentMailbox` mutant + `AgentQueueSnapshot` lecture), injecté dans `GetProjectWorkState`. ## Choix notables - **Tickets human INCLUS** (pas de filtrage par source). `TicketWorkSource::Human`/`Agent{agent_id}` mappé dans les deux sens, couvert par le test `workstate_maps_human_and_agent_ticket_sources`. La file liste tous les tickets quelle que soit l'origine. - **Statut dérivé, jamais stocké** : InProgress = ticket dont l'id == ticket busy courant de l'agent ; les autres Queued ; agent idle → tout Queued. - **ISP** : port lecture distinct plutôt que d'élargir `AgentMailbox` ; observer la file ne la mute jamais (test dédié). - **task_preview borné** à 160 car., texte intégral jamais envoyé à l'UI, longueur réelle via `task_len`. - **Frontière manifest préservée** : seuls les agents du manifest reçoivent des tickets ; file d'un agent absent du manifest ignorée (test dédié). ## Commandes exécutées (à l'instant) + résultats réels `cargo test -p domain -p application -p infrastructure -p app-tauri` → **tout vert, 0 échec**. - domain : 176 unit (+1 nouveau `queued_ticket_snapshot_…`) - infrastructure : 228 unit (+6 nouveaux `snapshot_*` : empty/fifo-order/metadata/read-only/après resolve/après cancel) - application : 43 unit + `workstate.rs` **12** (+7 nouveaux : sans-queue, fifo, busy→in-progress, idle→tout-queued, agent-hors-manifest, troncature preview, sources human+agent) - app-tauri : lib 47 + `dto_agents.rs` (+1 `project_work_state_dto_serialises_tickets_camelcase`) Aucun warning bloquant. ## Ce qu'il reste (selon moi) 1. **Frontend (périmètre DevFrontend)** : aucune consommation TS/React des nouveaux champs DTO `tickets` — rendu du panneau de délégations non fait. 2. **Commit/merge (périmètre Git)** : 14 fichiers non commités ; le code backend est prêt à committer. À noter : les fichiers `.ideai/` modifiés (conversations/handoff/log/layouts) sont du **bruit runtime**, pas la feature. 3. **Validation e2e/AppImage** : non faite (binaire qui tourne = AppImage, pas les sources). 4. **Cadrage Architect** : si tu veux une validation formelle du nouveau port `AgentQueueSnapshot` dans `architect.md`, je l'ai conçu en cohérence ISP mais ce n'est pas tranché dans la cartographie. En résumé : backend Lot B **complet et vert** au niveau tests unitaires/intégration ; restent le front, le commit Git et la validation e2e. \ No newline at end of file diff --git a/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/log.jsonl b/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/log.jsonl index 5470ff9..09005a5 100644 --- a/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/log.jsonl +++ b/.ideai/conversations/d5068bc8-786e-0428-1d14-83b93961a15e/log.jsonl @@ -69,3 +69,6 @@ {"id":"00aa406e-b715-4f9b-9978-f52b9a37e27f","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781945759965,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Diagnostic : blocage environnement/sandbox, pas régression applicative liée au MVP `agent-skill-awareness-v2` ni aux changements récents.\n\nConstats :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` échoue exactement comme QA : `lay corpse socket: Os { code: 1, kind: PermissionDenied, message: \"Operation not permitted\" }` sur `std::os::unix::net::UnixListener::bind`.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` échoue car `bind_endpoint` retourne `None` pour `/run/user/1000/idea-mcp/.sock`.\n- Même en forçant `XDG_RUNTIME_DIR=/tmp`, les tests loopback échouent : `bind_endpoint returned None for \"/tmp/idea-mcp/.sock\"`.\n- Smoke test Rust minimal hors repo, compilé dans `/tmp`, confirme que le sandbox interdit `AF_UNIX` path sockets partout :\n - `/tmp/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n - `/run/user/1000/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n\nInterprétation :\n- Les tests qui échouent nécessitent un vrai listener Unix domain socket. L’échec se produit avant la logique IdeA, au niveau syscall de bind socket.\n- Le code récent `agent-skill-awareness-v2` ne touche pas `app-tauri`, `state.rs`, `mcp_endpoint.rs`, `mcp_bridge.rs`, ni la dérivation/binding des endpoints.\n- `mcp_endpoint` choisit correctement `$XDG_RUNTIME_DIR` puis `/tmp`; le fait que `/tmp` échoue aussi exclut un simple problème de permissions de `/run/user/1000`.\n\nCommandes de vérification exécutées :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue `PermissionDenied`, reproduit QA.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue `bind_endpoint returned None`, reproduit QA.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue aussi `PermissionDenied`.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue aussi `bind_endpoint returned None`.\n- Smoke Rust minimal avec `UnixListener::bind` dans `/tmp` et `/run/user/1000` => échoue `PermissionDenied` dans les deux cas.\n\nCommandes alternatives vertes dans ce sandbox, en excluant uniquement les tests qui nécessitent un vrai socket Unix :\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat : OK, 9 passed, 4 filtered out.\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat : OK, 39 passed, 8 filtered out.\n\nAucune correction code/test appliquée. Je n’ai pas touché aux fichiers runtime `.ideai`."} {"id":"bd1d1082-e11d-4754-a83f-ba89233b25e4","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781946742079,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A backend/Tauri du chantier `UX conversations/délégations + read-model live-state minimal` sur la branche `feature/conversation-ux-live-state`. Respecte le cadrage Architect: read-only, sans nouvelle persistance durable, pas de lecture log/handoff, pas de FIFO complète.\n\nObjectif backend:\n- Ajouter un read-model `ProjectWorkState` qui liste les agents du manifeste avec leur état live et busy.\n- Agréger depuis les stores/registres existants: manifeste agents via context store, `LiveSessions`/registries existants, `InputMediator::busy_state`.\n- Conserver l'ordre manifeste.\n- Ignorer les agents live absents du manifeste.\n- Exposer une commande Tauri `get_project_work_state(projectId)`.\n\nContrats recommandés par Architect:\n- `ProjectWorkState { agents: Vec }`\n- `AgentWorkState { agent_id, name, profile_id, live: Option, busy }`\n- `LiveWorkSession { node_id, session_id, kind: Pty|Structured }` si raisonnable; sinon explique pourquoi tu omets `kind`.\n- Busy DTO camelCase/tag `state`: `idle` ou `busy { ticket, sinceMs }` selon les types existants.\n\nFichiers probables: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, peut-être `crates/application/src/terminal/registry.rs`; Tauri `dto.rs`, `commands.rs`, `state.rs`, `lib.rs`.\n\nTests attendus:\n- tests application `workstate`: aucun live/busy; live PTY ou structured; busy; agent live hors manifeste ignoré.\n- tests DTO/commande si conventions existantes.\n- Exécute au minimum `cargo test -p application workstate`, `cargo check -p app-tauri`, et les tests app-tauri pertinents que tu peux lancer dans ce sandbox. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."} {"id":"5d78a64e-ce73-4fca-b62d-7af302dbac90","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971233449,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Lot A backend/Tauri implémenté sur `feature/conversation-ux-live-state`, sans commit.\n\nFichiers modifiés pour le lot :\n- `crates/application/src/workstate/mod.rs` (nouveau) : read-model `GetProjectWorkState`, `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`.\n- `crates/application/src/lib.rs` : exports du module workstate et des snapshots live typés.\n- `crates/application/src/terminal/mod.rs` : exports `LiveSessionKind` / `LiveSessionSnapshot`.\n- `crates/application/src/terminal/registry.rs` : ajout de `LiveSessionKind`, `LiveSessionSnapshot`, `LiveSessions::live_agent_snapshots()` ; `live_agents()` existant inchangé.\n- `crates/application/tests/workstate.rs` (nouveau) : tests du read-model.\n- `crates/app-tauri/src/state.rs` : construction/câblage du use case dans `AppState` avec `AgentContextStore`, `LiveSessions`, `InputMediator`.\n- `crates/app-tauri/src/commands.rs` : commande `get_project_work_state(projectId)`.\n- `crates/app-tauri/src/dto.rs` : DTO `ProjectWorkStateDto`, `AgentWorkStateDto`, `LiveWorkSessionDto`, `LiveWorkSessionKindDto` ; `busy` réutilise `domain::AgentBusyState` (`state: idle|busy`, `sinceMs`).\n- `crates/app-tauri/src/lib.rs` : enregistrement Tauri command.\n- `crates/app-tauri/tests/dto_agents.rs` : test DTO camelCase/live/busy.\n\nComportement :\n- Read-only, aucune nouvelle persistance durable.\n- Aucune lecture log/handoff, aucune FIFO complète exposée.\n- Agents listés dans l’ordre du manifeste.\n- Agents live absents du manifeste ignorés.\n- Live session expose `nodeId`, `sessionId`, `kind: pty|structured`.\n\nCommandes exécutées côté DevBackend :\n- `cargo fmt -p application -p app-tauri` : OK.\n- `cargo test -p application workstate` : OK, 5 tests workstate passés.\n- `cargo check -p app-tauri` : OK.\n- `cargo test -p app-tauri --test dto_agents project_work_state -- --nocapture` : OK, 1 passed.\n- `cargo test -p app-tauri --test dto_agents` : OK, 19 passed.\n- `cargo test -p app-tauri --test list_live_agents_r0b` : OK, 5 passed.\n- `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` : OK, 39 passed, 8 filtered out.\n- `cargo test -p application` : OK, application complète verte. Warning existant/non lié : `CapturingFs::writes` unused dans `crates/application/tests/orchestrator_service.rs:1642`.\n\nValidation QA : vert. QA a relu le périmètre, confirmé read-only / ordre manifeste / absence log-handoff-persistance, et réexécuté les commandes clés avec succès. Même réserve non bloquante que précédemment : les 8 tests app-tauri filtrés exigent un vrai socket Unix loopback, bloqué dans ce sandbox par `UnixListener::bind` `PermissionDenied`; à exécuter hors sandbox permissif socket Unix.\n\nRisques / notes :\n- `kind` est inclus car l’agrégateur sait distinguer PTY vs structured via les deux registres.\n- Le read-model retourne un instantané live/busy au moment de l’appel ; pas de garantie transactionnelle entre registre live et busy state, ce qui est cohérent avec un read-model live minimal.\n- `git status` montre aussi des fichiers `.ideai/*` runtime modifiés par l’orchestration/conversation ; je ne les ai pas édités pour ce lot et ils ne font pas partie du livrable."} +{"id":"7b0a1b7a-bfad-4d97-89f8-f4bb9b32740d","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971869030,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B backend/Tauri `workstate delegation/queue snapshot` sur la branche `feature/workstate-delegation-queue`. Respecte le cadrage Architect.\n\nObjectif:\n- Ajouter un port read-only `AgentQueueSnapshot` dans `domain/src/mailbox.rs` avec snapshots clonés des tickets, sans exposer ni cloner les senders.\n- Implémenter ce port pour `InMemoryMailbox` en conservant l'ordre FIFO et positions recalculées.\n- Étendre `GetProjectWorkState` pour inclure `agents[].tickets`, en croisant snapshot queue + `InputMediator::busy_state` pour dériver `inProgress` vs `queued`.\n- Conserver manifest boundary: tickets d'agents absents du manifeste ignorés.\n- Générer `taskPreview` côté application: trim, espaces normalisés, coupure ~160 caractères; conserver `taskLen` original utile.\n- Exposer DTO Tauri camelCase dans la commande existante `get_project_work_state`.\n\nDécision de périmètre: afficher les tickets `source == Agent` en priorité pour délégations; si tu inclus aussi human, explique et teste le libellé/source. Ne change pas la sémantique enqueue/resolve/cancel. Pas de nouvelle persistance, pas lecture log/handoff, pas événement queue changé.\n\nTests attendus:\n- infrastructure mailbox snapshot: ordre/metadata/read-only/update after resolve/cancel.\n- application workstate: queue vide, deux tickets ordonnés, busy first inProgress, idle queued, absent manifest ignored, preview truncation, source mapping.\n- app-tauri DTO serialization tickets.\n\nCommandes: `cargo fmt`, `cargo test -p infrastructure mailbox --lib`, `cargo test -p application --test workstate`, `cargo test -p app-tauri --test dto_agents`, `cargo check -p app-tauri`, et autres pertinentes. Ne committe pas. Ignore les fichiers runtime `.ideai/*` dirty, ne les modifie pas volontairement."} +{"id":"2c25d734-281e-4f2a-850e-684fec6e0ba7","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973784611,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Reprise orchestration Main. Je retrouve la branche `feature/workstate-delegation-queue` avec des modifications backend/Tauri non committées pour le Lot B `workstate delegation/queue snapshot` (ticket mentionné par l'utilisateur: dbca904a-2843-4f41-9926-d13b7804c70f). Peux-tu confirmer ton état exact: périmètre implémenté, choix notables (notamment tickets human inclus ou exclus), commandes exécutées et résultats réels, et ce qu'il reste selon toi ? Ne modifie rien, ne committe pas."} +{"id":"b0d07b39-565c-4d30-a9ae-0c3721338aeb","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973900817,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"État DevBackend — branche `feature/workstate-delegation-queue`, Lot B « workstate delegation/queue snapshot » (ticket dbca904a). **Rien committé, rien modifié pendant ce diagnostic.**\n\n## Périmètre implémenté\nExtension du read-model live-state (Lot A) pour exposer, **par agent**, ses tickets de délégation en file FIFO. 4 couches touchées (sens hexagonal respecté) :\n\n- **domain/mailbox.rs** : nouveau value object `QueuedTicketSnapshot` (données du ticket + `position`, 0 = tête ; **jamais** le `oneshot::Sender`) + nouveau **port lecture seule** `AgentQueueSnapshot` (`queue_for(agent) -> Vec`), **ségrégé (ISP)** du port mutant `AgentMailbox`. Re-export depuis `domain/lib.rs`.\n- **infrastructure/mailbox/mod.rs** : `impl AgentQueueSnapshot for InMemoryMailbox` — clone les données sous le lock, recalcule la position depuis l'ordre courant, lecture pure (n'altère pas la file).\n- **application/workstate/mod.rs** : nouveaux `AgentTicketState`, `TicketWorkStatus` (InProgress/Queued), `TicketWorkSource` (Human/Agent), `task_preview` (160 car., espaces normalisés) + `task_len`. `GetProjectWorkState` gagne une dépendance `queue: Arc` ; champ `AgentWorkState.tickets` peuplé.\n- **app-tauri/dto.rs** : `AgentTicketStateDto`, `TicketWorkStatusDto`, `TicketWorkSourceDto` (camelCase, enum source taggée `kind`), champ `AgentWorkStateDto.tickets`.\n- **app-tauri/state.rs** : composition root — même `InMemoryMailbox` concret partagé en **deux vues Arc** (`AgentMailbox` mutant + `AgentQueueSnapshot` lecture), injecté dans `GetProjectWorkState`.\n\n## Choix notables\n- **Tickets human INCLUS** (pas de filtrage par source). `TicketWorkSource::Human`/`Agent{agent_id}` mappé dans les deux sens, couvert par le test `workstate_maps_human_and_agent_ticket_sources`. La file liste tous les tickets quelle que soit l'origine.\n- **Statut dérivé, jamais stocké** : InProgress = ticket dont l'id == ticket busy courant de l'agent ; les autres Queued ; agent idle → tout Queued.\n- **ISP** : port lecture distinct plutôt que d'élargir `AgentMailbox` ; observer la file ne la mute jamais (test dédié).\n- **task_preview borné** à 160 car., texte intégral jamais envoyé à l'UI, longueur réelle via `task_len`.\n- **Frontière manifest préservée** : seuls les agents du manifest reçoivent des tickets ; file d'un agent absent du manifest ignorée (test dédié).\n\n## Commandes exécutées (à l'instant) + résultats réels\n`cargo test -p domain -p application -p infrastructure -p app-tauri` → **tout vert, 0 échec**.\n- domain : 176 unit (+1 nouveau `queued_ticket_snapshot_…`)\n- infrastructure : 228 unit (+6 nouveaux `snapshot_*` : empty/fifo-order/metadata/read-only/après resolve/après cancel)\n- application : 43 unit + `workstate.rs` **12** (+7 nouveaux : sans-queue, fifo, busy→in-progress, idle→tout-queued, agent-hors-manifest, troncature preview, sources human+agent)\n- app-tauri : lib 47 + `dto_agents.rs` (+1 `project_work_state_dto_serialises_tickets_camelcase`)\nAucun warning bloquant.\n\n## Ce qu'il reste (selon moi)\n1. **Frontend (périmètre DevFrontend)** : aucune consommation TS/React des nouveaux champs DTO `tickets` — rendu du panneau de délégations non fait.\n2. **Commit/merge (périmètre Git)** : 14 fichiers non commités ; le code backend est prêt à committer. À noter : les fichiers `.ideai/` modifiés (conversations/handoff/log/layouts) sont du **bruit runtime**, pas la feature.\n3. **Validation e2e/AppImage** : non faite (binaire qui tourne = AppImage, pas les sources).\n4. **Cadrage Architect** : si tu veux une validation formelle du nouveau port `AgentQueueSnapshot` dans `architect.md`, je l'ai conçu en cohérence ISP mais ce n'est pas tranché dans la cartographie.\n\nEn résumé : backend Lot B **complet et vert** au niveau tests unitaires/intégration ; restent le front, le commit Git et la validation e2e."} diff --git a/.ideai/layouts.json b/.ideai/layouts.json index 773029a..1f6a499 100644 --- a/.ideai/layouts.json +++ b/.ideai/layouts.json @@ -18,8 +18,9 @@ "type": "leaf", "node": { "id": "d4b8c0d1-a44a-4c45-bbe9-26991f79b465", - "session": "d5e00a5c-591e-4061-9104-c9ea7c9e01c3", - "agent": "a6ced819-b893-4213-b003-9e9dc79b9641" + "session": "813f17c2-d056-4875-b9a0-3c18e4e40776", + "agent": "a6ced819-b893-4213-b003-9e9dc79b9641", + "agentWasRunning": true } }, "weight": 1.0 @@ -36,8 +37,9 @@ "type": "leaf", "node": { "id": "71564af2-673a-46c7-a0c0-877f89f6e49e", - "session": "cadc1432-ccc3-4849-94bb-5cf342a85fe1", - "agent": "aefdbd61-e3d4-4bc1-9f42-c259446a97b5" + "session": "384778a6-216f-47ed-90f0-67de6812b59d", + "agent": "73c853d1-c0fd-463b-ad17-1d24fefa371f", + "agentWasRunning": true } }, "weight": 1.0 @@ -47,7 +49,9 @@ "type": "leaf", "node": { "id": "8529e97f-ce06-490c-b3d0-531c3dfee442", - "session": "b7e0ab21-25bf-4158-8fcc-e2c67d49868e" + "session": "fac01ad4-0e99-4b2a-bdec-621493cf9c57", + "agent": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "agentWasRunning": true } }, "weight": 1.0 @@ -61,6 +65,19 @@ } } } + }, + { + "id": "74489208-01a7-4005-aba4-b9c41ab843fd", + "name": "Git Graph", + "kind": "gitGraph", + "tree": { + "root": { + "type": "leaf", + "node": { + "id": "2396a80e-bc98-47cd-b711-c7cff0010bb6" + } + } + } } ] }