-
Notifications
You must be signed in to change notification settings - Fork 313
fix: resolve critical bugs causing agent failures #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add documentation files for the fork: - FORK_README.md: Overview of fork features, configuration guide, how to stay updated with upstream, rollback instructions - FORK_CHANGELOG.md: Detailed changelog for all fork modifications These files document the differences from upstream and help users understand and manage the forked features. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add autocoder_config.py with full configuration schema for all planned features: - QualityGatesConfig: lint, type-check, custom scripts - GitWorkflowConfig: feature branches, trunk, none modes - ErrorRecoveryConfig: max retries, skip/escalate thresholds - CompletionConfig: auto-stop at 100%, regression cycles - CiCdConfig: provider and environments - SecurityScanningConfig: dependencies, secrets, injection patterns - LoggingConfig: level, structured output, timestamps Provides deep merge with defaults and convenience getters for each config section. Extends existing project_config.py pattern. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add error recovery system to handle stuck and failing features: Database changes (api/database.py): - Add failure_reason, failure_count, last_failure_at columns - Add quality_result column for quality gate results - Add auto-migration for new columns Progress tracking (progress.py): - Add clear_stuck_features() to clear orphaned in_progress flags Agent startup (agent.py): - Call clear_stuck_features() on agent startup - Prevents features from being stuck after interrupted sessions This addresses the common issue where features remain stuck with in_progress=True when agents are stopped mid-work. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new MCP tools for error recovery: - feature_report_failure: Report failures with escalation recommendations - count < 3: retry - count >= 3: skip - count >= 5: decompose - count >= 7: escalate to human - feature_get_stuck: Get all features with failure_count > 0 - feature_clear_all_in_progress: Clear all stuck features at once - feature_reset_failure: Reset failure counter for a feature feat(quality-gates): add feature_verify_quality MCP tool Add quality gates MCP tools: - feature_verify_quality: Run lint and type-check before marking passing - Modify feature_mark_passing to enforce quality checks in strict mode - Auto-detect linters: ESLint, Biome, ruff, flake8 - Auto-detect type checkers: TypeScript tsc, Python mypy In strict mode (default), feature_mark_passing BLOCKS if quality checks fail. Agent must fix issues and retry. Addresses leonvanzyl#68 (Agent skips features without testing) Addresses leonvanzyl#69 (Test evidence storage) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add quality_gates.py with: - Auto-detection of JS/TS linters (ESLint, Biome, npm lint) - Auto-detection of Python linters (ruff, flake8) - Auto-detection of type checkers (TypeScript tsc, Python mypy) - Support for custom quality scripts (.autocoder/quality-checks.sh) - Configuration loading from .autocoder/config.json Functions: - run_lint_check(): Run detected linter - run_type_check(): Run detected type checker - run_custom_script(): Run custom quality script - verify_quality(): Run all configured checks - load_quality_config(): Load config with defaults Results are stored in QualityGateResult TypedDict with: passed, timestamp, checks (dict), summary Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create the analyzers module with base class for stack detection: - analyzers/__init__.py: Module entry point, exports StackDetector - analyzers/base_analyzer.py: Abstract BaseAnalyzer class with: - RouteInfo, ComponentInfo, EndpointInfo, AnalysisResult TypedDicts - can_analyze() method to detect if analyzer applies - analyze() method to extract routes, components, endpoints - Helper methods: _read_file_safe(), _find_files() This provides the foundation for all stack-specific analyzers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add stack_detector.py that orchestrates multiple analyzers: - StackDetector class: Loads and runs all analyzers - detect(): Full analysis with routes, components, endpoints - detect_quick(): Fast detection for UI preview - StackDetectionResult: Complete result with: - detected_stacks list with confidence scores - primary_frontend, primary_backend, database - all_routes, all_endpoints, all_components - summary string for display The detector runs analyzers in order (more specific first like Next.js before React) and aggregates results. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add react_analyzer.py for React ecosystem detection: - Detects: React, React+Vite, Create React App, Next.js - Extracts routes from: - React Router (<Route> elements, createBrowserRouter) - Next.js App Router (app/page.tsx files) - Next.js Pages Router (pages/*.tsx files) - Extracts API endpoints from Next.js: - pages/api/*.ts (Pages Router) - app/api/route.ts (App Router, detects exported methods) - Extracts components from components/ and pages/ directories Detection heuristics: - package.json: "next", "react", "vite", "react-scripts" - Config files: next.config.js, vite.config.ts - Entry files: src/App.tsx, pages/_app.tsx, app/layout.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add node_analyzer.py for Node.js backend detection: - Detects: Express, NestJS, Fastify, Koa, generic Node.js - Extracts API endpoints from: - Express: router.get(), app.post() patterns - NestJS: @get(), @post() decorators with @controller() prefix - Fastify: fastify.get() patterns - Extracts components: - middleware/ files - services/ files and *.service.ts - models/ files and *.model.ts Detection heuristics: - package.json: "express", "@nestjs/core", "fastify", "koa" - File structure: routes/, controllers/, middleware/ - Entry files: app.js, server.js, src/index.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add python_analyzer.py for Python web framework detection: - Detects: FastAPI, Django, Flask - Extracts API endpoints from: - FastAPI: @app.get(), @router.post() with APIRouter prefix - Django: path() and re_path() in urls.py files - Flask: @app.route() with methods parameter, Blueprint prefixes - Extracts components: - models.py and models/ directories - views.py and views/ directories - services/ and *_service.py files Detection heuristics: - manage.py -> Django - requirements.txt/pyproject.toml: "fastapi", "flask", "django" - File imports: "from fastapi import", "from flask import" Converts Django <type:name> params to :name format. Parses Flask methods=['GET', 'POST'] syntax. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add vue_analyzer.py for Vue.js ecosystem detection:
- Detects: Vue.js, Vue+Vite, Vue CLI, Nuxt.js
- Extracts routes from:
- Vue Router: { path: '/about' } in router files
- Nuxt pages/ directory (file-based routing)
- Extracts API endpoints from Nuxt server/api directory:
- Detects HTTP method from filename (e.g., users.get.ts)
- Handles dynamic routes [id].ts
- Extracts components from:
- components/ directory
- views/ directory
- layouts/ directory
- pages/ directory (Nuxt)
Detection heuristics:
- package.json: "nuxt", "vue", "vite", "@vue/cli-service"
- Config files: nuxt.config.js/ts
- Entry files: src/App.vue, src/main.ts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document the Stack Detector implementation: - List all analyzer modules and their capabilities - Add usage example - Create supported stacks table with indicators - Document detection heuristics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add feature_extractor.py to transform detected routes/endpoints into Autocoder features: Functions: - extract_features(): Transform StackDetectionResult into features - extract_from_project(): One-step detection and extraction - features_to_bulk_create_format(): Format for feature_create_bulk MCP Feature generation: - Routes -> "View X page" navigation features - API endpoints -> "API: List/Create/Update/Delete X" features - Components -> Page/Model/Service features (lower priority) - Infrastructure -> Basic startup and health check features Naming heuristics: - /users -> "View users page" - /users/:id -> "View user details page" - POST /api/users -> "API: Create users" - DELETE /api/users/:id -> "API: Delete users" Each feature includes: - category: Navigation, API, Authentication, etc. - name: Human-readable feature name - description: What the feature should do - steps: Test/verification steps All features marked as pending (passes=False) for verification. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add server/routers/import_project.py with endpoints: - POST /api/import/analyze: Analyze project directory, detect stack - POST /api/import/extract-features: Generate features from analysis - POST /api/import/create-features: Create features in project database - GET /api/import/quick-detect: Quick stack detection for UI preview Response models: - AnalyzeResponse: Detected stacks, counts, summary - ExtractFeaturesResponse: Features with category, steps, confidence - CreateFeaturesResponse: Creation confirmation Integration: - Added import_project_router to server/routers/__init__.py - Registered router in server/main.py Usage flow: 1. User selects directory 2. POST /analyze to detect stack 3. POST /extract-features to generate features 4. User reviews and optionally edits features 5. POST /create-features to import into project Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document the Import Project REST API: - List all endpoints with methods and descriptions - Explain feature extraction logic - Add usage examples with curl commands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update the Planned Features checklist: - [x] Quality Gates (Phase 1) - [x] Stack Detector (Phase 2) - [x] Feature Extractor (Phase 2) - [x] Import Wizard API (Phase 2) - [x] Error Recovery (Phase 3) Split Import Wizard into API (done) and UI (pending). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add StructuredLogger class with JSON format, agent/feature/tool tracking - Add LogQuery class for filtering, search, timeline, and stats - Add REST API endpoints for log querying and export - Support JSONL, JSON, CSV export formats Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SecurityScanner class for vulnerability detection - Scan dependencies with npm audit / pip-audit - Detect hardcoded secrets (API keys, passwords, tokens) - Pattern matching for SQL injection, XSS, command injection - Add REST API endpoints for scanning and reports - Save reports to .autocoder/security-reports/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GitWorkflow class for branch management - Support modes: feature_branches, trunk, none - Auto-create branches like feature/42-user-can-login - Add REST API endpoints for workflow operations - Configurable via .autocoder/config.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add integrations/ci module for GitHub Actions - Generate CI, Security, and Deploy workflows - Auto-detect tech stack (Node.js, Python, TypeScript) - Add REST API endpoints for workflow generation - Support workflow preview without saving - Add PyYAML dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add templates module with library loader - Create 5 starter templates: - saas-starter: Multi-tenant SaaS - ecommerce: Online store - admin-dashboard: Admin panel - blog-cms: Content management - api-service: RESTful API - Add REST API endpoints for template management - Generate app_spec.txt from templates - Support design tokens customization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add useImportProject hook for import workflow state - Add ImportProjectModal component with multi-step wizard: - Folder selection - Stack detection display - Feature extraction and review - Feature selection (toggle individual features) - Project registration and feature creation - Modify NewProjectModal to add "Import Existing" option - Integrate with /api/import/* REST endpoints Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ReviewAgent module with: - AST-based Python analysis (unused imports, naming, error handling) - Pattern-based JavaScript/TypeScript analysis - Security vulnerability detection - Code complexity checks - REST API endpoints for running reviews and managing reports - Feature creation from review issues Configurable checks: dead_code, naming, error_handling, security, complexity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In `@agent.py`:
- Around line 215-235: The exception handler that currently calls
clear_stuck_features on parent-process detection failure must be changed to skip
clearing to avoid race conditions: in the parent detection block (where
parent_process = psutil.Process().parent() and parent_name is computed) remove
the fallback clear in the except clause and instead log/ignore the error so
clear_stuck_features(project_dir) is only called when parent_name check confirms
it's safe; additionally, change the session loop sleep from await
asyncio.sleep(1) to await asyncio.sleep(3) (the asyncio.sleep call in the main
agent session loop) to enforce the required 3-second delay between sessions.
In `@auto_documentation.py`:
- Around line 313-331: The glob pattern "**/routes/**/*.{js,ts}" is invalid
because Path.glob doesn't expand braces; update the file discovery in the loop
that builds endpoints (the route_file iteration) to only return .js and .ts
files — e.g., either iterate two globs for "*.js" and "*.ts" or use a broader
glob/rglob and filter by route_file.suffix in {".js", ".ts"} before reading
content and creating APIEndpoint entries (ensure you keep the existing matches,
APIEndpoint creation, and exception handling).
In `@mcp_server/feature_mcp.py`:
- Around line 231-264: The callsite in feature_verify_quality is passing
outdated kwargs (run_lint, run_type_check, run_custom) to verify_quality which
now expects do_lint, do_type_check, do_custom; update the verify_quality
invocation in feature_verify_quality (and the other similar call around the
276-333 region) to use do_lint=checks_config.get("lint", True),
do_type_check=checks_config.get("type_check", True), do_custom=True and keep
passing custom_script_path=checks_config.get("custom_script") as before so the
keyword names match the verify_quality function signature.
In `@server/routers/review.py`:
- Around line 137-167: The run_code_review endpoint passes request.files
directly into ReviewAgent (see run_code_review, ReviewAgent, get_project_dir,
request.files, project_dir), which allows absolute paths or '..' sequences to
escape the project directory; fix by normalizing and validating each file path
before calling agent.review: compute project_root = Path(project_dir).resolve(),
for each entry in request.files create a Path(f) (disallow empty/absolute
strings), resolve it relative to project_root (e.g.,
project_root.joinpath(f).resolve()) and ensure the resolved path is inside
project_root (use Path.resolve() and check startswith/project_root in place of
accepting the raw Path), reject or skip any file that fails validation and
return a 400 error (or omit invalid files) so only validated paths are passed
into ReviewAgent.
In `@server/routers/security.py`:
- Around line 118-125: The call to security_scanner.scan_project bypasses the
hierarchical allowlist/blocklist validation; update the code so scan_project (or
the caller) validates any shell commands (e.g., the npm/pip audit invocations)
using the existing security.py functions—import and use get_effective_commands
and/or bash_security_hook to evaluate commands against the hierarchy (hardcoded
blocklist → org blocklist → org allowlist → global allowlist → project
allowlist) before calling subprocess.run; if a command is disallowed, log/return
an error and do not execute it.
In `@ui/src/components/NewProjectModal.tsx`:
- Line 40: The unused state variable _projectType is causing CI to fail; replace
the current destructuring const [_projectType, setProjectType] =
useState<ProjectType | null>(null) by ignoring the unused value and only keeping
the setter (e.g., const [, setProjectType] = useState<ProjectType | null>(null))
so setProjectType can still be used in NewProjectModal without referencing
_projectType; update any imports/typing if necessary to keep ProjectType and
useState usage consistent.
🟠 Major comments (17)
server/services/autocoder_config.py-226-228 (1)
226-228: Shallow copy may cause mutation issues with nested dictionaries.
DEFAULT_CONFIG.copy()creates a shallow copy, so nested dictionaries (likequality_gates,git_workflow, etc.) will still reference the original objects. If a caller modifies the returned config, it could corruptDEFAULT_CONFIGfor subsequent calls.🐛 Proposed fix using copy.deepcopy
import json import logging +import copy from pathlib import Path from typing import Any, TypedDictThen replace all
DEFAULT_CONFIG.copy()usages:- return DEFAULT_CONFIG.copy() + return copy.deepcopy(DEFAULT_CONFIG)Apply this change at lines 228, 239, 247, and 250.
api/database.py-545-581 (1)
545-581: Same race condition pattern in atomic_update_priority_to_end.Similar to
atomic_mark_passing, this function reads the feature state before updating but doesn't useBEGIN IMMEDIATEto acquire a write lock. Thesession.refresh(feature)after commit could also fail if the feature was concurrently deleted.Proposed fix
def atomic_update_priority_to_end(session_maker, feature_id: int) -> dict: - session = session_maker() try: - # First get current state - feature = session.query(Feature).filter(Feature.id == feature_id).first() - ... - session.execute(text("""..."""), {"id": feature_id}) - session.commit() - session.refresh(feature) - ... + with atomic_transaction(session_maker) as session: + feature = session.query(Feature).filter(Feature.id == feature_id).first() + if feature is None: + return {"success": False, "error": f"Feature {feature_id} not found"} + if feature.passes: + return {"success": False, "error": "Cannot skip a feature that is already passing"} + + old_priority = feature.priority + name = feature.name + + session.execute(text(""" + UPDATE features + SET priority = (SELECT COALESCE(MAX(priority), 0) + 1 FROM features), + in_progress = 0 + WHERE id = :id + """), {"id": feature_id}) + + # Re-fetch to get new priority + session.flush() + result = session.execute( + text("SELECT priority FROM features WHERE id = :id"), + {"id": feature_id} + ).fetchone() + new_priority = result[0] if result else old_priority + 1 + + return { + "success": True, + "id": feature_id, + "name": name, + "old_priority": old_priority, + "new_priority": new_priority, + } except Exception as e: - session.rollback() return {"success": False, "error": str(e)} - finally: - session.close()api/database.py-494-528 (1)
494-528: Potential race condition in atomic_mark_passing.The function performs a read (to get the feature name) followed by a write, but without using
BEGIN IMMEDIATE. Another process could modify or delete the feature between the read and write. Consider using theatomic_transactioncontext manager or restructuring to avoid the read-before-write pattern.Proposed fix using atomic_transaction
def atomic_mark_passing(session_maker, feature_id: int) -> dict: - session = session_maker() - try: - # First get the feature name for the response - feature = session.query(Feature).filter(Feature.id == feature_id).first() - if feature is None: - return {"success": False, "error": f"Feature {feature_id} not found"} - - name = feature.name - - # Atomic update - session.execute(text(""" - UPDATE features - SET passes = 1, in_progress = 0 - WHERE id = :id - """), {"id": feature_id}) - session.commit() - - return {"success": True, "feature_id": feature_id, "name": name} - except Exception as e: - session.rollback() - return {"success": False, "error": str(e)} - finally: - session.close() + try: + with atomic_transaction(session_maker) as session: + feature = session.query(Feature).filter(Feature.id == feature_id).first() + if feature is None: + return {"success": False, "error": f"Feature {feature_id} not found"} + + name = feature.name + session.execute(text(""" + UPDATE features + SET passes = 1, in_progress = 0 + WHERE id = :id + """), {"id": feature_id}) + return {"success": True, "feature_id": feature_id, "name": name} + except Exception as e: + return {"success": False, "error": str(e)}server/routers/documentation.py-274-301 (1)
274-301: Delete endpoint shares incomplete path validation.The same
".." in filenamecheck at line 286 has the same bypass concerns asget_doc_content. Consider extracting path validation to a shared helper function.ui/src/components/ImportProjectModal.tsx-96-122 (1)
96-122: Multiple stale state reads in async handler.This handler has two async operations (
createProject.mutateAsyncandcreateFeatures) with a stale state check at line 112. IfcreateFeaturessets an error state, the check won't catch it.Consider restructuring to use returned results or callbacks for reliable error detection.
ui/src/components/ImportProjectModal.tsx-77-86 (1)
77-86: Same stale state issue applies here.The
state.step !== 'error'check afterawait extractFeatures()has the same race condition ashandleFolderSelect. Additionally,state.featuresResulton line 82 may also be stale.ui/src/components/ImportProjectModal.tsx-69-75 (1)
69-75: Potential stale state check after async operation.The check
state.step !== 'error'reads thestateobject captured at render time, not the updated state afteranalyze()completes. Since React state updates are asynchronous, this condition may not reflect the actual post-analysis state.Consider having
analyze()return a result or use a callback pattern:Suggested approach
const handleFolderSelect = async (path: string) => { setStep('analyzing') - await analyze(path) - if (state.step !== 'error') { - setStep('detected') - } + const result = await analyze(path) + if (result.success) { + setStep('detected') + } }This requires
analyze()to return a result object. Alternatively, useuseEffectto react tostate.stepchanges.server/routers/templates.py-252-266 (1)
252-266: User-controlled path without traversal protection.
request.project_diris used directly to construct aPathand create directories/files. A malicious input like../../etc/could write files outside intended locations.Consider validating the path:
Proposed fix
# Create project directory project_dir = Path(request.project_dir) + + # Validate path to prevent traversal attacks + try: + project_dir = project_dir.resolve() + # Optionally check against an allowed base directory + except (ValueError, OSError) as e: + raise HTTPException(status_code=400, detail=f"Invalid project path: {e}") + + if ".." in request.project_dir: + raise HTTPException(status_code=400, detail="Path traversal not allowed") + prompts_dir = project_dir / "prompts" prompts_dir.mkdir(parents=True, exist_ok=True)server/routers/documentation.py-204-206 (1)
204-206: Path traversal check is incomplete.The
".." in filenamecheck can be bypassed with URL-encoded sequences (%2e%2e) or on some systems with alternative representations. Additionally, symlinks could escape the project directory.Consider using path resolution to validate containment:
Proposed fix
# Validate filename to prevent path traversal if ".." in filename: raise HTTPException(status_code=400, detail="Invalid filename") file_path = project_dir / filename + + # Ensure resolved path is within project directory + try: + resolved = file_path.resolve() + if not resolved.is_relative_to(project_dir.resolve()): + raise HTTPException(status_code=400, detail="Access denied: path outside project") + except (ValueError, OSError): + raise HTTPException(status_code=400, detail="Invalid path")Note:
is_relative_to()requires Python 3.9+.parallel_orchestrator.py-870-873 (1)
870-873: Usewarning()instead ofwarn()to avoid AttributeError.The structured logger exposes
warning();warn()will raise at runtime on the max‑retry path.🔧 Suggested fix
- self._logger.warn("Feature exceeded max retries", feature_id=feature_id, failure_count=failure_count) + self._logger.warning("Feature exceeded max retries", feature_id=feature_id, failure_count=failure_count)structured_logging.py-523-547 (1)
523-547: Export withlimit=1000000could cause memory issues.Loading up to 1 million log entries into memory before writing could exhaust memory for large log databases. Consider streaming the export using a cursor-based approach.
♻️ Suggested streaming approach
def export_logs( self, output_path: Path, format: Literal["json", "jsonl", "csv"] = "jsonl", **filters, ) -> int: - # Get all matching logs - logs = self.query(limit=1000000, **filters) + # Stream logs to avoid memory issues + conn = self._connect() + cursor = conn.cursor() + # Build query with filters (reuse existing logic) + # ... + + count = 0 + if format == "jsonl": + with open(output_path, "w") as f: + for row in cursor: + f.write(json.dumps(dict(row)) + "\n") + count += 1 + # ... handle other formats similarly + + conn.close() + return countserver/routers/review.py-267-300 (1)
267-300: Wrap DB session cleanup in a finally block to prevent resource leaks on exceptions.Currently, if an exception occurs during
session.add(),session.commit(), or the query, theexceptblock at line 310 is triggered without closing the session, causing a connection leak. Use atry/finallystructure to guarantee cleanup.Suggested fix
session = get_session(db_path) + try: # Get max priority for ordering max_priority = session.query(Feature.priority).order_by(Feature.priority.desc()).first() current_priority = (max_priority[0] if max_priority else 0) + 1 for issue in request.issues: # Create feature from issue feature = Feature( priority=current_priority, category=issue.get("category", "Code Review"), name=issue.get("name", issue.get("title", "Review Issue")), description=issue.get("description", ""), steps=json.dumps(issue.get("steps", ["Fix the identified issue"])), passes=False, in_progress=False, ) session.add(feature) current_priority += 1 created_features.append( { "priority": feature.priority, "category": feature.category, "name": feature.name, "description": feature.description, } ) session.commit() - session.close() + finally: + session.close()server/routers/import_project.py-119-165 (1)
119-165: Userun_in_threadpoolto avoid blocking the event loop during analysis/extraction.
StackDetector.detect(),extract_from_project(), andStackDetector.detect_quick()are synchronous and perform heavy I/O operations (recursive file scanning withrglob(), reading multiple config files). Calling them directly insideasync defendpoints blocks the event loop; wrap them withrun_in_threadpool.Suggested fix
+from starlette.concurrency import run_in_threadpool @@ - result = detector.detect() + result = await run_in_threadpool(detector.detect) @@ - result = extract_from_project(project_dir) + result = await run_in_threadpool(extract_from_project, project_dir) @@ - result = detector.detect_quick() + result = await run_in_threadpool(detector.detect_quick)Also applies to: 171-214, 290-317
server/routers/security.py-98-125 (1)
98-125: Userun_in_threadpoolto avoid blocking the event loop.
scan_project(...)is synchronous and CPU/IO-heavy (subprocess calls with 120s timeouts, file reads). Calling it directly inasync defwill block the FastAPI event loop.Proposed fix
+from starlette.concurrency import run_in_threadpool - result = scan_project( - project_dir, - scan_dependencies=request.scan_dependencies, - scan_secrets=request.scan_secrets, - scan_code=request.scan_code, - ) + result = await run_in_threadpool( + scan_project, + project_dir, + scan_dependencies=request.scan_dependencies, + scan_secrets=request.scan_secrets, + scan_code=request.scan_code, + )server/routers/visual_regression.py-170-238 (1)
170-238: Avoid exposing raw exception details in 500 responses.Returning
str(e)can leak internal paths/config. Log the exception server‑side and return a generic message; apply the same change to all handlers using this pattern.🔐 Example adjustment for one handler (apply similarly elsewhere)
- except Exception as e: - logger.error(f"Visual test failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) + except Exception: + logger.exception("Visual test failed") + raise HTTPException(status_code=500, detail="Visual test failed")Also applies to: 241-259, 294-315, 317-338, 340-367, 403-423
server/routers/visual_regression.py-152-162 (1)
152-162: Restrict project paths to avoid arbitrary filesystem access.Allowing
project_nameto resolve to any directory enables callers to operate on unintended paths if this API is exposed. Prefer registry-backed paths (or a strict allowlist) and 404 otherwise.🔒 Proposed fix (disable arbitrary path fallback)
def get_project_dir(project_name: str) -> Path: """Get project directory from name or path.""" project_path = get_project_path(project_name) if project_path: return Path(project_path) - - path = Path(project_name) - if path.exists() and path.is_dir(): - return path - raise HTTPException(status_code=404, detail=f"Project not found: {project_name}")templates/library.py-243-314 (1)
243-314: Escape XML special characters to keep output valid.User‑provided names/descriptions (and template values) can contain
&,<, etc., which will break XML parsing.🛠️ Suggested escaping within generate_app_spec()
def generate_app_spec( template: Template, app_name: str, customizations: Optional[dict] = None, ) -> str: @@ customizations = customizations or {} + + def _xml_escape(value: str) -> str: + return ( + value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("'", "'") + ) @@ - f" <name>{app_name}</name>", - f" <description>{template.description}</description>", + f" <name>{_xml_escape(app_name)}</name>", + f" <description>{_xml_escape(template.description)}</description>", @@ - xml_parts.append(f" <frontend>{template.tech_stack.frontend}</frontend>") + xml_parts.append(f" <frontend>{_xml_escape(template.tech_stack.frontend)}</frontend>") @@ - xml_parts.append(f" <backend>{template.tech_stack.backend}</backend>") + xml_parts.append(f" <backend>{_xml_escape(template.tech_stack.backend)}</backend>") @@ - xml_parts.append(f" <database>{template.tech_stack.database}</database>") + xml_parts.append(f" <database>{_xml_escape(template.tech_stack.database)}</database>") @@ - xml_parts.append(f" <auth>{template.tech_stack.auth}</auth>") + xml_parts.append(f" <auth>{_xml_escape(template.tech_stack.auth)}</auth>") @@ - xml_parts.append(f" <styling>{template.tech_stack.styling}</styling>") + xml_parts.append(f" <styling>{_xml_escape(template.tech_stack.styling)}</styling>") @@ - xml_parts.append(f" <{color_name}>{color_value}</{color_name}>") + xml_parts.append(f" <{color_name}>{_xml_escape(color_value)}</{color_name}>") @@ - xml_parts.append(f" <category name=\"{category_title}\">") + xml_parts.append(f" <category name=\"{_xml_escape(category_title)}\">") @@ - xml_parts.append(f" <feature>{feature_name}</feature>") + xml_parts.append(f" <feature>{_xml_escape(feature_name)}</feature>")
🟡 Minor comments (16)
ui/src/hooks/useImportProject.ts-192-212 (1)
192-212: Feature identity based on name + category may cause issues.The toggle logic identifies features by
nameandcategorycombination. If two features have identical names within the same category (edge case), toggling one would affect both.Consider using a unique identifier if the API provides one, or document this constraint.
ui/src/hooks/useImportProject.ts-100-103 (1)
100-103: Error response parsing may throw if body isn't JSON.If the server returns a non-JSON error response (e.g., 500 with HTML),
response.json()will throw, potentially masking the original error.Proposed fix
if (!response.ok) { - const error = await response.json() - throw new Error(error.detail || 'Failed to analyze project') + const error = await response.json().catch(() => ({})) + throw new Error(error.detail || `Analysis failed (HTTP ${response.status})`) }Apply the same pattern to the other fetch error handlers at lines 132-134 and 172-174.
.claude/templates/coding_prompt.template.md-243-246 (1)
243-246: Fix curl fallback logic for health check.The current
&&logic means if the first curl succeeds, the second won't run (which is correct), but the negation with!combined with&&creates confusing semantics. If the intent is "fail if neither endpoint responds," the logic should use||for the fallback.Proposed fix
- if ! curl -f http://localhost:${PORT:-3000}/api/health && ! curl -f http://localhost:${PORT:-3000}; then + if ! curl -sf http://localhost:${PORT:-3000}/api/health && ! curl -sf http://localhost:${PORT:-3000}; then echo "ERROR: Server failed to start after restart" exit 1 fiNote: Added
-sflag to suppress progress output. The logic is actually correct as written (both must fail for the error), but consider adding a brief comment explaining the fallback pattern.design_tokens.py-541-566 (1)
541-566:generate_allreturns content strings, but docstring says paths.If callers expect file paths, they’ll receive CSS/SCSS content instead. Consider returning the output paths and generating files separately.
🔧 Suggested fix
- results = { - "css": str(self.generate_css(tokens, output / "tokens.css")), - "scss": str(self.generate_scss(tokens, output / "_tokens.scss")), - } + css_path = output / "tokens.css" + scss_path = output / "_tokens.scss" + self.generate_css(tokens, css_path) + self.generate_scss(tokens, scss_path) + results = { + "css": str(css_path), + "scss": str(scss_path), + } @@ - results["tailwind"] = str( - self.generate_tailwind_config(tokens, output / "tailwind.tokens.js") - ) + tailwind_path = output / "tailwind.tokens.js" + self.generate_tailwind_config(tokens, tailwind_path) + results["tailwind"] = str(tailwind_path)auto_documentation.py-334-354 (1)
334-354: Missingnode_modulesexclusion for Express route scanning.The Python file scanning (lines 335-336) excludes
node_modulesandvenv, but the Express route scanning (lines 313-331) does not. This could cause performance issues or incorrect results when scanning largenode_modulesdirectories.server/routers/logs.py-149-151 (1)
149-151: Deprecateddatetime.utcnow()usage.Multiple endpoints use
datetime.utcnow()which is deprecated in Python 3.12+. Usedatetime.now(timezone.utc)instead for consistency with structured_logging.py.🔧 Suggested fix
+from datetime import datetime, timedelta, timezone ... if since_hours: - since = datetime.utcnow() - timedelta(hours=since_hours) + since = datetime.now(timezone.utc) - timedelta(hours=since_hours)Apply to all occurrences in
query_logs,get_timeline,get_stats, andexport_logs.Also applies to: 198-198, 232-232, 265-265
integrations/ci/__init__.py-24-29 (1)
24-29:generate_ci_confignot included in__all__.The
generate_ci_configfunction is defined but not added to__all__, which could cause issues withfrom integrations.ci import *usage patterns.🔧 Suggested fix
__all__ = [ "generate_github_workflow", "generate_all_workflows", "GitHubWorkflow", "WorkflowTrigger", + "generate_ci_config", ]Also applies to: 32-32
structured_logging.py-436-439 (1)
436-439: Timezone inconsistency:datetime.utcnow()vstimezone.utc.Line 147 uses
datetime.now(timezone.utc)but lines 437-439 usedatetime.utcnow(). The latter is deprecated in Python 3.12+. Usedatetime.now(timezone.utc)consistently.🔧 Suggested fix
# Default to last 24 hours if not since: - since = datetime.utcnow() - timedelta(hours=24) + since = datetime.now(timezone.utc) - timedelta(hours=24) if not until: - until = datetime.utcnow() + until = datetime.now(timezone.utc)Apply the same fix to
get_agent_stats()and other methods usingutcnow().integrations/ci/github_actions.py-141-143 (1)
141-143: Node version extraction may produce invalid version strings.The code strips
>=^~from the version string but doesn't handle other common patterns like>=18.0.0 <20or18.x. This could result in unexpected version strings in the workflow.🔧 More robust version extraction
# Node version from engines engines = pkg.get("engines", {}) if "node" in engines: - version = engines["node"].strip(">=^~") - if version and version[0].isdigit(): - stack["node_version"] = version.split(".")[0] + import re + version_str = engines["node"] + # Extract first numeric version (e.g., ">=18.0.0" -> "18") + match = re.search(r'(\d+)', version_str) + if match: + stack["node_version"] = match.group(1)structured_logging.py-360-362 (1)
360-362: SQL injection risk via LIKE pattern.The
searchparameter is directly interpolated into a LIKE pattern. While parameterized queries are used, special LIKE characters (%,_) in user input could cause unexpected matching behavior. Consider escaping LIKE wildcards.🔒 Suggested fix
if search: + # Escape LIKE wildcards to prevent pattern injection + escaped_search = search.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") - conditions.append("message LIKE ?") - params.append(f"%{search}%") + conditions.append("message LIKE ? ESCAPE '\\'") + params.append(f"%{escaped_search}%")analyzers/vue_analyzer.py-131-134 (1)
131-134: Hardcoded confidence inanalyze()ignores detection confidence.The
analyze()method returns a hardcodedconfidence: 0.85regardless of the actual confidence determined incan_analyze(). Consider storing the detection confidence and returning it here.🐛 Proposed fix
def __init__(self, project_dir: Path): super().__init__(project_dir) self._detected_stack = "vue" + self._confidence = 0.0 def can_analyze(self) -> tuple[bool, float]: # ... existing detection logic ... if "nuxt" in deps or "nuxt3" in deps: self._detected_stack = "nuxt" - confidence = 0.95 - return True, confidence + self._confidence = 0.95 + return True, self._confidence # ... apply similar pattern to other detection branches ... def analyze(self) -> AnalysisResult: # ... return { "stack_name": self._detected_stack, - "confidence": 0.85, + "confidence": self._confidence or 0.85, # ... }analyzers/vue_analyzer.py-205-244 (1)
205-244: Inconsistent method detection between.tsand.jsAPI files.Method detection from filenames (e.g.,
users.get.ts→ GET) is only applied to.tsfiles (lines 216-221). The.jsfile handling (lines 232-244) always assignsmethod="ALL", missing the same logic.🐛 Proposed fix - extract shared logic
+ def _parse_api_file(self, api_file: Path, api_dir: Path) -> EndpointInfo: + """Parse an API file and return endpoint info.""" + rel_path = api_file.relative_to(api_dir) + route_path = "/api/" + str(rel_path.with_suffix("")) + route_path = route_path.replace("/index", "") + route_path = re.sub(r"\[([^\]]+)\]", r":\1", route_path) + + # Detect method from filename + method = "ALL" + for m in ["get", "post", "put", "patch", "delete"]: + if api_file.stem.endswith(f".{m}") or api_file.stem == m: + method = m.upper() + route_path = route_path.replace(f".{m}", "") + break + + return { + "path": route_path, + "method": method, + "handler": "handler", + "file": str(api_file.relative_to(self.project_dir)), + "description": f"{method} {route_path}", + } def _extract_nuxt_api_routes(self) -> list[EndpointInfo]: # ... for api_dir in api_dirs: if not api_dir.exists(): continue - for api_file in api_dir.rglob("*.ts"): - # ... existing logic ... - for api_file in api_dir.rglob("*.js"): - # ... existing logic ... + for api_file in api_dir.rglob("*.[tj]s"): + endpoints.append(self._parse_api_file(api_file, api_dir))server/routers/review.py-232-240 (1)
232-240: Validate filename before existence checks.Currently
report_path.exists()is evaluated before filename validation, which can leak information about files outside the reports directory. Validate first, then check existence.🛠️ Proposed fix
- report_path = project_dir / ".autocoder" / "review-reports" / filename - - if not report_path.exists(): - raise HTTPException(status_code=404, detail=f"Report not found: {filename}") - # Validate filename to prevent path traversal if ".." in filename or "/" in filename or "\\" in filename: raise HTTPException(status_code=400, detail="Invalid filename") + + report_path = project_dir / ".autocoder" / "review-reports" / filename + if not report_path.exists(): + raise HTTPException(status_code=404, detail=f"Report not found: {filename}")templates/library.py-130-154 (1)
130-154:preview_imageis dropped into_dict().This field is present on the model but not returned, which can break consumers expecting it.
✅ Include preview_image in serialization
def to_dict(self) -> dict: """Convert to dictionary.""" return { @@ "estimated_features": self.estimated_features, "tags": self.tags, "difficulty": self.difficulty, + "preview_image": self.preview_image, }analyzers/react_analyzer.py-173-264 (1)
173-264: Include.jsfiles and.tsfiles for Pages Router to capture all Next.js routes.App Router supports
page.tsx,page.jsx, andpage.js(note:page.tsis not supported since pages are React components). The current code only scans.tsxand.jsx, missingpage.jsroutes. Pages Router supports.tsx,.ts,.jsx, and.jsextensions, but the code only scans.tsxand.jsx, missing.tsand.jsroutes entirely. This omission will miss route extraction in JavaScript/TypeScript-only projects.server/routers/cicd.py-206-219 (1)
206-219: Inconsistent handling of.ymlvs.yamlextensions.
list_workflowsonly globs for*.ymlfiles (line 206), butget_workflow_contentaccepts both.ymland.yamlextensions (line 240). This inconsistency means.yamlworkflow files won't appear in the listing but can still be retrieved directly.🔧 Proposed fix to include both extensions
- for file in workflows_dir.glob("*.yml"): + for file in workflows_dir.glob("*.y*ml"):Or more explicitly:
for ext in ("*.yml", "*.yaml"): for file in workflows_dir.glob(ext): # ... process file
| from security_scanner import scan_project | ||
|
|
||
| result = scan_project( | ||
| project_dir, | ||
| scan_dependencies=request.scan_dependencies, | ||
| scan_secrets=request.scan_secrets, | ||
| scan_code=request.scan_code, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "(security\.py|security_scanner\.py)"Repository: leonvanzyl/autocoder
Length of output: 139
🏁 Script executed:
wc -l security.py security_scanner.py server/routers/security.pyRepository: leonvanzyl/autocoder
Length of output: 152
🏁 Script executed:
cat -n server/routers/security.py | head -140 | tail -30Repository: leonvanzyl/autocoder
Length of output: 1299
🏁 Script executed:
# Search for the scan_project function in security_scanner.py
rg -n "def scan_project" security_scanner.py -A 20Repository: leonvanzyl/autocoder
Length of output: 699
🏁 Script executed:
# Search for SecurityScanner class and its scan method
rg -n "class SecurityScanner" security_scanner.py -A 30Repository: leonvanzyl/autocoder
Length of output: 1095
🏁 Script executed:
# Continue reading the scan method implementation
rg -n "def scan" security_scanner.py -A 80 | head -120Repository: leonvanzyl/autocoder
Length of output: 3789
🏁 Script executed:
# Check if security_scanner.py imports or uses security.py
rg -n "from security|import security" security_scanner.pyRepository: leonvanzyl/autocoder
Length of output: 46
🏁 Script executed:
# Check what's in security.py - look for allowlist/blocklist implementation
rg -n "allowlist|blocklist|ALLOWED_COMMANDS" security.pyRepository: leonvanzyl/autocoder
Length of output: 934
🏁 Script executed:
# Read security.py to understand the hierarchical allowlist implementation
cat -n security.py | head -100Repository: leonvanzyl/autocoder
Length of output: 3132
🏁 Script executed:
# Continue reading security.py to see the hierarchical allowlist system
cat -n security.py | sed -n '100,250p'Repository: leonvanzyl/autocoder
Length of output: 5260
🏁 Script executed:
# Continue reading to find the validation functions
cat -n security.py | sed -n '650,750p'Repository: leonvanzyl/autocoder
Length of output: 4364
🏁 Script executed:
# Find the main command validation function
rg -n "def validate_bash_command|def validate_command" security.py -A 30 | head -80Repository: leonvanzyl/autocoder
Length of output: 46
🏁 Script executed:
# Search for the pre-tool-use hook mentioned in the code
rg -n "pre_tool_use|Pre-tool-use" security.py -A 40Repository: leonvanzyl/autocoder
Length of output: 2790
🏁 Script executed:
# Find the full function starting at line 823
rg -n "def.*validate.*bash|^def.*hook" security.py | grep -B2 -A2 "823"Repository: leonvanzyl/autocoder
Length of output: 46
🏁 Script executed:
# Get the function name around line 823
sed -n '815,830p' security.pyRepository: leonvanzyl/autocoder
Length of output: 566
🏁 Script executed:
# Get the full bash_security_hook function
sed -n '821,920p' security.pyRepository: leonvanzyl/autocoder
Length of output: 3818
The security_scanner does not enforce hierarchical allowlist validation.
The endpoint calls security_scanner.scan_project() which executes npm audit and pip audit via subprocess.run() without validating commands through the hierarchical allowlist system defined in security.py. The security_scanner module does not import or use the allowlist/blocklist validation required by coding guidelines. The hierarchical system exists in security.py (get_effective_commands(), bash_security_hook()) but is not applied here. Commands should be validated against: hardcoded blocklist → org blocklist → org allowlist → global allowlist → project allowlist before execution.
🤖 Prompt for AI Agents
In `@server/routers/security.py` around lines 118 - 125, The call to
security_scanner.scan_project bypasses the hierarchical allowlist/blocklist
validation; update the code so scan_project (or the caller) validates any shell
commands (e.g., the npm/pip audit invocations) using the existing security.py
functions—import and use get_effective_commands and/or bash_security_hook to
evaluate commands against the hierarchy (hardcoded blocklist → org blocklist →
org allowlist → global allowlist → project allowlist) before calling
subprocess.run; if a command is disallowed, log/return an error and do not
execute it.
- Remove duplicate get_logger import - Fix variable name do_custom vs run_custom Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update feature_mcp.py to use do_lint/do_type_check/do_custom instead of run_lint/run_type_check/run_custom after function signature change Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix glob pattern in auto_documentation.py (Python glob doesn't support brace expansion) - Remove unused variables in NewProjectModal.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused imports across multiple files (auto-fixed) - Fix ambiguous variable names (l -> lightness) in design_tokens.py - Remove unused variable assignments in feature_extractor.py and git_workflow.py - Sort import blocks per ruff I001 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In `@design_tokens.py`:
- Around line 541-572: generate_all currently places stringified contents into
results while the docstring promises file paths; update it to actually write
files and return their paths: ensure output dir (self.project_dir / "src" /
"styles" or provided output_dir) is created, call generate_css, generate_scss,
and generate_tailwind_config so that either (A) those functions return the
content and generate_all writes that content to output / "tokens.css", output /
"_tokens.scss", output / "tailwind.tokens.js" and stores their Path (or
str(Path)) in results, or (B) refactor those functions to return a Path after
writing; pick one consistent approach and update results to use the file Path
strings; keep validate_contrast(tokens) behavior unchanged and still include its
issues under "contrast_issues".
In `@git_workflow.py`:
- Around line 389-392: The current sequence in the method using
self._run_git("checkout", "--", ".") followed by self._run_git("clean", "-fd")
is dangerous because it will delete all untracked files; change this to a safer
behavior: either remove the self._run_git("clean", "-fd") call entirely so only
tracked files are reset, or replace it with a dry-run and explicit
logging/confirmation (use self._run_git("clean", "-fdn") to capture what would
be removed, log that output, and only call "clean -fd" if an explicit user/flag
confirmation is provided); update the method documentation/comments to clearly
state the destructive behavior if you keep deletion as an option.
- Around line 507-518: The try/except around load_config silently swallows all
errors; change the bare except to catch Exception as e and log the failure
(including the exception and context like project_dir) before falling back to
defaults. Specifically, around the call to load_config(project_dir) and
subsequent use of config/git_config/mode/branch_prefix/main_branch/auto_merge,
replace "except Exception: pass" with a handler that uses
logging.getLogger(__name__).exception(...) (or an existing logger) to record the
error and a clear message that config loading failed, then continue using the
defaults.
In `@integrations/ci/github_actions.py`:
- Around line 392-414: The CodeQL init step builds the languages value as a
comma-separated string using ", ".join(...) which produces "javascript, python"
but CodeQL expects a YAML list; update the init step in codeql_steps (the dict
with "name": "Initialize CodeQL") to pass languages as a list instead of a
joined string — e.g., construct the "languages" value as a list
comprehension/filter from the same conditionals (using "javascript" if
stack["has_node"] else None, "python" if stack["has_python"] else None) and
filter out None, so the action receives a proper list; locate this change near
codeql_steps and _checkout_step usage.
In `@server/routers/design_tokens.py`:
- Around line 120-130: The fallback in get_project_dir allows arbitrary
filesystem access by accepting absolute paths or paths with upward traversal;
change get_project_dir to reject any project_name that is an absolute path or
contains parent-segment traversal (e.g., '..') and only accept project_name
values that resolve via get_project_path; if get_project_path returns None,
raise the 404 immediately instead of treating project_name as a filesystem path.
Locate get_project_dir and remove/guard the Path(project_name) branch so that
only registered projects returned by get_project_path are used, returning a 404
for any unsafe or unknown input.
- Around line 206-225: The generate_token_files endpoint currently uses
project_dir / output_dir directly which allows absolute paths or path traversal
to write outside the project; before calling DesignTokensManager.generate_all,
resolve the combined path (e.g., resolved = (project_dir /
output_dir).resolve()) and ensure resolved is a child of project_dir.resolve()
(containment check), and if not, raise/return a 4xx error (or refuse the
request); keep all logic in generate_token_files and pass only the validated
resolved path into manager.generate_all to prevent directory escape.
In `@server/routers/documentation.py`:
- Around line 284-288: The delete endpoint's filename validation is vulnerable
to path traversal; replace the naive ".." check in the delete handler (where
variables filename, file_path, and project_dir are used) with a resolved-path
containment check: compute resolved_path = (project_dir / filename).resolve()
and ensure resolved_path is within project_dir.resolve() (e.g., by comparing
str(resolved_path).startswith(str(project_dir.resolve())) or using
os.path.commonpath); if not, raise HTTPException(status_code=400,
detail="Invalid filename"). This mirrors the fix applied to get_doc_content and
prevents traversal via symlinks or ../ sequences.
- Around line 203-207: The current naive check for ".." in filename is
insufficient; instead resolve the constructed path and verify it lies inside the
project directory. Build file_path from project_dir and filename, compute
resolved_target = file_path.resolve(), compute base = project_dir.resolve(),
then ensure resolved_target is within base (e.g., using
resolved_target.is_relative_to(base) or catching ValueError from
resolved_target.relative_to(base)); reject requests (raise HTTPException 400) if
the resolved path is outside base or if filename is absolute/contains null
bytes. Update the logic around the filename/project_dir/file_path variables
accordingly.
In `@server/routers/import_project.py`:
- Around line 40-45: The validate_path function currently only checks for the
literal substring ".." and null bytes; update validate_path to decode
URL-encodings and normalize Unicode, reject null bytes, then use pathlib.Path
and os.path.realpath to resolve symlinks and normalize the path; specifically,
call urllib.parse.unquote on the input, unicodedata.normalize (e.g. NFC/NFKC),
then construct a Path and inspect its parts (reject if any part == "..") instead
of substring checks, and finally compare the resolved realpath against an
allowed base directory (or otherwise ensure it stays under the intended root) to
prevent symlink/escape attacks while still permitting legit names containing
consecutive dots like "my..project". Ensure the function still returns a boolean
and reference validate_path when making these changes.
In `@server/routers/templates.py`:
- Around line 237-283: The apply_template endpoint is vulnerable because it uses
request.project_dir directly; update apply_template to validate and sanitize
project_dir before creating/writing paths by reusing the existing validate_path
(or equivalent) from import_project.py: call validate_path(request.project_dir)
(or copy its logic) to reject absolute, parent-traversal, and disallowed
locations, convert the validated path to a safe absolute Path, and only then
create prompts_dir, write app_spec.txt, and return the response; ensure any
validation failure raises HTTPException(400) and keep the try/except behavior in
apply_template.
In `@structured_logging.py`:
- Around line 142-155: The emit method in StructuredLogHandler currently sets
level using record.levelname.lower(), which yields "warning" but your LogLevel
enum expects "warn"; update emit (StructuredLogHandler.emit) to normalize level
names to the declared LogLevel values (e.g., map "warning" -> "warn", "err" ->
"error" if needed) before constructing StructuredLogEntry so stored entry.level
matches LogLevel and filtering by "warn" works as documented.
In `@templates/library.py`:
- Around line 263-292: The XML building code directly interpolates unescaped
user values (app_name, template.description, and color names/values in the
colors loop) into tags; fix by importing and using xml.sax.saxutils.escape on
all interpolated content (escape app_name and template.description before adding
the f-strings, and escape color_value inside the for loop). Also stop using raw
color_name as an XML tag (in the for loop that appends f"
<{color_name}>{color_value}</{color_name}>") — instead produce a safe element
like f' <color
name="{xml_escape(color_name)}">{xml_escape(color_value)}</color>' (or otherwise
sanitize color_name to a safe attribute) so tag names cannot be injected. Update
the xml_parts construction where xml_parts is extended and in the color loop to
apply these escapes consistently.
🟡 Minor comments (10)
ui/src/components/NewProjectModal.tsx-77-80 (1)
77-80: Clear stale errors after a valid folder selection.If a user previously hit “Please select a project folder first,” that error can persist into the method step even after selecting a folder. Clear the error when a valid path is chosen.
🛠️ Minimal fix
const handleFolderSelect = (path: string) => { - setProjectPath(path) // Use selected path directly - no subfolder creation + setProjectPath(path) // Use selected path directly - no subfolder creation + setError(null) changeStep('method') }git_workflow.py-229-231 (1)
229-231: Silentstash popfailure may leave working directory in an inconsistent state.If
stash popfails (e.g., due to merge conflicts), the failure is silently ignored. The user won't be aware that their changes are stuck in the stash, and the returnedWorkflowResultwill indicate success despite incomplete state restoration.Suggested improvement
# Apply stashed changes if any if had_changes: - self._run_git("stash", "pop", check=False) + pop_result = self._run_git("stash", "pop", check=False) + if pop_result.returncode != 0: + logger.warning(f"Stash pop had conflicts, manual resolution needed: {pop_result.stderr}") + return WorkflowResult( + success=True, + message=f"Created branch: {branch_name} (stashed changes had conflicts, run 'git stash pop' manually)", + branch_name=branch_name, + previous_branch=current_branch, + )security_scanner.py-467-510 (1)
467-510:pip-auditruns with-r requirements.txtwithout verifying file existence.The
_run_pip_auditmethod passes-r requirements.txttopip-audit, but the caller_scan_dependenciesonly checks ifrequirements.txtORpyproject.tomlexists. If onlypyproject.tomlexists,pip-auditwill fail to findrequirements.txt.🔧 Suggested fix
def _run_pip_audit(self, result: ScanResult) -> None: """Run pip-audit and parse results.""" result.scans_run.append("pip_audit") + req_file = self.project_dir / "requirements.txt" + if not req_file.exists(): + # No requirements.txt, skip pip-audit + return + # Try pip-audit first pip_audit_path = shutil.which("pip-audit") if pip_audit_path:server/routers/design_tokens.py-164-203 (1)
164-203: Allow explicit clearing of token sections.
Truthiness checks skip empty dict/list updates, so clients can’t clear a section. Useis not Noneto distinguish “unset” from “set empty.”✅ Proposed fix
- if request.colors: + if request.colors is not None: current.colors.update(request.colors) - if request.spacing: + if request.spacing is not None: current.spacing = request.spacing - if request.typography: + if request.typography is not None: current.typography.update(request.typography) - if request.borders: + if request.borders is not None: current.borders.update(request.borders) - if request.shadows: + if request.shadows is not None: current.shadows.update(request.shadows) - if request.animations: + if request.animations is not None: current.animations.update(request.animations)design_tokens.py-47-52 (1)
47-52: Handle 3‑digit hex colors before HSL conversion.
#RGBinputs are accepted by validation but are parsed incorrectly here. Normalize short hex before conversion.🎨 Proposed fix
- hex_color = self.value.lstrip("#") + hex_color = self.value.lstrip("#") + if len(hex_color) == 3: + hex_color = "".join(ch * 2 for ch in hex_color)review_agent.py-227-243 (1)
227-243: Checkgit diff-treefailures to avoid silent skips.
A non-zero return code currently yields empty results without any signal. Log and skip when the command fails.🧭 Proposed fix
result = subprocess.run( ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit], cwd=self.project_dir, capture_output=True, text=True, ) + if result.returncode != 0: + logger.warning( + f"git diff-tree failed for {commit}: {result.stderr.strip()}" + ) + continueanalyzers/python_analyzer.py-108-176 (1)
108-176: Use detected confidence instead of a constant.
analyze()currently reports0.85regardless of how the framework was detected, which can under/overstate certainty. Consider reusingcan_analyze()to populate the result.✅ Proposed fix
def analyze(self) -> AnalysisResult: """Analyze the Python project.""" + # Refresh detection to get the current confidence value + _, confidence = self.can_analyze() @@ return { "stack_name": self._detected_stack, - "confidence": 0.85, + "confidence": confidence, "routes": routes,agent.py-144-145 (1)
144-145: Uselogger.warning()instead of deprecatedlogger.warn().
logger.warn()is deprecated since Python 3.2. Uselogger.warning()instead for consistency and forward compatibility.🐛 Fix all occurrences
- if logger: - logger.warn("Rate limit detected", retry_seconds=retry_seconds) + if logger: + logger.warning("Rate limit detected", retry_seconds=retry_seconds)Also apply to lines 351, 358, 363, 444, 450.
templates/library.py-301-306 (1)
301-306: Feature names should also be XML-escaped.Feature names from the template could contain special characters.
🛠️ Add escaping
for category, feature_names in template.feature_categories.items(): category_title = category.replace("_", " ").title() - xml_parts.append(f" <category name=\"{category_title}\">") + xml_parts.append(f' <category name="{xml_escape(category_title)}">') for feature_name in feature_names: - xml_parts.append(f" <feature>{feature_name}</feature>") + xml_parts.append(f" <feature>{xml_escape(feature_name)}</feature>") xml_parts.append(" </category>")auto_documentation.py-269-286 (1)
269-286: Database session may leak on exception.If an exception occurs after
get_session()but beforesession.close()(lines 273-283), the session won't be closed. Consider using a context manager or try/finally.🐛 Suggested fix
try: from api.database import Feature, get_session session = get_session(db_path) - db_features = session.query(Feature).order_by(Feature.priority).all() - - for f in db_features: - features.append( - { - "category": f.category, - "name": f.name, - "description": f.description, - "status": "completed" if f.passes else "pending", - } - ) - session.close() + try: + db_features = session.query(Feature).order_by(Feature.priority).all() + for f in db_features: + features.append( + { + "category": f.category, + "name": f.name, + "description": f.description, + "status": "completed" if f.passes else "pending", + } + ) + finally: + session.close() except Exception as e: logger.warning(f"Error reading features.db: {e}")
🧹 Nitpick comments (30)
integrations/ci/github_actions.py (2)
83-92: Specify explicit UTF-8 encoding when writing workflow files.File operations should use explicit encoding to ensure consistent behavior across platforms and prevent encoding-related issues.
♻️ Proposed fix
def save(self, project_dir: Path) -> Path: """Save workflow to .github/workflows directory.""" workflows_dir = project_dir / ".github" / "workflows" workflows_dir.mkdir(parents=True, exist_ok=True) output_path = workflows_dir / self.filename - with open(output_path, "w") as f: + with open(output_path, "w", encoding="utf-8") as f: f.write(self.to_yaml()) return output_path
115-144: Use context manager for file reading and narrow exception handling.The file is opened with
open()but there's alsoread_text()used elsewhere. The exception handler catchesKeyErrorwhichjson.loadwon't raise directly—consider catchingjson.JSONDecodeErrorandOSErrorinstead.♻️ Proposed fix
if package_json.exists(): stack["has_node"] = True try: - with open(package_json) as f: + with open(package_json, encoding="utf-8") as f: pkg = json.load(f) deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} # ... rest of detection logic - except (json.JSONDecodeError, KeyError): + except (json.JSONDecodeError, OSError): passintegrations/ci/__init__.py (1)
24-29: Addgenerate_ci_configto__all__for consistent public API.The
generate_ci_configfunction is defined in this module but not exported in__all__, making the public API inconsistent.♻️ Proposed fix
__all__ = [ "generate_github_workflow", "generate_all_workflows", "GitHubWorkflow", "WorkflowTrigger", + "generate_ci_config", ]server/routers/cicd.py (3)
227-254: Define a response model for consistency and API documentation.Other endpoints use
response_modelfor type safety and OpenAPI documentation. This endpoint returns an untyped dict.♻️ Proposed fix
+class WorkflowContentResponse(BaseModel): + """Response with workflow file content.""" + + filename: str + content: str + -@router.get("/workflows/{project_name}/{filename}") +@router.get("/workflows/{project_name}/{filename}", response_model=WorkflowContentResponse) async def get_workflow_content(project_name: str, filename: str): """ Get the content of a specific workflow file. """ # ... existing code ... try: content = workflow_path.read_text() - return { + return WorkflowContentResponse( - "filename": filename, - "content": content, - } + filename=filename, + content=content, + ) except Exception as e: + logger.exception(f"Error reading workflow: {e}") raise HTTPException(status_code=500, detail=f"Error reading workflow: {str(e)}")
236-245: Consider usingPath.resolve()for more robust path traversal prevention.The current check catches obvious patterns but could miss symlink-based escapes. Using
resolve()and verifying the result is under the expected directory provides stronger protection.🔒 Proposed enhancement
# Security: validate filename if ".." in filename or "/" in filename or "\\" in filename: raise HTTPException(status_code=400, detail="Invalid filename") if not filename.endswith((".yml", ".yaml")): raise HTTPException(status_code=400, detail="Invalid workflow filename") workflow_path = project_dir / ".github" / "workflows" / filename + # Ensure resolved path stays within workflows directory + workflows_dir = (project_dir / ".github" / "workflows").resolve() + try: + workflow_path = workflow_path.resolve() + if not workflow_path.is_relative_to(workflows_dir): + raise HTTPException(status_code=400, detail="Invalid filename") + except ValueError: + raise HTTPException(status_code=400, detail="Invalid filename") + if not workflow_path.exists(): raise HTTPException(status_code=404, detail="Workflow not found")
150-152: Avoid redundant exception message in f-string.
logger.exception()automatically includes the traceback. The exception message in the f-string is redundant.♻️ Proposed fix
except Exception as e: - logger.exception(f"Error generating workflows: {e}") + logger.exception("Error generating workflows") raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")The same pattern applies to line 188.
ui/src/components/NewProjectModal.tsx (1)
277-281: Add dialog semantics (and verify focus/ESC behavior).The custom modal markup no longer inherits accessible dialog semantics. Please add
role="dialog"/aria-modaland a label id, and confirm focus trapping + Escape handling (or switch to Radix Dialog for this). Apply consistently to the folder modal too.♿ Suggested baseline semantics
- <div - className="neo-modal w-full max-w-lg" - onClick={(e) => e.stopPropagation()} - > + <div + className="neo-modal w-full max-w-lg" + role="dialog" + aria-modal="true" + aria-labelledby="new-project-modal-title" + onClick={(e) => e.stopPropagation()} + > @@ - <h2 className="font-display font-bold text-xl text-[var(--color-neo-text)]"> + <h2 + id="new-project-modal-title" + className="font-display font-bold text-xl text-[var(--color-neo-text)]" + >git_workflow.py (1)
29-36: Consider consolidating duplicateBranchInfodefinitions.This file defines
BranchInfoas a dataclass, whileserver/routers/git_workflow.pydefines a separate PydanticBranchInfomodel with identical fields. Having two definitions creates maintenance overhead—changes must be synchronized manually.Consider either:
- Importing and reusing the dataclass in the router (converting to Pydantic on response), or
- Defining a single canonical model that both modules share.
server/routers/git_workflow.py (1)
114-136: Blocking I/O in async endpoints may degrade server performance.All endpoints are defined as
async defbut call synchronous blocking operations (subprocess calls viaworkflow.get_status(),start_feature(), etc.). This blocks the event loop and can cause poor performance under concurrent load.Either:
- Change to regular
defendpoints—FastAPI will run them in a threadpool automatically- Wrap blocking calls with
await asyncio.to_thread(...)(Python 3.9+)Option 1: Use sync endpoints (simpler)
`@router.get`("/status/{project_name}", response_model=GitStatusResponse) -async def get_git_status(project_name: str): +def get_git_status(project_name: str): """ Get current git workflow status for a project.Apply the same change to all endpoints in this router.
Option 2: Use asyncio.to_thread for blocking calls
import asyncio `@router.get`("/status/{project_name}", response_model=GitStatusResponse) async def get_git_status(project_name: str): # ... workflow = get_workflow(project_dir) status = await asyncio.to_thread(workflow.get_status) return GitStatusResponse(**status)server/routers/security.py (3)
26-30: Consider extracting_get_project_pathto a shared utility.This helper is duplicated across multiple routers (
import_project.py,cicd.py,git_workflow.py,logs.py). Consider extracting it to a shared module likeserver/utils.pyorserver/common.pyto reduce duplication.
126-135:report_saved=Trueis hardcoded regardless of actual save outcome.The scanner's
_save_reportmethod could fail silently (it catches no exceptions but also doesn't propagate failures). Consider passingsave_report=Trueto the scanner and returning the actual outcome, or documenting that this flag indicates intent rather than success.
172-181: Path traversal protection is good; consider adding canonical path check for defense-in-depth.The current validation is solid. For additional safety, you could also verify the resolved path is within the expected directory:
♻️ Optional enhancement
# Security: validate filename to prevent path traversal if ".." in filename or "/" in filename or "\\" in filename: raise HTTPException(status_code=400, detail="Invalid filename") if not filename.startswith("security_scan_") or not filename.endswith(".json"): raise HTTPException(status_code=400, detail="Invalid report filename") report_path = project_dir / ".autocoder" / "security-reports" / filename + # Defense-in-depth: verify resolved path is within expected directory + reports_dir = project_dir / ".autocoder" / "security-reports" + if not report_path.resolve().is_relative_to(reports_dir.resolve()): + raise HTTPException(status_code=400, detail="Invalid filename") + if not report_path.exists(): raise HTTPException(status_code=404, detail="Report not found")security_scanner.py (1)
367-370: Consider usingdatetime.now(timezone.utc)instead ofdatetime.utcnow().
datetime.utcnow()is deprecated in Python 3.12+. The recommended alternative isdatetime.now(timezone.utc).♻️ Suggested update
+from datetime import datetime, timezone - scan_time=datetime.utcnow().isoformat() + "Z", + scan_time=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),And for the timestamp in
_save_report:- timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")analyzers/stack_detector.py (3)
118-120: Use logging instead of print() for library warnings.Using
print()in a library module makes it difficult to control output in production. Consider using the standard logging module for consistency with the rest of the codebase.♻️ Suggested refactor
+import logging + +logger = logging.getLogger(__name__) + ... except Exception as e: # Log but don't fail - continue with other analyzers - print(f"Warning: {analyzer.stack_name} analyzer failed: {e}") + logger.warning(f"{analyzer.stack_name} analyzer failed: {e}")
91-91: Consider extracting the confidence threshold as a class constant.The magic number
0.3is used in bothdetect()anddetect_quick(). Extracting it as a class constant improves maintainability.♻️ Suggested refactor
class StackDetector: + MIN_CONFIDENCE_THRESHOLD = 0.3 + ... - if can_analyze and confidence > 0.3: # Minimum confidence threshold + if can_analyze and confidence > self.MIN_CONFIDENCE_THRESHOLD:
96-104: Category mapping is reasonable but could be extended.The stack-to-category mapping handles common frameworks. Consider adding
"koa","fastify"to the backend list sinceNodeAnalyzerdetects these frameworks per the relevant code snippets.server/routers/import_project.py (3)
29-31: Avoid runtime sys.path manipulation.Modifying
sys.pathat import time is fragile and can cause issues in testing or when the module is imported from different contexts. Consider using proper package structure or relative imports instead.
241-282: Database session should use a context manager pattern.The current implementation relies on a
finallyblock for cleanup, but ifcreate_database()fails after creating the session, the session won't be closed properly. Consider using a context manager or the pattern used elsewhere in the codebase.♻️ Suggested refactor
try: from api.database import Feature, create_database + from contextlib import closing # Initialize database engine, SessionLocal = create_database(project_dir) - session = SessionLocal() - - try: + with closing(SessionLocal()) as session: # Get starting priority from sqlalchemy import func max_priority = session.query(func.max(Feature.priority)).scalar() or 0 ... session.commit() return CreateFeaturesResponse(...) - finally: - session.close() except Exception as e: ...
289-319: Consider using POST for/quick-detectfor consistency.Other similar endpoints (
/analyze,/extract-features) use POST with a request body. Using GET with a query parameter here creates an inconsistent API surface and exposes the path in server logs/URL history.♻️ Suggested change for consistency
-@router.get("/quick-detect") -async def quick_detect(path: str): +@router.post("/quick-detect") +async def quick_detect(request: AnalyzeRequest): """ Quick detection endpoint for UI preview. ... """ - if not validate_path(path): + if not validate_path(request.path): raise HTTPException(status_code=400, detail="Invalid path") - project_dir = Path(path).resolve() + project_dir = Path(request.path).resolve() ...templates/library.py (1)
171-176: Add explicit encoding and improve error handling.
open()should specifyencoding="utf-8"for cross-platform consistency.- Silently returning
Noneon any exception makes debugging difficult. Consider logging the error.♻️ Suggested fix
+import logging + +logger = logging.getLogger(__name__) + ... try: - with open(template_path) as f: + with open(template_path, encoding="utf-8") as f: data = yaml.safe_load(f) return Template.from_dict(template_id, data) - except Exception: + except Exception as e: + logger.warning(f"Failed to load template {template_id}: {e}") return Noneserver/routers/templates.py (1)
286-316: Consider adding a response model for/featuresendpoint.Unlike other endpoints, this one returns a plain dict. Adding a response model would provide better API documentation and validation.
♻️ Suggested addition
+class TemplateFeaturesResponse(BaseModel): + """Response with template features.""" + template_id: str + features: list[dict] + count: int + by_category: dict[str, int] + -@router.get("/{template_id}/features") +@router.get("/{template_id}/features", response_model=TemplateFeaturesResponse) async def get_template_features(template_id: str):auto_documentation.py (2)
72-74:datetime.utcnow()is deprecated in Python 3.12+.This method is deprecated and will be removed in future Python versions. Consider using timezone-aware datetimes instead.
♻️ Suggested fix
+from datetime import datetime, timezone ... def __post_init__(self): if not self.generated_at: - self.generated_at = datetime.utcnow().isoformat() + "Z" + self.generated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
333-334: Silent exception handling hinders debugging.Multiple methods use bare
except: passblocks (lines 196, 333, 356, 408, 441, 455) which silently swallow errors. While partial results are acceptable for documentation generation, consider logging warnings to aid debugging when extraction fails unexpectedly.♻️ Example improvement
except Exception: - pass + logger.debug(f"Could not extract routes from {route_file}: skipping")server/routers/documentation.py (3)
165-173: Consider using ISO format for timestamps.Using
__str__()onst_mtimeproduces a raw float string. Consider formatting as ISO timestamp for better readability and consistency with other parts of the codebase.♻️ Suggested improvement
+from datetime import datetime ... stat = file_path.stat() files.append( DocFile( filename=md_file, path=md_file, size=stat.st_size, - modified=stat.st_mtime.__str__(), + modified=datetime.fromtimestamp(stat.st_mtime).isoformat(), ) )
90-100:get_project_diris duplicated across multiple routers.This helper function is identical in
documentation.py,design_tokens.py,review.py, andvisual_regression.py. Consider extracting to a shared utility module to reduce duplication.# server/routers/utils.py (new file) from pathlib import Path from fastapi import HTTPException from registry import get_project_path def get_project_dir(project_name: str) -> Path: """Get project directory from name or path.""" project_path = get_project_path(project_name) if project_path: return Path(project_path) path = Path(project_name) if path.exists() and path.is_dir(): return path raise HTTPException(status_code=404, detail=f"Project not found: {project_name}")
236-256: Preview duplicates README generation logic.The preview endpoint reimplements README markdown generation instead of reusing
DocumentationGenerator.write_readme. Consider refactoringwrite_readmeto optionally return content without writing, or extract the generation logic to a shared method.server/routers/visual_regression.py (1)
377-391: Path validation could be strengthened with resolution check.While the string-based validation (checking for
..,/,\) is reasonable, consider adding a resolution check to ensure the final path stays within the expected directory. The fixed subdirectory structure (.visual-snapshots) provides some protection, but defense in depth is advisable for file-serving endpoints.♻️ Example fix
if ".." in filename or "/" in filename or "\\" in filename: raise HTTPException(status_code=400, detail="Invalid filename") if not filename.endswith(".png"): raise HTTPException(status_code=400, detail="Only PNG files are supported") snapshot_path = project_dir / ".visual-snapshots" / snapshot_type / filename + # Verify path is within expected directory + try: + snapshot_path.resolve().relative_to((project_dir / ".visual-snapshots").resolve()) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid filename") + if not snapshot_path.exists():visual_regression.py (3)
116-118:datetime.utcnow()is deprecated in Python 3.12+.Same issue as in
auto_documentation.py. This appears in multiple places: lines 118, 291, 365, 388.♻️ Suggested fix
+from datetime import datetime, timezone ... def __post_init__(self): if not self.test_time: - self.test_time = datetime.utcnow().isoformat() + "Z" + self.test_time = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
309-313: Moveimport shutilto module level.The
shutilimport appears inside methods (lines 311, 412). For consistency and slight performance benefit on repeated calls, move it to the top of the file with other imports.
250-264: Pixel-by-pixel iteration may be slow for large images.For a 1920×1080 image, this iterates over ~2M pixels. Consider using NumPy for vectorized operations if performance becomes an issue with large or numerous screenshots.
♻️ Example optimization using NumPy
import numpy as np # Convert to numpy arrays diff_array = np.array(diff) baseline_rgba = np.array(baseline.convert("RGBA")) # Find pixels where sum > 30 mask = np.sum(diff_array, axis=2) > 30 # Create highlight overlay highlight = baseline_rgba.copy() highlight[mask] = [255, 0, 0, 128] # Red highlight result = Image.fromarray(highlight) result.save(diff_path)
| try: | ||
| # Discard uncommitted changes | ||
| self._run_git("checkout", "--", ".", check=False) | ||
| self._run_git("clean", "-fd", check=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
git clean -fd is destructive and may delete untracked files unintentionally.
The combination of checkout -- . and clean -fd removes all uncommitted changes AND all untracked files/directories. This could delete important files that were never committed (new source files, local configs, test data, IDE files, etc.).
Consider either:
- Removing the
clean -fdcall, only discarding changes to tracked files - Using
clean -fdn(dry-run) first and logging what would be deleted - Documenting this behavior prominently in the API response/docs so users are warned
Safer alternative that only resets tracked files
try:
# Discard uncommitted changes
self._run_git("checkout", "--", ".", check=False)
- self._run_git("clean", "-fd", check=False)
+ # Note: Not running 'git clean' to preserve untracked files
+ # Users who want to remove untracked files should do so manually🤖 Prompt for AI Agents
In `@git_workflow.py` around lines 389 - 392, The current sequence in the method
using self._run_git("checkout", "--", ".") followed by self._run_git("clean",
"-fd") is dangerous because it will delete all untracked files; change this to a
safer behavior: either remove the self._run_git("clean", "-fd") call entirely so
only tracked files are reset, or replace it with a dry-run and explicit
logging/confirmation (use self._run_git("clean", "-fdn") to capture what would
be removed, log that output, and only call "clean -fd" if an explicit user/flag
confirmation is provided); update the method documentation/comments to clearly
state the destructive behavior if you keep deletion as an option.
| try: | ||
| from server.services.autocoder_config import load_config | ||
|
|
||
| config = load_config(project_dir) | ||
| git_config = config.get("git_workflow", {}) | ||
|
|
||
| mode = git_config.get("mode", "trunk") | ||
| branch_prefix = git_config.get("branch_prefix", "feature/") | ||
| main_branch = git_config.get("main_branch", "main") | ||
| auto_merge = git_config.get("auto_merge", False) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent exception swallowing masks configuration errors.
The bare except Exception: pass silently discards all errors during config loading, including ImportError, file permission issues, JSON parsing failures, and any bugs in the config loader. Users get default settings with no indication that their configuration wasn't applied.
Log the error to aid debugging
try:
from server.services.autocoder_config import load_config
config = load_config(project_dir)
git_config = config.get("git_workflow", {})
mode = git_config.get("mode", "trunk")
branch_prefix = git_config.get("branch_prefix", "feature/")
main_branch = git_config.get("main_branch", "main")
auto_merge = git_config.get("auto_merge", False)
- except Exception:
- pass
+ except Exception as e:
+ logger.debug(f"Could not load git workflow config, using defaults: {e}")🤖 Prompt for AI Agents
In `@git_workflow.py` around lines 507 - 518, The try/except around load_config
silently swallows all errors; change the bare except to catch Exception as e and
log the failure (including the exception and context like project_dir) before
falling back to defaults. Specifically, around the call to
load_config(project_dir) and subsequent use of
config/git_config/mode/branch_prefix/main_branch/auto_merge, replace "except
Exception: pass" with a handler that uses
logging.getLogger(__name__).exception(...) (or an existing logger) to record the
error and a clear message that config loading failed, then continue using the
defaults.
| codeql_steps = [ | ||
| _checkout_step(), | ||
| { | ||
| "name": "Initialize CodeQL", | ||
| "uses": "github/codeql-action/init@v3", | ||
| "with": { | ||
| "languages": ", ".join( | ||
| filter(None, [ | ||
| "javascript" if stack["has_node"] else None, | ||
| "python" if stack["has_python"] else None, | ||
| ]) | ||
| ), | ||
| }, | ||
| }, | ||
| { | ||
| "name": "Autobuild", | ||
| "uses": "github/codeql-action/autobuild@v3", | ||
| }, | ||
| { | ||
| "name": "Perform CodeQL Analysis", | ||
| "uses": "github/codeql-action/analyze@v3", | ||
| }, | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CodeQL languages should be a YAML list, not a comma-separated string.
The languages field in CodeQL action expects a YAML list (or newline-separated string), but ", ".join(...) produces a comma-space-separated string like "javascript, python". This may cause CodeQL to fail or only analyze the first language.
🐛 Proposed fix
codeql_steps = [
_checkout_step(),
{
"name": "Initialize CodeQL",
"uses": "github/codeql-action/init@v3",
"with": {
- "languages": ", ".join(
- filter(None, [
- "javascript" if stack["has_node"] else None,
- "python" if stack["has_python"] else None,
- ])
- ),
+ "languages": list(filter(None, [
+ "javascript" if stack["has_node"] else None,
+ "python" if stack["has_python"] else None,
+ ])),
},
},🤖 Prompt for AI Agents
In `@integrations/ci/github_actions.py` around lines 392 - 414, The CodeQL init
step builds the languages value as a comma-separated string using ", ".join(...)
which produces "javascript, python" but CodeQL expects a YAML list; update the
init step in codeql_steps (the dict with "name": "Initialize CodeQL") to pass
languages as a list instead of a joined string — e.g., construct the "languages"
value as a list comprehension/filter from the same conditionals (using
"javascript" if stack["has_node"] else None, "python" if stack["has_python"]
else None) and filter out None, so the action receives a proper list; locate
this change near codeql_steps and _checkout_step usage.
| def get_project_dir(project_name: str) -> Path: | ||
| """Get project directory from name or path.""" | ||
| project_path = get_project_path(project_name) | ||
| if project_path: | ||
| return Path(project_path) | ||
|
|
||
| path = Path(project_name) | ||
| if path.exists() and path.is_dir(): | ||
| return path | ||
|
|
||
| raise HTTPException(status_code=404, detail=f"Project not found: {project_name}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent arbitrary filesystem access via project_name.
The fallback to Path(project_name) allows absolute paths or .. segments, enabling access outside registered projects. Consider rejecting unsafe paths or requiring registry lookups only.
🔒 Proposed fix (block traversal/absolute paths)
- path = Path(project_name)
- if path.exists() and path.is_dir():
- return path
+ path = Path(project_name)
+ if path.is_absolute() or ".." in path.parts:
+ raise HTTPException(status_code=400, detail="Invalid project path")
+ if path.exists() and path.is_dir():
+ return path.resolve()🤖 Prompt for AI Agents
In `@server/routers/design_tokens.py` around lines 120 - 130, The fallback in
get_project_dir allows arbitrary filesystem access by accepting absolute paths
or paths with upward traversal; change get_project_dir to reject any
project_name that is an absolute path or contains parent-segment traversal
(e.g., '..') and only accept project_name values that resolve via
get_project_path; if get_project_path returns None, raise the 404 immediately
instead of treating project_name as a filesystem path. Locate get_project_dir
and remove/guard the Path(project_name) branch so that only registered projects
returned by get_project_path are used, returning a 404 for any unsafe or unknown
input.
| # Validate filename | ||
| if ".." in filename: | ||
| raise HTTPException(status_code=400, detail="Invalid filename") | ||
|
|
||
| file_path = project_dir / filename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same path traversal vulnerability in delete endpoint.
Apply the same fix as suggested for get_doc_content to properly validate the resolved path stays within the project directory.
🤖 Prompt for AI Agents
In `@server/routers/documentation.py` around lines 284 - 288, The delete
endpoint's filename validation is vulnerable to path traversal; replace the
naive ".." check in the delete handler (where variables filename, file_path, and
project_dir are used) with a resolved-path containment check: compute
resolved_path = (project_dir / filename).resolve() and ensure resolved_path is
within project_dir.resolve() (e.g., by comparing
str(resolved_path).startswith(str(project_dir.resolve())) or using
os.path.commonpath); if not, raise HTTPException(status_code=400,
detail="Invalid filename"). This mirrors the fix applied to get_doc_content and
prevents traversal via symlinks or ../ sequences.
| def validate_path(path: str) -> bool: | ||
| """Validate path to prevent traversal attacks.""" | ||
| # Allow absolute paths but check for common attack patterns | ||
| if ".." in path or "\x00" in path: | ||
| return False | ||
| return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path validation is insufficient against traversal attacks.
The current check only looks for literal ".." strings but doesn't handle URL-encoded variants (%2e%2e), Unicode normalization attacks, or symlink traversal. Additionally, checking ".." as a substring could incorrectly reject legitimate paths like /home/user/my..project/.
🛠️ Proposed fix
def validate_path(path: str) -> bool:
"""Validate path to prevent traversal attacks."""
- # Allow absolute paths but check for common attack patterns
- if ".." in path or "\x00" in path:
+ # Check for null bytes
+ if "\x00" in path:
+ return False
+
+ # Resolve the path and check it doesn't escape via parent traversal
+ try:
+ resolved = Path(path).resolve()
+ # Verify the resolved path doesn't contain parent directory escapes
+ # by checking that normalizing doesn't reveal hidden traversal
+ normalized = str(resolved)
+ if ".." in normalized:
+ return False
+ except (OSError, ValueError):
return False
+
return True🤖 Prompt for AI Agents
In `@server/routers/import_project.py` around lines 40 - 45, The validate_path
function currently only checks for the literal substring ".." and null bytes;
update validate_path to decode URL-encodings and normalize Unicode, reject null
bytes, then use pathlib.Path and os.path.realpath to resolve symlinks and
normalize the path; specifically, call urllib.parse.unquote on the input,
unicodedata.normalize (e.g. NFC/NFKC), then construct a Path and inspect its
parts (reject if any part == "..") instead of substring checks, and finally
compare the resolved realpath against an allowed base directory (or otherwise
ensure it stays under the intended root) to prevent symlink/escape attacks while
still permitting legit names containing consecutive dots like "my..project".
Ensure the function still returns a boolean and reference validate_path when
making these changes.
- Add feature_verify_quality to FEATURE_MCP_TOOLS (fixes permission error) - Remove orphaned feature_release_testing from allowed tools - Remove docker-compose from DANGEROUS_COMMANDS (can now be project-allowed) - Add browser file upload pattern guidance to coding prompt - Add warning about unavailable browser_run_code tool - Document heredoc sandbox error and alternatives Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @.claude/templates/coding_prompt.template.md:
- Line 197: The grep pattern `new Map\(\)\|new Set\(\)` in the template will
generate many false positives; update the check in
`.claude/templates/coding_prompt.template.md` to either remove this generic
Map/Set search or replace it with a narrower pattern that targets persistent
store usage (e.g., look for `new Map`/`new Set` combined with variable names
like store|db|data|persist` or other project-specific identifiers), and update
the instruction on line ~200 about "any grep hits require investigation/fix" to
exclude benign Map/Set usages so developers aren't forced to investigate
standard caches/lookups; reference the grep pattern string `new Map\(\)\|new
Set\(\)` when making the change.
In `@security.py`:
- Around line 146-182: The fallback in _extract_primary_command currently
returns the first token even if the segment contains shell chaining/redirect
operators, enabling allowlist bypass; modify _extract_primary_command to first
scan the original segment for any shell operators or redirection symbols (e.g.,
&&, \|\|, \|, ;, &, >, <, `$(...)`, `;`, and backticks) and if any are present
return None (refuse fallback), otherwise proceed with the existing env-var
stripping and basename extraction; apply the same conservative
operator-detection change to the other shlex-failure fallback function elsewhere
in this file so both fallbacks behave identically.
| def _extract_primary_command(segment: str) -> str | None: | ||
| """ | ||
| Fallback command extraction when shlex fails. | ||
|
|
||
| Extracts the first word that looks like a command, handling cases | ||
| like complex docker exec commands with nested quotes. | ||
|
|
||
| Args: | ||
| segment: The command segment to parse | ||
|
|
||
| Returns: | ||
| The primary command name, or None if extraction fails | ||
| """ | ||
| # Remove leading whitespace | ||
| segment = segment.lstrip() | ||
|
|
||
| if not segment: | ||
| return None | ||
|
|
||
| # Skip env var assignments at start (VAR=value cmd) | ||
| words = segment.split() | ||
| while words and "=" in words[0] and not words[0].startswith("="): | ||
| words = words[1:] | ||
|
|
||
| if not words: | ||
| return None | ||
|
|
||
| # Extract first token (the command) | ||
| first_word = words[0] | ||
|
|
||
| # Match valid command characters (alphanumeric, dots, underscores, hyphens, slashes) | ||
| match = re.match(r"^([a-zA-Z0-9_./-]+)", first_word) | ||
| if match: | ||
| cmd = match.group(1) | ||
| return os.path.basename(cmd) | ||
|
|
||
| return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make shlex-failure fallback conservative to avoid allowlist bypass.
When shlex.split() fails, _extract_primary_command() only returns the first token. If the segment still contains chained operators (e.g., &&, ||, |, &), subsequent commands are silently skipped, so blocked commands can pass if parsing fails for valid shell syntax. Consider refusing fallback when operators are present, or only allowing fallback for a single-operator-free segment.
🔧 Suggested hardening
except ValueError:
# Malformed command (unclosed quotes, etc.)
- # Try fallback extraction instead of blocking entirely
- fallback_cmd = _extract_primary_command(segment)
- if fallback_cmd:
- commands.append(fallback_cmd)
- continue
+ # Fail-safe: only allow fallback for single, operator-free segments
+ if re.search(r"[|&;]", segment):
+ continue
+ fallback_cmd = _extract_primary_command(segment)
+ if fallback_cmd:
+ commands.append(fallback_cmd)
+ continueAlso applies to: 212-220
🤖 Prompt for AI Agents
In `@security.py` around lines 146 - 182, The fallback in _extract_primary_command
currently returns the first token even if the segment contains shell
chaining/redirect operators, enabling allowlist bypass; modify
_extract_primary_command to first scan the original segment for any shell
operators or redirection symbols (e.g., &&, \|\|, \|, ;, &, >, <, `$(...)`, `;`,
and backticks) and if any are present return None (refuse fallback), otherwise
proceed with the existing env-var stripping and basename extraction; apply the
same conservative operator-detection change to the other shlex-failure fallback
function elsewhere in this file so both fallbacks behave identically.
Fixes: - Replace deprecated logger.warn() with logger.warning() in agent.py and parallel_orchestrator.py - Replace deprecated datetime.utcnow() with datetime.now(timezone.utc) across 7 files - Fix race condition in agent.py exception handler (don't clear stuck features on failure) - Fix session delay in agent.py from 1s to 3s (per CLAUDE.md documentation) - Fix shallow copy issue in autocoder_config.py using copy.deepcopy() - Fix path traversal vulnerability in review.py (validate filename before path construction) - Add XML escaping in templates/library.py to prevent XML injection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Additional Fixes Pushed (commit 0a02601)Addressed remaining CodeRabbit review issues: Deprecation Fixes
Security & Correctness Fixes
Other Fixes
Verification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent.py (1)
341-438: Useis_rate_limit_error(response)instead of duplicating pattern matching logic.
RATE_LIMIT_PATTERNScontains simple string patterns, not regex, so substring matching works correctly. However, sinceis_rate_limit_error()is already imported and implements the same logic, use the shared helper for consistency and to avoid duplication.♻️ Proposed fix
- response_lower = response.lower() - if any(pattern in response_lower for pattern in RATE_LIMIT_PATTERNS): + if is_rate_limit_error(response):
🤖 Fix all issues with AI agents
In `@auto_documentation.py`:
- Around line 585-591: The grouping logic assigns an empty string when ep.path
== "/" because ep.path.split("/")[1] yields '', so update the base calculation
in the loop over docs.api_endpoints to normalize paths: strip leading/trailing
slashes, then take the first segment, and fallback to "root" when nothing
remains; i.e., replace the current base = ep.path.split("/")[1] if "/" in
ep.path else "root" with a normalized computation that handles "/" and empty
paths (affecting the grouped dict population for ep.path).
- Around line 269-284: The import for get_session is invalid because
api.database exposes get_db not get_session; either change the import to use the
correct provider (import get_session from server.services.assistant_database
where get_session(project_dir: Path) is defined) or add a thin wrapper
get_session(project_dir) in api.database that delegates to the existing service;
then replace the manual session lifecycle in the code that queries Feature with
a context manager (use "with get_session(db_path) as session" or the
sessionmaker context provided by the chosen get_session) so the session is
always closed even on exceptions; keep using Feature and
order_by(Feature.priority) as shown.
In `@parallel_orchestrator.py`:
- Around line 1248-1267: The DB session opened via orchestrator.get_session()
must be closed regardless of errors: wrap the session usage (stuck_features
query, loop updating Feature.in_progress and session.commit()) in a
try/except/finally or try/finally so that session.close() always runs; on
exception call session.rollback() before closing and re-raise or log as
appropriate. Specifically, keep using orchestrator.get_session(), query
api.database.Feature, perform the updates and commit with session.commit(), but
ensure session.close() is moved into a finally block and add session.rollback()
in the exception path to avoid leaking transactions or connections.
In `@security_scanner.py`:
- Around line 399-409: The _scan_dependencies check correctly detects
pyproject.toml but _run_pip_audit still only invokes pip-audit with "-r
requirements.txt", so projects that only use pyproject.toml are missed; update
_run_pip_audit to detect if requirements.txt exists and call pip-audit with "-r
requirements.txt" otherwise run "pip-audit ." (scan the project directory) so
pyproject.toml-based projects are scanned; locate the _run_pip_audit helper and
change its invocation/arguments accordingly to branch between requirements mode
and project-directory mode.
In `@server/routers/logs.py`:
- Around line 152-163: The total count is computed with query.count(...) but
that method doesn't accept tool_name or search, so totals will be wrong when
those filters are used; update the count function in structured_logging.py to
accept tool_name and search (keeping existing params level, agent_id,
feature_id, since) and apply the same filtering logic as query.query, then
change the call in logs.py to pass tool_name and search into
query.count(tool_name=tool_name, search=search), and update any other
callers/tests to match the new signature so count reflects the same filtered
result set.
In `@server/routers/review.py`:
- Around line 267-308: The code can leak DB sessions when an exception is raised
after get_session(db_path); wrap the logic that uses session (the for loop,
session.add, session.commit, and session.close) in a try/finally or use a
context manager so session.close() always runs; specifically ensure the session
returned by get_session is closed in a finally block (or by using a with-style
context) around the work that creates Feature objects and calls
session.commit(), and remove session.close() from the normal path so it’s
guaranteed to execute even on exceptions raised in the feature creation loop or
commit; keep references to get_session, session, session.commit, session.close,
and CreateFeaturesResponse when making the change.
In `@server/services/autocoder_config.py`:
- Around line 187-209: The _deep_merge function currently calls base.copy(),
which preserves references to nested dicts and can mutate shared defaults (e.g.,
DEFAULT_CONFIG); change it to perform a deep copy of base before merging (use
copy.deepcopy) so nested structures are detached, ensure import copy is added,
and keep the existing recursive merge logic in _deep_merge to populate and
return the merged dict without retaining references to the original nested
objects.
In `@visual_regression.py`:
- Line 441: The "modified" timestamp is created with
datetime.fromtimestamp(stat.st_mtime) which yields a naive datetime while the
module uses timezone-aware datetimes; replace it with a timezone-aware
conversion such as datetime.fromtimestamp(stat.st_mtime, timezone.utc) (or
datetime.utcfromtimestamp(...).replace(tzinfo=timezone.utc)) and ensure timezone
is imported (timezone from datetime) so the "modified" value is consistent with
datetime.now(timezone.utc).
- Around line 163-211: The capture_screenshot method uses the raw name to build
filenames which enables path traversal; add a class-level validation helper
(e.g., sanitize_baseline_name or validate_baseline_name) that rejects or
normalizes suspicious inputs (disallow path separators, .. segments, absolute
paths, and empty names) and call it at the start of capture_screenshot (and
likewise in update_baseline, delete_baseline, test_page) to ensure the name is
safe before creating filename/output_path; update capture_screenshot to use the
sanitized name when constructing filename and returning the Path.
🧹 Nitpick comments (12)
visual_regression.py (3)
233-234: Image handles not explicitly closed.
Image.open()returns file handles that should be closed after use to prevent resource leaks, especially when processing many images.♻️ Proposed fix: Use context managers
- baseline = Image.open(baseline_path).convert("RGB") - current = Image.open(current_path).convert("RGB") + with Image.open(baseline_path) as baseline_img, Image.open(current_path) as current_img: + baseline = baseline_img.convert("RGB") + current = current_img.convert("RGB") + # ... rest of comparison logic indented within this blockAlternatively, explicitly close after use or use a try/finally pattern.
256-260: Pixel-by-pixel loop is O(width × height) and will be slow for large images.For a 1920×1080 screenshot, this iterates ~2 million pixels. Consider using NumPy or Pillow's vectorized operations for significant speedup.
♻️ Proposed optimization using NumPy
+import numpy as np + # Generate highlighted diff image if diff_percentage > 0: - # Create diff overlay - diff_highlight = Image.new("RGBA", baseline.size, (0, 0, 0, 0)) - draw = ImageDraw.Draw(diff_highlight) - - for y in range(baseline.size[1]): - for x in range(baseline.size[0]): - pixel = diff.getpixel((x, y)) - if sum(pixel) > 30: - draw.point((x, y), fill=(255, 0, 0, 128)) # Red highlight - - # Composite with original - result = Image.alpha_composite(baseline.convert("RGBA"), diff_highlight) + # Vectorized diff highlight using NumPy + diff_array = np.array(diff) + baseline_rgba = np.array(baseline.convert("RGBA")) + + # Create mask where pixel sum > 30 + mask = np.sum(diff_array, axis=2) > 30 + + # Apply red highlight (255, 0, 0, 128) where mask is True + baseline_rgba[mask] = ( + (baseline_rgba[mask] * 0.5 + np.array([255, 0, 0, 128]) * 0.5) + .astype(np.uint8) + ) + + result = Image.fromarray(baseline_rgba, "RGBA") result.save(diff_path)This reduces O(n) Python loop iterations to vectorized NumPy operations.
431-433: Filename parsing may incorrectly split names containing underscores.Using
rsplit("_", 1)assumes viewport is the last underscore-delimited segment. A test named"login_page"with viewport"desktop"produces filename"login_page_desktop.png", which correctly parses. However, if viewport names ever contain underscores (e.g.,"desktop_hd"), parsing would break.This is a minor edge case given current viewport naming, but worth documenting or validating viewport names don't contain underscores.
auto_documentation.py (3)
426-429: Sensitive key detection may be overly broad.The substring check
"key" in key.lower()will match variable names likeMONKEY_IDorKEYBOARD_TYPE. Consider using word boundaries or more specific patterns, though this conservative approach is acceptable from a security perspective.♻️ Optional: More precise pattern matching
- if any( - s in key.lower() for s in ["secret", "password", "key", "token"] - ): + sensitive_patterns = ["secret", "password", "api_key", "api-key", + "apikey", "token", "auth", "credential"] + if any( + s in key.lower() for s in sensitive_patterns + ) or key.lower().endswith("_key"):
506-514: Hardcoded assumption about setup steps structure.Lines 508 and 512 assume the first 2 items are prerequisites and the rest are installation steps. This could produce incorrect output if
_extract_setup_steps()returns fewer than 2 items or if the order changes.♻️ Consider defensive handling
if docs.setup_steps: lines.append("## Getting Started\n") - lines.append("### Prerequisites\n") - for step in docs.setup_steps[:2]: # First few are usually prerequisites - lines.append(f"- {step}") - lines.append("") - lines.append("### Installation\n") - for i, step in enumerate(docs.setup_steps[2:], 1): - lines.append(f"{i}. {step}") - lines.append("") + prereq_count = min(2, len(docs.setup_steps)) + if prereq_count > 0: + lines.append("### Prerequisites\n") + for step in docs.setup_steps[:prereq_count]: + lines.append(f"- {step}") + lines.append("") + if len(docs.setup_steps) > prereq_count: + lines.append("### Installation\n") + for i, step in enumerate(docs.setup_steps[prereq_count:], 1): + lines.append(f"{i}. {step}") + lines.append("")
311-311: Moveitertoolsimport to module level.The
chainimport fromitertoolsis a standard library module and should be at the top of the file with other imports for consistency and slight performance benefit (avoiding repeated import lookups).♻️ Move import to module level
At the top of the file (after line 28):
from itertools import chainThen remove line 311:
- from itertools import chainsecurity_scanner.py (4)
106-122: Consider removing redundant severity computation into_dict().The
by_severitycounts (lines 115-120) duplicate the severity counts already computed and stored inself.summary(set inscan()at lines 384-387). This creates a maintenance risk if the summary structure changes.♻️ Suggested simplification
def to_dict(self) -> dict: """Convert to dictionary.""" return { "project_dir": self.project_dir, "scan_time": self.scan_time, "vulnerabilities": [v.to_dict() for v in self.vulnerabilities], "summary": self.summary, "scans_run": self.scans_run, "total_issues": len(self.vulnerabilities), - "by_severity": { - "critical": len([v for v in self.vulnerabilities if v.severity == Severity.CRITICAL]), - "high": len([v for v in self.vulnerabilities if v.severity == Severity.HIGH]), - "medium": len([v for v in self.vulnerabilities if v.severity == Severity.MEDIUM]), - "low": len([v for v in self.vulnerabilities if v.severity == Severity.LOW]), - "info": len([v for v in self.vulnerabilities if v.severity == Severity.INFO]), - }, }The
summarydict already contains severity counts. If a separateby_severitykey is needed for API compatibility, consider referencingself.summarydirectly instead of recomputing.
459-465: Consider logging exceptions instead of silently ignoring them.Silent
passon exceptions makes debugging difficult when npm audit fails unexpectedly. Adding a debug/warning log would help diagnose issues in production without breaking the scan flow.♻️ Suggested improvement
+import logging + +logger = logging.getLogger(__name__) + # ... in _run_npm_audit: except json.JSONDecodeError: - pass + logger.debug("Failed to parse npm audit JSON output") except subprocess.TimeoutExpired: - pass + logger.warning("npm audit timed out after 120 seconds") except FileNotFoundError: - pass + logger.debug("npm not found, skipping npm audit")
608-609: Broad exception handling silently swallows all errors.The bare
except Exception: continuemakes it difficult to diagnose issues like permission errors or unexpected file states. Consider logging at debug level.♻️ Suggested improvement
except Exception: - continue + logger.debug(f"Failed to scan file for secrets: {file_path}", exc_info=True) + continue
648-659: Missing return type annotation.The method is missing a return type annotation. Adding it improves IDE support and documentation.
♻️ Suggested fix
+from typing import Iterator, Optional + def _iter_files( self, extensions: set[str], skip_dirs: set[str] - ): + ) -> Iterator[Path]: """Iterate over files with given extensions, skipping certain directories."""server/routers/review.py (1)
311-330: Path traversal validation occurs after path construction - inconsistent withget_report.In
get_report(lines 232-234), validation correctly happens before path construction. Here, the path is constructed at line 317, but validation is at lines 320-321. While the validation still works since it checks the rawfilename, reordering improves clarity and maintains consistency.♻️ Proposed fix - validate before path construction
`@router.delete`("/reports/{project_name}/{filename}") async def delete_report(project_name: str, filename: str): """ Delete a specific review report. """ - project_dir = get_project_dir(project_name) - report_path = project_dir / ".autocoder" / "review-reports" / filename - # Validate filename to prevent path traversal if ".." in filename or "/" in filename or "\\" in filename: raise HTTPException(status_code=400, detail="Invalid filename") + project_dir = get_project_dir(project_name) + report_path = project_dir / ".autocoder" / "review-reports" / filename + if not report_path.exists():server/routers/logs.py (1)
266-274: Sanitizeproject_namebefore embedding in filename.
request.project_nameis user input included directly in the filename at line 269. While the export is written to a fixed directory, unsanitized project names with special characters could create problematic filenames or cause issues in downstream processing.♻️ Proposed fix - sanitize project name
+import re + +def _sanitize_filename(name: str) -> str: + """Remove unsafe characters from filename component.""" + return re.sub(r'[^\w\-.]', '_', name) + # In export_logs: # Create temp file for export suffix = f".{request.format}" if request.format != "jsonl" else ".jsonl" timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") - filename = f"logs_{request.project_name}_{timestamp}{suffix}" + safe_name = _sanitize_filename(request.project_name) + filename = f"logs_{safe_name}_{timestamp}{suffix}"
| try: | ||
| from api.database import Feature, get_session | ||
|
|
||
| session = get_session(db_path) | ||
| db_features = session.query(Feature).order_by(Feature.priority).all() | ||
|
|
||
| for f in db_features: | ||
| features.append( | ||
| { | ||
| "category": f.category, | ||
| "name": f.name, | ||
| "description": f.description, | ||
| "status": "completed" if f.passes else "pending", | ||
| } | ||
| ) | ||
| session.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find get_session function definition
rg -n "def get_session" --type py -A 5Repository: leonvanzyl/autocoder
Length of output: 3145
🏁 Script executed:
# Check how get_session is used across the codebase
rg -n "get_session\(" --type py -B 2 -A 2Repository: leonvanzyl/autocoder
Length of output: 17733
🏁 Script executed:
# Look for the api/database module specifically
fd -t f "database\.py" -x cat -n {}Repository: leonvanzyl/autocoder
Length of output: 37633
Fix missing get_session() function in api.database module.
The import from api.database import Feature, get_session will fail—get_session() does not exist in api/database.py. The file contains get_db() (which takes no arguments) but no get_session() function. Either import from the correct module (likely server/services/assistant_database.py with signature get_session(project_dir: Path)) or implement the missing function in api/database.py. Additionally, replace manual session management with a context manager to ensure proper cleanup regardless of success or exception.
🤖 Prompt for AI Agents
In `@auto_documentation.py` around lines 269 - 284, The import for get_session is
invalid because api.database exposes get_db not get_session; either change the
import to use the correct provider (import get_session from
server.services.assistant_database where get_session(project_dir: Path) is
defined) or add a thin wrapper get_session(project_dir) in api.database that
delegates to the existing service; then replace the manual session lifecycle in
the code that queries Feature with a context manager (use "with
get_session(db_path) as session" or the sessionmaker context provided by the
chosen get_session) so the session is always closed even on exceptions; keep
using Feature and order_by(Feature.priority) as shown.
| # Clear any stuck features from previous interrupted sessions | ||
| # This is the RIGHT place to clear - BEFORE spawning any agents | ||
| # Agents will NO LONGER clear features on their individual startups (see agent.py fix) | ||
| try: | ||
| session = orchestrator.get_session() | ||
| cleared_count = 0 | ||
|
|
||
| # Get all features marked in_progress | ||
| from api.database import Feature | ||
| stuck_features = session.query(Feature).filter( | ||
| Feature.in_progress == True | ||
| ).all() | ||
|
|
||
| for feature in stuck_features: | ||
| feature.in_progress = False | ||
| cleared_count += 1 | ||
|
|
||
| session.commit() | ||
| session.close() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Close the DB session in a finally block.
If an exception fires before session.close(), the connection can leak; ensure closure on all paths.
🛠️ Proposed fix
- try:
- session = orchestrator.get_session()
- cleared_count = 0
+ session = None
+ try:
+ session = orchestrator.get_session()
+ cleared_count = 0
...
- session.commit()
- session.close()
+ session.commit()
...
- except Exception as e:
+ except Exception as e:
print(f"[ORCHESTRATOR] Warning: Failed to clear stuck features: {e}", flush=True)
+ finally:
+ if session is not None:
+ session.close()🤖 Prompt for AI Agents
In `@parallel_orchestrator.py` around lines 1248 - 1267, The DB session opened via
orchestrator.get_session() must be closed regardless of errors: wrap the session
usage (stuck_features query, loop updating Feature.in_progress and
session.commit()) in a try/except/finally or try/finally so that session.close()
always runs; on exception call session.rollback() before closing and re-raise or
log as appropriate. Specifically, keep using orchestrator.get_session(), query
api.database.Feature, perform the updates and commit with session.commit(), but
ensure session.close() is moved into a finally block and add session.rollback()
in the exception path to avoid leaking transactions or connections.
| def _scan_dependencies(self, result: ScanResult) -> None: | ||
| """Scan dependencies for known vulnerabilities.""" | ||
| # Check for npm | ||
| if (self.project_dir / "package.json").exists(): | ||
| self._run_npm_audit(result) | ||
|
|
||
| # Check for Python | ||
| if (self.project_dir / "requirements.txt").exists() or ( | ||
| self.project_dir / "pyproject.toml" | ||
| ).exists(): | ||
| self._run_pip_audit(result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if pip-audit supports pyproject.toml directly
pip-audit --help 2>/dev/null | grep -i "pyproject\|toml" || echo "pip-audit help not available or no pyproject.toml support mentioned"Repository: leonvanzyl/autocoder
Length of output: 131
🏁 Script executed:
# Find and examine the _run_pip_audit method in security_scanner.py
rg -A 20 "def _run_pip_audit" security_scanner.pyRepository: leonvanzyl/autocoder
Length of output: 826
🏁 Script executed:
# Also check the file structure to understand the context better
wc -l security_scanner.pyRepository: leonvanzyl/autocoder
Length of output: 87
🏁 Script executed:
# Get the full _run_pip_audit method to see all error handling
rg -A 50 "def _run_pip_audit" security_scanner.pyRepository: leonvanzyl/autocoder
Length of output: 2437
🏁 Script executed:
# Research pip-audit actual capabilities - check if it supports pyproject.toml
web_search
Does pip-audit support pyproject.toml natively?Repository: leonvanzyl/autocoder
Length of output: 157
🌐 Web query:
pip-audit pyproject.toml support capabilities documentation
💡 Result:
What pip-audit supports for pyproject.toml
- Auditing a local project directory:
pip-audit .will treat the directory as a “local Python project” and (currently) looks forpyproject.tomlas the project file it can audit. It also supports auditing PEP 751 lockfiles namedpylock.*.toml. The README states: “onlypyproject.tomlandpylock.*.tomlare supported.” [1] - Auditing lockfiles (
pylock.*.toml): Since pip-audit 2.9.0 (April 7, 2025), you can use--lockedto auditpylock.*.tomlfiles instead ofpyproject.toml. [2] - GitHub Action parity: The official
pypa/gh-action-pip-auditexposes alocked: trueoption to enable auditingpylock.*.tomllockfiles. [3]
What pip-audit does not support (yet) regarding pyproject.toml
- Tool configuration via
[tool.pip-audit]inpyproject.tomlis not supported today (it’s an open feature request). [4]
Key docs to reference
pip-auditREADME (usage/examples; project + lockfile support) [1]- pip-audit 2.9.0 announcement (PEP 751 /
--locked) [2] - GitHub Action docs (
lockedinput) [3] - Feature request for
[tool.pip-audit]configuration [4]
Sources: [1] [2] [3] [4]
Fix _run_pip_audit to properly scan pyproject.toml projects.
The condition checks for pyproject.toml existence, but _run_pip_audit only runs pip-audit with -r requirements.txt. Projects using modern pyproject.toml without requirements.txt won't have their dependencies scanned. Use pip-audit . to scan the project directory directly, which natively supports both requirements.txt and pyproject.toml.
🤖 Prompt for AI Agents
In `@security_scanner.py` around lines 399 - 409, The _scan_dependencies check
correctly detects pyproject.toml but _run_pip_audit still only invokes pip-audit
with "-r requirements.txt", so projects that only use pyproject.toml are missed;
update _run_pip_audit to detect if requirements.txt exists and call pip-audit
with "-r requirements.txt" otherwise run "pip-audit ." (scan the project
directory) so pyproject.toml-based projects are scanned; locate the
_run_pip_audit helper and change its invocation/arguments accordingly to branch
between requirements mode and project-directory mode.
| logs = query.query( | ||
| level=level, | ||
| agent_id=agent_id, | ||
| feature_id=feature_id, | ||
| tool_name=tool_name, | ||
| search=search, | ||
| since=since, | ||
| limit=limit, | ||
| offset=offset, | ||
| ) | ||
|
|
||
| total = query.count(level=level, agent_id=agent_id, feature_id=feature_id, since=since) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate structured_logging.py
find . -name "structured_logging.py" -type fRepository: leonvanzyl/autocoder
Length of output: 87
🏁 Script executed:
#!/bin/bash
# Search for the count() method definition to verify its signature
ast-grep --pattern $'def count(self, $$$):
$$$
}'Repository: leonvanzyl/autocoder
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Also search for the query() method to compare
ast-grep --pattern $'def query(self, $$$):
$$$
}'Repository: leonvanzyl/autocoder
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Get the size of structured_logging.py first
wc -l ./structured_logging.pyRepository: leonvanzyl/autocoder
Length of output: 91
🏁 Script executed:
#!/bin/bash
# Search for count() and query() methods in structured_logging.py
rg "def (count|query)\(" ./structured_logging.py -A 5Repository: leonvanzyl/autocoder
Length of output: 455
🏁 Script executed:
#!/bin/bash
# Locate logs.py
find . -name "logs.py" -path "*/routers/*" -type fRepository: leonvanzyl/autocoder
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Get full signatures of count() and query() methods
rg "def (count|query)\(" ./structured_logging.py -A 15Repository: leonvanzyl/autocoder
Length of output: 935
🏁 Script executed:
#!/bin/bash
# Check the logs.py file around lines 152-163
sed -n '145,170p' ./server/routers/logs.pyRepository: leonvanzyl/autocoder
Length of output: 768
Total count may not match filtered results when tool_name or search is used.
The query() call (lines 152-161) filters by tool_name and search, but count() (line 163) doesn't include these filters. The count() method signature only accepts level, agent_id, feature_id, and since.
This causes total to overcount when tool_name or search filters are applied, potentially misleading clients about pagination.
🤔 Suggested approaches
- Add
tool_nameandsearchparameters tocount()instructured_logging.pyfor consistency. - Document the limitation that
totalreflects broader filters. - Use
len(logs)as a fallback whentool_nameorsearchis provided (though this only works for single-page results).
🤖 Prompt for AI Agents
In `@server/routers/logs.py` around lines 152 - 163, The total count is computed
with query.count(...) but that method doesn't accept tool_name or search, so
totals will be wrong when those filters are used; update the count function in
structured_logging.py to accept tool_name and search (keeping existing params
level, agent_id, feature_id, since) and apply the same filtering logic as
query.query, then change the call in logs.py to pass tool_name and search into
query.count(tool_name=tool_name, search=search), and update any other
callers/tests to match the new signature so count reflects the same filtered
result set.
Fixes: - server/services/autocoder_config.py: Use deepcopy in _deep_merge to prevent mutation of DEFAULT_CONFIG - server/routers/review.py: Add finally block to properly close session on exceptions - visual_regression.py: Add path traversal validation in update_baseline and delete_baseline - visual_regression.py: Fix timezone in fromtimestamp() for consistency - api/database.py: Use atomic_transaction in atomic_mark_passing and atomic_update_priority_to_end to prevent race conditions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Additional Fixes Pushed (commit 0830aa9)Addressed more CodeRabbit review issues: Security Fixes
Race Condition Fixes
Resource Leak Fixes
Other Fixes
Verification |
- Fix LogLevel normalization: convert 'warning' to 'warn' for consistency - Fix _deep_merge: use copy.deepcopy() to prevent DEFAULT_CONFIG mutation - Fix security.py: make shlex fallback conservative when operators present
- review.py: Add path traversal validation for files parameter - review.py: Fix session closure in try/finally block - github_actions.py: CodeQL languages as list instead of comma-separated string - design_tokens.py: Fix generate_all docstring (returns content strings) - templates.py: Add path validation for project_dir in /apply endpoint - security_scanner.py: Support pyproject.toml in pip-audit scans - logs.py: Fix count filter mismatch (add tool_name/search params) - git_workflow.py: Add confirm_clean param for destructive git clean - git_workflow.py: Log config load errors instead of silently passing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@api/database.py`:
- Around line 581-598: The atomic_get_next_priority function currently opens a
raw session and SELECTs MAX(priority) without transactional serialization,
allowing duplicate priorities under concurrent callers; fix it by using the
existing atomic_transaction helper to run the MAX(priority) + 1 read inside a
serialized transaction (i.e., replace the manual session creation and
try/finally with a "with atomic_transaction(session_maker) as session:" block),
perform the SELECT COALESCE(MAX(priority),0)+1 within that block, and return the
value so the read is properly serialized and the session lifecycle is managed by
atomic_transaction.
In `@server/routers/review.py`:
- Around line 249-312: The handler create_features_from_issues currently imports
a non-existent get_session and mis-handles Feature.steps and priority assignment
under concurrency; replace the import/use of get_session with the real session
factory from api.database (e.g., obtain an Engine and instantiate a SQLAlchemy
Session via sessionmaker bound to that engine for the db_path), set
Feature.steps to the actual Python list (steps = issue.get("steps", ["Fix the
identified issue"])) instead of json.dumps, and compute/assign priority inside a
transaction-safe query (use a SELECT MAX(Feature.priority) executed within the
same Session with a row/transaction lock such as with_for_update() or use a DB
sequence/autoincrement) so concurrent requests cannot receive duplicate
priorities; ensure you still session.add(feature), commit, and session.close()
using the real Session object.
In `@server/services/autocoder_config.py`:
- Around line 303-360: The getters (get_quality_gates_config,
get_git_workflow_config, get_error_recovery_config, get_completion_config,
get_security_scanning_config, get_logging_config) should validate that the
loaded section is a dict and fall back to DEFAULT_CONFIG[...] when it's not
(e.g., None or other non-dict), because _deep_merge can produce non-dict values;
update each getter to check isinstance(section, dict) and return the default
section when the check fails, and likewise make the is_* helpers
(is_quality_gates_enabled, is_strict_quality_mode, is_security_scanning_enabled)
robust by reading their config via the validated getters (or using dict.get
safely) so they never call .get on a non-dict.
🧹 Nitpick comments (1)
visual_regression.py (1)
233-238: Use context managers for PIL Image.open.
Image.open()leaves file handles open until GC runs; with many comparisons this can exhaust descriptors. Wrap opens inwithblocks.♻️ Proposed refactor
- baseline = Image.open(baseline_path).convert("RGB") - current = Image.open(current_path).convert("RGB") + with Image.open(baseline_path) as baseline_img, Image.open(current_path) as current_img: + baseline = baseline_img.convert("RGB") + current = current_img.convert("RGB")
| def atomic_get_next_priority(session_maker) -> int: | ||
| """Atomically get the next available priority. | ||
|
|
||
| Uses a transaction to ensure consistent reads. | ||
|
|
||
| Args: | ||
| session_maker: SQLAlchemy sessionmaker | ||
|
|
||
| Returns: | ||
| Next priority value (max + 1, or 1 if no features exist) | ||
| """ | ||
| session = session_maker() | ||
| try: | ||
| result = session.execute(text(""" | ||
| SELECT COALESCE(MAX(priority), 0) + 1 FROM features | ||
| """)).fetchone() | ||
| return result[0] | ||
| finally: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid duplicate priorities under concurrent callers.
Line 594-596 reads MAX(priority) without an IMMEDIATE lock, so two workers can compute the same next priority. Wrap the read in atomic_transaction to serialize.
🐛 Proposed fix
def atomic_get_next_priority(session_maker) -> int:
"""Atomically get the next available priority.
@@
- session = session_maker()
- try:
- result = session.execute(text("""
- SELECT COALESCE(MAX(priority), 0) + 1 FROM features
- """)).fetchone()
- return result[0]
- finally:
- session.close()
+ with atomic_transaction(session_maker) as session:
+ result = session.execute(text("""
+ SELECT COALESCE(MAX(priority), 0) + 1 FROM features
+ """)).fetchone()
+ return result[0]🤖 Prompt for AI Agents
In `@api/database.py` around lines 581 - 598, The atomic_get_next_priority
function currently opens a raw session and SELECTs MAX(priority) without
transactional serialization, allowing duplicate priorities under concurrent
callers; fix it by using the existing atomic_transaction helper to run the
MAX(priority) + 1 read inside a serialized transaction (i.e., replace the manual
session creation and try/finally with a "with atomic_transaction(session_maker)
as session:" block), perform the SELECT COALESCE(MAX(priority),0)+1 within that
block, and return the value so the read is properly serialized and the session
lifecycle is managed by atomic_transaction.
| @router.post("/create-features", response_model=CreateFeaturesResponse) | ||
| async def create_features_from_issues(request: CreateFeaturesRequest): | ||
| """ | ||
| Create features from review issues. | ||
|
|
||
| Converts review issues into trackable features that can be assigned | ||
| to coding agents for resolution. | ||
| """ | ||
| from api.database import Feature, get_session | ||
|
|
||
| project_dir = get_project_dir(request.project_name) | ||
| db_path = project_dir / "features.db" | ||
|
|
||
| if not db_path.exists(): | ||
| raise HTTPException(status_code=404, detail="Project database not found") | ||
|
|
||
| created_features = [] | ||
| session = None | ||
|
|
||
| try: | ||
| session = get_session(db_path) | ||
|
|
||
| # Get max priority for ordering | ||
| max_priority = session.query(Feature.priority).order_by(Feature.priority.desc()).first() | ||
| current_priority = (max_priority[0] if max_priority else 0) + 1 | ||
|
|
||
| for issue in request.issues: | ||
| # Create feature from issue | ||
| feature = Feature( | ||
| priority=current_priority, | ||
| category=issue.get("category", "Code Review"), | ||
| name=issue.get("name", issue.get("title", "Review Issue")), | ||
| description=issue.get("description", ""), | ||
| steps=json.dumps(issue.get("steps", ["Fix the identified issue"])), | ||
| passes=False, | ||
| in_progress=False, | ||
| ) | ||
|
|
||
| session.add(feature) | ||
| current_priority += 1 | ||
|
|
||
| created_features.append( | ||
| { | ||
| "priority": feature.priority, | ||
| "category": feature.category, | ||
| "name": feature.name, | ||
| "description": feature.description, | ||
| } | ||
| ) | ||
|
|
||
| session.commit() | ||
|
|
||
| return CreateFeaturesResponse( | ||
| created=len(created_features), | ||
| features=created_features, | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Failed to create features: {e}") | ||
| raise HTTPException(status_code=500, detail=str(e)) | ||
| finally: | ||
| if session is not None: | ||
| session.close() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix DB session acquisition + steps payload type + priority race.
- Line 257 imports
get_session, which doesn’t exist inapi/database.py→ runtime import error. - Line 282 stores
stepsas a JSON string;Feature.stepsis a JSON array and UI expectsstring[]. - Lines 271-288 compute max priority without a write lock; concurrent requests can assign duplicate priorities.
🐛 Proposed fix (one possible approach)
- from api.database import Feature, get_session
+ from api.database import Feature, create_database, atomic_transaction
@@
- session = None
-
- try:
- session = get_session(db_path)
-
- # Get max priority for ordering
- max_priority = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
- current_priority = (max_priority[0] if max_priority else 0) + 1
-
- for issue in request.issues:
- # Create feature from issue
- feature = Feature(
- priority=current_priority,
- category=issue.get("category", "Code Review"),
- name=issue.get("name", issue.get("title", "Review Issue")),
- description=issue.get("description", ""),
- steps=json.dumps(issue.get("steps", ["Fix the identified issue"])),
- passes=False,
- in_progress=False,
- )
-
- session.add(feature)
- current_priority += 1
-
- created_features.append(
- {
- "priority": feature.priority,
- "category": feature.category,
- "name": feature.name,
- "description": feature.description,
- }
- )
-
- session.commit()
+ _, SessionLocal = create_database(project_dir)
+
+ try:
+ with atomic_transaction(SessionLocal) as session:
+ # Get max priority for ordering (protected by write lock)
+ max_priority = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
+ current_priority = (max_priority[0] if max_priority else 0) + 1
+
+ for issue in request.issues:
+ # Create feature from issue
+ feature = Feature(
+ priority=current_priority,
+ category=issue.get("category", "Code Review"),
+ name=issue.get("name", issue.get("title", "Review Issue")),
+ description=issue.get("description", ""),
+ steps=issue.get("steps", ["Fix the identified issue"]),
+ passes=False,
+ in_progress=False,
+ )
+
+ session.add(feature)
+ current_priority += 1
+
+ created_features.append(
+ {
+ "priority": feature.priority,
+ "category": feature.category,
+ "name": feature.name,
+ "description": feature.description,
+ }
+ )🤖 Prompt for AI Agents
In `@server/routers/review.py` around lines 249 - 312, The handler
create_features_from_issues currently imports a non-existent get_session and
mis-handles Feature.steps and priority assignment under concurrency; replace the
import/use of get_session with the real session factory from api.database (e.g.,
obtain an Engine and instantiate a SQLAlchemy Session via sessionmaker bound to
that engine for the db_path), set Feature.steps to the actual Python list (steps
= issue.get("steps", ["Fix the identified issue"])) instead of json.dumps, and
compute/assign priority inside a transaction-safe query (use a SELECT
MAX(Feature.priority) executed within the same Session with a row/transaction
lock such as with_for_update() or use a DB sequence/autoincrement) so concurrent
requests cannot receive duplicate priorities; ensure you still
session.add(feature), commit, and session.close() using the real Session object.
| def get_quality_gates_config(project_dir: Path) -> QualityGatesConfig: | ||
| """Get quality gates configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("quality_gates", DEFAULT_CONFIG["quality_gates"]) | ||
|
|
||
|
|
||
| def get_git_workflow_config(project_dir: Path) -> GitWorkflowConfig: | ||
| """Get git workflow configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("git_workflow", DEFAULT_CONFIG["git_workflow"]) | ||
|
|
||
|
|
||
| def get_error_recovery_config(project_dir: Path) -> ErrorRecoveryConfig: | ||
| """Get error recovery configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("error_recovery", DEFAULT_CONFIG["error_recovery"]) | ||
|
|
||
|
|
||
| def get_completion_config(project_dir: Path) -> CompletionConfig: | ||
| """Get completion configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("completion", DEFAULT_CONFIG["completion"]) | ||
|
|
||
|
|
||
| def get_security_scanning_config(project_dir: Path) -> SecurityScanningConfig: | ||
| """Get security scanning configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("security_scanning", DEFAULT_CONFIG["security_scanning"]) | ||
|
|
||
|
|
||
| def get_logging_config(project_dir: Path) -> LoggingConfig: | ||
| """Get logging configuration for a project.""" | ||
| config = load_autocoder_config(project_dir) | ||
| return config.get("logging", DEFAULT_CONFIG["logging"]) | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Feature Enable/Disable Checks | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| def is_quality_gates_enabled(project_dir: Path) -> bool: | ||
| """Check if quality gates are enabled for a project.""" | ||
| config = get_quality_gates_config(project_dir) | ||
| return config.get("enabled", True) | ||
|
|
||
|
|
||
| def is_strict_quality_mode(project_dir: Path) -> bool: | ||
| """Check if strict quality mode is enabled (blocks feature_mark_passing on failure).""" | ||
| config = get_quality_gates_config(project_dir) | ||
| return config.get("enabled", True) and config.get("strict_mode", True) | ||
|
|
||
|
|
||
| def is_security_scanning_enabled(project_dir: Path) -> bool: | ||
| """Check if security scanning is enabled for a project.""" | ||
| config = get_security_scanning_config(project_dir) | ||
| return config.get("enabled", True) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against non-dict sections to avoid runtime crashes.
If a user config sets a section to null or a non-dict (valid JSON), _deep_merge overwrites defaults and the getters return a non-dict, causing is_* helpers to throw AttributeError on .get. Add a defensive section validator and fall back to defaults.
🛠️ Suggested fix
@@
def get_quality_gates_config(project_dir: Path) -> QualityGatesConfig:
"""Get quality gates configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("quality_gates", DEFAULT_CONFIG["quality_gates"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "quality_gates", DEFAULT_CONFIG["quality_gates"])
@@
def get_git_workflow_config(project_dir: Path) -> GitWorkflowConfig:
"""Get git workflow configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("git_workflow", DEFAULT_CONFIG["git_workflow"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "git_workflow", DEFAULT_CONFIG["git_workflow"])
@@
def get_error_recovery_config(project_dir: Path) -> ErrorRecoveryConfig:
"""Get error recovery configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("error_recovery", DEFAULT_CONFIG["error_recovery"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "error_recovery", DEFAULT_CONFIG["error_recovery"])
@@
def get_completion_config(project_dir: Path) -> CompletionConfig:
"""Get completion configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("completion", DEFAULT_CONFIG["completion"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "completion", DEFAULT_CONFIG["completion"])
@@
def get_security_scanning_config(project_dir: Path) -> SecurityScanningConfig:
"""Get security scanning configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("security_scanning", DEFAULT_CONFIG["security_scanning"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "security_scanning", DEFAULT_CONFIG["security_scanning"])
@@
def get_logging_config(project_dir: Path) -> LoggingConfig:
"""Get logging configuration for a project."""
- config = load_autocoder_config(project_dir)
- return config.get("logging", DEFAULT_CONFIG["logging"])
+ config = load_autocoder_config(project_dir)
+ return _get_section(config, "logging", DEFAULT_CONFIG["logging"])
+
+def _get_section(config: dict, key: str, default: dict) -> dict:
+ section = config.get(key)
+ if isinstance(section, dict):
+ return section
+ logger.warning("Invalid config section for %s; using defaults", key)
+ return copy.deepcopy(default)🤖 Prompt for AI Agents
In `@server/services/autocoder_config.py` around lines 303 - 360, The getters
(get_quality_gates_config, get_git_workflow_config, get_error_recovery_config,
get_completion_config, get_security_scanning_config, get_logging_config) should
validate that the loaded section is a dict and fall back to DEFAULT_CONFIG[...]
when it's not (e.g., None or other non-dict), because _deep_merge can produce
non-dict values; update each getter to check isinstance(section, dict) and
return the default section when the check fails, and likewise make the is_*
helpers (is_quality_gates_enabled, is_strict_quality_mode,
is_security_scanning_enabled) robust by reading their config via the validated
getters (or using dict.get safely) so they never call .get on a non-dict.
Security fixes: - Path traversal protection in documentation.py using Path.resolve() and is_relative_to() - Path traversal protection in templates.py with validation against system paths - Path traversal protection in review.py for request.files validation Performance fixes: - Cursor-based streaming in structured_logging.py export_logs() to prevent OOM - Context managers for PIL Image.open() in visual_regression.py React state fixes: - Return boolean success values from async hooks in useImportProject.ts - Use returned values instead of stale state checks in ImportProjectModal.tsx - Add useEffect for category expansion to avoid stale closure issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add LogLevel "warning" variant alongside "warn" for compatibility - Normalize "warning" -> "warn" when storing logs for consistency - Use consistent timestamp format (Z suffix) in all query functions - Use context managers for SQLite connections to prevent leaks on errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@server/routers/documentation.py`:
- Around line 257-265: PreviewResponse.description is declared as str but
docs.description may be None, causing Pydantic validation errors; fix by either
updating the PreviewResponse model to allow None (make description: str | None
or Optional[str]) or coerce the value when constructing the response (use
description=docs.description or "" to ensure a string); update the code around
the PreviewResponse constructor in the router to use one of these approaches and
keep the field and variable names PreviewResponse and docs.description unchanged
so the change is localized.
- Around line 145-147: The handler currently logs and returns the raw exception
string (e) which can leak sensitive details; change the error handling in the
documentation generation block to log the full exception and stacktrace
server-side (e.g., use logger.exception or logger.error with the exception) but
raise HTTPException with a generic client-facing message (e.g., "Internal error
generating documentation") instead of str(e); update the catch block that
references logger and HTTPException to perform server-only detailed logging and
return a non-sensitive detail to the client.
In `@structured_logging.py`:
- Around line 365-369: The LIKE clause escapes percent/underscore but doesn't
tell SQLite to use backslash as the escape character; update the condition built
where `search` is handled so the SQL uses an ESCAPE clause (e.g., append
"message LIKE ? ESCAPE '\\'") and keep the escaped pattern you build in
`escaped_search` that you append to `params`; modify the string added to
`conditions` (not the escape logic) so SQLite treats `\%` and `\_` literally
while still using the existing `search`, `escaped_search`, `conditions`, and
`params` variables.
🧹 Nitpick comments (10)
server/routers/templates.py (2)
252-264: Consider strengthening path validation with a more complete blocklist.The current
sensitive_pathsblocklist is missing several critical system directories:/sys,/proc,/dev,/boot,/lib,/lib64,/opt,/srv, and/mnt. Additionally, the prefix matching withstartswith(p)may cause false positives (e.g.,/variablewould be blocked by/var).Consider:
- Expanding the blocklist with additional system paths.
- Using
startswith(p + "/") or project_dir_str == pfor boundary-accurate matching.- Reusing the existing
validate_pathfromimport_project.pyfor consistency.♻️ Proposed improvement
# Ensure project_dir is an absolute path within allowed boundaries # (i.e., not trying to write to system directories) project_dir_str = str(project_dir) - sensitive_paths = ["/etc", "/usr", "/bin", "/sbin", "/var", "/root", "/home/root"] - if any(project_dir_str.startswith(p) for p in sensitive_paths): + sensitive_paths = [ + "/etc", "/usr", "/bin", "/sbin", "/var", "/root", "/home/root", + "/sys", "/proc", "/dev", "/boot", "/lib", "/lib64", "/opt", "/srv", "/mnt" + ] + if any(project_dir_str == p or project_dir_str.startswith(p + "/") for p in sensitive_paths): raise HTTPException(status_code=400, detail="Invalid project directory: cannot write to system paths")
298-328: Consider adding a response model for consistency.Unlike other endpoints that use typed Pydantic response models (
TemplateListResponse,TemplateDetail, etc.), this endpoint returns a plain dict. Adding a response model would provide consistent API documentation and response validation.♻️ Proposed response model
class TemplateFeaturesResponse(BaseModel): """Response with template features.""" template_id: str features: list[dict] count: int by_category: dict[str, int]Then update the endpoint:
-@router.get("/{template_id}/features") +@router.get("/{template_id}/features", response_model=TemplateFeaturesResponse) async def get_template_features(template_id: str):structured_logging.py (3)
53-70: Consider usingis not Noneconsistently for optional field checks.Lines 60, 64 use truthiness (
if self.agent_id:,if self.tool_name:) which excludes empty strings, while lines 62, 66 correctly useis not None. This inconsistency could cause issues if an empty string is ever a valid value.♻️ Suggested fix for consistency
if self.agent_id: + if self.agent_id is not None: result["agent_id"] = self.agent_id if self.feature_id is not None: result["feature_id"] = self.feature_id - if self.tool_name: + if self.tool_name is not None: result["tool_name"] = self.tool_name
185-197: Cleanup on every emit may impact write performance.The current implementation counts all rows and potentially deletes on every log insert. For high-throughput logging, this adds overhead. Consider periodic cleanup (e.g., every 100 inserts) or background cleanup.
♻️ Suggested optimization using periodic cleanup
def __init__( self, db_path: Path, agent_id: Optional[str] = None, max_entries: int = 10000, ): super().__init__() self.db_path = db_path self.agent_id = agent_id self.max_entries = max_entries self._lock = threading.Lock() + self._insert_count = 0 + self._cleanup_interval = 100 # Check cleanup every N inserts self._init_database() def emit(self, record: logging.LogRecord) -> None: # ... insert logic ... - # Cleanup old entries if over limit - cursor.execute("SELECT COUNT(*) FROM logs") - count = cursor.fetchone()[0] - if count > self.max_entries: - delete_count = count - self.max_entries - cursor.execute( - """ - DELETE FROM logs WHERE id IN ( - SELECT id FROM logs ORDER BY timestamp ASC LIMIT ? - ) - """, - (delete_count,), - ) + # Periodic cleanup to avoid checking on every insert + self._insert_count += 1 + if self._insert_count >= self._cleanup_interval: + self._insert_count = 0 + cursor.execute("SELECT COUNT(*) FROM logs") + count = cursor.fetchone()[0] + if count > self.max_entries: + delete_count = count - self.max_entries + cursor.execute( + """ + DELETE FROM logs WHERE id IN ( + SELECT id FROM logs ORDER BY timestamp ASC LIMIT ? + ) + """, + (delete_count,), + )
517-543: Offset-based pagination is less efficient for large datasets.The
_iter_logsmethod uses offset-based pagination which requires SQLite to scan and skip rows on each batch. For very large log tables, cursor-based pagination (e.g.,WHERE id > last_seen_id) would be more efficient. However, this is acceptable for typical log export volumes.ui/src/hooks/useImportProject.ts (2)
90-120: Consider adding request cancellation to prevent state updates on unmounted components.The async callbacks don't support cancellation. If the component unmounts while a request is in-flight, the
setStatecalls in the.then/.catchhandlers could trigger React warnings about updating unmounted components.♻️ Suggested pattern using AbortController
// Example for the analyze callback - apply similar pattern to extractFeatures/createFeatures const analyze = useCallback(async (path: string, signal?: AbortSignal): Promise<boolean> => { setState(prev => ({ ...prev, step: 'analyzing', projectPath: path, error: null })) try { const response = await fetch(`${API_BASE_URL}/api/import/analyze`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }), signal, }) // ... rest of implementation } catch (err) { if (err instanceof Error && err.name === 'AbortError') { return false // Request was cancelled, don't update state } // ... existing error handling } }, [])The calling component can then manage the AbortController in a
useEffectcleanup.
87-196: Consider using TanStack Query for data fetching consistency.The coding guidelines specify using TanStack Query for the React frontend. While the custom state machine pattern here works for the multi-step wizard workflow, adopting
useMutationfrom TanStack Query would provide built-in loading states, error handling, automatic cancellation on unmount, and consistency with other data-fetching patterns in the codebase.As per coding guidelines: "Use React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI, and dagre for the React frontend application".
server/routers/documentation.py (2)
90-101: Minor: RedundantPathwrapping.
get_project_pathalready returns aPathobject per the registry module, so wrapping it again on line 94 is unnecessary.♻️ Suggested simplification
def get_project_dir(project_name: str) -> Path: """Get project directory from name or path.""" project_path = get_project_path(project_name) if project_path: - return Path(project_path) + return project_path path = Path(project_name)
165-173: Usestr()instead of__str__(); consider ISO timestamp format.Direct
__str__()calls are non-idiomatic. Additionally, raw float timestamps may be harder for API consumers to work with compared to ISO format.♻️ Suggested improvement
+from datetime import datetime, timezone + # In list_docs function: - modified=stat.st_mtime.__str__(), + modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),visual_regression.py (1)
252-268: Only persist diff images when failing the threshold.Line 252 writes diffs even for passing comparisons, which can bloat snapshot storage over time. Consider writing diffs only when
diff_percentage > self.threshold.♻️ Suggested tweak
- if diff_percentage > 0: + if diff_percentage > self.threshold:
| if search: | ||
| conditions.append("message LIKE ?") | ||
| # Escape LIKE wildcards to prevent unexpected query behavior | ||
| escaped_search = search.replace("%", "\\%").replace("_", "\\_") | ||
| params.append(f"%{escaped_search}%") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing ESCAPE clause for LIKE query.
The code escapes % and _ characters (lines 368-369), but the SQL LIKE clause doesn't include the ESCAPE directive, so SQLite won't interpret \% and \_ as literal characters.
🐛 Proposed fix
if search:
- conditions.append("message LIKE ?")
+ conditions.append("message LIKE ? ESCAPE '\\'")
# Escape LIKE wildcards to prevent unexpected query behavior
escaped_search = search.replace("%", "\\%").replace("_", "\\_")
params.append(f"%{escaped_search}%")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if search: | |
| conditions.append("message LIKE ?") | |
| # Escape LIKE wildcards to prevent unexpected query behavior | |
| escaped_search = search.replace("%", "\\%").replace("_", "\\_") | |
| params.append(f"%{escaped_search}%") | |
| if search: | |
| conditions.append("message LIKE ? ESCAPE '\\'") | |
| # Escape LIKE wildcards to prevent unexpected query behavior | |
| escaped_search = search.replace("%", "\\%").replace("_", "\\_") | |
| params.append(f"%{escaped_search}%") |
🤖 Prompt for AI Agents
In `@structured_logging.py` around lines 365 - 369, The LIKE clause escapes
percent/underscore but doesn't tell SQLite to use backslash as the escape
character; update the condition built where `search` is handled so the SQL uses
an ESCAPE clause (e.g., append "message LIKE ? ESCAPE '\\'") and keep the
escaped pattern you build in `escaped_search` that you append to `params`;
modify the string added to `conditions` (not the escape logic) so SQLite treats
`\%` and `\_` literally while still using the existing `search`,
`escaped_search`, `conditions`, and `params` variables.
- design_tokens.py: generate_all now returns file paths (not content) - auto_documentation.py: fix endpoint grouping for "/" path - api/database.py: use BEGIN IMMEDIATE for atomic_get_next_priority - server/routers/documentation.py: make description optional in PreviewResponse - coding_prompt.template.md: improve grep pattern to reduce false positives Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use logger.exception for full server-side logging and return generic error messages to clients to prevent information leakage. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…#119) Prove acceptance gate returns verdict='passed' when all deterministic validators pass with no llm_judge involvement. - Creates real file at tmp_path/test_output.txt - Creates AcceptanceSpec with file_exists validator - Evaluates via AcceptanceGate.evaluate() - Asserts passed=True, verdict='passed', gate_mode='all_pass' - Verifies only deterministic validator types used - All 44 tests in test_dspy_pipeline_e2e.py pass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Hey @cabana8471-arch, thanks for putting this together! There's clearly a lot of valuable work here. However, I'm finding it difficult to review because the PR scope is much larger than the title suggests. The description mentions 3 bug fixes, but the PR actually includes 16,000+ lines of new functionality across 62 files:
Would you be open to splitting this into smaller, focused PRs? For example:
Smaller PRs would:
Happy to help prioritize which pieces should go first. The bug fixes in particular look solid and could probably merge quickly on their own! |
|
Thanks for the detailed feedback @leonvanzyl! You're absolutely right - this PR grew way beyond its original scope. I'll split this into smaller, focused PRs as you suggested:
Closing this one and will open the bug fixes PR first. Thanks for your patience! |
- Fix LogLevel normalization: convert 'warning' to 'warn' for consistency - Fix _deep_merge: use copy.deepcopy() to prevent DEFAULT_CONFIG mutation - Fix security.py: make shlex fallback conservative when operators present
- review.py: Add path traversal validation for files parameter - review.py: Fix session closure in try/finally block - github_actions.py: CodeQL languages as list instead of comma-separated string - design_tokens.py: Fix generate_all docstring (returns content strings) - templates.py: Add path validation for project_dir in /apply endpoint - security_scanner.py: Support pyproject.toml in pip-audit scans - logs.py: Fix count filter mismatch (add tool_name/search params) - git_workflow.py: Add confirm_clean param for destructive git clean - git_workflow.py: Log config load errors instead of silently passing
Summary
This PR fixes 3 bugs discovered by analyzing error logs (61 errors in a production logs.db):
'bool' object is not callablecrash inquality_gates.py- parameter names shadowed function namessecurity.pyfor complex docker commands with nested quotes that failshlex.split()/tmpaccess for temp files)Changes
quality_gates.pyrun_lint→do_lint,run_type_check→do_type_check,run_custom→do_customto avoid shadowing function namessecurity.py_extract_primary_command()fallback when shlex fails on complex nested quotes.claude/templates/coding_prompt.template.md-mflags patternTest plan
verify_quality()no longer crashes with "'bool' object is not callable"extract_commands('docker exec container php -r "echo \"test\";"')returns['docker']instead of[]🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Enhancements
Reliability
✏️ Tip: You can customize this high-level summary in your review settings.