feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139

État d'intégration confiné à la branche batch. Les tickets #119 (skills →
capacités agent découvrables), #122 (override permissions par défaut), #131
(effort par agent/presets) et #132 (outil MCP d'édition du contexte projet)
sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance
plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de
câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs,
backend/dto.rs), inséparable sans staging interactif (indisponible ici).

Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée.
NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:06:23 +02:00
parent 22c6bd803d
commit 171c6c923c
59 changed files with 3654 additions and 291 deletions

View File

@ -47,7 +47,8 @@ use crate::error::AppError;
use crate::orchestrator::rendezvous::{run_inactivity_watchdog, WatchdogOutcome};
use crate::orchestrator::{
ProposeContext, ProposeContextInput, ProposeOutcome, ReadContext, ReadContextInput, ReadMemory,
ReadMemoryInput, WriteMemory, WriteMemoryInput,
ReadMemoryInput, UpdateProjectContext, UpdateProjectContextInput, WriteMemory,
WriteMemoryInput,
};
use crate::skill::{CreateSkill, CreateSkillInput, ReadSkill, ReadSkillInput};
use crate::terminal::{CloseTerminal, CloseTerminalInput, StructuredSessions, TerminalSessions};
@ -534,6 +535,8 @@ pub struct ContextGuardUseCases {
pub read_context: Arc<ReadContext>,
/// Proposition/écriture d'un contexte `.md` IdeA sous le garde.
pub propose_context: Arc<ProposeContext>,
/// Écriture directe stricte du contexte projet global.
pub update_project_context: Arc<UpdateProjectContext>,
/// Lecture mémoire sous read-lease.
pub read_memory: Arc<ReadMemory>,
/// Écriture mémoire sous write-lease.
@ -1241,6 +1244,14 @@ impl OrchestratorService {
self.propose_context(project, target, content, requester)
.await
}
OrchestratorCommand::UpdateProjectContext {
content,
if_match,
requester,
} => {
self.update_project_context(project, content, if_match, requester)
.await
}
OrchestratorCommand::ReadMemory { slug, requester } => {
self.read_memory(project, slug, requester).await
}
@ -1363,7 +1374,7 @@ impl OrchestratorService {
target: Option<String>,
requester: ConversationParty,
) -> Result<OrchestratorOutcome, AppError> {
let md = self
let out = self
.require_context_guard()?
.read_context
.execute(ReadContextInput {
@ -1372,9 +1383,13 @@ impl OrchestratorService {
requester,
})
.await?;
let mut text = out.content.into_string();
if let Some(version) = &out.version {
text.push_str(&format!("\n\n<!-- idea-context-version: {version} -->"));
}
Ok(OrchestratorOutcome {
detail: format!("read {} context", target.as_deref().unwrap_or("project")),
reply: Some(md.into_string()),
reply: Some(text),
})
}
@ -1411,6 +1426,31 @@ impl OrchestratorService {
})
}
/// `context.update` → strict direct write of the global project context.
async fn update_project_context(
&self,
project: &Project,
content: String,
if_match: Option<String>,
requester: ConversationParty,
) -> Result<OrchestratorOutcome, AppError> {
let out = self
.require_context_guard()?
.update_project_context
.execute(UpdateProjectContextInput {
project: project.clone(),
content,
if_match,
requester,
})
.await?;
Ok(OrchestratorOutcome {
detail: format!("wrote project context (version {})", out.new_version),
reply: None,
})
}
/// `memory.read` → reads a note (or the index) under a shared read-lease; the
/// content is returned inline in the outcome's `reply`.
async fn read_memory(
@ -2783,6 +2823,7 @@ impl OrchestratorService {
description: None,
content,
scope,
kind: domain::SkillKind::Workflow,
project_root: project.root.clone(),
})
.await?;