chore(wip): checkpoint P8/C avant chantier Codex inter-agents

Sauvegarde de l'arbre de travail en cours (persistance P8, conversations
C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le
support de la délégation inter-agents pour les profils Codex.

Le round-trip inter-agent question/réponse est couvert sans tokens par
les tests loopback existants (state::mcp_e2e_loopback_tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 21:42:53 +02:00
parent 4509f0db9d
commit fdcf16c387
76 changed files with 3783 additions and 1404 deletions

View File

@ -19,7 +19,8 @@
use domain::ids::ProfileId;
use domain::profile::{
AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport, StructuredAdapter,
AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport,
StructuredAdapter,
};
/// A fixed UUID namespace used to derive stable ids for reference profiles.
@ -161,7 +162,9 @@ mod mcp_tests {
let mcp = profile(slug).mcp.expect("mcp present");
assert_eq!(
mcp.config,
McpConfigStrategy::ConfigFile { target: ".mcp.json".to_owned() },
McpConfigStrategy::ConfigFile {
target: ".mcp.json".to_owned()
},
"profile `{slug}` should declare `.mcp.json`"
);
assert_eq!(mcp.transport, McpTransport::Stdio);

View File

@ -461,8 +461,8 @@ impl ChangeAgentProfile {
mutated_agent = Some(agent);
}
}
let agent = mutated_agent
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
let agent =
mutated_agent.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
let manifest = AgentManifest::new(manifest.version, entries)
.map_err(|e| AppError::Invalid(e.to_string()))?;
self.contexts
@ -524,8 +524,10 @@ impl ChangeAgentProfile {
// Capture the preserved pair id (stable across all leaves of this
// agent) — used to relaunch with handoff re-injection (P7).
if pair_id.is_none() {
if let Some(cid) =
named.tree.leaf(leaf_id).and_then(|l| l.conversation_id.clone())
if let Some(cid) = named
.tree
.leaf(leaf_id)
.and_then(|l| l.conversation_id.clone())
{
pair_id = Some(cid);
}
@ -1300,14 +1302,13 @@ impl LaunchAgent {
// pure partagée avec `resolve_conversation` (aucun couplage à
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
let pair_conversation_id =
input.conversation_id.clone().unwrap_or_else(|| {
ConversationId::for_pair(
ConversationParty::User,
ConversationParty::agent(agent.id),
)
.to_string()
});
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
ConversationId::for_pair(
ConversationParty::User,
ConversationParty::agent(agent.id),
)
.to_string()
});
return self
.launch_structured(
factory.as_ref(),
@ -1679,10 +1680,18 @@ impl LaunchAgent {
///
/// Dispatch by [`McpConfigStrategy`]:
/// - `ConfigFile { target }` → write `<run_dir>/<target>` (e.g. `.mcp.json`) with
/// the IdeA MCP server declaration. **Non-clobbering and best-effort**, exactly
/// like [`Self::seed_cli_permissions`]: an existing file (possibly user-edited)
/// is left untouched, and any write/exists failure is swallowed so it **never**
/// fails the launch.
/// the IdeA MCP server declaration. **Best-effort** (any write/exists failure is
/// swallowed so it **never** fails the launch), with two clobber regimes:
/// * with an injected [`McpRuntime`] (real app-tauri launch) the file is
/// **regenerated and clobbered on every (re)launch**, exactly like the
/// convention file. The declaration's `command` carries the **stable** IdeA
/// exe path (`$APPIMAGE`) and the project's live endpoint — both drift between
/// runs (the AppImage mount path changes at each remount/reboot), so a stale
/// `.mcp.json` would point the bridge at a dead binary/socket. `.mcp.json` is
/// IdeA-managed (not user-edited), so clobbering is safe;
/// * without a runtime (orchestrator / hot-swap / tests) only a degraded minimal
/// declaration is available, so the write stays **non-clobbering** (mirrors
/// [`Self::seed_cli_permissions`]) — never overwriting a real declaration.
/// - `Flag { flag }` → append the flag + run-dir config path to [`SpawnSpec::args`].
/// - `Env { var }` → append the variable (run-dir config path) to [`SpawnSpec::env`].
///
@ -1708,17 +1717,33 @@ impl LaunchAgent {
};
match &mcp.config {
domain::profile::McpConfigStrategy::ConfigFile { target } => {
// Non-clobbering, best-effort — mirrors `seed_cli_permissions`.
let path = RemotePath::new(join(run_dir, target));
match self.fs.exists(&path).await {
Ok(true) => {}
Ok(false) => {
let declaration = mcp_server_declaration(mcp.transport, runtime);
// Best-effort: a write failure must not fail the launch.
let declaration = mcp_server_declaration(mcp.transport, runtime);
match runtime {
// Real launch (app-tauri injected the runtime): the declaration
// carries the **stable** IdeA executable path (`$APPIMAGE`, resolved
// by `idea_exe_path`) and the project's live loopback endpoint. Both
// can drift between runs — the AppImage internal mount path changes
// at every remount/reboot — so the file MUST be regenerated and
// **clobbered** on every (re)launch, exactly like the convention file
// (`apply_injection`). `.mcp.json` is IdeA-managed, not user-edited,
// so there is nothing to preserve. Best-effort: a write failure must
// never fail the launch.
Some(_) => {
let _ = self.fs.write(&path, declaration.as_bytes()).await;
}
// An exists() probe failure is swallowed too (best-effort).
Err(_) => {}
// Internal launch (orchestrator / hot-swap / tests): we only have a
// degraded **minimal** declaration (no endpoint/project/requester).
// Stay non-clobbering — mirrors `seed_cli_permissions` — so we never
// overwrite a real declaration previously written by an app-tauri
// launch with the minimal one.
None => match self.fs.exists(&path).await {
Ok(true) => {}
Ok(false) => {
let _ = self.fs.write(&path, declaration.as_bytes()).await;
}
Err(_) => {}
},
}
}
domain::profile::McpConfigStrategy::Flag { flag } => {
@ -1976,6 +2001,7 @@ fn claude_settings_seed(project_root: &str) -> String {
]
}},
"skipDangerousModePermissionPrompt": true,
"enabledMcpjsonServers": ["idea"],
"sandbox": {{
"enabled": false
}}
@ -2347,11 +2373,7 @@ mod tests {
// Exact line format: `- [Title](.ideai/memory/slug.md) — hook (type)`.
assert!(doc.contains("- [Alpha](.ideai/memory/alpha-note.md) — the first hook (user)"));
assert!(
doc.contains(
"- [Beta](.ideai/memory/beta-note.md) — the second hook (reference)"
)
);
assert!(doc.contains("- [Beta](.ideai/memory/beta-note.md) — the second hook (reference)"));
// Deterministic order: first entry precedes the second.
let alpha_at = doc.find("[Alpha]").unwrap();
@ -2533,15 +2555,7 @@ mod tests {
MemoryType::Project,
)];
let h = handoff("Résumé : on a fini l'étape 2.", Some("Livrer le lot P7"));
let doc = compose_convention_file(
"/root",
"",
"# Persona",
&[],
&memory,
Some(&h),
false,
);
let doc = compose_convention_file("/root", "", "# Persona", &[], &memory, Some(&h), false);
assert!(
doc.contains("# Reprise de la conversation"),
@ -2636,6 +2650,8 @@ mod tests {
// Valid JSON.
let parsed: serde_json::Value = serde_json::from_str(&json).expect("seed is valid JSON");
assert_eq!(parsed["permissions"]["defaultMode"], "bypassPermissions");
// IdeA MCP server pre-approved so idea_* tools load without a prompt.
assert_eq!(parsed["enabledMcpjsonServers"][0], "idea");
assert_eq!(
parsed["permissions"]["additionalDirectories"][0],
"/home/me/proj"

View File

@ -20,19 +20,17 @@ pub use structured::send_blocking;
pub use catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
pub use resume::{
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
};
pub use lifecycle::{
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, HandoffProvider,
ProviderSessionProvider,
LaunchAgent,
LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, McpRuntime,
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredSessionDescriptor,
UpdateAgentContext,
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
ListAgentsOutput, McpRuntime, ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput,
ReadAgentContextOutput, StructuredSessionDescriptor, UpdateAgentContext,
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET,
};
pub use resume::{
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
};
pub use usecases::{
ConfigureProfiles, ConfigureProfilesInput, ConfigureProfilesOutput, DeleteProfile,
DeleteProfileInput, DetectProfiles, DetectProfilesInput, DetectProfilesOutput, FirstRunState,