codeframe-ai 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Execution environment sandbox abstraction.
|
|
2
|
+
|
|
3
|
+
Provides ExecutionContext and IsolationLevel for isolating task execution
|
|
4
|
+
from the shared filesystem, plus worktree registry helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from codeframe.core.sandbox.context import (
|
|
8
|
+
ExecutionContext,
|
|
9
|
+
IsolationLevel,
|
|
10
|
+
create_execution_context,
|
|
11
|
+
)
|
|
12
|
+
from codeframe.core.sandbox.worktree import (
|
|
13
|
+
MergeResult,
|
|
14
|
+
TaskWorktree,
|
|
15
|
+
WorktreeRegistry,
|
|
16
|
+
get_base_branch,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ExecutionContext",
|
|
21
|
+
"IsolationLevel",
|
|
22
|
+
"create_execution_context",
|
|
23
|
+
"MergeResult",
|
|
24
|
+
"TaskWorktree",
|
|
25
|
+
"WorktreeRegistry",
|
|
26
|
+
"get_base_branch",
|
|
27
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""ExecutionContext abstraction for task isolation.
|
|
2
|
+
|
|
3
|
+
Defines the IsolationLevel enum and ExecutionContext dataclass that allow
|
|
4
|
+
conductor.py and agent adapters to run tasks in isolated environments.
|
|
5
|
+
|
|
6
|
+
Isolation levels:
|
|
7
|
+
NONE — shared filesystem, preserves current behavior (default)
|
|
8
|
+
WORKTREE — git worktree per task, safe for parallel execution
|
|
9
|
+
CLOUD — E2B Linux VM per task (reserved, raises NotImplementedError)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IsolationLevel(str, Enum):
|
|
21
|
+
"""Task execution isolation strategy."""
|
|
22
|
+
|
|
23
|
+
NONE = "none"
|
|
24
|
+
WORKTREE = "worktree"
|
|
25
|
+
CLOUD = "cloud"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ExecutionContext:
|
|
30
|
+
"""Execution environment for a single task run.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
task_id: Task being executed.
|
|
34
|
+
isolation: Isolation strategy in use.
|
|
35
|
+
workspace_path: Root path the agent should use for all file I/O.
|
|
36
|
+
cleanup: Called after task completion to release resources.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
task_id: str
|
|
40
|
+
isolation: IsolationLevel
|
|
41
|
+
workspace_path: Path
|
|
42
|
+
cleanup: Callable[[], None]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_execution_context(
|
|
46
|
+
task_id: str,
|
|
47
|
+
isolation: IsolationLevel,
|
|
48
|
+
repo_path: Path,
|
|
49
|
+
) -> ExecutionContext:
|
|
50
|
+
"""Create an ExecutionContext for the given isolation level.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
task_id: Task identifier (used as worktree directory name).
|
|
54
|
+
isolation: Desired isolation level.
|
|
55
|
+
repo_path: Canonical repository root path.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ExecutionContext with workspace_path and cleanup configured.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
NotImplementedError: If isolation is CLOUD (future E2B phase).
|
|
62
|
+
subprocess.CalledProcessError: If git worktree creation fails.
|
|
63
|
+
"""
|
|
64
|
+
if isolation == IsolationLevel.NONE:
|
|
65
|
+
return ExecutionContext(
|
|
66
|
+
task_id=task_id,
|
|
67
|
+
isolation=isolation,
|
|
68
|
+
workspace_path=repo_path,
|
|
69
|
+
cleanup=lambda: None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if isolation == IsolationLevel.WORKTREE:
|
|
73
|
+
from codeframe.core.worktrees import TaskWorktree, WorktreeRegistry, get_base_branch
|
|
74
|
+
|
|
75
|
+
worktree = TaskWorktree()
|
|
76
|
+
registry = WorktreeRegistry()
|
|
77
|
+
base_branch = get_base_branch(repo_path)
|
|
78
|
+
worktree_path = worktree.create(repo_path, task_id, base_branch=base_branch)
|
|
79
|
+
registry.register(repo_path, task_id, batch_id="unknown")
|
|
80
|
+
|
|
81
|
+
def cleanup() -> None:
|
|
82
|
+
worktree.cleanup(repo_path, task_id)
|
|
83
|
+
registry.unregister(repo_path, task_id)
|
|
84
|
+
|
|
85
|
+
return ExecutionContext(
|
|
86
|
+
task_id=task_id,
|
|
87
|
+
isolation=isolation,
|
|
88
|
+
workspace_path=worktree_path,
|
|
89
|
+
cleanup=cleanup,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if isolation == IsolationLevel.CLOUD:
|
|
93
|
+
raise NotImplementedError(
|
|
94
|
+
"IsolationLevel.CLOUD is reserved for the future E2B agent adapter phase. "
|
|
95
|
+
"Use 'none' or 'worktree' instead."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
raise ValueError(f"Unknown isolation level: {isolation}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Worktree registry re-export for the sandbox namespace.
|
|
2
|
+
|
|
3
|
+
Re-exports ``TaskWorktree``, ``MergeResult``, ``WorktreeRegistry``, and
|
|
4
|
+
``get_base_branch`` from ``codeframe.core.worktrees`` so callers can import
|
|
5
|
+
from a single ``codeframe.core.sandbox`` sub-package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeframe.core.worktrees import (
|
|
9
|
+
MergeResult,
|
|
10
|
+
TaskWorktree,
|
|
11
|
+
WorktreeRegistry,
|
|
12
|
+
get_base_branch,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"MergeResult",
|
|
17
|
+
"TaskWorktree",
|
|
18
|
+
"WorktreeRegistry",
|
|
19
|
+
"get_base_branch",
|
|
20
|
+
]
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Schedule management for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
This module provides v2-compatible wrappers around the scheduling functionality.
|
|
4
|
+
It bridges v2 Workspace/Task models with the v1 TaskScheduler.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from codeframe.core.workspace import Workspace
|
|
15
|
+
from codeframe.core import tasks
|
|
16
|
+
from codeframe.planning.task_scheduler import TaskScheduler
|
|
17
|
+
from codeframe.agents.dependency_resolver import DependencyResolver
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _utc_now() -> datetime:
|
|
23
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
24
|
+
return datetime.now(timezone.utc)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# V2-Compatible Data Classes
|
|
29
|
+
# ============================================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TaskAssignment:
|
|
34
|
+
"""Assignment of a task to a time slot and agent (v2 compatible)."""
|
|
35
|
+
|
|
36
|
+
task_id: str # v2 uses string UUIDs
|
|
37
|
+
title: str
|
|
38
|
+
start_time: float # Hours from project start
|
|
39
|
+
end_time: float # Hours from project start
|
|
40
|
+
assigned_agent: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ScheduleResult:
|
|
45
|
+
"""Result of task scheduling (v2 compatible)."""
|
|
46
|
+
|
|
47
|
+
task_assignments: list[TaskAssignment]
|
|
48
|
+
total_duration: float
|
|
49
|
+
agents_used: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class CompletionPrediction:
|
|
54
|
+
"""Prediction for project completion (v2 compatible)."""
|
|
55
|
+
|
|
56
|
+
predicted_date: datetime
|
|
57
|
+
confidence_early: datetime
|
|
58
|
+
confidence_late: datetime
|
|
59
|
+
remaining_hours: float
|
|
60
|
+
completed_percentage: float
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class BottleneckInfo:
|
|
65
|
+
"""Information about a scheduling bottleneck (v2 compatible)."""
|
|
66
|
+
|
|
67
|
+
task_id: str # v2 uses string UUIDs
|
|
68
|
+
task_title: str
|
|
69
|
+
bottleneck_type: str # "duration", "dependencies", "resource"
|
|
70
|
+
impact_hours: float
|
|
71
|
+
recommendation: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# Schedule Functions
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_schedule(
|
|
80
|
+
workspace: Workspace,
|
|
81
|
+
agents: int = 1,
|
|
82
|
+
) -> ScheduleResult:
|
|
83
|
+
"""Get the schedule for a workspace.
|
|
84
|
+
|
|
85
|
+
Uses Critical Path Method to assign start/end times while
|
|
86
|
+
respecting dependency constraints and agent availability.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
workspace: Target workspace
|
|
90
|
+
agents: Number of parallel agents/workers (default: 1)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
ScheduleResult with task assignments
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If no tasks found in workspace
|
|
97
|
+
"""
|
|
98
|
+
# Load tasks from workspace
|
|
99
|
+
task_list = tasks.list_tasks(workspace, limit=1000)
|
|
100
|
+
if not task_list:
|
|
101
|
+
raise ValueError("No tasks found in workspace")
|
|
102
|
+
|
|
103
|
+
# Build ID mapping (v2 string UUID -> v1 integer)
|
|
104
|
+
# We use task index as the integer ID
|
|
105
|
+
uuid_to_int: dict[str, int] = {}
|
|
106
|
+
int_to_uuid: dict[int, str] = {}
|
|
107
|
+
task_lookup: dict[str, tasks.Task] = {}
|
|
108
|
+
|
|
109
|
+
for idx, task in enumerate(task_list):
|
|
110
|
+
int_id = idx + 1 # 1-indexed for v1 compatibility
|
|
111
|
+
uuid_to_int[task.id] = int_id
|
|
112
|
+
int_to_uuid[int_id] = task.id
|
|
113
|
+
task_lookup[task.id] = task
|
|
114
|
+
|
|
115
|
+
# Build v1-compatible task structures
|
|
116
|
+
v1_tasks = []
|
|
117
|
+
task_durations: dict[int, float] = {}
|
|
118
|
+
|
|
119
|
+
for task in task_list:
|
|
120
|
+
int_id = uuid_to_int[task.id]
|
|
121
|
+
# Create a simple object with required attributes
|
|
122
|
+
v1_task = _V1TaskAdapter(
|
|
123
|
+
id=int_id,
|
|
124
|
+
title=task.title,
|
|
125
|
+
status=task.status,
|
|
126
|
+
estimated_hours=task.estimated_hours,
|
|
127
|
+
)
|
|
128
|
+
v1_tasks.append(v1_task)
|
|
129
|
+
|
|
130
|
+
# Get duration (default to 1 hour if not specified)
|
|
131
|
+
duration = task.estimated_hours if task.estimated_hours and task.estimated_hours > 0 else 1.0
|
|
132
|
+
task_durations[int_id] = duration
|
|
133
|
+
|
|
134
|
+
# Build dependency resolver with v1 integer IDs
|
|
135
|
+
resolver = DependencyResolver()
|
|
136
|
+
|
|
137
|
+
# Add dependencies (convert v2 depends_on to v1 format)
|
|
138
|
+
for task in task_list:
|
|
139
|
+
int_id = uuid_to_int[task.id]
|
|
140
|
+
if task.depends_on:
|
|
141
|
+
for dep_uuid in task.depends_on:
|
|
142
|
+
if dep_uuid in uuid_to_int:
|
|
143
|
+
dep_int_id = uuid_to_int[dep_uuid]
|
|
144
|
+
resolver.dependencies.setdefault(int_id, set()).add(dep_int_id)
|
|
145
|
+
resolver.dependents.setdefault(dep_int_id, set()).add(int_id)
|
|
146
|
+
resolver.all_tasks.add(int_id)
|
|
147
|
+
resolver.all_tasks.add(dep_int_id)
|
|
148
|
+
else:
|
|
149
|
+
resolver.all_tasks.add(int_id)
|
|
150
|
+
|
|
151
|
+
# Schedule tasks
|
|
152
|
+
scheduler = TaskScheduler()
|
|
153
|
+
v1_schedule = scheduler.schedule_tasks(
|
|
154
|
+
tasks=v1_tasks,
|
|
155
|
+
task_durations=task_durations,
|
|
156
|
+
resolver=resolver,
|
|
157
|
+
agents_available=agents,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Convert back to v2 format
|
|
161
|
+
assignments = []
|
|
162
|
+
for int_id, v1_assignment in v1_schedule.task_assignments.items():
|
|
163
|
+
uuid = int_to_uuid.get(int_id)
|
|
164
|
+
if uuid:
|
|
165
|
+
task = task_lookup[uuid]
|
|
166
|
+
assignments.append(TaskAssignment(
|
|
167
|
+
task_id=uuid,
|
|
168
|
+
title=task.title,
|
|
169
|
+
start_time=v1_assignment.start_time,
|
|
170
|
+
end_time=v1_assignment.end_time,
|
|
171
|
+
assigned_agent=v1_assignment.assigned_agent,
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
# Sort by start time
|
|
175
|
+
assignments.sort(key=lambda a: a.start_time)
|
|
176
|
+
|
|
177
|
+
return ScheduleResult(
|
|
178
|
+
task_assignments=assignments,
|
|
179
|
+
total_duration=v1_schedule.total_duration,
|
|
180
|
+
agents_used=v1_schedule.agents_used,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def predict_completion(
|
|
185
|
+
workspace: Workspace,
|
|
186
|
+
hours_per_day: float = 8.0,
|
|
187
|
+
start_date: Optional[datetime] = None,
|
|
188
|
+
) -> CompletionPrediction:
|
|
189
|
+
"""Predict project completion date.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
workspace: Target workspace
|
|
193
|
+
hours_per_day: Working hours per day (default: 8)
|
|
194
|
+
start_date: Project start date (default: now)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
CompletionPrediction with predicted date and confidence interval
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ValueError: If no tasks found in workspace
|
|
201
|
+
"""
|
|
202
|
+
if start_date is None:
|
|
203
|
+
start_date = _utc_now()
|
|
204
|
+
|
|
205
|
+
# Load tasks
|
|
206
|
+
task_list = tasks.list_tasks(workspace, limit=1000)
|
|
207
|
+
if not task_list:
|
|
208
|
+
raise ValueError("No tasks found in workspace")
|
|
209
|
+
|
|
210
|
+
# Build ID mapping
|
|
211
|
+
uuid_to_int: dict[str, int] = {}
|
|
212
|
+
int_to_uuid: dict[int, str] = {}
|
|
213
|
+
|
|
214
|
+
for idx, task in enumerate(task_list):
|
|
215
|
+
int_id = idx + 1
|
|
216
|
+
uuid_to_int[task.id] = int_id
|
|
217
|
+
int_to_uuid[int_id] = task.id
|
|
218
|
+
|
|
219
|
+
# Build v1-compatible structures
|
|
220
|
+
v1_tasks = []
|
|
221
|
+
task_durations: dict[int, float] = {}
|
|
222
|
+
current_progress: dict[int, str] = {}
|
|
223
|
+
|
|
224
|
+
for task in task_list:
|
|
225
|
+
int_id = uuid_to_int[task.id]
|
|
226
|
+
v1_task = _V1TaskAdapter(
|
|
227
|
+
id=int_id,
|
|
228
|
+
title=task.title,
|
|
229
|
+
status=task.status,
|
|
230
|
+
estimated_hours=task.estimated_hours,
|
|
231
|
+
)
|
|
232
|
+
v1_tasks.append(v1_task)
|
|
233
|
+
|
|
234
|
+
duration = task.estimated_hours if task.estimated_hours and task.estimated_hours > 0 else 1.0
|
|
235
|
+
task_durations[int_id] = duration
|
|
236
|
+
|
|
237
|
+
# Track completed tasks
|
|
238
|
+
if task.status.value.upper() in ("DONE", "COMPLETED"):
|
|
239
|
+
current_progress[int_id] = "completed"
|
|
240
|
+
|
|
241
|
+
# Build dependency resolver
|
|
242
|
+
resolver = DependencyResolver()
|
|
243
|
+
for task in task_list:
|
|
244
|
+
int_id = uuid_to_int[task.id]
|
|
245
|
+
if task.depends_on:
|
|
246
|
+
for dep_uuid in task.depends_on:
|
|
247
|
+
if dep_uuid in uuid_to_int:
|
|
248
|
+
dep_int_id = uuid_to_int[dep_uuid]
|
|
249
|
+
resolver.dependencies.setdefault(int_id, set()).add(dep_int_id)
|
|
250
|
+
resolver.dependents.setdefault(dep_int_id, set()).add(int_id)
|
|
251
|
+
resolver.all_tasks.add(int_id)
|
|
252
|
+
resolver.all_tasks.add(dep_int_id)
|
|
253
|
+
else:
|
|
254
|
+
resolver.all_tasks.add(int_id)
|
|
255
|
+
|
|
256
|
+
# Get schedule and prediction
|
|
257
|
+
scheduler = TaskScheduler()
|
|
258
|
+
v1_schedule = scheduler.schedule_tasks(
|
|
259
|
+
tasks=v1_tasks,
|
|
260
|
+
task_durations=task_durations,
|
|
261
|
+
resolver=resolver,
|
|
262
|
+
agents_available=1,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
v1_prediction = scheduler.predict_completion_date(
|
|
266
|
+
schedule=v1_schedule,
|
|
267
|
+
current_progress=current_progress,
|
|
268
|
+
start_date=start_date,
|
|
269
|
+
hours_per_day=hours_per_day,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return CompletionPrediction(
|
|
273
|
+
predicted_date=v1_prediction.predicted_date,
|
|
274
|
+
confidence_early=v1_prediction.confidence_interval["early"],
|
|
275
|
+
confidence_late=v1_prediction.confidence_interval["late"],
|
|
276
|
+
remaining_hours=v1_prediction.remaining_hours,
|
|
277
|
+
completed_percentage=v1_prediction.completed_percentage,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_bottlenecks(workspace: Workspace) -> list[BottleneckInfo]:
|
|
282
|
+
"""Identify scheduling bottlenecks for a workspace.
|
|
283
|
+
|
|
284
|
+
Identifies:
|
|
285
|
+
- Long duration tasks on critical path
|
|
286
|
+
- Tasks with many dependents causing delays
|
|
287
|
+
- Resource constraints limiting parallelization
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
workspace: Target workspace
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of BottleneckInfo objects
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
ValueError: If no tasks found in workspace
|
|
297
|
+
"""
|
|
298
|
+
# Load tasks
|
|
299
|
+
task_list = tasks.list_tasks(workspace, limit=1000)
|
|
300
|
+
if not task_list:
|
|
301
|
+
raise ValueError("No tasks found in workspace")
|
|
302
|
+
|
|
303
|
+
# Build ID mapping
|
|
304
|
+
uuid_to_int: dict[str, int] = {}
|
|
305
|
+
int_to_uuid: dict[int, str] = {}
|
|
306
|
+
task_lookup: dict[str, tasks.Task] = {}
|
|
307
|
+
|
|
308
|
+
for idx, task in enumerate(task_list):
|
|
309
|
+
int_id = idx + 1
|
|
310
|
+
uuid_to_int[task.id] = int_id
|
|
311
|
+
int_to_uuid[int_id] = task.id
|
|
312
|
+
task_lookup[task.id] = task
|
|
313
|
+
|
|
314
|
+
# Build v1-compatible structures
|
|
315
|
+
v1_tasks = []
|
|
316
|
+
task_durations: dict[int, float] = {}
|
|
317
|
+
|
|
318
|
+
for task in task_list:
|
|
319
|
+
int_id = uuid_to_int[task.id]
|
|
320
|
+
v1_task = _V1TaskAdapter(
|
|
321
|
+
id=int_id,
|
|
322
|
+
title=task.title,
|
|
323
|
+
status=task.status,
|
|
324
|
+
estimated_hours=task.estimated_hours,
|
|
325
|
+
)
|
|
326
|
+
v1_tasks.append(v1_task)
|
|
327
|
+
|
|
328
|
+
duration = task.estimated_hours if task.estimated_hours and task.estimated_hours > 0 else 1.0
|
|
329
|
+
task_durations[int_id] = duration
|
|
330
|
+
|
|
331
|
+
# Build dependency resolver
|
|
332
|
+
resolver = DependencyResolver()
|
|
333
|
+
for task in task_list:
|
|
334
|
+
int_id = uuid_to_int[task.id]
|
|
335
|
+
if task.depends_on:
|
|
336
|
+
for dep_uuid in task.depends_on:
|
|
337
|
+
if dep_uuid in uuid_to_int:
|
|
338
|
+
dep_int_id = uuid_to_int[dep_uuid]
|
|
339
|
+
resolver.dependencies.setdefault(int_id, set()).add(dep_int_id)
|
|
340
|
+
resolver.dependents.setdefault(dep_int_id, set()).add(int_id)
|
|
341
|
+
resolver.all_tasks.add(int_id)
|
|
342
|
+
resolver.all_tasks.add(dep_int_id)
|
|
343
|
+
else:
|
|
344
|
+
resolver.all_tasks.add(int_id)
|
|
345
|
+
|
|
346
|
+
# Get schedule and bottlenecks
|
|
347
|
+
scheduler = TaskScheduler()
|
|
348
|
+
v1_schedule = scheduler.schedule_tasks(
|
|
349
|
+
tasks=v1_tasks,
|
|
350
|
+
task_durations=task_durations,
|
|
351
|
+
resolver=resolver,
|
|
352
|
+
agents_available=1,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
v1_bottlenecks = scheduler.identify_bottlenecks(
|
|
356
|
+
schedule=v1_schedule,
|
|
357
|
+
task_durations=task_durations,
|
|
358
|
+
resolver=resolver,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Convert to v2 format
|
|
362
|
+
bottlenecks = []
|
|
363
|
+
for v1_bn in v1_bottlenecks:
|
|
364
|
+
uuid = int_to_uuid.get(v1_bn.task_id)
|
|
365
|
+
if uuid:
|
|
366
|
+
task = task_lookup[uuid]
|
|
367
|
+
bottlenecks.append(BottleneckInfo(
|
|
368
|
+
task_id=uuid,
|
|
369
|
+
task_title=task.title,
|
|
370
|
+
bottleneck_type=v1_bn.bottleneck_type,
|
|
371
|
+
impact_hours=v1_bn.impact_hours,
|
|
372
|
+
recommendation=v1_bn.recommendation,
|
|
373
|
+
))
|
|
374
|
+
|
|
375
|
+
return bottlenecks
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ============================================================================
|
|
379
|
+
# Internal Helpers
|
|
380
|
+
# ============================================================================
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class _V1TaskAdapter:
|
|
384
|
+
"""Adapter to make v2 tasks compatible with v1 TaskScheduler."""
|
|
385
|
+
|
|
386
|
+
def __init__(
|
|
387
|
+
self,
|
|
388
|
+
id: int,
|
|
389
|
+
title: str,
|
|
390
|
+
status: Any,
|
|
391
|
+
estimated_hours: Optional[float],
|
|
392
|
+
):
|
|
393
|
+
self.id = id
|
|
394
|
+
self.title = title
|
|
395
|
+
self.status = status
|
|
396
|
+
self.estimated_hours = estimated_hours
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Standalone stall detection for agent execution.
|
|
2
|
+
|
|
3
|
+
Provides a synchronous, non-threaded detector that tracks time since
|
|
4
|
+
the last meaningful agent activity. Use ``StallMonitor`` (in
|
|
5
|
+
``stall_monitor.py``) for the threaded watchdog that wraps this logic.
|
|
6
|
+
|
|
7
|
+
This module has no runtime dependencies beyond the stdlib.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StallAction(str, Enum):
|
|
15
|
+
"""Recovery action when a stall is detected.
|
|
16
|
+
|
|
17
|
+
RETRY - Kill the current attempt and retry the task.
|
|
18
|
+
BLOCKER - Create a blocker for human intervention.
|
|
19
|
+
FAIL - Transition the task to FAILED.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
RETRY = "retry"
|
|
23
|
+
BLOCKER = "blocker"
|
|
24
|
+
FAIL = "fail"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StallDetectedError(Exception):
|
|
28
|
+
"""Raised when a stall is detected with RETRY action.
|
|
29
|
+
|
|
30
|
+
Propagates up to ``execute_agent()`` so the runtime can re-invoke
|
|
31
|
+
the agent with context about why the previous attempt stalled.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, elapsed_s: float, iterations: int, last_tool: str = "") -> None:
|
|
35
|
+
self.elapsed_s = elapsed_s
|
|
36
|
+
self.iterations = iterations
|
|
37
|
+
self.last_tool = last_tool
|
|
38
|
+
super().__init__(
|
|
39
|
+
f"Agent stalled after {elapsed_s:.0f}s "
|
|
40
|
+
f"(iterations={iterations}, last_tool={last_tool!r})"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StallDetector:
|
|
45
|
+
"""Tracks elapsed time since the last recorded activity.
|
|
46
|
+
|
|
47
|
+
Call ``record_activity()`` after each tool execution or meaningful
|
|
48
|
+
event. Call ``is_stalled()`` to check whether the configured
|
|
49
|
+
timeout has been exceeded.
|
|
50
|
+
|
|
51
|
+
A ``timeout_s`` of 0 or negative disables detection (``is_stalled``
|
|
52
|
+
always returns ``False``).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, timeout_s: float = 300) -> None:
|
|
56
|
+
self.timeout_s = timeout_s
|
|
57
|
+
self._last_activity: float = time.monotonic()
|
|
58
|
+
|
|
59
|
+
def record_activity(self) -> None:
|
|
60
|
+
"""Reset the inactivity timer to *now*."""
|
|
61
|
+
self._last_activity = time.monotonic()
|
|
62
|
+
|
|
63
|
+
def is_stalled(self) -> bool:
|
|
64
|
+
"""Return ``True`` if no activity for longer than *timeout_s*."""
|
|
65
|
+
if self.timeout_s <= 0:
|
|
66
|
+
return False
|
|
67
|
+
return (time.monotonic() - self._last_activity) > self.timeout_s
|
|
68
|
+
|
|
69
|
+
def elapsed_since_activity_ms(self) -> int:
|
|
70
|
+
"""Milliseconds since the last recorded activity."""
|
|
71
|
+
return int((time.monotonic() - self._last_activity) * 1000)
|