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:
@ -194,6 +194,9 @@ pub struct OrchestratorRequest {
|
||||
/// `memory.write`, cadrage C7). Required by those actions, ignored otherwise.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
/// Optional optimistic-concurrency version for `context.update`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub if_match: Option<String>,
|
||||
/// Target memory note slug for the memory tools (`memory.read`/`memory.write`,
|
||||
/// cadrage C7). Required by `memory.write`; optional for `memory.read` (absent ⇒
|
||||
/// the aggregated index). Ignored by the other actions.
|
||||
@ -342,6 +345,16 @@ pub enum OrchestratorCommand {
|
||||
/// The proposing party (handshake identity).
|
||||
requester: ConversationParty,
|
||||
},
|
||||
/// Directly update the global project context. Strict, orchestrator-only
|
||||
/// counterpart to [`Self::ProposeContext`].
|
||||
UpdateProjectContext {
|
||||
/// The new Markdown body.
|
||||
content: String,
|
||||
/// Optional expected current version.
|
||||
if_match: Option<String>,
|
||||
/// The writing party (handshake identity).
|
||||
requester: ConversationParty,
|
||||
},
|
||||
/// Read a memory note under the [`crate::fileguard::FileGuard`] (cadrage C7).
|
||||
/// `slug` absent = the aggregated `MEMORY.md` index; otherwise one note.
|
||||
ReadMemory {
|
||||
@ -500,6 +513,11 @@ impl OrchestratorRequest {
|
||||
content: self.require("content", action, self.content.as_deref())?,
|
||||
requester: self.requester_party(),
|
||||
}),
|
||||
"context.update" => Ok(OrchestratorCommand::UpdateProjectContext {
|
||||
content: self.require("content", action, self.content.as_deref())?,
|
||||
if_match: self.if_match.clone(),
|
||||
requester: self.requester_party(),
|
||||
}),
|
||||
"memory.read" => Ok(OrchestratorCommand::ReadMemory {
|
||||
slug: self.optional_slug(),
|
||||
requester: self.requester_party(),
|
||||
@ -1089,6 +1107,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_update_command_parses_content_and_if_match() {
|
||||
let uid = uuid::Uuid::from_u128(42);
|
||||
let r = req(&format!(
|
||||
r##"{{ "type":"context.update", "requestedBy":"{uid}", "content":"# body", "ifMatch":"abc123" }}"##
|
||||
));
|
||||
assert_eq!(
|
||||
r.validate().unwrap(),
|
||||
OrchestratorCommand::UpdateProjectContext {
|
||||
content: "# body".to_owned(),
|
||||
if_match: Some("abc123".to_owned()),
|
||||
requester: ConversationParty::agent(AgentId::from_uuid(uid)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_update_requires_content() {
|
||||
let missing = req(r#"{ "type":"context.update", "ifMatch":"abc123" }"#);
|
||||
assert_eq!(
|
||||
missing.validate(),
|
||||
Err(OrchestratorError::MissingField {
|
||||
action: "context.update".to_owned(),
|
||||
field: "content".to_owned(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_read_optional_slug() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user