Skip to content

Conversation

@cabana8471-arch
Copy link
Contributor

@cabana8471-arch cabana8471-arch commented Jan 27, 2026

Summary

This PR fixes 3 bugs discovered by analyzing error logs (61 errors in a production logs.db):

  • Critical: Fix 'bool' object is not callable crash in quality_gates.py - parameter names shadowed function names
  • Medium: Add fallback parser in security.py for complex docker commands with nested quotes that fail shlex.split()
  • Low: Add heredoc warning to coding prompt template (sandbox blocks /tmp access for temp files)

Changes

File Change
quality_gates.py Rename parameters run_lintdo_lint, run_type_checkdo_type_check, run_customdo_custom to avoid shadowing function names
security.py Add _extract_primary_command() fallback when shlex fails on complex nested quotes
.claude/templates/coding_prompt.template.md Add Git commit section warning against heredocs, show multiple -m flags pattern

Test 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 []
  • All 162 security unit tests pass
  • All 9 security integration tests pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Project import wizard and UI modal, template catalog, CI/CD workflow generator, visual regression testing, design tokens manager, auto documentation generator, security scanner, automated code review, git-backed feature workflow, import/extract/create project APIs, logs/review/security/docs endpoints.
  • Enhancements

    • Mandatory database persistence checks and infrastructure verification, stricter mock-data detection and restart persistence tests, configurable quality gates, structured persistent logging with query/export.
  • Reliability

    • Safer concurrent DB access, atomic transactions, improved orchestrator/agent robustness and cleanup.

✏️ Tip: You can customize this high-level summary in your review settings.

cabana8471-arch and others added 30 commits January 21, 2026 10:12
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>
Copy link

@coderabbitai coderabbitai bot left a 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 (like quality_gates, git_workflow, etc.) will still reference the original objects. If a caller modifies the returned config, it could corrupt DEFAULT_CONFIG for subsequent calls.

🐛 Proposed fix using copy.deepcopy
 import json
 import logging
+import copy
 from pathlib import Path
 from typing import Any, TypedDict

Then 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 use BEGIN IMMEDIATE to acquire a write lock. The session.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 the atomic_transaction context 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 filename check at line 286 has the same bypass concerns as get_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.mutateAsync and createFeatures) with a stale state check at line 112. If createFeatures sets 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 after await extractFeatures() has the same race condition as handleFolderSelect. Additionally, state.featuresResult on 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 the state object captured at render time, not the updated state after analyze() 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, use useEffect to react to state.step changes.

server/routers/templates.py-252-266 (1)

252-266: User-controlled path without traversal protection.

request.project_dir is used directly to construct a Path and 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 filename check 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: Use warning() instead of warn() 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 with limit=1000000 could 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 count
server/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, the except block at line 310 is triggered without closing the session, causing a connection leak. Use a try/finally structure 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: Use run_in_threadpool to avoid blocking the event loop during analysis/extraction.

StackDetector.detect(), extract_from_project(), and StackDetector.detect_quick() are synchronous and perform heavy I/O operations (recursive file scanning with rglob(), reading multiple config files). Calling them directly inside async def endpoints blocks the event loop; wrap them with run_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: Use run_in_threadpool to avoid blocking the event loop.

scan_project(...) is synchronous and CPU/IO-heavy (subprocess calls with 120s timeouts, file reads). Calling it directly in async def will 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_name to 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("&", "&amp;")
+                 .replace("<", "&lt;")
+                 .replace(">", "&gt;")
+                 .replace('"', "&quot;")
+                 .replace("'", "&apos;")
+        )
@@
-        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 name and category combination. 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
   fi

Note: Added -s flag 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_all returns 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: Missing node_modules exclusion for Express route scanning.

The Python file scanning (lines 335-336) excludes node_modules and venv, but the Express route scanning (lines 313-331) does not. This could cause performance issues or incorrect results when scanning large node_modules directories.

server/routers/logs.py-149-151 (1)

149-151: Deprecated datetime.utcnow() usage.

Multiple endpoints use datetime.utcnow() which is deprecated in Python 3.12+. Use datetime.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, and export_logs.

Also applies to: 198-198, 232-232, 265-265

integrations/ci/__init__.py-24-29 (1)

24-29: generate_ci_config not included in __all__.

The generate_ci_config function is defined but not added to __all__, which could cause issues with from 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() vs timezone.utc.

Line 147 uses datetime.now(timezone.utc) but lines 437-439 use datetime.utcnow(). The latter is deprecated in Python 3.12+. Use datetime.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 using utcnow().

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 <20 or 18.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 search parameter 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 in analyze() ignores detection confidence.

The analyze() method returns a hardcoded confidence: 0.85 regardless of the actual confidence determined in can_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 .ts and .js API files.

Method detection from filenames (e.g., users.get.ts → GET) is only applied to .ts files (lines 216-221). The .js file handling (lines 232-244) always assigns method="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_image is dropped in to_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 .js files and .ts files for Pages Router to capture all Next.js routes.

App Router supports page.tsx, page.jsx, and page.js (note: page.ts is not supported since pages are React components). The current code only scans .tsx and .jsx, missing page.js routes. Pages Router supports .tsx, .ts, .jsx, and .js extensions, but the code only scans .tsx and .jsx, missing .ts and .js routes entirely. This omission will miss route extraction in JavaScript/TypeScript-only projects.

server/routers/cicd.py-206-219 (1)

206-219: Inconsistent handling of .yml vs .yaml extensions.

list_workflows only globs for *.yml files (line 206), but get_workflow_content accepts both .yml and .yaml extensions (line 240). This inconsistency means .yaml workflow 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

Comment on lines +118 to +125
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,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.py

Repository: leonvanzyl/autocoder

Length of output: 152


🏁 Script executed:

cat -n server/routers/security.py | head -140 | tail -30

Repository: 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 20

Repository: leonvanzyl/autocoder

Length of output: 699


🏁 Script executed:

# Search for SecurityScanner class and its scan method
rg -n "class SecurityScanner" security_scanner.py -A 30

Repository: leonvanzyl/autocoder

Length of output: 1095


🏁 Script executed:

# Continue reading the scan method implementation
rg -n "def scan" security_scanner.py -A 80 | head -120

Repository: 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.py

Repository: 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.py

Repository: leonvanzyl/autocoder

Length of output: 934


🏁 Script executed:

# Read security.py to understand the hierarchical allowlist implementation
cat -n security.py | head -100

Repository: 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 -80

Repository: 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 40

Repository: 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.py

Repository: leonvanzyl/autocoder

Length of output: 566


🏁 Script executed:

# Get the full bash_security_hook function
sed -n '821,920p' security.py

Repository: 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.

cabana8471-arch and others added 4 commits January 27, 2026 21:38
- 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>
Copy link

@coderabbitai coderabbitai bot left a 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: Silent stash pop failure may leave working directory in an inconsistent state.

If stash pop fails (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 returned WorkflowResult will 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-audit runs with -r requirements.txt without verifying file existence.

The _run_pip_audit method passes -r requirements.txt to pip-audit, but the caller _scan_dependencies only checks if requirements.txt OR pyproject.toml exists. If only pyproject.toml exists, pip-audit will fail to find requirements.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. Use is not None to 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.
#RGB inputs 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: Check git diff-tree failures 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()}"
+                    )
+                    continue
analyzers/python_analyzer.py-108-176 (1)

108-176: Use detected confidence instead of a constant.
analyze() currently reports 0.85 regardless of how the framework was detected, which can under/overstate certainty. Consider reusing can_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: Use logger.warning() instead of deprecated logger.warn().

logger.warn() is deprecated since Python 3.2. Use logger.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 before session.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 also read_text() used elsewhere. The exception handler catches KeyError which json.load won't raise directly—consider catching json.JSONDecodeError and OSError instead.

♻️ 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):
             pass
integrations/ci/__init__.py (1)

24-29: Add generate_ci_config to __all__ for consistent public API.

The generate_ci_config function 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_model for 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 using Path.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-modal and 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 duplicate BranchInfo definitions.

This file defines BranchInfo as a dataclass, while server/routers/git_workflow.py defines a separate Pydantic BranchInfo model with identical fields. Having two definitions creates maintenance overhead—changes must be synchronized manually.

Consider either:

  1. Importing and reusing the dataclass in the router (converting to Pydantic on response), or
  2. 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 def but call synchronous blocking operations (subprocess calls via workflow.get_status(), start_feature(), etc.). This blocks the event loop and can cause poor performance under concurrent load.

Either:

  1. Change to regular def endpoints—FastAPI will run them in a threadpool automatically
  2. 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_path to 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 like server/utils.py or server/common.py to reduce duplication.


126-135: report_saved=True is hardcoded regardless of actual save outcome.

The scanner's _save_report method could fail silently (it catches no exceptions but also doesn't propagate failures). Consider passing save_report=True to 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 using datetime.now(timezone.utc) instead of datetime.utcnow().

datetime.utcnow() is deprecated in Python 3.12+. The recommended alternative is datetime.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.3 is used in both detect() and detect_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 since NodeAnalyzer detects these frameworks per the relevant code snippets.

server/routers/import_project.py (3)

29-31: Avoid runtime sys.path manipulation.

Modifying sys.path at 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 finally block for cleanup, but if create_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-detect for 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.

  1. open() should specify encoding="utf-8" for cross-platform consistency.
  2. Silently returning None on 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 None
server/routers/templates.py (1)

286-316: Consider adding a response model for /features endpoint.

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: pass blocks (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__() on st_mtime produces 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_dir is duplicated across multiple routers.

This helper function is identical in documentation.py, design_tokens.py, review.py, and visual_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 refactoring write_readme to 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: Move import shutil to module level.

The shutil import 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)

Comment on lines +389 to +392
try:
# Discard uncommitted changes
self._run_git("checkout", "--", ".", check=False)
self._run_git("clean", "-fd", check=False)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. Removing the clean -fd call, only discarding changes to tracked files
  2. Using clean -fdn (dry-run) first and logging what would be deleted
  3. 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.

Comment on lines +507 to +518
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +392 to +414
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",
},
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +120 to +130
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}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +284 to +288
# Validate filename
if ".." in filename:
raise HTTPException(status_code=400, detail="Invalid filename")

file_path = project_dir / filename
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +40 to +45
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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>
Copy link

@coderabbitai coderabbitai bot left a 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.

Comment on lines +146 to +182
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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)
+            continue

Also 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>
@cabana8471-arch
Copy link
Contributor Author

Additional Fixes Pushed (commit 0a02601)

Addressed remaining CodeRabbit review issues:

Deprecation Fixes

  • logger.warn()logger.warning() (agent.py: 6 instances, parallel_orchestrator.py: 1 instance)
  • datetime.utcnow()datetime.now(timezone.utc) (7 files, 15 instances total)

Security & Correctness Fixes

  • Race condition fix in agent.py: Exception handler no longer calls clear_stuck_features() on failure - prevents race conditions in parallel mode
  • Path traversal fix in server/routers/review.py: Validate filename BEFORE constructing file path
  • XML injection fix in templates/library.py: Added xml_escape() for all user-provided values

Other Fixes

  • ✅ Session delay in agent.py: Changed from 1s to 3s (per CLAUDE.md documentation)
  • ✅ Shallow copy fix in autocoder_config.py: Use copy.deepcopy() for nested dicts

Verification

✓ python3 -m ruff check . → All checks passed!
✓ npm run lint (UI) → 0 errors

Copy link

@coderabbitai coderabbitai bot left a 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: Use is_rate_limit_error(response) instead of duplicating pattern matching logic.

RATE_LIMIT_PATTERNS contains simple string patterns, not regex, so substring matching works correctly. However, since is_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 block

Alternatively, 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 like MONKEY_ID or KEYBOARD_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: Move itertools import to module level.

The chain import from itertools is 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 chain

Then remove line 311:

-        from itertools import chain
security_scanner.py (4)

106-122: Consider removing redundant severity computation in to_dict().

The by_severity counts (lines 115-120) duplicate the severity counts already computed and stored in self.summary (set in scan() 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 summary dict already contains severity counts. If a separate by_severity key is needed for API compatibility, consider referencing self.summary directly instead of recomputing.


459-465: Consider logging exceptions instead of silently ignoring them.

Silent pass on 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: continue makes 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 with get_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 raw filename, 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: Sanitize project_name before embedding in filename.

request.project_name is 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}"

Comment on lines +269 to +284
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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find get_session function definition
rg -n "def get_session" --type py -A 5

Repository: 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 2

Repository: 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.

Comment on lines +1248 to +1267
# 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()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +399 to +409
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.py

Repository: leonvanzyl/autocoder

Length of output: 826


🏁 Script executed:

# Also check the file structure to understand the context better
wc -l security_scanner.py

Repository: 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.py

Repository: 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 for pyproject.toml as the project file it can audit. It also supports auditing PEP 751 lockfiles named pylock.*.toml. The README states: “only pyproject.toml and pylock.*.toml are supported.” [1]
  • Auditing lockfiles (pylock.*.toml): Since pip-audit 2.9.0 (April 7, 2025), you can use --locked to audit pylock.*.toml files instead of pyproject.toml. [2]
  • GitHub Action parity: The official pypa/gh-action-pip-audit exposes a locked: true option to enable auditing pylock.*.toml lockfiles. [3]

What pip-audit does not support (yet) regarding pyproject.toml

  • Tool configuration via [tool.pip-audit] in pyproject.toml is not supported today (it’s an open feature request). [4]

Key docs to reference

  • pip-audit README (usage/examples; project + lockfile support) [1]
  • pip-audit 2.9.0 announcement (PEP 751 / --locked) [2]
  • GitHub Action docs (locked input) [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.

Comment on lines +152 to +163
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate structured_logging.py
find . -name "structured_logging.py" -type f

Repository: 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.py

Repository: 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 5

Repository: leonvanzyl/autocoder

Length of output: 455


🏁 Script executed:

#!/bin/bash
# Locate logs.py
find . -name "logs.py" -path "*/routers/*" -type f

Repository: 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 15

Repository: 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.py

Repository: 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
  1. Add tool_name and search parameters to count() in structured_logging.py for consistency.
  2. Document the limitation that total reflects broader filters.
  3. Use len(logs) as a fallback when tool_name or search is 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>
@cabana8471-arch
Copy link
Contributor Author

Additional Fixes Pushed (commit 0830aa9)

Addressed more CodeRabbit review issues:

Security Fixes

  • Path traversal validation in visual_regression.py: Added input validation in update_baseline() and delete_baseline() to prevent path traversal attacks

Race Condition Fixes

  • atomic_mark_passing in api/database.py: Now uses atomic_transaction with BEGIN IMMEDIATE for proper write locking
  • atomic_update_priority_to_end in api/database.py: Now uses atomic_transaction with BEGIN IMMEDIATE for proper write locking

Resource Leak Fixes

  • Session cleanup in server/routers/review.py: Added finally block to ensure session is always closed, even on exceptions

Other Fixes

  • Deep merge mutation in autocoder_config.py: _deep_merge now uses copy.deepcopy(base) to prevent mutation of DEFAULT_CONFIG
  • Timezone consistency in visual_regression.py: fromtimestamp() now uses tz=timezone.utc for consistency

Verification

✓ python3 -m ruff check . → All checks passed!

getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 28, 2026
getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 28, 2026
- 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
getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 28, 2026
- 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
Copy link

@coderabbitai coderabbitai bot left a 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 in with blocks.

♻️ 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")

Comment on lines 581 to 598
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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +249 to +312
@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()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix DB session acquisition + steps payload type + priority race.

  • Line 257 imports get_session, which doesn’t exist in api/database.py → runtime import error.
  • Line 282 stores steps as a JSON string; Feature.steps is a JSON array and UI expects string[].
  • 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.

Comment on lines +303 to +360
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)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

cabana8471-arch and others added 2 commits January 28, 2026 06:50
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>
Copy link

@coderabbitai coderabbitai bot left a 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_paths blocklist is missing several critical system directories: /sys, /proc, /dev, /boot, /lib, /lib64, /opt, /srv, and /mnt. Additionally, the prefix matching with startswith(p) may cause false positives (e.g., /variable would be blocked by /var).

Consider:

  1. Expanding the blocklist with additional system paths.
  2. Using startswith(p + "/") or project_dir_str == p for boundary-accurate matching.
  3. Reusing the existing validate_path from import_project.py for 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 using is not None consistently 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 use is 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_logs method 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 setState calls in the .then/.catch handlers 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 useEffect cleanup.


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 useMutation from 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: Redundant Path wrapping.

get_project_path already returns a Path object 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: Use str() 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:

Comment on lines +365 to +369
if search:
conditions.append("message LIKE ?")
# Escape LIKE wildcards to prevent unexpected query behavior
escaped_search = search.replace("%", "\\%").replace("_", "\\_")
params.append(f"%{escaped_search}%")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

cabana8471-arch and others added 2 commits January 28, 2026 22:29
- 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>
rudiheydra added a commit to rudiheydra/AutoBuildr that referenced this pull request Jan 28, 2026
…#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>
rudiheydra added a commit to rudiheydra/AutoBuildr that referenced this pull request Jan 28, 2026
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leonvanzyl
Copy link
Owner

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:

  • 12+ new modules (analyzers, templates, integrations, etc.)
  • 10 new API routers (~52 endpoints)
  • New UI components
  • Structured logging system
  • CI/CD workflow generation
  • Security scanner
  • Visual regression testing
  • And more...

Would you be open to splitting this into smaller, focused PRs? For example:

  1. Bug fixes PR - The actual fixes mentioned in the description (security.py fallback, quality_gates parameter shadowing, heredoc warning)
  2. Structured logging PR - The logging system + tests
  3. Project import PR - Analyzers + import wizard
  4. API extensions PR - New routers grouped by functionality
  5. Infrastructure features PR - The mandatory database verification features

Smaller PRs would:

  • Be easier to review thoroughly
  • Reduce risk of regressions
  • Allow valuable fixes to merge faster
  • Make it easier to identify issues if something breaks

Happy to help prioritize which pieces should go first. The bug fixes in particular look solid and could probably merge quickly on their own!

@cabana8471-arch
Copy link
Contributor Author

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:

  1. Bug fixes PR - Just the security.py shlex fallback + heredoc warning
  2. Quality Gates PR - The quality checking module with its fixes
  3. Structured Logging PR - Logging system + tests
  4. Other PRs - Remaining features grouped by functionality

Closing this one and will open the bug fixes PR first. Thanks for your patience!

getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 29, 2026
- 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
getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 29, 2026
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants