chantier: durcissement reprise headless inter-projets + persistance profils Opencode

- Implémentation reprise de session Opencode headless (conversational_recovery)
- Persistance des profils IA Opencode dans .ideai/memory avec JSON Schema
- Gestion des permissions MCP pour agents externes
- Tests QA verts : structured_launch_d3, conversation_log, tickets_missing_carnet, agents, ProfilesSettings
This commit is contained in:
2026-07-28 12:23:34 +02:00
parent 78f7c8fe2d
commit 045e0984cc
15 changed files with 782 additions and 49 deletions

View File

@ -34,7 +34,7 @@ use tokio::task::JoinHandle;
use uuid::Uuid;
use application::{
AssignIssueAgentInput, AssignSkillToAgentInput, AssignTicketToSprintInput,
AppError, AssignIssueAgentInput, AssignSkillToAgentInput, AssignTicketToSprintInput,
AttachLiveAgentInput, AuthenticateSessionInput, ChangeAgentProfileInput, CloseTerminalInput,
CreateAgentInput, CreateMemoryInput, CreateSkillInput, CreateSprintInput, DeleteAgentInput,
DeleteEmbedderProfileInput, DeleteIssueInput, DeleteMemoryInput, DeleteSkillInput,
@ -2983,17 +2983,17 @@ async fn invoke_ticket_read(args: &Value, state: &BackendCore) -> Result<Value,
.await
.map_err(ErrorDto::from)?
.issue;
let fallback_carnet = issue.carnet.as_str().to_owned();
let carnet = if request.include_carnet.unwrap_or(false) {
Some(
state
.read_issue_carnet
.execute(ReadIssueCarnetInput { project, issue_ref })
.await
.map_err(ErrorDto::from)?
.carnet
.carnet
.as_str()
.to_owned(),
read_ticket_carnet_body_best_effort(
&state.read_issue_carnet,
&project,
issue_ref,
fallback_carnet,
)
.await
.map_err(ErrorDto::from)?,
)
} else {
None
@ -3001,6 +3001,25 @@ async fn invoke_ticket_read(args: &Value, state: &BackendCore) -> Result<Value,
serde_json::to_value(TicketDto::from_issue(issue, carnet)).map_err(serialization_error)
}
async fn read_ticket_carnet_body_best_effort(
read_carnet: &application::ReadIssueCarnet,
project: &Project,
issue_ref: domain::IssueRef,
fallback_carnet: String,
) -> Result<String, AppError> {
match read_carnet
.execute(ReadIssueCarnetInput {
project: project.clone(),
issue_ref,
})
.await
{
Ok(out) => Ok(out.carnet.carnet.as_str().to_owned()),
Err(AppError::NotFound(_)) => Ok(fallback_carnet),
Err(err) => Err(err),
}
}
async fn invoke_ticket_list(args: &Value, state: &BackendCore) -> Result<Value, ErrorDto> {
let request = required_request::<TicketListRequestDto>("ticket_list", args)?;
let project = resolve_project_readonly(&request.project_id, state).await?;
@ -7902,6 +7921,34 @@ mod tests {
assert_eq!(read["ref"], ticket_ref);
assert!(read["carnet"].is_string());
let project = state
.app
.open_project
.execute(OpenProjectInput {
project_id: ProjectId::from_uuid(Uuid::parse_str(&project_id).unwrap()),
})
.await
.expect("test project is readable")
.project;
std::fs::remove_file(
std::path::PathBuf::from(project.root.as_str())
.join(".ideai")
.join("tickets")
.join(ticket_ref.trim_start_matches('#'))
.join("carnet.md"),
)
.expect("test removes ticket carnet");
let (status, read_without_carnet_file) = invoke_request_for_test(
Arc::clone(&state),
&cookie,
"ticket_read",
json!({ "request": { "projectId": project_id, "ref": ticket_ref, "includeCarnet": true } }),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(read_without_carnet_file["ref"], ticket_ref);
assert_eq!(read_without_carnet_file["carnet"], "");
let (status, list) = invoke_request_for_test(
Arc::clone(&state),
&cookie,