From 4f1d757dd8581f36f8d04e1db443a220eb00a8a4 Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sat, 11 Apr 2026 18:05:20 +0200 Subject: [PATCH] fix: more git integration fixes, refs NOISSUE --- ai_software_factory/agents/gitea.py | 18 ++++++++++++++++-- ai_software_factory/agents/orchestrator.py | 14 ++++++++++++++ ai_software_factory/dashboard_ui.py | 15 +++++++++++++++ ai_software_factory/main.py | 12 ++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ai_software_factory/agents/gitea.py b/ai_software_factory/agents/gitea.py index 522661c..37288c3 100644 --- a/ai_software_factory/agents/gitea.py +++ b/ai_software_factory/agents/gitea.py @@ -58,6 +58,18 @@ class GiteaAPI: """Build a Gitea API URL from a relative path.""" return f"{self.base_url}/api/v1/{path.lstrip('/')}" + def _normalize_pull_request_head(self, head: str | None, owner: str | None = None) -> str | None: + """Return a Gitea-compatible head ref for pull request creation.""" + normalized = (head or '').strip() + if not normalized: + return None + if ':' in normalized: + return normalized + effective_owner = (owner or self.owner or '').strip() + if not effective_owner: + return normalized + return f"{effective_owner}:{normalized}" + def build_repo_git_url(self, owner: str | None = None, repo: str | None = None) -> str | None: """Build the clone URL for a repository.""" _owner = owner or self.owner @@ -222,11 +234,12 @@ class GiteaAPI: """Create a pull request.""" _owner = owner or self.owner _repo = repo or self.repo + normalized_head = self._normalize_pull_request_head(head, _owner) payload = { "title": title, "body": body, "base": base, - "head": head or f"{_owner}-{_repo}-ai-gen-{hash(title) % 10000}", + "head": normalized_head or f"{_owner}:{_owner}-{_repo}-ai-gen-{hash(title) % 10000}", } return await self._request("POST", f"repos/{_owner}/{_repo}/pulls", payload) @@ -242,11 +255,12 @@ class GiteaAPI: """Synchronously create a pull request.""" _owner = owner or self.owner _repo = repo or self.repo + normalized_head = self._normalize_pull_request_head(head, _owner) payload = { "title": title, "body": body, "base": base, - "head": head or f"{_owner}-{_repo}-ai-gen-{hash(title) % 10000}", + "head": normalized_head or f"{_owner}:{_owner}-{_repo}-ai-gen-{hash(title) % 10000}", } return self._request_sync("POST", f"repos/{_owner}/{_repo}/pulls", payload) diff --git a/ai_software_factory/agents/orchestrator.py b/ai_software_factory/agents/orchestrator.py index b36c6fd..c2708dc 100644 --- a/ai_software_factory/agents/orchestrator.py +++ b/ai_software_factory/agents/orchestrator.py @@ -593,6 +593,16 @@ class AgentOrchestrator: f"Prompt: {self.prompt_text or self.description}\n\n" f"Branch: {self.branch_name}" ) + pull_request_debug = self.ui_manager.ui_data.setdefault('git', {}).setdefault('pull_request_debug', {}) + pull_request_request = { + 'owner': self.repo_owner, + 'repo': self.repo_name, + 'title': title, + 'body': body, + 'base': 'main', + 'head': self.gitea_api._normalize_pull_request_head(self.branch_name, self.repo_owner) or self.branch_name, + } + pull_request_debug['request'] = pull_request_request result = await self.gitea_api.create_pull_request( title=title, body=body, @@ -601,7 +611,9 @@ class AgentOrchestrator: base='main', head=self.branch_name, ) + pull_request_debug['response'] = result if result.get('error'): + pull_request_debug['status'] = 'error' raise RuntimeError(f"Unable to create pull request: {result.get('error')}") pr_number = result.get('number') or result.get('id') or 0 @@ -616,6 +628,8 @@ class AgentOrchestrator: 'merged': bool(result.get('merged')), 'pr_state': result.get('state', 'open'), } + pull_request_debug['status'] = 'created' + pull_request_debug['resolved'] = pr_data if self.db_manager and self.history: self.db_manager.save_pr_data(self.history.id, pr_data) self.active_pull_request = self.db_manager.get_open_pull_request(project_id=self.project_id) if self.db_manager else pr_data diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py index dbbb3c1..c00dd7b 100644 --- a/ai_software_factory/dashboard_ui.py +++ b/ai_software_factory/dashboard_ui.py @@ -261,6 +261,21 @@ def _render_generation_diagnostics(ui_data: dict | None) -> None: ui.label(f"Remote push error: {git_debug['remote_error']}").classes('factory-code') if git_debug.get('error'): ui.label(f"Git error: {git_debug['error']}").classes('factory-code') + pull_request_debug = git_debug.get('pull_request_debug') if isinstance(git_debug.get('pull_request_debug'), dict) else {} + if pull_request_debug: + ui.label('Pull request creation').style('font-weight: 700; color: #2f241d;') + if pull_request_debug.get('status'): + ui.label(str(pull_request_debug['status'])).classes('factory-chip') + if pull_request_debug.get('request'): + with ui.expansion('PR request payload').classes('w-full q-mt-sm'): + ui.label(json.dumps(pull_request_debug['request'], indent=2, sort_keys=True)).classes('factory-code') + if pull_request_debug.get('response'): + with ui.expansion('PR API response').classes('w-full q-mt-sm'): + ui.label(json.dumps(pull_request_debug['response'], indent=2, sort_keys=True)).classes('factory-code') + if pull_request_debug.get('resolved'): + resolved = pull_request_debug['resolved'] + if resolved.get('pr_url'): + ui.link('Open pull request', resolved['pr_url'], new_tab=True).classes('factory-code') def _render_timeline(events: list[dict]) -> None: diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py index 1b1607e..8ad3517 100644 --- a/ai_software_factory/main.py +++ b/ai_software_factory/main.py @@ -241,6 +241,17 @@ def _serialize_project_log(log: ProjectLog) -> dict: } +def _ensure_summary_mentions_pull_request(summary_message: str, pull_request: dict | None) -> str: + """Append the pull request URL to chat summaries when one exists.""" + if not isinstance(pull_request, dict): + return summary_message + pr_url = (pull_request.get('pr_url') or '').strip() + if not pr_url or pr_url in summary_message: + return summary_message + separator = '' if summary_message.endswith(('.', '!', '?')) else '.' + return f"{summary_message}{separator} Review PR: {pr_url}" + + def _serialize_system_log(log: SystemLog) -> dict: """Serialize a system log row.""" return { @@ -391,6 +402,7 @@ async def _run_generation( 'logs': [log.get('message', '') for log in response_data.get('logs', []) if isinstance(log, dict)], } summary_message, summary_trace = await ChangeSummaryGenerator().summarize_with_trace(summary_context) + summary_message = _ensure_summary_mentions_pull_request(summary_message, response_data.get('pull_request')) if orchestrator.db_manager and orchestrator.history and orchestrator.prompt_audit: orchestrator.db_manager.log_llm_trace( project_id=project_id,