feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic

Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).

Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 08:56:17 +02:00
parent 40982d44da
commit 287681c198
57 changed files with 1758 additions and 420 deletions

View File

@ -76,6 +76,7 @@ impl FakeContexts {
manifest: AgentManifest {
version: 1,
entries: Vec::new(),
orchestrator: None,
},
contents: HashMap::new(),
saves: 0,
@ -1058,7 +1059,10 @@ async fn swap_claude_to_codex_removes_claude_seed_and_projects_codex() {
"the orphan .claude seed must be removed; removed={:?}",
f.fs.removed()
);
assert!(!f.fs.has_file(&seed_path), "the seed is gone from the FS state");
assert!(
!f.fs.has_file(&seed_path),
"the seed is gone from the FS state"
);
// The relaunch projected the Codex sandbox config + args.
assert!(
@ -1066,7 +1070,10 @@ async fn swap_claude_to_codex_removes_claude_seed_and_projects_codex() {
"the relaunch must project the Codex config"
);
let toml = String::from_utf8(f.fs.read_file(&codex_path).unwrap()).unwrap();
assert!(toml.contains("sandbox_mode = \"workspace-write\""), "{toml}");
assert!(
toml.contains("sandbox_mode = \"workspace-write\""),
"{toml}"
);
assert!(
f.pty.last_spawn_args().contains(&"--sandbox".to_owned()),
"sandbox args folded into the relaunch spawn: {:?}",
@ -1102,7 +1109,10 @@ async fn swap_claude_to_claude_does_not_remove_seed() {
f.fs.removed()
);
// It is still present (re-clobbered by the relaunch's projection).
assert!(f.fs.has_file(&seed_path), "the seed survives the same-family swap");
assert!(
f.fs.has_file(&seed_path),
"the seed survives the same-family swap"
);
}
/// (3) **Codex→Claude**: Codex owns no Replace file ⇒ nothing is removed on the
@ -1138,13 +1148,19 @@ async fn swap_codex_to_claude_removes_nothing_and_keeps_codex_config() {
f.fs.removed()
);
// The co-owned Codex config is untouched by cleanup…
assert!(f.fs.has_file(&codex_path), "the .codex/config.toml is never deleted");
assert!(
f.fs.has_file(&codex_path),
"the .codex/config.toml is never deleted"
);
assert!(
!f.fs.removed().iter().any(|p| p == &codex_path),
"the .codex/config.toml is not in the removed list"
);
// …and the relaunch projected the new Claude seed.
assert!(f.fs.has_file(&seed_path), "the relaunch writes the Claude seed");
assert!(
f.fs.has_file(&seed_path),
"the relaunch writes the Claude seed"
);
}
/// (4a) **No-op (no registry)**: without a projector registry wired on the swap,
@ -1153,7 +1169,8 @@ async fn swap_codex_to_claude_removes_nothing_and_keeps_codex_config() {
async fn swap_without_registry_removes_nothing() {
let agent = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(1));
// The default fixture wires NO projector registry on the swap.
let f = fixture_with_profiles(&agent, vec![claude_profile(pid(1)), codex_profile(pid(2))]).await;
let f =
fixture_with_profiles(&agent, vec![claude_profile(pid(1)), codex_profile(pid(2))]).await;
let seed_path = format!("{}/{CLAUDE_SEED_REL}", run_dir_of(&agent.id));
f.fs.put(&seed_path, b"{\"permissions\":{}}");
@ -1251,7 +1268,8 @@ async fn swap_with_cleanup_preserves_pair_id_and_handoff() {
let run_dir = run_dir_of(&agent.id);
f.fs.put(&format!("{run_dir}/{CLAUDE_SEED_REL}"), b"{}");
let pair = pair_uuid();
f.handoffs.seed(&pair, "État au dernier tour : LP3-4.", Some("objectif"));
f.handoffs
.seed(&pair, "État au dernier tour : LP3-4.", Some("objectif"));
let host = nid(1);
seed_live_agent_session(&f.sessions, agent.id, host, sid(42));
@ -1273,7 +1291,10 @@ async fn swap_with_cleanup_preserves_pair_id_and_handoff() {
// …and the pair id was preserved on the persisted leaf (P8d invariant).
let (conv, running) = leaf_state(&f.fs, host).expect("leaf persisted");
assert_eq!(conv.as_deref(), Some(pair.as_str()), "pair id preserved");
assert!(!running, "engine running flag reset by invalidate_engine_link");
assert!(
!running,
"engine running flag reset by invalidate_engine_link"
);
// …and the handoff (keyed by that pair id) was re-injected into the new engine's
// convention file — proving the relaunch threaded the preserved pair id. (The