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

@ -325,6 +325,28 @@ pub struct AgentProfile {
/// un profil sans motif sérialise exactement comme avant.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_ready_pattern: Option<String>,
/// Séquence de soumission écrite **après** le texte d'une délégation pour la
/// faire valider par la CLI (§20.3, fix Bug 1). Le portail d'écriture (front)
/// écrit d'abord le texte (sans `\n`, pour esquiver la détection de paste de
/// la TUI), puis **cette séquence seule** après un court délai.
///
/// `None` ⇒ défaut `"\r"` appliqué **au point d'usage** (front), jamais codé
/// en dur dans le domaine (model-agnostic, déclaratif). Une autre TUI peut
/// exiger une séquence différente → c'est précisément pourquoi c'est donnée.
///
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** : un profil
/// sans cette clé sérialise exactement comme avant.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submit_sequence: Option<String>,
/// Délai (ms) entre l'écriture du texte et celle de [`Self::submit_sequence`]
/// (§20.3, fix Bug 1). Évite que la TUI absorbe la soumission comme un paste.
///
/// `None` ⇒ défaut (~60 ms) appliqué **au point d'usage** (front), jamais codé
/// en dur dans le domaine.
///
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** de sérialisation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submit_delay_ms: Option<u32>,
}
/// Embedding strategy of an [`EmbedderProfile`] (LOT C, étage 2 vectoriel).
@ -461,6 +483,8 @@ impl AgentProfile {
structured_adapter: None,
mcp: None,
prompt_ready_pattern: None,
submit_sequence: None,
submit_delay_ms: None,
})
}
@ -491,6 +515,23 @@ impl AgentProfile {
self
}
/// Builder : fixe la [`Self::submit_sequence`] (§20.3, fix Bug 1) et renvoie le
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
/// profils qui s'en remettent au défaut `"\r"` ne l'appellent simplement pas.
#[must_use]
pub fn with_submit_sequence(mut self, sequence: impl Into<String>) -> Self {
self.submit_sequence = Some(sequence.into());
self
}
/// Builder : fixe le [`Self::submit_delay_ms`] (§20.3, fix Bug 1) et renvoie le
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel).
#[must_use]
pub const fn with_submit_delay_ms(mut self, delay_ms: u32) -> Self {
self.submit_delay_ms = Some(delay_ms);
self
}
/// Indique si ce profil peut être **proposé à la sélection/création** d'un
/// agent (§17.3, lot D7). Source **unique** de vérité : un profil n'est
/// sélectionnable que s'il porte un [`StructuredAdapter`], c'est-à-dire s'il
@ -611,10 +652,9 @@ mod mcp_tests {
#[test]
fn mcp_config_strategy_uses_tagged_camel_case() {
// The `strategy` tag and camelCase rename are part of the wire contract.
let json = serde_json::to_string(
&McpConfigStrategy::config_file(".mcp.json").expect("valid"),
)
.expect("serialise");
let json =
serde_json::to_string(&McpConfigStrategy::config_file(".mcp.json").expect("valid"))
.expect("serialise");
assert!(json.contains("\"strategy\":\"configFile\""), "got: {json}");
}
@ -651,7 +691,12 @@ mod mcp_tests {
#[test]
fn config_file_accepts_safe_relative_target() {
let s = McpConfigStrategy::config_file(".mcp.json").expect("safe relative");
assert_eq!(s, McpConfigStrategy::ConfigFile { target: ".mcp.json".to_owned() });
assert_eq!(
s,
McpConfigStrategy::ConfigFile {
target: ".mcp.json".to_owned()
}
);
}
#[test]
@ -663,7 +708,12 @@ mod mcp_tests {
#[test]
fn flag_accepts_non_empty() {
let s = McpConfigStrategy::flag("--mcp-config {path}").expect("non-empty");
assert_eq!(s, McpConfigStrategy::Flag { flag: "--mcp-config {path}".to_owned() });
assert_eq!(
s,
McpConfigStrategy::Flag {
flag: "--mcp-config {path}".to_owned()
}
);
}
#[test]
@ -675,7 +725,12 @@ mod mcp_tests {
#[test]
fn env_accepts_valid_name() {
let s = McpConfigStrategy::env("IDEA_MCP_SERVER").expect("valid");
assert_eq!(s, McpConfigStrategy::Env { var: "IDEA_MCP_SERVER".to_owned() });
assert_eq!(
s,
McpConfigStrategy::Env {
var: "IDEA_MCP_SERVER".to_owned()
}
);
}
// -- Lot C5 : prompt_ready_pattern (détection retour-de-prompt) --------------
@ -721,4 +776,60 @@ mod mcp_tests {
assert_eq!(profile, back);
assert_eq!(back.prompt_ready_pattern.as_deref(), Some("\n> "));
}
// -- §20 : submit_sequence / submit_delay_ms (portail d'écriture) ------------
#[test]
fn profile_default_has_no_submit_fields() {
let p = profile_without_mcp();
assert!(p.submit_sequence.is_none());
assert!(p.submit_delay_ms.is_none());
}
#[test]
fn profile_without_submit_fields_omits_keys_in_json() {
let json = serde_json::to_string(&profile_without_mcp()).expect("serialise");
assert!(
!json.contains("submitSequence"),
"no submitSequence key when None (zero regression); got: {json}"
);
assert!(
!json.contains("submitDelayMs"),
"no submitDelayMs key when None (zero regression); got: {json}"
);
}
#[test]
fn legacy_json_without_submit_fields_deserialises_to_none() {
let legacy = r#"{
"id": "00000000-0000-0000-0000-000000000000",
"name": "Dev",
"command": "claude",
"args": [],
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
"detect": null,
"cwdTemplate": "{agentRunDir}"
}"#;
let p: AgentProfile = serde_json::from_str(legacy).expect("legacy deserialise");
assert!(p.submit_sequence.is_none());
assert!(p.submit_delay_ms.is_none());
}
#[test]
fn with_submit_fields_set_and_round_trip_camel_case() {
let p = profile_without_mcp()
.with_submit_sequence("\r")
.with_submit_delay_ms(60);
assert_eq!(p.submit_sequence.as_deref(), Some("\r"));
assert_eq!(p.submit_delay_ms, Some(60));
let json = serde_json::to_string(&p).expect("serialise");
assert!(json.contains("submitSequence"), "key present: {json}");
assert!(json.contains("submitDelayMs"), "key present: {json}");
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
assert_eq!(p, back);
assert_eq!(back.submit_sequence.as_deref(), Some("\r"));
assert_eq!(back.submit_delay_ms, Some(60));
}
}