2 Commits
0.9.8 ... 0.9.9

Author SHA1 Message Date
ac75cc2e3a release: version 0.9.9 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 14s
Upload Python Package / deploy (push) Successful in 2m17s
2026-04-11 17:41:29 +02:00
f7f00d4e14 fix: add missing git binary, refs NOISSUE 2026-04-11 17:41:24 +02:00
7 changed files with 171 additions and 7 deletions

View File

@@ -12,7 +12,10 @@ WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies

View File

@@ -5,10 +5,21 @@ Changelog
(unreleased)
------------
Fix
~~~
- Add missing git binary, refs NOISSUE. [Simon Diesenreiter]
0.9.8 (2026-04-11)
------------------
Fix
~~~
- More file change fixes, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.9.7 (2026-04-11)
------------------

View File

@@ -1 +1 @@
0.9.8
0.9.9

View File

@@ -2272,6 +2272,7 @@ class DatabaseManager:
"timeline": [],
"issues": [],
"issue_work": [],
"ui_data": {},
}
# Get logs
@@ -2296,6 +2297,7 @@ class DatabaseManager:
llm_traces = self.get_llm_traces(project_id=project_id)
correlations = self.get_prompt_change_correlations(project_id=project_id)
code_changes, local_only_code_changes, orphan_code_changes = self._partition_code_changes(raw_code_changes, commits)
ui_data = self._get_latest_ui_snapshot_data(history.id)
repository = self._get_project_repository(history)
timeline = self.get_project_timeline(project_id=project_id)
repository_sync = self.get_repository_sync_status(project_id=project_id)
@@ -2395,6 +2397,7 @@ class DatabaseManager:
"repository_sync": repository_sync,
"issues": issues,
"issue_work": issue_work,
"ui_data": ui_data,
}
def get_prompt_events(self, project_id: str | None = None, limit: int = 100) -> list[dict]:

View File

@@ -388,7 +388,27 @@ class AgentOrchestrator:
},
expect_json=True,
)
raw_generated_paths = self._extract_raw_generated_paths(content)
generated_files = self._parse_generated_files(content)
accepted_paths = list(generated_files.keys())
rejected_paths = [path for path in raw_generated_paths if path not in accepted_paths]
generation_debug = {
'raw_paths': raw_generated_paths,
'accepted_paths': accepted_paths,
'rejected_paths': rejected_paths,
'existing_workspace': has_existing_files,
}
self.ui_manager.ui_data['generation_debug'] = generation_debug
self._append_log(
'LLM returned file candidates: '
f"raw={raw_generated_paths or []}; accepted={accepted_paths or []}; rejected={rejected_paths or []}."
)
self._log_system_debug(
'generation',
'LLM file candidates '
f"raw={raw_generated_paths or []}; accepted={accepted_paths or []}; rejected={rejected_paths or []}; "
f"existing_workspace={has_existing_files}",
)
if has_existing_files:
return generated_files, trace, True
merged_files = {**fallback_files, **generated_files}
@@ -647,6 +667,35 @@ class AgentOrchestrator:
if self.db_manager and self.history:
self.db_manager._log_action(self.history.id, "INFO", message)
def _log_system_debug(self, component: str, message: str, level: str = 'INFO') -> None:
"""Persist a system-level debug breadcrumb for generation and git decisions."""
if not self.db_manager:
return
self.db_manager.log_system_event(component=component, level=level, message=f"{self.project_id}: {message}")
def _extract_raw_generated_paths(self, content: str | None) -> list[str]:
"""Return all file paths proposed by the LLM response before safety filtering."""
if not content:
return []
try:
parsed = json.loads(content)
except Exception:
return []
raw_paths: list[str] = []
if isinstance(parsed, dict) and isinstance(parsed.get('files'), list):
for item in parsed['files']:
if not isinstance(item, dict):
continue
path = str(item.get('path') or '').strip()
if path:
raw_paths.append(path)
elif isinstance(parsed, dict):
for path in parsed.keys():
normalized_path = str(path).strip()
if normalized_path:
raw_paths.append(normalized_path)
return raw_paths
def _update_progress(self, progress: int, step: str, message: str) -> None:
self.progress = progress
self.current_step = step
@@ -810,11 +859,25 @@ class AgentOrchestrator:
async def _commit_to_git(self) -> None:
"""Commit changes to git."""
unique_files = list(dict.fromkeys(self.changed_files))
git_debug = self.ui_manager.ui_data.setdefault('git', {})
if not unique_files:
git_debug.update({
'commit_status': 'skipped',
'early_exit_reason': 'changed_files_empty',
'candidate_files': [],
})
self._append_log('Git commit skipped: no generated files were marked as changed.')
self._log_system_debug('git', 'Commit exited early because changed_files was empty.')
return
if not self.git_manager.is_git_available():
self.ui_manager.ui_data.setdefault('git', {})['error'] = 'git executable is not available in PATH'
git_debug.update({
'commit_status': 'error',
'early_exit_reason': 'git_unavailable',
'candidate_files': unique_files,
'error': 'git executable is not available in PATH',
})
self._append_log('Git commit skipped: git executable is not available in PATH')
self._log_system_debug('git', 'Commit exited early because git is unavailable.', level='ERROR')
return
try:
@@ -822,7 +885,23 @@ class AgentOrchestrator:
self.git_manager.init_repo()
base_commit = self.git_manager.current_head_or_none()
self.git_manager.add_files(unique_files)
if not self.git_manager.get_status():
status_after_add = self.git_manager.get_status()
if not status_after_add:
git_debug.update({
'commit_status': 'skipped',
'early_exit_reason': 'clean_after_staging',
'candidate_files': unique_files,
'status_after_add': '',
})
self._append_log(
'Git commit skipped: working tree was clean after staging candidate files '
f'{unique_files}. No repository diff was created.'
)
self._log_system_debug(
'git',
'Commit exited early because git status was clean after staging '
f'files={unique_files}',
)
return
commit_message = f"AI generation for prompt: {self.project_name}"
@@ -835,11 +914,17 @@ class AgentOrchestrator:
"scope": "local",
"branch": self.branch_name,
}
git_debug.update({
'commit_status': 'committed',
'early_exit_reason': None,
'candidate_files': unique_files,
'status_after_add': status_after_add,
})
remote_record = None
try:
remote_record = await self._push_remote_commit(commit_hash, commit_message, unique_files, base_commit)
except (RuntimeError, subprocess.CalledProcessError, FileNotFoundError) as remote_exc:
self.ui_manager.ui_data.setdefault("git", {})["remote_error"] = str(remote_exc)
git_debug["remote_error"] = str(remote_exc)
self._append_log(f"Remote git push skipped: {remote_exc}")
if remote_record:
@@ -849,8 +934,8 @@ class AgentOrchestrator:
if remote_record.get('pull_request'):
commit_record['pull_request'] = remote_record['pull_request']
self.ui_manager.ui_data['pull_request'] = remote_record['pull_request']
self.ui_manager.ui_data.setdefault("git", {})["latest_commit"] = commit_record
self.ui_manager.ui_data.setdefault("git", {})["commits"] = [commit_record]
git_debug["latest_commit"] = commit_record
git_debug["commits"] = [commit_record]
self._append_log(f"Recorded git commit {commit_hash[:12]} for generated files.")
if self.db_manager:
self.db_manager.log_commit(
@@ -896,7 +981,12 @@ class AgentOrchestrator:
commit_url=remote_record.get('commit_url') if remote_record else None,
)
except (RuntimeError, subprocess.CalledProcessError, FileNotFoundError) as exc:
self.ui_manager.ui_data.setdefault("git", {})["error"] = str(exc)
git_debug.update({
'commit_status': 'error',
'early_exit_reason': 'commit_exception',
'candidate_files': unique_files,
'error': str(exc),
})
self._append_log(f"Git commit skipped: {exc}")
async def _create_pr(self) -> None:

View File

@@ -214,6 +214,55 @@ def _render_commit_list(commits: list[dict]) -> None:
ui.link('Open compare view', compare_url, new_tab=True)
def _render_generation_diagnostics(ui_data: dict | None) -> None:
"""Render generation and git diagnostics from the latest UI snapshot."""
snapshot = ui_data if isinstance(ui_data, dict) else {}
generation_debug = snapshot.get('generation_debug') if isinstance(snapshot.get('generation_debug'), dict) else {}
git_debug = snapshot.get('git') if isinstance(snapshot.get('git'), dict) else {}
if not generation_debug and not git_debug:
ui.label('No generation diagnostics captured yet.').classes('factory-muted')
return
def _render_path_row(label: str, values: list[str]) -> None:
text = ', '.join(values) if values else 'none'
ui.label(f'{label}: {text}').classes('factory-muted' if values else 'factory-code')
with ui.column().classes('gap-3 w-full'):
if generation_debug:
with ui.column().classes('gap-1'):
ui.label('Generation filtering').style('font-weight: 700; color: #2f241d;')
ui.label(
'Existing workspace: '
+ ('yes' if generation_debug.get('existing_workspace') else 'no')
).classes('factory-muted')
_render_path_row('Raw paths', generation_debug.get('raw_paths') or [])
_render_path_row('Accepted paths', generation_debug.get('accepted_paths') or [])
_render_path_row('Rejected paths', generation_debug.get('rejected_paths') or [])
if git_debug:
with ui.column().classes('gap-1'):
ui.label('Git outcome').style('font-weight: 700; color: #2f241d;')
if git_debug.get('commit_status'):
with ui.row().classes('items-center gap-2'):
ui.label(git_debug['commit_status']).classes('factory-chip')
if git_debug.get('early_exit_reason'):
ui.label(git_debug['early_exit_reason']).classes('factory-chip')
if git_debug.get('candidate_files') is not None:
_render_path_row('Candidate files', git_debug.get('candidate_files') or [])
latest_commit = git_debug.get('latest_commit') if isinstance(git_debug.get('latest_commit'), dict) else {}
if latest_commit:
ui.label(
f"Latest commit: {(latest_commit.get('hash') or 'unknown')[:12]} · {latest_commit.get('scope') or 'local'}"
).classes('factory-muted')
if git_debug.get('status_after_add'):
with ui.expansion('Git status after staging').classes('w-full q-mt-sm'):
ui.label(str(git_debug['status_after_add'])).classes('factory-code')
if git_debug.get('remote_error'):
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')
def _render_timeline(events: list[dict]) -> None:
"""Render a mixed project timeline."""
if not events:
@@ -1576,6 +1625,9 @@ def create_dashboard():
'Sync Repo Activity',
on_click=lambda _=None, project_id=project['project_id']: sync_project_repository_action(project_id),
).props('outline color=secondary').classes('q-mt-md')
with ui.card().classes('q-pa-md'):
ui.label('Generation Diagnostics').style('font-weight: 700; color: #3a281a;')
_render_generation_diagnostics(project_bundle.get('ui_data'))
@ui.refreshable
def render_archived_panel() -> None:
@@ -1642,6 +1694,9 @@ def create_dashboard():
with ui.card().classes('q-pa-md'):
ui.label('Repository').style('font-weight: 700; color: #3a281a;')
_render_repository_block(project_bundle.get('repository') or project.get('repository'))
with ui.card().classes('q-pa-md'):
ui.label('Generation Diagnostics').style('font-weight: 700; color: #3a281a;')
_render_generation_diagnostics(project_bundle.get('ui_data'))
with ui.card().classes('q-pa-md'):
ui.label('Prompt').style('font-weight: 700; color: #3a281a;')
prompts = project_bundle.get('prompts', [])

View File

@@ -348,6 +348,8 @@ async def _run_generation(
response_data = _serialize_project(history)
response_data['logs'] = [_serialize_project_log(log) for log in project_logs]
response_data['ui_data'] = result.get('ui_data')
response_data['generation_debug'] = ((result.get('ui_data') or {}).get('generation_debug'))
response_data['git_debug'] = ((result.get('ui_data') or {}).get('git'))
response_data['features'] = request.features
response_data['tech_stack'] = request.tech_stack
response_data['project_root'] = result.get('project_root', str(_project_root(project_id)))