From 2530d28eb2ef4997ab63373d08e0760d5c0e9a49 Mon Sep 17 00:00:00 2001 From: Ion Andrei Cristian Date: Tue, 28 Apr 2026 14:31:04 +0300 Subject: [PATCH 1/3] Implement backup and restore safety features --- llm-git-conflict-resolve/skill/git_tools.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/llm-git-conflict-resolve/skill/git_tools.py b/llm-git-conflict-resolve/skill/git_tools.py index 21ebec80..51c1f245 100644 --- a/llm-git-conflict-resolve/skill/git_tools.py +++ b/llm-git-conflict-resolve/skill/git_tools.py @@ -4,6 +4,7 @@ import json import os import ast +import shutil def run_git_command(command): """Executes a git command and returns the output as a string.""" @@ -95,6 +96,21 @@ def verify_syntax(filepath): # 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") @@ -110,6 +126,10 @@ 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() # Command routing @@ -120,6 +140,9 @@ def main(): elif args.command == "extract": filepath = args.filepath + #Perform the backup before extracting the data + create_backup(filepath) + # 1. Extract Diff (Code) diff_data = { "base": get_file_content_at_stage(filepath, 1), @@ -143,6 +166,10 @@ def main(): result = verify_syntax(args.filepath) print(json.dumps(result, indent=2)) + elif args.command == "restore": + result = restore_backup(args.filepath) + print(json.dumps(result, indent=2)) + else: # If no command is provided parser.print_help() From dc2e2c8775fce8f252a3dc33b445f6be44284fab Mon Sep 17 00:00:00 2001 From: Ion Andrei Cristian Date: Sat, 9 May 2026 11:22:48 +0300 Subject: [PATCH 2/3] add JS/TS syntax validation and smart error extraction --- llm-git-conflict-resolve/skill/git_tools.py | 61 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/llm-git-conflict-resolve/skill/git_tools.py b/llm-git-conflict-resolve/skill/git_tools.py index 51c1f245..1b4038ad 100644 --- a/llm-git-conflict-resolve/skill/git_tools.py +++ b/llm-git-conflict-resolve/skill/git_tools.py @@ -69,6 +69,20 @@ def get_commit_context(filepath): "remote_intent": remote_msg.strip() if remote_msg else "Unknown (No MERGE_HEAD found)" } +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 { + "status": "error", + "message": message, + "details": details + } + def verify_syntax(filepath): """ Verifies file syntax. Currently supports Python via AST. @@ -92,7 +106,52 @@ 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."} From 2d82fb328f16380e1336b8d6d5adb44e45410f65 Mon Sep 17 00:00:00 2001 From: Ion Andrei Cristian Date: Fri, 15 May 2026 20:40:38 +0300 Subject: [PATCH 3/3] refactor: switch to sub-agent delegation for context analysis --- llm-git-conflict-resolve/skill/git_tools.py | 81 +++++++++++++++---- .../skill/instructions.md | 20 +++-- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/llm-git-conflict-resolve/skill/git_tools.py b/llm-git-conflict-resolve/skill/git_tools.py index 1b4038ad..c3850022 100644 --- a/llm-git-conflict-resolve/skill/git_tools.py +++ b/llm-git-conflict-resolve/skill/git_tools.py @@ -5,14 +5,15 @@ 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: @@ -33,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 @@ -50,23 +51,54 @@ def get_file_content_at_stage(filepath, stage): Stage 3 = Theirs (Remote/MERGE_HEAD) """ # git show :: - 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 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.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)" + "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): @@ -191,6 +223,10 @@ def main(): 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() @@ -198,7 +234,10 @@ def main(): 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) @@ -222,10 +261,20 @@ 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)) diff --git a/llm-git-conflict-resolve/skill/instructions.md b/llm-git-conflict-resolve/skill/instructions.md index 905e4f0a..88632547 100644 --- a/llm-git-conflict-resolve/skill/instructions.md +++ b/llm-git-conflict-resolve/skill/instructions.md @@ -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 ` 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 ` command?" + +### COMMAND: restore +**Triggers:** "restore", "revert", "undo", "back" +**Action:** +1. **Execute:** `python3 git_tools.py restore ` +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. --- @@ -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.