Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 151 additions & 16 deletions llm-git-conflict-resolve/skill/git_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
import json
import os
import ast
import shutil
import re
import shlex

def run_git_command(command):
"""Executes a git command and returns the output as a string."""
try:
result = subprocess.check_output(
command,
stderr=subprocess.STDOUT,
shell=True
shlex.split(command),
stderr=subprocess.STDOUT
)
return result.decode('utf-8').strip()
except subprocess.CalledProcessError as e:
Expand All @@ -32,8 +34,8 @@ def list_conflicted_files():
status = line[:2]
filepath = line[3:].strip()

# Simplified filter: if both sides modified (U) or added (A)
if 'U' in status or 'A' in status:
# Only exact two-character conflict statuses (avoids staged-but-not-conflicted false positives)
if status in {'UU', 'AA', 'AU', 'UA', 'DD', 'DU', 'UD'}:
conflicted_files.append({
"filepath": filepath,
"status": status
Expand All @@ -49,23 +51,68 @@ def get_file_content_at_stage(filepath, stage):
Stage 3 = Theirs (Remote/MERGE_HEAD)
"""
# git show :<stage>:<filepath>
content = run_git_command(f"git show :{stage}:{filepath}")
content = run_git_command(f"git show :{stage}:{shlex.quote(filepath)}")
return content if content is not None else ""

def parse_and_summarize_ai_context(commit_message):
"""
Searches for @ai-context and returns ONLY the path.
The actual reading and summarization will be delegated to a Sub-Agent
as defined in the system instructions.
"""
if not commit_message:
return None

# Search for the @ai-context <path> pattern
match = re.search(r'@ai-context\s+([^\s]+)', commit_message)
if match:
repo_root = run_git_command("git rev-parse --show-toplevel") or ""
context_path = os.path.abspath(match.group(1))
if os.path.exists(context_path) and context_path.startswith(repo_root + os.sep):
return context_path

return None

def get_commit_context(filepath):
"""
Extracts commit messages to understand semantic intent (Solution 3).
Extracts commit messages and identifies AI reasoning context paths.
"""
# HEAD is the current branch (Local)
local_msg = run_git_command(f"git log -1 --pretty=%B HEAD -- {filepath}")

# MERGE_HEAD is the incoming branch (Remote).
# It might be null if we are not in a standard merge, but we handle the case.
remote_msg = run_git_command(f"git log -1 --pretty=%B MERGE_HEAD -- {filepath}")
# 1. Extract commit messages from Git
local_msg = run_git_command(f"git log -1 --pretty=%B HEAD -- {shlex.quote(filepath)}")
remote_msg = run_git_command(f"git log -1 --pretty=%B MERGE_HEAD -- {shlex.quote(filepath)}")

# Ensure we have clean strings
local_msg = local_msg.strip() if local_msg else "Unknown (Manual changes or no commit msg)"
remote_msg = remote_msg.strip() if remote_msg else "Unknown (No MERGE_HEAD found)"

# 2. Identify @ai-context file paths (delegation mode)
local_ctx_path = parse_and_summarize_ai_context(local_msg)
remote_ctx_path = parse_and_summarize_ai_context(remote_msg)

# 3. Return organized data for the Orchestrator
return {
"local": {
"intent": local_msg,
"ai_context_path": local_ctx_path
},
"remote": {
"intent": remote_msg,
"ai_context_path": remote_ctx_path
}
}

def format_process_error(e, message):
"""
Helper function to format subprocess.CalledProcessError into a standard JSON response.
"""
# Try to extract the error from stderr, then from stdout, and finally fallback to str(e)
details = e.stderr.strip() if e.stderr else ""
if not details:
details = e.stdout.strip() if e.stdout else str(e)
return {
"local_intent": local_msg.strip() if local_msg else "Unknown (Manual changes or no commit msg)",
"remote_intent": remote_msg.strip() if remote_msg else "Unknown (No MERGE_HEAD found)"
"status": "error",
"message": message,
"details": details
}

def verify_syntax(filepath):
Expand All @@ -91,10 +138,70 @@ def verify_syntax(filepath):
"message": f"Syntax Error on line {e.lineno}: {e.msg}",
"details": str(e)
}


elif ext in ['.js', '.jsx', '.cjs', '.mjs']:
# Check if 'node' is installed on the system
if shutil.which("node") is None:
return {
"status": "warning",
"message": "Node.js is missing. JS verification was skipped.",
"suggestion": "To enable JS verification on Linux (Ubuntu/Debian), run:",
"install_command": "sudo apt update && sudo apt install nodejs npm"
}

try:
# Run node --check
subprocess.run(
["node", "--check", filepath],
capture_output=True,
text=True,
check=True
)
return {"status": "valid", "message": "JS syntax check passed."}
except subprocess.CalledProcessError as e:
return format_process_error(e, "JS Syntax Error")

# --- TYPESCRIPT ---
elif ext in ['.ts', '.tsx']:
# Check if 'tsc' (TypeScript compiler) is installed
if shutil.which("tsc") is None:
return {
"status": "warning",
"message": "TypeScript compiler (tsc) is missing. TS verification was skipped.",
"suggestion": "If you already have npm installed, install TypeScript globally by running:",
"install_command": "sudo npm install -g typescript"
}

try:
# Run tsc --noEmit (checks syntax and types without generating .js files)
subprocess.run(
["tsc", "--noEmit", filepath],
capture_output=True,
text=True,
check=True
)
return {"status": "valid", "message": "TS syntax check passed."}
except subprocess.CalledProcessError as e:
return format_process_error(e, "TS Syntax/Type Error")

# For other files, currently return valid (or implement specific linters)
return {"status": "valid", "message": f"No linter configured for {ext}, assuming valid."}

def create_backup(filepath):
"""Automatically creates a .bak copy of the file."""
if os.path.exists(filepath):
backup_path = f"{filepath}.bak"
shutil.copy2(filepath, backup_path)

def restore_backup(filepath):
"""Restore the file from the .bak copy."""
backup_path = f"{filepath}.bak"
if os.path.exists(backup_path):
# Overwrite the corrupted file with the backup
shutil.copy2(backup_path, filepath)
return {"status": "success", "message": f"The file has been restored to its original state in {backup_path}"}
return {"status": "error", "message": f"No backup found.({backup_path})."}

def main():
parser = argparse.ArgumentParser(description="Git Merge Conflict Tool for AI Agents")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
Expand All @@ -110,15 +217,29 @@ def main():
verify_parser = subparsers.add_parser("verify", help="Verify syntax")
verify_parser.add_argument("filepath", type=str, help="Path to the file to verify")

# --- Command: restore ---
restore_parser = subparsers.add_parser("restore", help="Restore the file from a .bak backup")
restore_parser.add_argument("filepath", type=str, help="Path to the file to be restored")

args = parser.parse_args()

def _in_repo(fp):
root = run_git_command("git rev-parse --show-toplevel") or ""
return os.path.abspath(fp).startswith(root + os.sep)

# Command routing
if args.command == "list":
result = list_conflicted_files()
print(json.dumps(result, indent=2))

elif args.command == "extract":
filepath = args.filepath
if not _in_repo(filepath):
print(json.dumps({"status": "error", "message": "Path outside repository"}))
sys.exit(1)

#Perform the backup before extracting the data
create_backup(filepath)

# 1. Extract Diff (Code)
diff_data = {
Expand All @@ -140,7 +261,21 @@ def main():
print(json.dumps(full_payload, indent=2))

elif args.command == "verify":
if not _in_repo(args.filepath):
print(json.dumps({"status": "error", "message": "Path outside repository"}))
sys.exit(1)
result = verify_syntax(args.filepath)
if result.get("status") == "valid":
bak = args.filepath + ".bak"
if os.path.exists(bak):
os.remove(bak)
print(json.dumps(result, indent=2))

elif args.command == "restore":
if not _in_repo(args.filepath):
print(json.dumps({"status": "error", "message": "Path outside repository"}))
sys.exit(1)
result = restore_backup(args.filepath)
print(json.dumps(result, indent=2))

else:
Expand Down
20 changes: 14 additions & 6 deletions llm-git-conflict-resolve/skill/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,19 @@ You must listen for the following keywords. Execute the action, then **guide the
3. **Report:** Report the status ("Syntax Valid" or "Error").
4. **Guidance Example:**
- If valid: "Syntax verified. Please run `git add <filepath>` manually to mark it resolved, then type `scan` to check for others."
- If error: "Syntax error detected. Shall I try to fix it?"
- If error: "Syntax error detected. Shall I try to fix it, or would you like to revert the file using the `restore <filepath>` command?"

### COMMAND: restore
**Triggers:** "restore", "revert", "undo", "back"
**Action:**
1. **Execute:** `python3 git_tools.py restore <filepath>`
2. **Report:** Tell the user if the restoration from the `.bak` file was successful based on the JSON output.
3. **Guidance Example:** "The file has been restored to its original conflicted state. We can try resolving it again, or you can check the code manually."

### COMMAND: help
**Triggers:** "help", "info", "commands"
**Action:**
1. List the available commands (`scan`, `resolve`, `apply`).
1. List the available commands (scan, resolve, apply, restore).
2. Explain the workflow briefly.

---
Expand All @@ -68,10 +75,11 @@ You must listen for the following keywords. Execute the action, then **guide the

You must use your understanding of programming logic to merge the files. Do not rely on git markers blindly.

### 1. Semantic Intent Analysis
- Read the commit messages in the `context` object to understand *why* changes were made on each branch.
- **Structural vs. Functional:** Distinguish between changes that alter code structure (renaming, moving functions) and changes that alter behavior (new logic, new values).
- **Adaptation:** If one branch changes the structure (e.g., renames a variable) and the other modifies the logic of that same variable, you must apply the new logic to the new name.
### 1. Semantic Intent & Sub-Agent Delegation
- **Context Path Extraction:** When you run the `extract` command, look for the `ai_context_path` in both the `local` and `remote` context objects.
- **Spawn Sub-Agents (CRITICAL):** If those paths exist, DO NOT guess the context. Spawn a sub-agent (using your platform's delegation capabilities and requesting a fast, lightweight model) and instruct it to read the files at those paths using your file-reading tools.
- **Summarization Request:** Tell your sub-agent to extract the core technical intent and return a concise, 5-sentence summary of why the code was written that way.
- **Wait and Synthesize:** Wait for the sub-agents to return the summaries. Once you have both summaries, mentally synthesize them to understand the true intent of both branches before writing any code.

### 2. Logic Synthesis
- **Preserve Intent:** Your goal is to produce code that satisfies the requirements of **both** branches simultaneously.
Expand Down