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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. 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)