Files
Aperant/auto-claude/merge/prompts.py
T
2025-12-16 12:48:36 +01:00

292 lines
8.9 KiB
Python

"""
AI Merge Prompt Templates
=========================
Templates for providing rich context to the AI merge resolver,
using the FileTimelineTracker's complete file evolution data.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .file_timeline import MergeContext, MainBranchEvent
def build_timeline_merge_prompt(context: "MergeContext") -> str:
"""
Build a complete merge prompt using FileTimelineTracker context.
This provides the AI with full situational awareness:
- Task's starting point (branch point)
- Complete main branch evolution since branch
- Task's intent and changes
- Other pending tasks that will merge later
Args:
context: MergeContext from FileTimelineTracker.get_merge_context()
Returns:
Formatted prompt string for AI merge resolution
"""
# Build main evolution section
main_evolution_section = _build_main_evolution_section(context)
# Build pending tasks section
pending_tasks_section = _build_pending_tasks_section(context)
prompt = f'''MERGING: {context.file_path}
TASK: {context.task_id} ({context.task_intent.title})
{"=" * 79}
TASK'S STARTING POINT
Branched from commit: {context.task_branch_point.commit_hash[:12]}
Branched at: {context.task_branch_point.timestamp}
{"" * 79}
```
{context.task_branch_point.content}
```
{"=" * 79}
{main_evolution_section}
CURRENT MAIN CONTENT (commit {context.current_main_commit[:12]}):
{"" * 79}
```
{context.current_main_content}
```
{"=" * 79}
TASK'S CHANGES
Intent: "{context.task_intent.description or context.task_intent.title}"
{"" * 79}
```
{context.task_worktree_content}
```
{"=" * 79}
{pending_tasks_section}
YOUR TASK:
1. Merge {context.task_id}'s changes into the current main version
2. PRESERVE all changes from main branch commits listed above
- Every human commit since the task branched must be retained
- Every previously merged task's changes must be retained
3. APPLY {context.task_id}'s changes
- Intent: {context.task_intent.description or context.task_intent.title}
- The task's changes should achieve its stated intent
4. ENSURE COMPATIBILITY with pending tasks
{_build_compatibility_instructions(context)}
5. OUTPUT only the complete merged file content
{"=" * 79}
'''
return prompt
def _build_main_evolution_section(context: "MergeContext") -> str:
"""Build the main branch evolution section of the prompt."""
if not context.main_evolution:
return f"""MAIN BRANCH EVOLUTION (0 commits since task branched)
{"" * 79}
No changes have been made to main branch since this task started.
"""
lines = [f"MAIN BRANCH EVOLUTION ({len(context.main_evolution)} commits since task branched)"]
lines.append("" * 79)
lines.append("")
for event in context.main_evolution:
source_label = event.source.upper()
if event.source == 'merged_task' and event.merged_from_task:
source_label = f"MERGED FROM {event.merged_from_task}"
lines.append(f'COMMIT {event.commit_hash[:12]} [{source_label}]: "{event.commit_message}"')
lines.append(f"Timestamp: {event.timestamp}")
if event.diff_summary:
lines.append(f"Changes: {event.diff_summary}")
else:
lines.append("Changes: See content evolution below")
lines.append("")
return "\n".join(lines)
def _build_pending_tasks_section(context: "MergeContext") -> str:
"""Build the other pending tasks section."""
separator = "" * 79
if not context.other_pending_tasks:
return f"""OTHER TASKS MODIFYING THIS FILE
{separator}
No other tasks are pending for this file.
"""
lines = ["OTHER TASKS ALSO MODIFYING THIS FILE (not yet merged)"]
lines.append("" * 79)
lines.append("")
for task in context.other_pending_tasks:
task_id = task.get("task_id", "unknown")
intent = task.get("intent", "No intent specified")
branch_point = task.get("branch_point", "unknown")[:12]
commits_behind = task.get("commits_behind", 0)
lines.append(f"{task_id} (branched at {branch_point}, {commits_behind} commits behind)")
lines.append(f' Intent: "{intent}"')
lines.append("")
return "\n".join(lines)
def _build_compatibility_instructions(context: "MergeContext") -> str:
"""Build compatibility instructions based on pending tasks."""
if not context.other_pending_tasks:
return "- No other tasks pending for this file"
lines = [f"- {len(context.other_pending_tasks)} other task(s) will merge after this"]
lines.append(" - Structure your merge to accommodate their upcoming changes:")
for task in context.other_pending_tasks:
task_id = task.get("task_id", "unknown")
intent = task.get("intent", "")
if intent:
lines.append(f" - {task_id}: {intent[:80]}...")
else:
lines.append(f" - {task_id}")
return "\n".join(lines)
def build_simple_merge_prompt(
file_path: str,
main_content: str,
worktree_content: str,
base_content: str | None,
spec_name: str,
language: str,
task_intent: dict | None = None,
) -> str:
"""
Build a simple three-way merge prompt (fallback when timeline not available).
This is the traditional merge prompt without full timeline context.
"""
intent_section = ""
if task_intent:
intent_section = f"""
=== FEATURE BRANCH INTENT ({spec_name}) ===
Task: {task_intent.get('title', spec_name)}
Description: {task_intent.get('description', 'No description')}
"""
if task_intent.get('spec_summary'):
intent_section += f"Summary: {task_intent['spec_summary']}\n"
base_section = base_content if base_content else "(File did not exist in common ancestor)"
prompt = f'''You are a code merge expert. Merge the following conflicting versions of a file.
FILE: {file_path}
The file was modified in both the main branch and in the "{spec_name}" feature branch.
Your task is to produce a merged version that incorporates ALL changes from both branches.
{intent_section}
=== COMMON ANCESTOR (base) ===
{base_section}
=== MAIN BRANCH VERSION ===
{main_content}
=== FEATURE BRANCH VERSION ({spec_name}) ===
{worktree_content}
MERGE RULES:
1. Keep ALL imports from both versions
2. Keep ALL new functions/components from both versions
3. If the same function was modified differently, combine the changes logically
4. Preserve the intent of BOTH branches - main's changes are important too
5. If there's a genuine semantic conflict (same thing done differently), prefer the feature branch version but include main's additions
6. The merged code MUST be syntactically valid {language}
Output ONLY the merged code, wrapped in triple backticks:
```{language}
merged code here
```
'''
return prompt
def optimize_prompt_for_length(
context: "MergeContext",
max_content_chars: int = 50000,
max_evolution_events: int = 10,
) -> "MergeContext":
"""
Optimize a MergeContext for prompt length by trimming large content.
For very long files or many commits, this summarizes the middle
parts to keep the prompt within reasonable bounds.
Args:
context: Original MergeContext
max_content_chars: Maximum characters for file content
max_evolution_events: Maximum main branch events to include
Returns:
Modified MergeContext with trimmed content
"""
# Trim main evolution to first N and last N events if too long
if len(context.main_evolution) > max_evolution_events:
half = max_evolution_events // 2
first_events = context.main_evolution[:half]
last_events = context.main_evolution[-half:]
# Create a placeholder event for the middle
from datetime import datetime
from .file_timeline import MainBranchEvent
omitted_count = len(context.main_evolution) - max_evolution_events
placeholder = MainBranchEvent(
commit_hash="...",
timestamp=datetime.now(),
content="[Content omitted for brevity]",
source="human",
commit_message=f"({omitted_count} commits omitted for brevity)",
)
context.main_evolution = first_events + [placeholder] + last_events
# Trim content if too long
def _trim_content(content: str, label: str) -> str:
if len(content) > max_content_chars:
half = max_content_chars // 2
return (
content[:half]
+ f"\n\n... [{label}: {len(content) - max_content_chars} chars omitted] ...\n\n"
+ content[-half:]
)
return content
context.task_branch_point.content = _trim_content(
context.task_branch_point.content, "branch point"
)
context.task_worktree_content = _trim_content(
context.task_worktree_content, "worktree"
)
context.current_main_content = _trim_content(
context.current_main_content, "main"
)
return context