feat(skill-awareness): T1→T5 — manifeste de skills + outil MCP idea_skill_read

Surface les skills assignés à un agent « à la MCP » : description sur l'entité
Skill (+ effective_description fallback), section « # Skills disponibles » haute
altitude dans le convention-file (mode MCP), outil read-only idea_skill_read
(résolution projet→global), use case ReadSkill (port SkillStore existant), et
câblage au composition root. Dump legacy du corps complet conservé en mode
sans-MCP (zéro régression). Rétro-compat index.json (serde default).

Tests : domain+application+infrastructure 1212 passed / 0 failed (23 ajoutés).
Reste : T6 (champ description front) + T7 (e2e après rebuild AppImage).

NB topologie : commit réalisé par l'orchestrateur car l'agent Git était
injoignable (bug de livraison cold-start) ; à faire relire/rebaser par Git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 15:08:33 +02:00
parent 7db444a31d
commit 11dc8d7ba3
13 changed files with 758 additions and 27 deletions

View File

@ -43,7 +43,7 @@ use crate::orchestrator::{
ProposeContext, ProposeContextInput, ProposeOutcome, ReadContext, ReadContextInput, ReadMemory,
ReadMemoryInput, WriteMemory, WriteMemoryInput,
};
use crate::skill::{CreateSkill, CreateSkillInput};
use crate::skill::{CreateSkill, CreateSkillInput, ReadSkill, ReadSkillInput};
use crate::terminal::{CloseTerminal, CloseTerminalInput, StructuredSessions, TerminalSessions};
/// Default terminal geometry for an orchestrator-launched agent cell. The UI
@ -303,6 +303,11 @@ pub struct OrchestratorService {
/// [`Self::with_memory_harvest`] ; `None` ⇒ aucun harvest (zéro régression). Un
/// échec de harvest ne transforme **jamais** un succès de délégation en erreur.
memory_harvest: Option<Arc<HarvestMemoryFromTurn>>,
/// Use case de lecture d'un skill **par nom** (`idea_skill_read`, feature
/// « skills à la MCP »). Compose le port `SkillStore` existant (aucun nouveau
/// port). Injecté via [`Self::with_read_skill`] ; `None` ⇒ la commande
/// `skill.read` renvoie une erreur typée (call sites/tests legacy restent verts).
read_skill: Option<Arc<ReadSkill>>,
}
/// Bundle des quatre use cases C7 sous [`domain::fileguard::FileGuard`], injectés
@ -367,9 +372,19 @@ impl OrchestratorService {
clock: None,
structured: None,
memory_harvest: None,
read_skill: None,
}
}
/// Branche le use case [`ReadSkill`] (`skill.read`/`idea_skill_read`). Builder
/// additif (signature de [`Self::new`] inchangée) ; `None` ⇒ la commande
/// renvoie une erreur typée « not configured ».
#[must_use]
pub fn with_read_skill(mut self, read_skill: Arc<ReadSkill>) -> Self {
self.read_skill = Some(read_skill);
self
}
/// Branche le registre des [`StructuredSessions`] (§17.5) pour router un `ask`
/// vers une cible **structurée** sur sa propre session. Builder additif (signature
/// de [`Self::new`] inchangée) ; `None` ⇒ chemin PTY/mailbox legacy uniquement.
@ -588,6 +603,9 @@ impl OrchestratorService {
OrchestratorCommand::ReadMemory { slug, requester } => {
self.read_memory(project, slug, requester).await
}
OrchestratorCommand::ReadSkill { name, requester } => {
self.read_skill(project, name, requester).await
}
OrchestratorCommand::WriteMemory {
slug,
content,
@ -682,6 +700,31 @@ impl OrchestratorService {
})
}
/// `skill.read` → resolves a skill by name (project scope first, then global)
/// and returns its Markdown body inline in the outcome's `reply`. Read-only:
/// `requester` is accepted for symmetry with the other read tools but skill
/// reads take no [`domain::fileguard::FileGuard`] lease.
async fn read_skill(
&self,
project: &Project,
name: String,
_requester: ConversationParty,
) -> Result<OrchestratorOutcome, AppError> {
let read_skill = self.read_skill.as_deref().ok_or_else(|| {
AppError::Invalid("the idea_skill_read tool is not configured".to_owned())
})?;
let md = read_skill
.execute(ReadSkillInput {
name: name.clone(),
project_root: project.root.clone(),
})
.await?;
Ok(OrchestratorOutcome {
detail: format!("read skill {name}"),
reply: Some(md.into_string()),
})
}
/// `memory.write` → writes a note under an exclusive write-lease.
async fn write_memory(
&self,
@ -1867,6 +1910,9 @@ impl OrchestratorService {
.create_skill
.execute(CreateSkillInput {
name: name.clone(),
// `idea_create_skill` carries no description argument; the affordance
// falls back to the body's first line (see `effective_description`).
description: None,
content,
scope,
project_root: project.root.clone(),