use crate::config::OrchestratorConfig;
use crate::error::{OrchestratorError, Result};
use crate::history::{ResolveAttempt, ResolveContext};
use crate::vcs::{VcsBackend, WorkspaceManager};
use std::time::Instant;
use tokio::sync::mpsc;
use tracing::{info, warn};
#[cfg(test)]
pub(crate) mod fixtures;
use super::events::{send_event, ParallelEvent};
use super::resolve_state::{
self, BatchState, GitResolveEvidence, MergeAuthorizationLatch, ResolveEvidence,
SequentialMergeItem,
};
use super::types::{resolve_failure_detail, ResolveFailureClassification};
#[derive(Debug)]
pub enum ResolveFailure {
Exhausted {
attempts: u32,
classification: ResolveFailureClassification,
detail: String,
},
Unclassified(OrchestratorError),
}
impl std::fmt::Display for ResolveFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exhausted { detail, .. } => write!(f, "{}", detail),
Self::Unclassified(error) => write!(f, "{}", error),
}
}
}
impl From<OrchestratorError> for ResolveFailure {
fn from(error: OrchestratorError) -> Self {
Self::Unclassified(error)
}
}
struct AutoResolveGuard {
counter: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl AutoResolveGuard {
fn new(counter: std::sync::Arc<std::sync::atomic::AtomicUsize>) -> Self {
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Self { counter }
}
}
impl Drop for AutoResolveGuard {
fn drop(&mut self) {
self.counter
.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
pub(super) async fn fail_resolve(
event_tx: &Option<mpsc::Sender<ParallelEvent>>,
change_ids: &[String],
attempts: u32,
classification: ResolveFailureClassification,
summary: String,
) -> ResolveFailure {
let detail = resolve_failure_detail(attempts, classification, &summary);
send_event(
event_tx,
ParallelEvent::ConflictResolutionFailed {
error: detail.clone(),
},
)
.await;
for change_id in change_ids {
send_event(
event_tx,
ParallelEvent::ResolveFailed {
change_id: change_id.to_string(),
error: detail.clone(),
},
)
.await;
}
ResolveFailure::Exhausted {
attempts,
classification,
detail,
}
}
pub async fn detect_conflicts(workspace_manager: &dyn WorkspaceManager) -> Result<Vec<String>> {
workspace_manager
.detect_conflicts()
.await
.map_err(OrchestratorError::from)
}
pub async fn get_vcs_status(workspace_manager: &dyn WorkspaceManager) -> Result<String> {
workspace_manager
.get_status()
.await
.map_err(OrchestratorError::from)
}
pub async fn get_vcs_log_for_revisions(
workspace_manager: &dyn WorkspaceManager,
revisions: &[String],
) -> Result<String> {
workspace_manager
.get_log_for_revisions(revisions)
.await
.map_err(OrchestratorError::from)
}
#[allow(clippy::too_many_arguments)]
pub async fn resolve_conflicts_with_retry(
workspace_manager: &dyn WorkspaceManager,
config: &OrchestratorConfig,
event_tx: &Option<mpsc::Sender<ParallelEvent>>,
revisions: &[String],
change_ids: &[String],
vcs_error: &str,
max_retries: u32,
ai_runner: crate::ai_command_runner::AiCommandRunner,
auto_resolve_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
) -> std::result::Result<(), ResolveFailure> {
let _guard = AutoResolveGuard::new(auto_resolve_count);
send_event(event_tx, ParallelEvent::ConflictResolutionStarted).await;
let conflict_files = detect_conflicts(workspace_manager).await?;
let conflict_files_str = conflict_files.join(", ");
let vcs_status = get_vcs_status(workspace_manager).await.unwrap_or_default();
let vcs_log = get_vcs_log_for_revisions(workspace_manager, revisions)
.await
.unwrap_or_default();
let vcs_prompt_prefix = workspace_manager.conflict_resolution_prompt();
let mut resolve_context = ResolveContext::new(max_retries);
let combined_change_id = change_ids.join("+");
let initial_resolve_prompt = crate::agent::append_optional_prompt(
build_conflict_resolve_prompt(
config.get_resolve_skill(),
vcs_prompt_prefix,
&revisions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vcs_error,
&vcs_status,
&vcs_log,
&conflict_files_str,
),
config.get_resolve_append_prompt(),
);
let template = config.get_resolve_command()?;
let initial_command =
crate::config::OrchestratorConfig::expand_prompt(template, &initial_resolve_prompt);
let initial_command =
crate::config::expand::expand_conflict_files(&initial_command, &conflict_files_str);
for change_id in change_ids {
send_event(
event_tx,
ParallelEvent::ResolveStarted {
change_id: change_id.to_string(),
command: initial_command.clone(),
},
)
.await;
}
let mut final_classification = ResolveFailureClassification::RetriesExhausted;
let mut final_summary = format!("Failed to resolve conflicts after {} attempts", max_retries);
for attempt in 1..=max_retries {
let start = Instant::now();
info!(
"Conflict resolution attempt {}/{} for files: {}",
attempt, max_retries, conflict_files_str
);
let mut resolve_prompt = build_conflict_resolve_prompt(
config.get_resolve_skill(),
vcs_prompt_prefix,
&revisions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vcs_error,
&vcs_status,
&vcs_log,
&conflict_files_str,
);
let continuation_context = resolve_context.format_continuation_context();
if !continuation_context.is_empty() {
resolve_prompt = format!("{}\n\n{}", resolve_prompt, continuation_context);
}
resolve_prompt = crate::agent::append_optional_prompt(
resolve_prompt,
config.get_resolve_append_prompt(),
);
let template = config.get_resolve_command()?;
let command = crate::config::OrchestratorConfig::expand_prompt(template, &resolve_prompt);
let (mut child, mut rx) = ai_runner
.execute_streaming_with_retry(
&command,
Some(workspace_manager.repo_root()),
Some("resolve"),
None,
)
.await?;
let mut output_collector = crate::history::OutputCollector::new();
while let Some(line) = rx.recv().await {
let text = match &line {
crate::ai_command_runner::OutputLine::Stdout(s) => {
output_collector.add_stdout(s);
s.clone()
}
crate::ai_command_runner::OutputLine::Stderr(s) => {
output_collector.add_stderr(s);
s.clone()
}
};
send_event(
event_tx,
ParallelEvent::ResolveOutput {
change_id: combined_change_id.clone(),
output: text.clone(),
iteration: Some(attempt),
},
)
.await;
}
let status = child.wait().await.map_err(|e| {
OrchestratorError::AgentCommand(format!(
"Resolve command failed in workspace '{}' (attempt {}): {}",
workspace_manager.repo_root().display(),
attempt,
e
))
})?;
let status_success = status.success();
let remaining_conflicts = detect_conflicts(workspace_manager).await?;
let duration = start.elapsed();
if remaining_conflicts.is_empty() {
if !status_success {
warn!(
"Resolve command exited non-zero but conflicts cleared (attempt {}/{})",
attempt, max_retries
);
}
resolve_context.record(ResolveAttempt {
attempt,
command_success: status_success,
verification_success: true,
duration,
continuation_reason: None,
exit_code: status.code(),
stdout_tail: output_collector.stdout_tail(),
stderr_tail: output_collector.stderr_tail(),
});
send_event(event_tx, ParallelEvent::ConflictResolutionCompleted).await;
return Ok(());
}
let (continuation_reason, classification) = if status_success {
let reason = format!(
"Conflicts still present after resolution attempt: {}",
remaining_conflicts.join(", ")
);
warn!("{}", reason);
(
Some(reason),
ResolveFailureClassification::UnresolvedConflict,
)
} else {
let reason = format!(
"Resolution command failed with exit code: {:?}",
status.code()
);
warn!(
"Resolution attempt {} failed with exit code: {:?}",
attempt,
status.code()
);
(
Some(reason),
ResolveFailureClassification::ResolveAgentFailed,
)
};
final_classification = classification;
final_summary = continuation_reason.clone().unwrap_or_default();
resolve_context.record(ResolveAttempt {
attempt,
command_success: status_success,
verification_success: false,
duration,
continuation_reason,
exit_code: status.code(),
stdout_tail: output_collector.stdout_tail(),
stderr_tail: output_collector.stderr_tail(),
});
}
Err(fail_resolve(
event_tx,
change_ids,
max_retries,
final_classification,
final_summary,
)
.await)
}
#[derive(Clone)]
pub struct ResolveMergesWithRetryArgs<'a> {
pub workspace_manager: &'a dyn WorkspaceManager,
pub config: &'a OrchestratorConfig,
pub event_tx: &'a Option<mpsc::Sender<ParallelEvent>>,
pub items: &'a [SequentialMergeItem],
pub target_branch: &'a str,
pub base_revision: &'a str,
pub max_retries: u32,
pub ai_runner: crate::ai_command_runner::AiCommandRunner,
pub auto_resolve_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
pub publication_owns_completion: bool,
}
async fn send_resolve_completed(
event_tx: &Option<mpsc::Sender<ParallelEvent>>,
change_ids: &[String],
publication_owns_completion: bool,
) {
if publication_owns_completion {
return;
}
for change_id in change_ids {
send_event(
event_tx,
ParallelEvent::ResolveCompleted {
change_id: change_id.to_string(),
worktree_change_ids: None,
},
)
.await;
}
}
pub(crate) async fn render_worktree_locations(
evidence: &dyn ResolveEvidence,
items: &[SequentialMergeItem],
) -> String {
let mut lines = Vec::with_capacity(items.len());
for item in items {
let identity = evidence
.validate_worktree(&item.archive_path, &item.revision)
.await;
let location = match identity.resolved() {
Some((path, tip)) => format!(
"- {} => {} (change_id: {}, tip: {})",
item.revision,
path.display(),
item.change_id,
tip
),
None => format!(
"- {} => {} (change_id: {}, unvalidated: {})",
item.revision,
item.archive_path.display(),
item.change_id,
match &identity {
crate::vcs::git::commands::WorktreeIdentity::Unsafe { reason } =>
reason.as_str(),
_ => "unknown",
}
),
};
lines.push(location);
}
lines.join("\n")
}
pub async fn resolve_merges_with_retry(
args: ResolveMergesWithRetryArgs<'_>,
) -> std::result::Result<(), ResolveFailure> {
let ResolveMergesWithRetryArgs {
workspace_manager,
config,
event_tx,
items,
target_branch,
base_revision,
max_retries,
ai_runner,
auto_resolve_count,
publication_owns_completion,
} = args;
let _guard = AutoResolveGuard::new(auto_resolve_count);
send_event(event_tx, ParallelEvent::ConflictResolutionStarted).await;
let revisions = SequentialMergeItem::revisions(items);
let revisions = revisions.as_slice();
let change_ids = SequentialMergeItem::change_ids(items);
let change_ids = change_ids.as_slice();
let is_git = matches!(
workspace_manager.backend_type(),
VcsBackend::Git | VcsBackend::Auto
);
let evidence = GitResolveEvidence::new(workspace_manager.repo_root());
let merge_authorization = MergeAuthorizationLatch::new();
let mut batch_state = if is_git {
Some(
resolve_state::classify_batch_with_latch(
&evidence,
items,
base_revision,
&merge_authorization,
)
.await,
)
} else {
None
};
if batch_state.as_ref().is_some_and(BatchState::is_complete) {
info!(
"Sequential merge batch is already complete for revisions: {}",
revisions.join(", ")
);
send_event(event_tx, ParallelEvent::ConflictResolutionCompleted).await;
send_resolve_completed(event_tx, change_ids, publication_owns_completion).await;
return Ok(());
}
if let Some(state) = batch_state
.as_ref()
.filter(|state| !state.allows_agent_action())
{
let reason = state.diagnosis();
warn!(
"Sequential merge batch state withholds agent action: {}",
reason.replace('\n', " | ")
);
return Err(fail_resolve(
event_tx,
change_ids,
0,
ResolveFailureClassification::EvidenceWithheld,
reason,
)
.await);
}
let conflict_files = detect_conflicts(workspace_manager).await?;
let conflict_files_str = conflict_files.join(", ");
let vcs_status = get_vcs_status(workspace_manager).await.unwrap_or_default();
let vcs_log = get_vcs_log_for_revisions(workspace_manager, revisions)
.await
.unwrap_or_default();
let vcs_prompt_prefix = workspace_manager.conflict_resolution_prompt();
let merge_plan = items
.iter()
.map(|item| format!("- {} => {}", item.revision, item.change_id))
.collect::<Vec<_>>()
.join("\n");
let worktree_locations = if is_git {
render_worktree_locations(&evidence, items).await
} else {
items
.iter()
.map(|item| {
format!(
"- {} => {} (change_id: {})",
item.revision,
item.archive_path.display(),
item.change_id
)
})
.collect::<Vec<_>>()
.join("\n")
};
let phase_diagnosis = batch_state
.as_ref()
.map(BatchState::diagnosis)
.unwrap_or_default();
let mut resolve_context = ResolveContext::new(max_retries);
let combined_change_id = change_ids.join("+");
let initial_resolve_prompt = crate::agent::append_optional_prompt(
build_sequential_merge_resolve_prompt(
config.get_resolve_skill(),
vcs_prompt_prefix,
target_branch,
base_revision,
&merge_plan,
&worktree_locations,
&vcs_status,
&vcs_log,
&conflict_files_str,
&phase_diagnosis,
),
config.get_resolve_append_prompt(),
);
let template = config.get_resolve_command()?;
let initial_command =
crate::config::OrchestratorConfig::expand_prompt(template, &initial_resolve_prompt);
let initial_command =
crate::config::expand::expand_conflict_files(&initial_command, &conflict_files_str);
for change_id in change_ids {
send_event(
event_tx,
ParallelEvent::ResolveStarted {
change_id: change_id.to_string(),
command: initial_command.clone(),
},
)
.await;
}
let mut final_classification = ResolveFailureClassification::RetriesExhausted;
let mut final_summary = format!("Failed to resolve merges after {} attempts", max_retries);
for attempt in 1..=max_retries {
let start = Instant::now();
info!(
"Merge resolution attempt {}/{} for branches: {}",
attempt,
max_retries,
revisions.join(", ")
);
let phase_diagnosis = batch_state
.as_ref()
.map(BatchState::diagnosis)
.unwrap_or_default();
let mut resolve_prompt = build_sequential_merge_resolve_prompt(
config.get_resolve_skill(),
vcs_prompt_prefix,
target_branch,
base_revision,
&merge_plan,
&worktree_locations,
&vcs_status,
&vcs_log,
&conflict_files_str,
&phase_diagnosis,
);
let continuation_context = resolve_context.format_continuation_context();
if !continuation_context.is_empty() {
resolve_prompt = format!("{}\n\n{}", resolve_prompt, continuation_context);
}
resolve_prompt = crate::agent::append_optional_prompt(
resolve_prompt,
config.get_resolve_append_prompt(),
);
let template = config.get_resolve_command()?;
let command = crate::config::OrchestratorConfig::expand_prompt(template, &resolve_prompt);
let (mut child, mut rx) = ai_runner
.execute_streaming_with_retry(
&command,
Some(workspace_manager.repo_root()),
Some("resolve"),
None,
)
.await?;
let mut output_collector = crate::history::OutputCollector::new();
while let Some(line) = rx.recv().await {
let text = match &line {
crate::ai_command_runner::OutputLine::Stdout(s) => {
output_collector.add_stdout(s);
s.clone()
}
crate::ai_command_runner::OutputLine::Stderr(s) => {
output_collector.add_stderr(s);
s.clone()
}
};
send_event(
event_tx,
ParallelEvent::ResolveOutput {
change_id: combined_change_id.clone(),
output: text.clone(),
iteration: Some(attempt),
},
)
.await;
}
let status = child.wait().await.map_err(|e| {
OrchestratorError::AgentCommand(format!(
"Resolve command failed in workspace '{}' (attempt {}): {}",
workspace_manager.repo_root().display(),
attempt,
e
))
})?;
let status_success = status.success();
let duration = start.elapsed();
let remaining_conflicts = detect_conflicts(workspace_manager).await?;
if remaining_conflicts.is_empty() {
if is_git {
let state = resolve_state::classify_batch_with_latch(
&evidence,
items,
base_revision,
&merge_authorization,
)
.await;
let complete = state.is_complete();
let retryable = state.allows_agent_action();
let reason = state.diagnosis();
batch_state = Some(state);
if !complete && !retryable {
warn!(
"Sequential merge batch state withholds agent action after resolve attempt {}/{}: {}",
attempt,
max_retries,
reason.replace('\n', " | ")
);
return Err(fail_resolve(
event_tx,
change_ids,
attempt,
ResolveFailureClassification::EvidenceWithheld,
reason,
)
.await);
}
if !complete {
warn!(
"Sequential merge batch incomplete after resolve attempt {}/{}: {}",
attempt,
max_retries,
reason.replace('\n', " | ")
);
send_event(
event_tx,
ParallelEvent::ResolveOutput {
change_id: combined_change_id.clone(),
output: reason.clone(),
iteration: Some(attempt),
},
)
.await;
resolve_context.record(ResolveAttempt {
attempt,
command_success: status_success,
verification_success: false,
duration,
continuation_reason: Some(reason),
exit_code: status.code(),
stdout_tail: output_collector.stdout_tail(),
stderr_tail: output_collector.stderr_tail(),
});
continue;
}
}
if !status_success {
warn!(
"Resolve command exited non-zero but goals met (attempt {}/{})",
attempt, max_retries
);
}
resolve_context.record(ResolveAttempt {
attempt,
command_success: status_success,
verification_success: true,
duration,
continuation_reason: None,
exit_code: status.code(),
stdout_tail: output_collector.stdout_tail(),
stderr_tail: output_collector.stderr_tail(),
});
send_event(event_tx, ParallelEvent::ConflictResolutionCompleted).await;
send_resolve_completed(event_tx, change_ids, publication_owns_completion).await;
return Ok(());
}
let (continuation_reason, classification) = if status_success {
let reason = format!(
"Conflicts still present after merge resolution attempt: {}",
remaining_conflicts.join(", ")
);
warn!("{}", reason);
(
Some(reason),
ResolveFailureClassification::UnresolvedConflict,
)
} else {
let reason = format!(
"Merge resolution command failed with exit code: {:?}",
status.code()
);
warn!(
"Merge resolution attempt {} failed with exit code: {:?}",
attempt,
status.code()
);
(
Some(reason),
ResolveFailureClassification::ResolveAgentFailed,
)
};
final_classification = classification;
final_summary = continuation_reason.clone().unwrap_or_default();
resolve_context.record(ResolveAttempt {
attempt,
command_success: status_success,
verification_success: false,
duration,
continuation_reason,
exit_code: status.code(),
stdout_tail: output_collector.stdout_tail(),
stderr_tail: output_collector.stderr_tail(),
});
}
Err(fail_resolve(
event_tx,
change_ids,
max_retries,
final_classification,
final_summary,
)
.await)
}
fn build_conflict_resolve_prompt(
resolve_skill: &str,
vcs_prompt_prefix: &str,
revisions: &[&str],
vcs_error: &str,
vcs_status: &str,
vcs_log: &str,
conflict_files_str: &str,
) -> String {
format!(
"{}\n\n\
{}\n\n\
Conflicting revisions: {}\n\n\
VCS error output:\n\
{}\n\n\
Current VCS status:\n\
{}\n\n\
VCS log for conflicting changes:\n\
{}\n\n\
Conflicting files: {}",
crate::agent::prompt::skill_prelude(resolve_skill),
vcs_prompt_prefix,
revisions.join(", "),
vcs_error,
vcs_status,
vcs_log,
conflict_files_str
)
}
#[allow(clippy::too_many_arguments)]
fn build_sequential_merge_resolve_prompt(
resolve_skill: &str,
vcs_prompt_prefix: &str,
target_branch: &str,
base_revision: &str,
merge_plan: &str,
worktree_locations: &str,
vcs_status: &str,
vcs_log: &str,
conflict_files_str: &str,
phase_diagnosis: &str,
) -> String {
let diagnosis_block = if phase_diagnosis.trim().is_empty() {
String::new()
} else {
format!(
"\n\nRepository-derived phase diagnosis:\n{}",
phase_diagnosis
)
};
format!(
"{}\n\n\
{}\n\n\
Operation: sequential merge\n\n\
Target branch: {}\n\
Base revision before merges: {}\n\
Merge plan (branch => change_id):\n{}\n\n\
Worktree directories (branch => path):\n{}\n\n\
Current VCS status:\n{}\n\n\
VCS log for branches:\n{}\n\n\
Conflicting files (repo root, if any): {}{}",
crate::agent::prompt::skill_prelude(resolve_skill),
vcs_prompt_prefix,
target_branch,
base_revision,
merge_plan,
worktree_locations,
vcs_status,
vcs_log,
conflict_files_str,
diagnosis_block
)
}
#[cfg(test)]
mod merge_authorization_retry_tests;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conflict_resolve_prompt_has_skill_prelude() {
let prompt = build_conflict_resolve_prompt(
crate::config::defaults::DEFAULT_RESOLVE_SKILL,
"Git conflict resolution:",
&["branch-a", "branch-b"],
"merge failed",
"UU file.rs",
"commit log here",
"file.rs",
);
assert!(prompt.contains("$cflx-resolve"));
assert!(prompt.contains("load skills: cflx-resolve"));
assert!(prompt.contains("branch-a, branch-b"));
assert!(prompt.contains("file.rs"));
}
#[test]
fn test_conflict_resolve_prompt_uses_custom_skill_prelude() {
let prompt = build_conflict_resolve_prompt(
"team-resolve",
"prefix",
&["rev1"],
"err",
"status",
"log",
"files",
);
assert!(prompt.contains("$team-resolve"));
assert!(prompt.contains("load skills: team-resolve"));
assert!(!prompt.contains("$cflx-resolve"));
}
#[test]
fn test_conflict_resolve_prompt_no_fixed_guidance() {
let prompt = build_conflict_resolve_prompt(
crate::config::defaults::DEFAULT_RESOLVE_SKILL,
"prefix",
&["rev1"],
"err",
"status",
"log",
"files",
);
assert!(
!prompt.contains("Please resolve the merge conflicts"),
"Resolution instruction must not be in Rust prompt"
);
assert!(
!prompt.contains("Safety Constraints"),
"Safety constraints must not be in Rust prompt"
);
assert!(
!prompt.contains("--no-verify"),
"Safety rules must not be in Rust prompt"
);
}
#[test]
fn resolve_append_prompt_is_applied_to_conflict_resolve_command_tail() {
let prompt = crate::agent::append_optional_prompt(
build_conflict_resolve_prompt(
crate::config::defaults::DEFAULT_RESOLVE_SKILL,
"Git conflict resolution:",
&["branch-a", "branch-b"],
"merge failed",
"UU file.rs",
"commit log here",
"file.rs",
),
Some("resolve tail {change_id}"),
);
let command =
crate::config::OrchestratorConfig::expand_prompt("agent --prompt '{prompt}'", &prompt);
assert_eq!(command.matches("resolve tail {change_id}").count(), 1);
assert!(command.ends_with("resolve tail {change_id}'"));
assert!(!command.contains("branch-a resolve tail"));
}
#[test]
fn test_sequential_merge_prompt_has_skill_prelude() {
let prompt = build_sequential_merge_resolve_prompt(
crate::config::defaults::DEFAULT_RESOLVE_SKILL,
"Git conflict resolution:",
"main",
"abc123",
"- branch-a => change-1",
"- branch-a => /path (change_id: change-1)",
"clean",
"log entries",
"(none)",
"phase: final_merge_missing",
);
assert!(prompt.contains("$cflx-resolve"));
assert!(prompt.contains("load skills: cflx-resolve"));
assert!(prompt.contains("Operation: sequential merge"));
assert!(prompt.contains("Target branch: main"));
assert!(prompt.contains("abc123"));
assert!(prompt.contains("change-1"));
}
#[test]
fn test_sequential_merge_prompt_uses_custom_skill_prelude() {
let prompt = build_sequential_merge_resolve_prompt(
"team-resolve",
"prefix",
"main",
"base",
"plan",
"locations",
"status",
"log",
"files",
"",
);
assert!(prompt.contains("$team-resolve"));
assert!(prompt.contains("load skills: team-resolve"));
assert!(!prompt.contains("$cflx-resolve"));
}
#[test]
fn test_sequential_merge_prompt_no_fixed_guidance() {
let prompt = build_sequential_merge_resolve_prompt(
crate::config::defaults::DEFAULT_RESOLVE_SKILL,
"prefix",
"main",
"base",
"plan",
"locations",
"status",
"log",
"files",
"",
);
assert!(
!prompt.contains("Requirements:"),
"Requirements section must not be in Rust prompt"
);
assert!(
!prompt.contains("Instructions (repeat"),
"Step-by-step instructions must not be in Rust prompt"
);
assert!(
!prompt.contains("Pre-sync base into"),
"Commit convention must not be in Rust prompt"
);
assert!(
!prompt.contains("Merge change:"),
"Merge commit convention must not be in Rust prompt"
);
assert!(
!prompt.contains("git merge --no-ff"),
"Git commands must not be in Rust prompt"
);
assert!(
!prompt.contains("git rm -rf"),
"Git cleanup commands must not be in Rust prompt"
);
assert!(
!prompt.contains("--no-verify"),
"Safety rules must not be in Rust prompt"
);
assert!(
!prompt.contains("Complete the merges"),
"Completion instruction must not be in Rust prompt"
);
}
}