feat(memory): récolte automatique contrôlée de la mémoire (Lot E1)

Câble la récolte automatique de mémoire de bout en bout, sous contrôle
explicite, du domaine jusqu'à l'UI :

- domain : modèle `memory_harvest` (candidats de récolte, décision) +
  exports `lib.rs`.
- application : use case `memory/harvest` branché dans `memory/mod`,
  `lib.rs`, le cycle de vie d'agent (`agent/lifecycle`) et
  l'orchestrateur (`orchestrator/service`).
- app-tauri : wiring runtime dans `state`.
- frontend : DTO `domain/index` + hook `useMemory`.

Couverture : domain `memory_harvest` (14), application `memory_harvest`
(5) et `orchestrator_service` (récolte, 4), plus patch test-only du mock
gateway `workState` (`mock.test.ts`) et UI `memory.test`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 02:00:05 +02:00
parent 33e65dec74
commit b12081be18
14 changed files with 1155 additions and 20 deletions

View File

@ -2624,6 +2624,7 @@ pub(crate) fn compose_convention_file(
);
}
out.push_str(skill_awareness(mcp_enabled));
out.push_str(memory_awareness());
out.push_str("---\n\n");
if !project_context.trim().is_empty() {
@ -2697,6 +2698,18 @@ fn skill_awareness(mcp_enabled: bool) -> &'static str {
}
}
/// The auto-memory harvest directive (Lot E1) injected in the `# Orchestration
/// IdeA` block, right after [`skill_awareness`]. Independent of MCP: the agent emits
/// the directive in its final reply text either way (IdeA harvests it from the
/// recorded `Response`, not via a tool call), so a single wording serves both modes.
fn memory_awareness() -> &'static str {
"**Mémoire IdeA** : si une décision, convention, préférence utilisateur ou fait projet \
durable mérite d'être capitalisé, ajoute à ta réponse finale un bloc fenced `idea-memory` \
avec `slug`, `title`, `type`, `description`, puis `---` et le corps Markdown. N'en écris pas \
pour les tâches temporaires, logs, tickets, résultats transitoires ou données sensibles ; \
réutilise le même `slug` pour remplacer une note existante.\n\n"
}
/// Renders a [`MemoryType`] as its stable lowercase label for the convention-file
/// memory section (`user`/`feedback`/`project`/`reference`).
#[must_use]
@ -2885,6 +2898,35 @@ mod tests {
);
}
#[test]
fn compose_convention_file_carries_memory_harvest_directive_both_modes() {
// The auto-memory harvest directive (Lot E1) is present in both MCP and
// file-protocol modes, positioned right after the Skills awareness and
// before the orchestration block's closing separator / project context.
for mcp_enabled in [true, false] {
let doc =
compose_convention_file("/root", "ctx", "# Persona", &[], &[], None, mcp_enabled);
assert!(
doc.contains("**Mémoire IdeA**"),
"memory harvest directive present (mcp_enabled={mcp_enabled})"
);
assert!(
doc.contains("bloc fenced `idea-memory`")
&& doc.contains("réutilise le même `slug`"),
"directive carries the idea-memory wording (mcp_enabled={mcp_enabled})"
);
// Position: after Skills awareness, before the project-context section.
let skills_at = doc.find("**Skills IdeA**").unwrap();
let memory_at = doc.find("**Mémoire IdeA**").unwrap();
let context_at = doc.find("# Contexte projet").unwrap();
assert!(
skills_at < memory_at && memory_at < context_at,
"order: skills awareness -> memory directive -> project context \
(mcp_enabled={mcp_enabled})"
);
}
}
#[test]
fn compose_convention_file_skill_awareness_uses_mcp_or_file_creation_path() {
let mcp_doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true);