From 99c3791ce331e66967b194c4e544dea9c0356283 Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:48:58 -0500 Subject: [PATCH 1/3] fix(migration): preserve literal external-agent references --- .../external-agent-migration/src/rewrite.rs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/codex-rs/external-agent-migration/src/rewrite.rs b/codex-rs/external-agent-migration/src/rewrite.rs index e1558850ddad..f0951d40a4d0 100644 --- a/codex-rs/external-agent-migration/src/rewrite.rs +++ b/codex-rs/external-agent-migration/src/rewrite.rs @@ -65,7 +65,10 @@ fn replace_with_boundaries(input: &str, needle: &str, replacement: &str) -> Stri let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before && boundary_after { + if boundary_before + && boundary_after + && !is_literal_reference_match(bytes, start, end) + { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end; @@ -104,7 +107,10 @@ fn replace_case_insensitive_with_boundaries( let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before && boundary_after { + if boundary_before + && boundary_after + && !is_literal_reference_match(bytes, start, end) + { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end; @@ -121,6 +127,32 @@ fn replace_case_insensitive_with_boundaries( output } +/// Product names embedded in filesystem paths, URLs, or dotted identifiers are +/// literal references to the source tool, not prose that should be retargeted. +fn is_literal_reference_match(bytes: &[u8], start: usize, end: usize) -> bool { + if let Some(before) = start.checked_sub(1).and_then(|idx| bytes.get(idx)) + && matches!(*before, b'/' | b'\\' | b'.') + { + return true; + } + + let Some(after) = bytes.get(end) else { + return false; + }; + if matches!(*after, b'/' | b'\\') { + return true; + } + + *after == b'.' + && bytes + .get(end + 1) + .is_some_and(|byte| is_reference_suffix_byte(*byte)) +} + +fn is_reference_suffix_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') +} + fn is_word_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || byte == b'_' } From 0e0122fcc058b880d8152a5fa5343f5cb1669d76 Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:49:22 -0500 Subject: [PATCH 2/3] test(migration): cover literal external-agent references --- .../src/rewrite_tests.rs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/codex-rs/external-agent-migration/src/rewrite_tests.rs b/codex-rs/external-agent-migration/src/rewrite_tests.rs index 8b103cb3e11c..55740f8e5830 100644 --- a/codex-rs/external-agent-migration/src/rewrite_tests.rs +++ b/codex-rs/external-agent-migration/src/rewrite_tests.rs @@ -4,6 +4,17 @@ use pretty_assertions::assert_eq; const PROFILE: RewriteProfile = RewriteProfile::new("SOURCE.md", &["source agent"]) .with_case_sensitive_term_variants(&["Source"]); +const CLAUDE_PROFILE: RewriteProfile = RewriteProfile::new( + "CLAUDE.md", + &[ + "claude code", + "claude-code", + "claude_code", + "claudecode", + "claude", + ], +); + #[test] fn rewrites_terms_only_at_word_boundaries() { assert_eq!( @@ -11,3 +22,47 @@ fn rewrites_terms_only_at_word_boundaries() { "AGENTS.md Codex Codex source_agent" ); } + +#[test] +fn preserves_source_specific_filesystem_references() { + assert_eq!( + PROFILE.rewrite( + "Read ./SOURCE.md, C:\\repo\\SOURCE.md, ~/.Source/config, and .Source before using Source." + ), + "Read ./SOURCE.md, C:\\repo\\SOURCE.md, ~/.Source/config, and .Source before using Codex." + ); +} + +#[test] +fn preserves_source_specific_urls_and_dotted_identifiers() { + assert_eq!( + PROFILE.rewrite( + "See https://Source.dev/docs, docs.Source.dev, and plugin.Source.config. Source." + ), + "See https://Source.dev/docs, docs.Source.dev, and plugin.Source.config. Codex." + ); +} + +#[test] +fn rewrites_standalone_doc_name_but_not_path_qualified_doc_name() { + assert_eq!( + PROFILE.rewrite("SOURCE.md lives beside ../SOURCE.md and $HOME/SOURCE.md."), + "AGENTS.md lives beside ../SOURCE.md and $HOME/SOURCE.md." + ); +} + +#[test] +fn preserves_claude_skill_paths_while_retargeting_prose() { + let input = concat!( + "Session transcripts live in ~/.claude/projects//.\n", + "See also .claude/commands/example.md and C:\\Users\\me\\.claude\\plans\\today.md.\n", + "Use Claude Code UI and read CLAUDE.md." + ); + let expected = concat!( + "Session transcripts live in ~/.claude/projects//.\n", + "See also .claude/commands/example.md and C:\\Users\\me\\.claude\\plans\\today.md.\n", + "Use Codex UI and read AGENTS.md." + ); + + assert_eq!(CLAUDE_PROFILE.rewrite(input), expected); +} From 7a57193110912b4b8c2343beede3f6cda0cf771f Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:54:16 -0500 Subject: [PATCH 3/3] style(migration): apply rustfmt --- codex-rs/external-agent-migration/src/rewrite.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/codex-rs/external-agent-migration/src/rewrite.rs b/codex-rs/external-agent-migration/src/rewrite.rs index f0951d40a4d0..4a4b2c0ae97d 100644 --- a/codex-rs/external-agent-migration/src/rewrite.rs +++ b/codex-rs/external-agent-migration/src/rewrite.rs @@ -65,10 +65,7 @@ fn replace_with_boundaries(input: &str, needle: &str, replacement: &str) -> Stri let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before - && boundary_after - && !is_literal_reference_match(bytes, start, end) - { + if boundary_before && boundary_after && !is_literal_reference_match(bytes, start, end) { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end; @@ -107,10 +104,7 @@ fn replace_case_insensitive_with_boundaries( let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before - && boundary_after - && !is_literal_reference_match(bytes, start, end) - { + if boundary_before && boundary_after && !is_literal_reference_match(bytes, start, end) { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end;