From nobody Sun Feb 8 16:50:38 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7CC94376BC7 for ; Fri, 19 Dec 2025 18:16:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1766168200; cv=none; b=uYQpP8KdNUufrxU6BYad/FhshVRW8Wbyih87YtfhzSifjF4wYvINlu3If4wsvnys433m5IPbiCRtXkcWZO+PqwDYkJM/h2MBrwyK7QSDzvCAdCMfZkH0sp6eRYzY0diQ49SP4wPfZ9k9ypfcS28G7iIZsQ83a0zM1pBgF6nVhr4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1766168200; c=relaxed/simple; bh=n0qDl4KomvHcC7qPdDhQvyvThXmFNHdTdaPzur2x0Rs=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=CVNO8jkAAAz5wKLB/IkANJvZfoHoISPi3xrw7lHVwUlDdLUGqZf+1nJOFdQkvh4izx3CGjokng/TAVeUH/YpxuoknLSFQUQa3RwuZyQ+vx/vte6wIWQCFMhkbv9Kjn1LIi+APlEB4IRqKoMwf0hI0tq83VazjojLjQjuWeBY0SM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=Ycl+brY4; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="Ycl+brY4" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 97AF6C116B1; Fri, 19 Dec 2025 18:16:39 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1766168200; bh=n0qDl4KomvHcC7qPdDhQvyvThXmFNHdTdaPzur2x0Rs=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Ycl+brY4miRj5aZxxbVdIyccpIVHdxgyGidqL4ggXuKozOYTvByks21Ep4vqhzs6Y dOYKtwIsayV/l9NiiGfSBWjrBO0qrSJbM43E1lu9TyKXEUmyaO5KOZASpWgSIZwMm/ xFcpIhSvMNCdVHiJNtdFu7jiGY2GiqyJSjes+ifIxo7S2DezVeIXG1nWjGuC48iob+ L+uI0NfziGsAfrQdDSUtKQT6M6MpUo9oTCuJk27EadHuf/j1275UCs2muPCOr3U346 VUjQhmXDVwe6P0cri3IDnxSPRCMbxM4HlkHf0m/SnzI4dS3FYo1JuKna1tIdN6u0kq JKz0MH3dCTLkg== From: Sasha Levin To: tools@kernel.org Cc: linux-kernel@vger.kernel.org, torvalds@linux-foundation.org, broonie@kernel.org, Sasha Levin Subject: [RFC 4/5] LLMinus: Add resolve command for LLM-assisted conflict resolution Date: Fri, 19 Dec 2025 13:16:28 -0500 Message-ID: <20251219181629.1123823-5-sashal@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251219181629.1123823-1-sashal@kernel.org> References: <20251219181629.1123823-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add the 'resolve' command that invokes an external LLM to assist with resolving current merge conflicts. This command: 1. Detects merge context: - Parses .git/MERGE_MSG for merge source (branch/tag/commit) - Identifies target branch from HEAD - Extracts merge message 2. Gathers conflict information: - Parses all conflicted files with markers - Supports both standard and diff3 style markers 3. Finds similar historical resolutions (when database available): - Uses graceful degradation if no database exists - Includes top 3 most similar historical resolutions 4. Builds a prompt that includes: - High-stakes framing about merge quality - Current conflict content (ours/theirs/base) - Similar historical resolutions with diffs - Investigation requirements (lore.kernel.org search, git history) - Resolution instructions 5. Invokes the specified command with prompt via stdin: - Supports commands like "claude" or "llm -m gpt-4" - Streams output directly to terminal The prompt design emphasizes: - Understanding before acting - Searching lore.kernel.org for maintainer guidance - Tracing git history to understand both sides - Flagging uncertainty rather than guessing Usage: llminus resolve "claude" llminus resolve "llm -m my-llm-x" Signed-off-by: Sasha Levin --- tools/llminus/src/main.rs | 477 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) diff --git a/tools/llminus/src/main.rs b/tools/llminus/src/main.rs index 1a045fa3174ea..c00f958a238f8 100644 --- a/tools/llminus/src/main.rs +++ b/tools/llminus/src/main.rs @@ -39,6 +39,11 @@ enum Commands { #[arg(default_value =3D "1")] n: usize, }, + /// Resolve current conflicts using an LLM + Resolve { + /// Command to invoke. The prompt will be passed via stdin. + command: String, + }, } =20 /// A single diff hunk representing a change region @@ -833,6 +838,423 @@ fn find(n: usize) -> Result<()> { Ok(()) } =20 +/// Context about the current merge operation +#[derive(Debug, Default)] +struct MergeContext { + /// The branch/tag/ref being merged (from MERGE_HEAD or MERGE_MSG) + merge_source: Option, + /// The target branch (HEAD) + head_branch: Option, + /// The merge message (from .git/MERGE_MSG) + merge_message: Option, +} + +/// Extract context about the current merge operation +fn get_merge_context() -> MergeContext { + let mut ctx =3D MergeContext::default(); + + // Get current branch name + ctx.head_branch =3D git_allow_fail(&["rev-parse", "--abbrev-ref", "HEA= D"]) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty() && s !=3D "HEAD"); + + // Try to read MERGE_MSG for merge context + if let Ok(merge_msg) =3D std::fs::read_to_string(".git/MERGE_MSG") { + ctx.merge_message =3D Some(merge_msg.clone()); + + // Parse merge source from MERGE_MSG + // Common formats: + // "Merge branch 'feature-branch'" + // "Merge tag 'v6.1'" + // "Merge remote-tracking branch 'origin/main'" + // "Merge commit 'abc123'" + let first_line =3D merge_msg.lines().next().unwrap_or(""); + if let Some(source) =3D parse_merge_source(first_line) { + ctx.merge_source =3D Some(source); + } + } + + // If no merge source found from MERGE_MSG, try to describe MERGE_HEAD + if ctx.merge_source.is_none() { + // Try to get a tag name for MERGE_HEAD + if let Some(tag) =3D git_allow_fail(&["describe", "--tags", "--exa= ct-match", "MERGE_HEAD"]) { + ctx.merge_source =3D Some(tag.trim().to_string()); + } else if let Some(branch) =3D git_allow_fail(&["name-rev", "--nam= e-only", "MERGE_HEAD"]) { + let branch =3D branch.trim(); + if !branch.is_empty() && branch !=3D "undefined" { + ctx.merge_source =3D Some(branch.to_string()); + } + } + } + + ctx +} + +/// Parse merge source from a merge message first line +fn parse_merge_source(line: &str) -> Option { + // "Merge branch 'feature'" -> "feature" + // "Merge tag 'v6.1'" -> "v6.1" + // "Merge remote-tracking branch 'origin/main'" -> "origin/main" + // "Merge commit 'abc123'" -> "abc123" + + let line =3D line.trim(); + + // Look for quoted source + if let Some(start) =3D line.find('\'') { + if let Some(end) =3D line[start + 1..].find('\'') { + return Some(line[start + 1..start + 1 + end].to_string()); + } + } + + // Look for "Merge X into Y" pattern without quotes + if line.starts_with("Merge ") { + let rest =3D &line[6..]; + // Skip "branch ", "tag ", "commit ", "remote-tracking branch " + let rest =3D rest + .strip_prefix("remote-tracking branch ") + .or_else(|| rest.strip_prefix("branch ")) + .or_else(|| rest.strip_prefix("tag ")) + .or_else(|| rest.strip_prefix("commit ")) + .unwrap_or(rest); + + // Take until " into " or end of line + if let Some(into_pos) =3D rest.find(" into ") { + return Some(rest[..into_pos].trim().to_string()); + } + let word =3D rest.split_whitespace().next()?; + if !word.is_empty() { + return Some(word.to_string()); + } + } + + None +} + +/// Get current conflicts from the working directory +fn get_current_conflicts() -> Result> { + check_repo()?; + + // Find current conflicts + let conflict_paths =3D get_conflicted_files()?; + if conflict_paths.is_empty() { + bail!("No conflicts detected. Run this command when you have activ= e merge conflicts."); + } + + // Parse all conflict regions + let mut all_conflicts =3D Vec::new(); + for path in &conflict_paths { + if let Ok(conflicts) =3D parse_conflict_file(path) { + all_conflicts.extend(conflicts); + } + } + + if all_conflicts.is_empty() { + bail!("Could not parse any conflict markers from the conflicted fi= les."); + } + + Ok(all_conflicts) +} + +/// Try to find similar resolutions, returns empty vec if no database or e= mbeddings +fn try_find_similar_resolutions(n: usize, conflicts: &[ConflictFile]) -> V= ec { + let store_path =3D Path::new(STORE_PATH); + if !store_path.exists() { + return Vec::new(); + } + + let store =3D match ResolutionStore::load(store_path) { + Ok(s) =3D> s, + Err(_) =3D> return Vec::new(), + }; + + let with_embeddings: Vec<_> =3D store.resolutions.iter() + .filter(|r| r.embedding.is_some()) + .collect(); + + if with_embeddings.is_empty() { + return Vec::new(); + } + + // Initialize embedding model + let mut model =3D match init_embedding_model() { + Ok(m) =3D> m, + Err(_) =3D> return Vec::new(), + }; + + // Generate embedding for current conflicts + let conflict_text: String =3D conflicts.iter() + .map(|c| c.to_embedding_text()) + .collect::>() + .join("\n---\n\n"); + + let query_embeddings =3D match model.embed(vec![conflict_text], None) { + Ok(e) =3D> e, + Err(_) =3D> return Vec::new(), + }; + let query_embedding =3D &query_embeddings[0]; + + // Compute similarities and take top N + let mut similarities: Vec<_> =3D with_embeddings.iter() + .map(|r| { + let sim =3D cosine_similarity(query_embedding, r.embedding.as_= ref().unwrap()); + (r, sim) + }) + .collect(); + + similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::= Ordering::Equal)); + + similarities.into_iter() + .take(n) + .map(|(r, sim)| SimilarResolution { + resolution: (*r).clone(), + similarity: sim, + }) + .collect() +} + +/// Build the LLM prompt for conflict resolution +fn build_resolve_prompt( + conflicts: &[ConflictFile], + similar: &[SimilarResolution], + merge_ctx: &MergeContext, +) -> String { + let mut prompt =3D String::new(); + + // Header with high-stakes framing + prompt.push_str("# Linux Kernel Merge Conflict Resolution\n\n"); + prompt.push_str("You are acting as an experienced kernel maintainer re= solving a merge conflict.\n\n"); + prompt.push_str("**Important:** Incorrect merge resolutions have histo= rically introduced subtle bugs "); + prompt.push_str("that affected millions of users and took months to di= agnose. A resolution that "); + prompt.push_str("compiles but has semantic errors is worse than no res= olution at all.\n\n"); + prompt.push_str("Take the time to fully understand both sides of the c= onflict before attempting "); + prompt.push_str("any resolution. If after investigation you're not con= fident, say so - it's "); + prompt.push_str("better to escalate to a human than to introduce a sub= tle bug.\n\n"); + + // Merge context + prompt.push_str("## Merge Context\n\n"); + if let Some(ref source) =3D merge_ctx.merge_source { + prompt.push_str(&format!("**Merging:** `{}`\n", source)); + } + if let Some(ref head) =3D merge_ctx.head_branch { + prompt.push_str(&format!("**Into:** `{}`\n", head)); + } + if let Some(ref msg) =3D merge_ctx.merge_message { + let first_line =3D msg.lines().next().unwrap_or(""); + prompt.push_str(&format!("**Merge message:** {}\n", first_line)); + } + prompt.push_str("\n"); + + // Current conflicts + prompt.push_str("## Current Conflicts\n\n"); + + for conflict in conflicts { + prompt.push_str(&format!("### File: {}\n\n", conflict.path)); + prompt.push_str("**Our version (HEAD):**\n```\n"); + prompt.push_str(&conflict.ours_content); + prompt.push_str("\n```\n\n"); + prompt.push_str("**Their version (being merged):**\n```\n"); + prompt.push_str(&conflict.theirs_content); + prompt.push_str("\n```\n\n"); + if let Some(ref base) =3D conflict.base_content { + prompt.push_str("**Base version (common ancestor):**\n```\n"); + prompt.push_str(base); + prompt.push_str("\n```\n\n"); + } + } + + // Similar historical resolutions (only if available) + if !similar.is_empty() { + prompt.push_str("## Similar Historical Resolutions\n\n"); + prompt.push_str("These conflicts were previously resolved in the L= inux kernel. Use `git show ` "); + prompt.push_str("to examine the full commit message and context - = maintainers often explain "); + prompt.push_str("their resolution rationale there.\n\n"); + + for (i, result) in similar.iter().enumerate() { + let r =3D &result.resolution; + prompt.push_str(&format!("### Historical Resolution {} (simila= rity: {:.1}%)\n\n", i + 1, result.similarity * 100.0)); + prompt.push_str(&format!("- **Commit:** `{}`\n", r.commit_hash= )); + prompt.push_str(&format!("- **Summary:** {}\n", r.commit_summa= ry)); + prompt.push_str(&format!("- **Author:** {}\n", r.author)); + prompt.push_str(&format!("- **Date:** {}\n", r.commit_date)); + prompt.push_str(&format!("- **Files:** {}\n\n", r.files.iter()= .map(|f| f.file_path.as_str()).collect::>().join(", "))); + + for file in &r.files { + prompt.push_str(&format!("#### {}\n\n", file.file_path)); + + if !file.ours_diff.is_empty() { + prompt.push_str("**Ours changed:**\n```diff\n"); + for hunk in &file.ours_diff { + prompt.push_str(&hunk.content); + prompt.push_str("\n"); + } + prompt.push_str("```\n\n"); + } + + if !file.theirs_diff.is_empty() { + prompt.push_str("**Theirs changed:**\n```diff\n"); + for hunk in &file.theirs_diff { + prompt.push_str(&hunk.content); + prompt.push_str("\n"); + } + prompt.push_str("```\n\n"); + } + + if !file.resolution_diff.is_empty() { + prompt.push_str("**Final resolution:**\n```diff\n"); + for hunk in &file.resolution_diff { + prompt.push_str(&hunk.content); + prompt.push_str("\n"); + } + prompt.push_str("```\n\n"); + } + } + } + } + + // Investigation requirement + prompt.push_str("## Investigation Required\n\n"); + prompt.push_str("Before attempting any resolution, you must conduct th= orough research. "); + prompt.push_str("Rushing to resolve without understanding is how subtl= e bugs get introduced. "); + prompt.push_str("Work through each phase below IN ORDER and document y= our findings.\n\n"); + + // Phase 1: Search lore.kernel.org + prompt.push_str("### Phase 1: Search lore.kernel.org for Maintainer Gu= idance (DO THIS FIRST)\n\n"); + prompt.push_str("**CRITICAL:** Before doing ANY other research, search= lore.kernel.org for existing guidance.\n"); + prompt.push_str("Maintainers often post merge resolution instructions = when they know conflicts will occur.\n\n"); + + if let Some(ref source) =3D merge_ctx.merge_source { + prompt.push_str(&format!("1. **Search for the merge itself:** `{}`= \n", source)); + prompt.push_str(&format!(" - URL: `https://lore.kernel.org/all/?= q=3D{}`\n", source.replace('/', "%2F"))); + } + prompt.push_str("2. **Search for conflict discussions:**\n"); + prompt.push_str(" - `\"merge conflict\"` + subsystem name\n"); + prompt.push_str(" - `\"conflicts with\"` + branch/tag name\n\n"); + + // Phase 2: Context + prompt.push_str("### Phase 2: Understand the Context\n\n"); + prompt.push_str("- **What subsystem is this?** Read the file and nearb= y files to understand its purpose.\n"); + prompt.push_str("- **Who maintains it?** Check `git log --oneline -20`= for recent authors.\n"); + prompt.push_str("- **What's the file's role?** Is it a driver, core su= bsystem, header, config?\n\n"); + + // Phase 3: Trace history + prompt.push_str("### Phase 3: Trace Each Side's History\n\n"); + prompt.push_str("**For 'ours' (HEAD):**\n"); + prompt.push_str("- Run `git log --oneline HEAD -- ` to see recen= t changes\n"); + prompt.push_str("- Find the commit that introduced our version of the = conflicted code\n"); + prompt.push_str("- Run `git show ` to read the full commit mes= sage\n\n"); + prompt.push_str("**For 'theirs' (MERGE_HEAD):**\n"); + prompt.push_str("- Run `git log --oneline MERGE_HEAD -- ` to see= their changes\n"); + prompt.push_str("- Find the commit that introduced their version\n"); + prompt.push_str("- Run `git show ` to read the full commit mes= sage\n\n"); + + // Resolution + prompt.push_str("## Resolution\n\n"); + prompt.push_str("Once you understand the conflict:\n\n"); + prompt.push_str("1. Edit the conflicted files to produce the correct m= erged result\n"); + prompt.push_str("2. Remove all conflict markers (`<<<<<<<`, `=3D=3D=3D= =3D=3D=3D=3D`, `>>>>>>>`)\n"); + prompt.push_str("3. Stage the resolved files with `git add`\n"); + prompt.push_str("4. Commit with a detailed message explaining your ana= lysis and resolution\n\n"); + + // If uncertain + prompt.push_str("## If Uncertain\n\n"); + prompt.push_str("If after investigation you're still uncertain about t= he correct resolution:\n\n"); + prompt.push_str("- Explain what you've learned and what remains unclea= r\n"); + prompt.push_str("- Describe the possible resolutions you see and their= tradeoffs\n"); + prompt.push_str("- Recommend whether a human maintainer should review\= n\n"); + prompt.push_str("It's better to flag uncertainty than to silently intr= oduce a bug.\n\n"); + + // Tools available + prompt.push_str("## Tools Available\n\n"); + prompt.push_str("You can use these to investigate:\n\n"); + prompt.push_str("```bash\n"); + if !similar.is_empty() { + prompt.push_str("# Examine historical resolution commits\n"); + for result in similar { + prompt.push_str(&format!("git show {}\n", result.resolution.co= mmit_hash)); + } + prompt.push_str("\n"); + } + prompt.push_str("# Understand merge parents\n"); + prompt.push_str("git show ^1 # ours\n"); + prompt.push_str("git show ^2 # theirs\n"); + prompt.push_str("```\n"); + + prompt +} + +fn resolve(command: &str) -> Result<()> { + use std::io::Write; + use std::process::Stdio; + + // Get merge context (what branch/tag is being merged) + let merge_ctx =3D get_merge_context(); + if let Some(ref source) =3D merge_ctx.merge_source { + println!("Merging: {}", source); + } + if let Some(ref head) =3D merge_ctx.head_branch { + println!("Into: {}", head); + } + + // Get current conflicts first + let conflicts =3D get_current_conflicts()?; + + println!("Found {} conflict(s)", conflicts.len()); + + // Try to find similar historical resolutions (gracefully handles miss= ing database) + println!("Looking for similar historical conflicts..."); + let similar =3D try_find_similar_resolutions(3, &conflicts); + + if similar.is_empty() { + println!("No historical resolution database found (run 'llminus le= arn' and 'llminus vectorize' to build one)"); + println!("Proceeding without historical examples..."); + } else { + println!("Found {} similar historical resolutions", similar.len()); + } + + // Build the prompt + println!("Building resolution prompt..."); + let prompt =3D build_resolve_prompt(&conflicts, &similar, &merge_ctx); + + println!("Prompt size: {} bytes", prompt.len()); + println!("\nInvoking: {}", command); + println!("{}", "=3D".repeat(80)); + + // Parse command (handle arguments) + let parts: Vec<&str> =3D command.split_whitespace().collect(); + if parts.is_empty() { + bail!("Empty command specified"); + } + + let cmd =3D parts[0]; + let args =3D &parts[1..]; + + // Spawn the command + let mut child =3D Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .spawn() + .with_context(|| format!("Failed to spawn command: {}", command))?; + + // Write prompt to stdin + if let Some(mut stdin) =3D child.stdin.take() { + stdin.write_all(prompt.as_bytes()) + .context("Failed to write prompt to command stdin")?; + } + + // Wait for completion + let status =3D child.wait().context("Failed to wait for command")?; + + println!("{}", "=3D".repeat(80)); + + if status.success() { + println!("\nCommand completed successfully."); + } else { + eprintln!("\nCommand exited with status: {}", status); + } + + Ok(()) +} + fn main() -> Result<()> { let cli =3D Cli::parse(); =20 @@ -840,6 +1262,7 @@ fn main() -> Result<()> { Commands::Learn { range } =3D> learn(range.as_deref()), Commands::Vectorize { batch_size } =3D> vectorize(batch_size), Commands::Find { n } =3D> find(n), + Commands::Resolve { command } =3D> resolve(&command), } } =20 @@ -909,6 +1332,60 @@ fn test_find_command_with_n() { } } =20 + #[test] + fn test_resolve_command_parses() { + let cli =3D Cli::try_parse_from(["llminus", "resolve", "my-llm"]).= unwrap(); + match cli.command { + Commands::Resolve { command } =3D> assert_eq!(command, "my-llm= "), + _ =3D> panic!("Expected Resolve command"), + } + } + + #[test] + fn test_resolve_command_with_args() { + let cli =3D Cli::try_parse_from(["llminus", "resolve", "my-llm --m= odel fancy"]).unwrap(); + match cli.command { + Commands::Resolve { command } =3D> assert_eq!(command, "my-llm= --model fancy"), + _ =3D> panic!("Expected Resolve command"), + } + } + + #[test] + fn test_parse_merge_source() { + // Standard branch merge + assert_eq!( + parse_merge_source("Merge branch 'feature-branch'"), + Some("feature-branch".to_string()) + ); + + // Tag merge + assert_eq!( + parse_merge_source("Merge tag 'v6.1'"), + Some("v6.1".to_string()) + ); + + // Remote tracking branch + assert_eq!( + parse_merge_source("Merge remote-tracking branch 'origin/main'= "), + Some("origin/main".to_string()) + ); + + // Commit merge + assert_eq!( + parse_merge_source("Merge commit 'abc123def'"), + Some("abc123def".to_string()) + ); + + // Branch with "into" target + assert_eq!( + parse_merge_source("Merge branch 'feature' into master"), + Some("feature".to_string()) + ); + + // Non-merge line + assert_eq!(parse_merge_source("Fix bug in foo"), None); + } + #[test] fn test_cosine_similarity() { // Identical vectors should have similarity 1.0 --=20 2.51.0