Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4e9e90
fix(tests): Clean up unused imports in 8 API test files
frankbria Nov 17, 2025
23f139e
fix(tests): Fix create_project parameter errors across 140 test insta…
frankbria Nov 17, 2025
2cfad51
fix(tests): Fix async/await issues in 3 integration tests
frankbria Nov 17, 2025
46f5c2c
fix(tests): Fix blocker schema and mock TestRunner in integration tests
frankbria Nov 17, 2025
3eff0c4
fix(tests): Update test_agent_lifecycle.py to use correct API schema
frankbria Nov 17, 2025
69c8e06
fix(tests): Prevent workspace collisions in test_agent_lifecycle.py
frankbria Nov 17, 2025
7414f4f
refactor(tests): Reorganize test suite into logical subdirectories
frankbria Nov 17, 2025
23178ab
chore: Add bandit security scanner and improve verification script
frankbria Nov 17, 2025
cc4728e
fix(tests): Fix API schema mismatches and workspace collisions
frankbria Nov 17, 2025
997cefd
fix(tests): Mass test suite fixes - 95%+ pass rate achieved
frankbria Nov 17, 2025
33a2d13
fix(tests): Final test suite fixes - approaching 100% pass rate
frankbria Nov 17, 2025
1e522a3
fix(tests): Fix retry logic and schema drift - 11/12 multi-agent test…
frankbria Nov 17, 2025
d2c754d
fix(tests): Fix blocker/retry interaction to prevent infinite loops
frankbria Nov 17, 2025
1fb0be2
fix(tests): Fix database schema mismatch, circular dependency detecti…
frankbria Nov 17, 2025
b6c3d15
fix(tests): Optimize API tests with class-scoped fixtures and functio…
frankbria Nov 18, 2025
11f9e16
fix(tests) Fix datetime timezone mismatch in blocker emtrics calcuation
frankbria Nov 18, 2025
4098450
docs(tests) added test issues documentation
frankbria Nov 18, 2025
bd5d7e9
Merge origin/main into fix/api-test-imports
frankbria Nov 18, 2025
d715d49
fix(server): Add missing Request import
frankbria Nov 18, 2025
b7d2904
fix(tests): Restore missing imports in API test files
frankbria Nov 18, 2025
4b9973a
fix(tests): Update API tests to use class-scoped api_client fixture
frankbria Nov 18, 2025
4e6d01f
fix(tests): Add __test__ = False to production classes to prevent pyt…
frankbria Nov 18, 2025
a4159c5
fix(lint): Auto-fix 72 linting errors with ruff
frankbria Nov 18, 2025
2600c7d
fix: Multiple bug fixes and test improvements
frankbria Nov 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ Thumbs.db
# Beads
.beads/
!.beads/*.jsonl
.codeframe/
.agent-tasks/
Binary file not shown.
25 changes: 12 additions & 13 deletions codeframe/agents/backend_worker_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,20 +851,19 @@ async def _self_correction_loop(
)

# Create blocker for manual intervention
cursor = self.db.conn.cursor()
cursor.execute(
"""
INSERT INTO blockers (task_id, severity, reason, question)
VALUES (?, ?, ?, ?)
""",
(
task_id,
"sync",
f"Tests still failing after {max_attempts} self-correction attempts",
"Please review the test failures and correction attempts, then provide manual fix.",
),
agent_id = getattr(self, "id", None) or f"backend-worker-{self.project_id}"
question = (
f"Tests still failing after {max_attempts} self-correction attempts. "
"Please review the test failures and correction attempts, then provide manual fix."
)

self.db.create_blocker(
agent_id=agent_id,
project_id=self.project_id,
task_id=task_id,
blocker_type="SYNC",
question=question,
)
self.db.conn.commit()

return False

Expand Down
2 changes: 0 additions & 2 deletions codeframe/agents/frontend_worker_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ def __init__(
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
self.client = AsyncAnthropic(api_key=self.api_key) if self.api_key else None
self.websocket_manager = websocket_manager
self.db = db
self.project_id = project_id
self.project_root = Path(__file__).parent.parent.parent # codeframe/
self.web_ui_root = self.project_root / "web-ui"
self.components_dir = self.web_ui_root / "src" / "components"
Expand Down
92 changes: 70 additions & 22 deletions codeframe/agents/lead_agent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Lead Agent orchestrator for CodeFRAME."""

import json
import logging
import asyncio
from typing import TYPE_CHECKING, List, Dict, Any, Optional
Expand Down Expand Up @@ -89,9 +90,11 @@ def __init__(
import git

project = self.db.get_project(project_id)
project_root_str = project.get("root_path")
project_root_str = project.get(
"workspace_path"
) # Fixed: use workspace_path per migration 002

# Only initialize GitWorkflowManager if root_path is set and is a valid git repo
# Only initialize GitWorkflowManager if workspace_path is set and is a valid git repo
self.git_workflow = None
if project_root_str:
try:
Expand Down Expand Up @@ -1209,10 +1212,32 @@ async def _execute_coordination_loop(
print(
f"πŸ”„ DEBUG: Task {task_id} failed, retry {retry_counts[task_id]}/{max_retries}"
)
# Check if task has pending SYNC blocker before resetting to pending
can_assign = await self.can_assign_task(task_id)
if can_assign:
# No blocker - reset to pending for retry
self.db.update_task(task_id, {"status": "pending"})
else:
# Has SYNC blocker - keep as blocked
self.db.update_task(task_id, {"status": "blocked"})
logger.info(
f"Task {task_id} kept as blocked due to pending SYNC blocker"
)
except Exception:
logger.exception(f"Error processing task {task_id}")
retry_counts[task_id] = retry_counts.get(task_id, 0) + 1
total_retries += 1
# Check if task has pending SYNC blocker before resetting to pending
can_assign = await self.can_assign_task(task_id)
if can_assign:
# No blocker - reset to pending for retry
self.db.update_task(task_id, {"status": "pending"})
else:
# Has SYNC blocker - keep as blocked
self.db.update_task(task_id, {"status": "blocked"})
logger.info(
f"Task {task_id} kept as blocked due to pending SYNC blocker"
)
else:
# No tasks running and none ready - check if we're stuck
if not self._all_tasks_complete():
Expand All @@ -1231,10 +1256,16 @@ async def _execute_coordination_loop(

# Calculate summary statistics
execution_time = time.time() - start_time
failed_count = len([t for t in tasks if self.db.get_task(t.id).get("status") == "failed"])
# Completed count = tasks in completed_tasks that are not failed
completed_count = len(
[t for t in tasks if t.id in self.dependency_resolver.completed_tasks]
[
t
for t in tasks
if t.id in self.dependency_resolver.completed_tasks
and self.db.get_task(t.id).get("status") != "failed"
]
)
failed_count = len([t for t in tasks if self.db.get_task(t.id).get("status") == "failed"])

summary = {
"total_tasks": len(tasks),
Expand Down Expand Up @@ -1364,8 +1395,8 @@ async def _assign_and_execute_task(self, task: Task, retry_counts: Dict[int, int
except Exception:
logger.exception(f"Task {task.id} execution failed")

# Update task status
self.db.update_task(task.id, {"status": "failed"})
# Don't update task status here - let coordination loop decide
# whether to retry or mark as permanently failed based on retry_counts

# Mark agent idle if it was assigned
try:
Expand Down Expand Up @@ -1411,36 +1442,53 @@ async def can_assign_task(self, task_id: int) -> bool:
return False

# Check if task depends on tasks with pending SYNC blockers
depends_on = task.get("depends_on", "")
if depends_on:
# Get all project tasks to resolve dependencies
all_tasks = self.db.get_project_tasks(self.project_id)

# Find the task this depends on
dependency_task = None
for t in all_tasks:
if t["task_number"] == depends_on:
dependency_task = t
break
depends_on_str = task.get("depends_on", "")
if depends_on_str and depends_on_str.strip():
# Parse depends_on field (JSON array or comma-separated format)
# Similar to dependency_resolver.py lines 71-83
depends_on_str = depends_on_str.strip()
dep_ids = []

if depends_on_str.startswith("[") and depends_on_str.endswith("]"):
# JSON array format: "[1, 2, 3]"
try:
dep_ids = json.loads(depends_on_str)
# Normalize to integers
dep_ids = [int(dep_id) for dep_id in dep_ids]
except (json.JSONDecodeError, ValueError, TypeError) as e:
logger.warning(
f"Invalid JSON in depends_on for task {task_id}: {depends_on_str}. Error: {e}"
)
dep_ids = []
else:
# Comma-separated format or single value
try:
dep_ids = [int(x.strip()) for x in depends_on_str.split(",") if x.strip()]
except ValueError:
logger.warning(
f"Invalid depends_on format for task {task_id}: {depends_on_str}"
)
dep_ids = []

if dependency_task:
# Check each dependency for SYNC blockers
for dep_id in dep_ids:
# Recursively check if dependency is blocked
can_assign_dependency = await self.can_assign_task(dependency_task["id"])
can_assign_dependency = await self.can_assign_task(dep_id)
if not can_assign_dependency:
logger.debug(
f"Task {task_id} blocked: depends on task {dependency_task['id']} "
f"Task {task_id} blocked: depends on task {dep_id} "
f"which has pending SYNC blocker"
)
return False

# Also check if dependency task has pending SYNC blocker
for blocker in blockers.get("blockers", []):
if (
blocker.get("task_id") == dependency_task["id"]
blocker.get("task_id") == dep_id
and blocker.get("blocker_type") == "SYNC"
):
logger.debug(
f"Task {task_id} blocked: dependency task {dependency_task['id']} "
f"Task {task_id} blocked: dependency task {dep_id} "
f"has pending SYNC blocker {blocker.get('id')}"
)
return False
Expand Down
4 changes: 2 additions & 2 deletions codeframe/agents/test_worker_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class TestWorkerAgent(WorkerAgent):
- Integrate with WebSocket broadcasts for test results
"""

__test__ = False # Not a test class - it's an agent that generates tests

def __init__(
self,
agent_id: str,
Expand Down Expand Up @@ -72,8 +74,6 @@ def __init__(
self.client = AsyncAnthropic(api_key=self.api_key) if self.api_key else None
self.websocket_manager = websocket_manager
self.max_correction_attempts = max_correction_attempts
self.db = db
self.project_id = project_id
self.project_root = Path(__file__).parent.parent.parent
self.tests_dir = self.project_root / "tests"

Expand Down
17 changes: 16 additions & 1 deletion codeframe/agents/worker_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(
agent_id: str,
agent_type: str,
provider: str,
project_id: int,
project_id: int | None = None,
Comment thread
frankbria marked this conversation as resolved.
maturity: AgentMaturity = AgentMaturity.D1,
system_prompt: str | None = None,
db: Optional[Any] = None,
Expand Down Expand Up @@ -69,6 +69,9 @@ async def flash_save(self) -> Dict[str, Any]:
if not self.db:
raise ValueError("Database not initialized. Pass db parameter to __init__")

if self.project_id is None:
raise ValueError("project_id is required to flash_save")

from codeframe.lib.context_manager import ContextManager

# Create context manager and execute flash save
Expand Down Expand Up @@ -97,6 +100,9 @@ async def should_flash_save(self) -> bool:
if not self.db:
raise ValueError("Database not initialized. Pass db parameter to __init__")

if self.project_id is None:
raise ValueError("project_id is required to should_flash_save")

from codeframe.lib.context_manager import ContextManager

# Create context manager and check threshold
Expand All @@ -119,6 +125,9 @@ async def save_context_item(self, item_type: ContextItemType, content: str) -> s
if not self.db:
raise ValueError("Database not initialized. Pass db parameter to __init__")

if self.project_id is None:
raise ValueError("project_id is required to save_context_item")

if not content or not content.strip():
raise ValueError("Content cannot be empty")

Expand Down Expand Up @@ -149,6 +158,9 @@ async def load_context(
if not self.db:
raise ValueError("Database not initialized. Pass db parameter to __init__")

if self.project_id is None:
raise ValueError("project_id is required to load_context")

# Call database list_context_items with:
# - project_id=self.project_id
# - agent_id=self.agent_id
Expand Down Expand Up @@ -217,6 +229,9 @@ async def update_tiers(self) -> int:
if not self.db:
raise ValueError("Database not initialized. Pass db parameter to __init__")

if self.project_id is None:
raise ValueError("project_id is required to update_tiers")

from codeframe.lib.context_manager import ContextManager

# Create context manager and trigger tier updates
Expand Down
10 changes: 9 additions & 1 deletion codeframe/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional, Dict, Any, Literal
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, Field, ConfigDict, field_validator


class TaskStatus(Enum):
Expand Down Expand Up @@ -221,6 +221,14 @@ class BlockerResolve(BaseModel):

answer: str = Field(..., min_length=1, max_length=5000)

@field_validator("answer")
@classmethod
def validate_answer_not_whitespace(cls, v: str) -> str:
"""Validate that answer is not empty or whitespace-only."""
if not v.strip():
raise ValueError("Answer cannot be empty or whitespace-only")
return v


class BlockerListResponse(BaseModel):
"""Response model for listing blockers."""
Expand Down
2 changes: 2 additions & 0 deletions codeframe/enforcement/adaptive_test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
class TestResult:
"""Results from running tests."""

__test__ = False # Not a test class - it's a data model for test results

success: bool # True if all tests passed
total_tests: int # Total number of tests
passed_tests: int # Number of passed tests
Expand Down
2 changes: 1 addition & 1 deletion codeframe/enforcement/language_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def _detect_javascript(self) -> Optional[LanguageInfo]:
def _detect_typescript(self) -> Optional[LanguageInfo]:
"""Detect TypeScript projects."""
tsconfig = self.project_path / "tsconfig.json"
package_json = self.project_path / "package.json"
self.project_path / "package.json"
Comment thread
frankbria marked this conversation as resolved.

if not tsconfig.exists():
return None
Expand Down
2 changes: 1 addition & 1 deletion codeframe/git/workflow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def create_feature_branch(self, issue_number: str, issue_title: str) -> str:
raise ValueError(f"Branch '{branch_name}' already exists")

# Create branch from current HEAD
new_branch = self.repo.create_head(branch_name)
self.repo.create_head(branch_name)

logger.info(f"Created feature branch: {branch_name}")

Expand Down
Loading