feat(orchestrator): backstop no-reply du rendez-vous inter-agents
Remédiation du wedge persistant après échec live du fix Finding A (77e62e5).
Détecte la fin de tour d'un agent sollicité qui n'a pas appelé idea_reply et
débloque l'agent demandeur au lieu de le laisser en attente indéfinie.
Ajoute le suivi de tour côté inspector Claude (claude_turn_watcher) et la
résolution des chemins de session (claude_paths), câblés dans le rendez-vous
idea_ask_agent ⇄ idea_reply.
Build workspace vert, suite complète verte, zéro warning.
Backstop NON prouvé levé en live : merge develop interdit tant que la levée
du wedge n'est pas validée en conditions réelles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
102
crates/infrastructure/src/inspector/claude_paths.rs
Normal file
102
crates/infrastructure/src/inspector/claude_paths.rs
Normal file
@ -0,0 +1,102 @@
|
||||
//! Shared Claude Code transcript-path helpers.
|
||||
//!
|
||||
//! Claude Code records each conversation as a JSONL transcript under the user's
|
||||
//! home directory:
|
||||
//!
|
||||
//! ```text
|
||||
//! <home>/.claude/projects/<encoded-cwd>/<engine-session-id>.jsonl
|
||||
//! ```
|
||||
//!
|
||||
//! where `<encoded-cwd>` is the agent's working directory with its path separators
|
||||
//! flattened to `-` (see [`encode_cwd`]). Both the [`ClaudeTranscriptInspector`] (which
|
||||
//! reads one known transcript) and the [`ClaudeTranscriptTurnWatcher`] (which watches the
|
||||
//! whole per-agent project dir) need to derive these paths, so the convention lives in
|
||||
//! **one** small, tested place: if Claude's exact encoding ever differs, only this seam
|
||||
//! changes.
|
||||
//!
|
||||
//! [`ClaudeTranscriptInspector`]: super::ClaudeTranscriptInspector
|
||||
//! [`ClaudeTranscriptTurnWatcher`]: super::ClaudeTranscriptTurnWatcher
|
||||
|
||||
use domain::ports::RemotePath;
|
||||
use domain::profile::{AgentProfile, ContextInjection};
|
||||
use domain::project::ProjectPath;
|
||||
|
||||
/// Encodes a cwd the way Claude Code names its per-project transcript folder.
|
||||
///
|
||||
/// **Assumption (documented, isolated, and unit-tested):** Claude flattens the
|
||||
/// absolute cwd into a single directory segment by replacing each path separator
|
||||
/// (`/` or `\`) — and the leading separator — with `-`. So
|
||||
/// `/home/anthony/Documents/Projects/IdeA` becomes
|
||||
/// `-home-anthony-Documents-Projects-IdeA`. We keep this convention in one small,
|
||||
/// tested function so that if Claude's exact encoding differs in some edge case, only
|
||||
/// this seam needs to change. We do not attempt to encode `.` or other characters, as
|
||||
/// the common case (clean absolute project paths) is covered and over-encoding would
|
||||
/// risk pointing at the wrong folder.
|
||||
pub(crate) fn encode_cwd(cwd: &str) -> String {
|
||||
cwd.chars()
|
||||
.map(|c| if c == '/' || c == '\\' { '-' } else { c })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `<home>/.claude/projects/<encoded-cwd>` — the directory holding **all** of one
|
||||
/// agent's conversation transcripts for `cwd` (its isolated run dir). Trailing path
|
||||
/// separators on `home_dir` are trimmed first.
|
||||
pub(crate) fn claude_project_dir(home_dir: &str, cwd: &ProjectPath) -> RemotePath {
|
||||
let base = home_dir.trim_end_matches(['/', '\\']);
|
||||
let encoded = encode_cwd(cwd.as_str());
|
||||
RemotePath::new(format!("{base}/.claude/projects/{encoded}"))
|
||||
}
|
||||
|
||||
/// `<home>/.claude/projects/<encoded-cwd>/<conversation-id>.jsonl`.
|
||||
pub(crate) fn claude_transcript_path(
|
||||
home_dir: &str,
|
||||
conversation_id: &str,
|
||||
cwd: &ProjectPath,
|
||||
) -> RemotePath {
|
||||
let dir = claude_project_dir(home_dir, cwd);
|
||||
RemotePath::new(format!("{}/{conversation_id}.jsonl", dir.0))
|
||||
}
|
||||
|
||||
/// Recognises a Claude profile by its conventional context file `CLAUDE.md`, mirroring
|
||||
/// the detection in `application::agent::lifecycle`. Shared by the inspector and the
|
||||
/// turn watcher so both agree on what they can read.
|
||||
pub(crate) fn supports_claude(profile: &AgentProfile) -> bool {
|
||||
matches!(
|
||||
&profile.context_injection,
|
||||
ContextInjection::ConventionFile { target }
|
||||
if target
|
||||
.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.unwrap_or(target)
|
||||
.eq_ignore_ascii_case("CLAUDE.md")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encode_cwd_flattens_separators_to_dash() {
|
||||
assert_eq!(
|
||||
encode_cwd("/home/anthony/Documents/Projects/IdeA"),
|
||||
"-home-anthony-Documents-Projects-IdeA"
|
||||
);
|
||||
assert_eq!(encode_cwd("/a/b"), "-a-b");
|
||||
// Windows-style separators flatten too.
|
||||
assert_eq!(encode_cwd("C:\\Users\\me"), "C:-Users-me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_dir_and_transcript_path_compose() {
|
||||
let cwd = ProjectPath::new("/a/b").expect("valid path");
|
||||
assert_eq!(
|
||||
claude_project_dir("/home/me/", &cwd).0,
|
||||
"/home/me/.claude/projects/-a-b"
|
||||
);
|
||||
assert_eq!(
|
||||
claude_transcript_path("/home/me", "conv-1", &cwd).0,
|
||||
"/home/me/.claude/projects/-a-b/conv-1.jsonl"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user