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,218 @@
|
|
|
1
|
+
"""Blocker detection — reusable pattern matching for agent blocker creation.
|
|
2
|
+
|
|
3
|
+
Classifies error text to determine whether a blocker should be created
|
|
4
|
+
(requires human intervention) or the agent should self-correct.
|
|
5
|
+
|
|
6
|
+
Pattern constants are defined here and imported by agent.py and react_agent.py.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Pattern constants (authoritative location — imported by agent.py)
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
# TRUE requirements ambiguity - create blocker immediately
|
|
16
|
+
# These are situations where the agent genuinely cannot proceed without human input
|
|
17
|
+
REQUIREMENTS_AMBIGUITY_PATTERNS = [
|
|
18
|
+
# True requirements conflicts
|
|
19
|
+
"conflicting requirements",
|
|
20
|
+
"spec unclear",
|
|
21
|
+
"specification unclear",
|
|
22
|
+
"requirements conflict",
|
|
23
|
+
"contradictory requirements",
|
|
24
|
+
# Business logic requiring domain knowledge
|
|
25
|
+
"business decision",
|
|
26
|
+
"business logic unclear",
|
|
27
|
+
"domain knowledge required",
|
|
28
|
+
"stakeholder decision",
|
|
29
|
+
# Security policy ambiguity
|
|
30
|
+
"security policy unclear",
|
|
31
|
+
"compliance requirement unclear",
|
|
32
|
+
"regulatory requirement",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Access/credentials issues - always create blocker
|
|
36
|
+
# These truly require human intervention
|
|
37
|
+
ACCESS_PATTERNS = [
|
|
38
|
+
"permission denied",
|
|
39
|
+
"access denied",
|
|
40
|
+
"authentication required",
|
|
41
|
+
"api key", # Covers "api key missing", "api key not configured", etc.
|
|
42
|
+
"credentials", # Covers "credentials missing", "credentials required", etc.
|
|
43
|
+
"secret required",
|
|
44
|
+
"token required",
|
|
45
|
+
"unauthorized",
|
|
46
|
+
"forbidden",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# External service issues - create blocker after retry
|
|
50
|
+
EXTERNAL_SERVICE_PATTERNS = [
|
|
51
|
+
"service unavailable",
|
|
52
|
+
"rate limited",
|
|
53
|
+
"quota exceeded",
|
|
54
|
+
"connection refused",
|
|
55
|
+
"timeout exceeded",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# TACTICAL decisions - agent should resolve autonomously, NEVER block
|
|
59
|
+
# These patterns indicate the agent is asking about implementation details
|
|
60
|
+
# it should decide on its own using project preferences or best practices
|
|
61
|
+
TACTICAL_DECISION_PATTERNS = [
|
|
62
|
+
# Implementation choices
|
|
63
|
+
"which approach",
|
|
64
|
+
"should i use",
|
|
65
|
+
"multiple options",
|
|
66
|
+
"design decision",
|
|
67
|
+
"please clarify",
|
|
68
|
+
"need clarification",
|
|
69
|
+
# File handling
|
|
70
|
+
"file already exists",
|
|
71
|
+
"overwrite",
|
|
72
|
+
"should i create",
|
|
73
|
+
"should i delete",
|
|
74
|
+
# Tooling choices
|
|
75
|
+
"which version",
|
|
76
|
+
"which package",
|
|
77
|
+
"which framework",
|
|
78
|
+
"install method",
|
|
79
|
+
"package manager",
|
|
80
|
+
# Configuration choices
|
|
81
|
+
"which configuration",
|
|
82
|
+
"which setting",
|
|
83
|
+
"default value",
|
|
84
|
+
"fixture scope",
|
|
85
|
+
"loop scope",
|
|
86
|
+
# Generic decision patterns
|
|
87
|
+
"what do you",
|
|
88
|
+
"do you want",
|
|
89
|
+
"would you like",
|
|
90
|
+
"prefer",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Combined pattern for human input (requirements + access + external)
|
|
94
|
+
# NOTE: Tactical patterns are explicitly EXCLUDED - agent handles these autonomously
|
|
95
|
+
HUMAN_INPUT_PATTERNS = (
|
|
96
|
+
REQUIREMENTS_AMBIGUITY_PATTERNS + ACCESS_PATTERNS + EXTERNAL_SERVICE_PATTERNS
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Error patterns that are technical and the agent should self-correct
|
|
100
|
+
# These are coding/execution errors the agent can fix by trying a different approach
|
|
101
|
+
TECHNICAL_ERROR_PATTERNS = [
|
|
102
|
+
# File/path issues - agent can find correct path or create file
|
|
103
|
+
"file not found",
|
|
104
|
+
"no such file",
|
|
105
|
+
"directory not found",
|
|
106
|
+
"path does not exist",
|
|
107
|
+
"filenotfounderror",
|
|
108
|
+
# Import/module issues - agent can fix imports
|
|
109
|
+
"module not found",
|
|
110
|
+
"import error",
|
|
111
|
+
"no module named",
|
|
112
|
+
"cannot find module",
|
|
113
|
+
"modulenotfounderror",
|
|
114
|
+
# Syntax/code issues - agent can fix code
|
|
115
|
+
"syntax error",
|
|
116
|
+
"syntaxerror",
|
|
117
|
+
"indentation error",
|
|
118
|
+
"name error",
|
|
119
|
+
"nameerror",
|
|
120
|
+
"type error",
|
|
121
|
+
"typeerror",
|
|
122
|
+
"attribute error",
|
|
123
|
+
"attributeerror",
|
|
124
|
+
"undefined",
|
|
125
|
+
"not defined",
|
|
126
|
+
# Command execution issues - agent can try different command
|
|
127
|
+
"command not found",
|
|
128
|
+
"exit code",
|
|
129
|
+
"non-zero exit",
|
|
130
|
+
# General coding issues
|
|
131
|
+
"missing", # usually missing import, argument, etc.
|
|
132
|
+
"expected",
|
|
133
|
+
"invalid",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Classification functions
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def classify_error_for_blocker(text: str) -> Optional[str]:
|
|
142
|
+
"""Classify error text into a blocker category.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
"requirements" | "access" | "external_service" if a blocker is warranted.
|
|
146
|
+
None if the agent should handle it autonomously (tactical/technical/unknown).
|
|
147
|
+
|
|
148
|
+
Priority order:
|
|
149
|
+
1. Tactical patterns → None (agent resolves autonomously)
|
|
150
|
+
2. Requirements ambiguity → "requirements"
|
|
151
|
+
3. Access/credentials → "access"
|
|
152
|
+
4. External service → "external_service"
|
|
153
|
+
5. Technical error patterns → None (agent self-corrects)
|
|
154
|
+
6. No match → None
|
|
155
|
+
"""
|
|
156
|
+
lower = text.lower()
|
|
157
|
+
|
|
158
|
+
# Tactical decisions — agent handles autonomously, never block
|
|
159
|
+
for pattern in TACTICAL_DECISION_PATTERNS:
|
|
160
|
+
if pattern in lower:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Requirements ambiguity — immediate blocker
|
|
164
|
+
for pattern in REQUIREMENTS_AMBIGUITY_PATTERNS:
|
|
165
|
+
if pattern in lower:
|
|
166
|
+
return "requirements"
|
|
167
|
+
|
|
168
|
+
# Access/credentials — immediate blocker
|
|
169
|
+
for pattern in ACCESS_PATTERNS:
|
|
170
|
+
if pattern in lower:
|
|
171
|
+
return "access"
|
|
172
|
+
|
|
173
|
+
# External service issues — deferred blocker (caller decides retry threshold)
|
|
174
|
+
for pattern in EXTERNAL_SERVICE_PATTERNS:
|
|
175
|
+
if pattern in lower:
|
|
176
|
+
return "external_service"
|
|
177
|
+
|
|
178
|
+
# Technical errors — agent self-corrects (checked after blocker categories
|
|
179
|
+
# since some technical patterns like "missing" overlap with access patterns)
|
|
180
|
+
for pattern in TECHNICAL_ERROR_PATTERNS:
|
|
181
|
+
if pattern in lower:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def should_create_blocker(text: str, attempt_count: int = 0) -> tuple[bool, str]:
|
|
188
|
+
"""Decide whether to create a blocker for the given error text.
|
|
189
|
+
|
|
190
|
+
This is the retry-aware public API for blocker decisions. Use it in
|
|
191
|
+
contexts where the caller tracks attempt counts (e.g., verification
|
|
192
|
+
retry loops). For one-shot detection — such as scanning an LLM
|
|
193
|
+
text-only response — use :func:`classify_error_for_blocker` directly
|
|
194
|
+
and treat any non-None category as an immediate blocker.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
text: Error or status text to evaluate.
|
|
198
|
+
attempt_count: Number of prior attempts. External service blockers
|
|
199
|
+
are only created when attempt_count > 1.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
(True, reason) if a blocker should be created.
|
|
203
|
+
(False, "") otherwise.
|
|
204
|
+
"""
|
|
205
|
+
category = classify_error_for_blocker(text)
|
|
206
|
+
|
|
207
|
+
if category == "requirements":
|
|
208
|
+
return True, "Requirements ambiguity detected — human clarification needed"
|
|
209
|
+
|
|
210
|
+
if category == "access":
|
|
211
|
+
return True, "Access or credentials issue — human intervention needed"
|
|
212
|
+
|
|
213
|
+
if category == "external_service":
|
|
214
|
+
if attempt_count > 1:
|
|
215
|
+
return True, "External service issue persists after retries — human intervention needed"
|
|
216
|
+
return False, ""
|
|
217
|
+
|
|
218
|
+
return False, ""
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Blocker management for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
Blockers represent human-in-the-loop questions that pause task execution
|
|
4
|
+
until answered by a human.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional, Union
|
|
14
|
+
|
|
15
|
+
from codeframe.core.workspace import Workspace, get_db_connection
|
|
16
|
+
from codeframe.core import events, runtime, tasks
|
|
17
|
+
from codeframe.core.state_machine import TaskStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _utc_now() -> datetime:
|
|
21
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
22
|
+
return datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BlockerStatus(str, Enum):
|
|
26
|
+
"""Status of a blocker."""
|
|
27
|
+
|
|
28
|
+
OPEN = "OPEN"
|
|
29
|
+
ANSWERED = "ANSWERED"
|
|
30
|
+
RESOLVED = "RESOLVED"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BlockerOrigin(str, Enum):
|
|
34
|
+
"""Who created the blocker."""
|
|
35
|
+
|
|
36
|
+
SYSTEM = "system"
|
|
37
|
+
AGENT = "agent"
|
|
38
|
+
HUMAN = "human"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Blocker:
|
|
43
|
+
"""Represents a blocker (human-in-the-loop question).
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
id: Unique blocker identifier (UUID)
|
|
47
|
+
workspace_id: Workspace this blocker belongs to
|
|
48
|
+
task_id: Optional task this blocker is associated with
|
|
49
|
+
question: The question being asked
|
|
50
|
+
answer: The answer provided (if answered)
|
|
51
|
+
status: Current blocker status
|
|
52
|
+
created_at: When the blocker was created
|
|
53
|
+
answered_at: When the blocker was answered (if answered)
|
|
54
|
+
created_by: Origin of the blocker (system, agent, or human)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
workspace_id: str
|
|
59
|
+
task_id: Optional[str]
|
|
60
|
+
question: str
|
|
61
|
+
answer: Optional[str]
|
|
62
|
+
status: BlockerStatus
|
|
63
|
+
created_at: datetime
|
|
64
|
+
answered_at: Optional[datetime]
|
|
65
|
+
created_by: BlockerOrigin = BlockerOrigin.HUMAN
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create(
|
|
69
|
+
workspace: Workspace,
|
|
70
|
+
question: str,
|
|
71
|
+
task_id: Optional[str] = None,
|
|
72
|
+
created_by: Union[BlockerOrigin, str] = BlockerOrigin.HUMAN,
|
|
73
|
+
) -> Blocker:
|
|
74
|
+
"""Create a new blocker.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
workspace: Target workspace
|
|
78
|
+
question: The question to ask
|
|
79
|
+
task_id: Optional associated task ID
|
|
80
|
+
created_by: Origin of the blocker ("system", "agent", or "human")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Created Blocker
|
|
84
|
+
"""
|
|
85
|
+
origin = BlockerOrigin(created_by)
|
|
86
|
+
blocker_id = str(uuid.uuid4())
|
|
87
|
+
now = _utc_now().isoformat()
|
|
88
|
+
|
|
89
|
+
conn = get_db_connection(workspace)
|
|
90
|
+
cursor = conn.cursor()
|
|
91
|
+
|
|
92
|
+
cursor.execute(
|
|
93
|
+
"""
|
|
94
|
+
INSERT INTO blockers (id, workspace_id, task_id, question, status, created_at, created_by)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
""",
|
|
97
|
+
(blocker_id, workspace.id, task_id, question, BlockerStatus.OPEN.value, now, origin.value),
|
|
98
|
+
)
|
|
99
|
+
conn.commit()
|
|
100
|
+
conn.close()
|
|
101
|
+
|
|
102
|
+
blocker = Blocker(
|
|
103
|
+
id=blocker_id,
|
|
104
|
+
workspace_id=workspace.id,
|
|
105
|
+
task_id=task_id,
|
|
106
|
+
question=question,
|
|
107
|
+
answer=None,
|
|
108
|
+
status=BlockerStatus.OPEN,
|
|
109
|
+
created_at=datetime.fromisoformat(now),
|
|
110
|
+
answered_at=None,
|
|
111
|
+
created_by=origin,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Emit blocker created event
|
|
115
|
+
events.emit_for_workspace(
|
|
116
|
+
workspace,
|
|
117
|
+
events.EventType.BLOCKER_CREATED,
|
|
118
|
+
{"blocker_id": blocker_id, "task_id": task_id, "question": question[:100]},
|
|
119
|
+
print_event=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Outbound webhook dispatch (issue #560) — fire-and-forget, never blocks
|
|
123
|
+
# blocker creation. A misconfigured webhook must not break the workflow.
|
|
124
|
+
_dispatch_blocker_webhook(workspace, blocker_id, task_id)
|
|
125
|
+
|
|
126
|
+
return blocker
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _dispatch_blocker_webhook(
|
|
130
|
+
workspace: Workspace, blocker_id: str, task_id: Optional[str]
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Best-effort outbound webhook for ``blocker.created``."""
|
|
133
|
+
try:
|
|
134
|
+
from codeframe.core.notifications_config import is_webhook_active
|
|
135
|
+
from codeframe.notifications.webhook import (
|
|
136
|
+
WebhookNotificationService,
|
|
137
|
+
format_blocker_payload,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
url = is_webhook_active(workspace)
|
|
141
|
+
if url is None:
|
|
142
|
+
return
|
|
143
|
+
svc = WebhookNotificationService(webhook_url=url, timeout=5)
|
|
144
|
+
svc.send_event_background(format_blocker_payload(blocker_id, task_id))
|
|
145
|
+
except Exception:
|
|
146
|
+
import logging
|
|
147
|
+
|
|
148
|
+
logging.getLogger(__name__).warning(
|
|
149
|
+
"Failed to dispatch blocker.created webhook for %s", blocker_id,
|
|
150
|
+
exc_info=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get(workspace: Workspace, blocker_id: str) -> Optional[Blocker]:
|
|
155
|
+
"""Get a blocker by ID.
|
|
156
|
+
|
|
157
|
+
Supports partial ID matching.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
workspace: Workspace to query
|
|
161
|
+
blocker_id: Blocker identifier (can be partial)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Blocker if found, None otherwise
|
|
165
|
+
"""
|
|
166
|
+
conn = get_db_connection(workspace)
|
|
167
|
+
cursor = conn.cursor()
|
|
168
|
+
|
|
169
|
+
# Try exact match first
|
|
170
|
+
cursor.execute(
|
|
171
|
+
"""
|
|
172
|
+
SELECT id, workspace_id, task_id, question, answer, status, created_at, answered_at,
|
|
173
|
+
COALESCE(created_by, 'human') as created_by
|
|
174
|
+
FROM blockers
|
|
175
|
+
WHERE workspace_id = ? AND id = ?
|
|
176
|
+
""",
|
|
177
|
+
(workspace.id, blocker_id),
|
|
178
|
+
)
|
|
179
|
+
row = cursor.fetchone()
|
|
180
|
+
|
|
181
|
+
# If no exact match, try prefix match
|
|
182
|
+
if not row:
|
|
183
|
+
cursor.execute(
|
|
184
|
+
"""
|
|
185
|
+
SELECT id, workspace_id, task_id, question, answer, status, created_at, answered_at,
|
|
186
|
+
COALESCE(created_by, 'human') as created_by
|
|
187
|
+
FROM blockers
|
|
188
|
+
WHERE workspace_id = ? AND id LIKE ?
|
|
189
|
+
""",
|
|
190
|
+
(workspace.id, f"{blocker_id}%"),
|
|
191
|
+
)
|
|
192
|
+
rows = cursor.fetchall()
|
|
193
|
+
if len(rows) == 1:
|
|
194
|
+
row = rows[0]
|
|
195
|
+
elif len(rows) > 1:
|
|
196
|
+
conn.close()
|
|
197
|
+
raise ValueError(f"Multiple blockers match '{blocker_id}'")
|
|
198
|
+
|
|
199
|
+
conn.close()
|
|
200
|
+
|
|
201
|
+
if not row:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
return _row_to_blocker(row)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def list_open(workspace: Workspace) -> list[Blocker]:
|
|
208
|
+
"""List open blockers.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
workspace: Workspace to query
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of open Blockers, oldest first
|
|
215
|
+
"""
|
|
216
|
+
return list_all(workspace, status=BlockerStatus.OPEN)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def list_all(
|
|
220
|
+
workspace: Workspace,
|
|
221
|
+
status: Optional[BlockerStatus] = None,
|
|
222
|
+
task_id: Optional[str] = None,
|
|
223
|
+
limit: int = 100,
|
|
224
|
+
) -> list[Blocker]:
|
|
225
|
+
"""List blockers with optional filters.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
workspace: Workspace to query
|
|
229
|
+
status: Optional status filter
|
|
230
|
+
task_id: Optional task filter
|
|
231
|
+
limit: Maximum blockers to return
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of Blockers, oldest first
|
|
235
|
+
"""
|
|
236
|
+
conn = get_db_connection(workspace)
|
|
237
|
+
cursor = conn.cursor()
|
|
238
|
+
|
|
239
|
+
query = """
|
|
240
|
+
SELECT id, workspace_id, task_id, question, answer, status, created_at, answered_at,
|
|
241
|
+
COALESCE(created_by, 'human') as created_by
|
|
242
|
+
FROM blockers
|
|
243
|
+
WHERE workspace_id = ?
|
|
244
|
+
"""
|
|
245
|
+
params: list = [workspace.id]
|
|
246
|
+
|
|
247
|
+
if status:
|
|
248
|
+
query += " AND status = ?"
|
|
249
|
+
params.append(status.value)
|
|
250
|
+
|
|
251
|
+
if task_id:
|
|
252
|
+
query += " AND task_id = ?"
|
|
253
|
+
params.append(task_id)
|
|
254
|
+
|
|
255
|
+
query += " ORDER BY created_at ASC LIMIT ?"
|
|
256
|
+
params.append(limit)
|
|
257
|
+
|
|
258
|
+
cursor.execute(query, params)
|
|
259
|
+
rows = cursor.fetchall()
|
|
260
|
+
conn.close()
|
|
261
|
+
|
|
262
|
+
return [_row_to_blocker(row) for row in rows]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def list_for_task(workspace: Workspace, task_id: str) -> list[Blocker]:
|
|
266
|
+
"""List all blockers for a specific task.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
workspace: Workspace to query
|
|
270
|
+
task_id: Task to filter by
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of Blockers for the task
|
|
274
|
+
"""
|
|
275
|
+
return list_all(workspace, task_id=task_id)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def answer(workspace: Workspace, blocker_id: str, text: str) -> Blocker:
|
|
279
|
+
"""Answer a blocker.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
workspace: Target workspace
|
|
283
|
+
blocker_id: Blocker to answer (can be partial ID)
|
|
284
|
+
text: Answer text
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Updated Blocker
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ValueError: If blocker not found or already resolved
|
|
291
|
+
"""
|
|
292
|
+
blocker = get(workspace, blocker_id)
|
|
293
|
+
if not blocker:
|
|
294
|
+
raise ValueError(f"Blocker not found: {blocker_id}")
|
|
295
|
+
|
|
296
|
+
if blocker.status == BlockerStatus.RESOLVED:
|
|
297
|
+
raise ValueError(f"Blocker already resolved: {blocker_id}")
|
|
298
|
+
|
|
299
|
+
now = _utc_now().isoformat()
|
|
300
|
+
|
|
301
|
+
conn = get_db_connection(workspace)
|
|
302
|
+
cursor = conn.cursor()
|
|
303
|
+
|
|
304
|
+
cursor.execute(
|
|
305
|
+
"""
|
|
306
|
+
UPDATE blockers
|
|
307
|
+
SET answer = ?, status = ?, answered_at = ?
|
|
308
|
+
WHERE id = ?
|
|
309
|
+
""",
|
|
310
|
+
(text, BlockerStatus.ANSWERED.value, now, blocker.id),
|
|
311
|
+
)
|
|
312
|
+
conn.commit()
|
|
313
|
+
conn.close()
|
|
314
|
+
|
|
315
|
+
# Emit blocker answered event
|
|
316
|
+
events.emit_for_workspace(
|
|
317
|
+
workspace,
|
|
318
|
+
events.EventType.BLOCKER_ANSWERED,
|
|
319
|
+
{"blocker_id": blocker.id, "task_id": blocker.task_id},
|
|
320
|
+
print_event=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
blocker.answer = text
|
|
324
|
+
blocker.status = BlockerStatus.ANSWERED
|
|
325
|
+
blocker.answered_at = datetime.fromisoformat(now)
|
|
326
|
+
|
|
327
|
+
# Automatically reset the associated task to READY so it can be restarted
|
|
328
|
+
# This eliminates the need for separate "work stop" and "work resume" commands
|
|
329
|
+
if blocker.task_id:
|
|
330
|
+
try:
|
|
331
|
+
# Stop the blocked run (marks it as FAILED)
|
|
332
|
+
active_run = runtime.get_active_run(workspace, blocker.task_id)
|
|
333
|
+
if active_run and active_run.status == runtime.RunStatus.BLOCKED:
|
|
334
|
+
runtime.stop_run(workspace, blocker.task_id)
|
|
335
|
+
# Task is now READY and can be restarted with `cf work start <id> --execute`
|
|
336
|
+
except ValueError:
|
|
337
|
+
# No active run found, just ensure task is READY
|
|
338
|
+
task = tasks.get(workspace, blocker.task_id)
|
|
339
|
+
if task and task.status == TaskStatus.BLOCKED:
|
|
340
|
+
tasks.update_status(workspace, blocker.task_id, TaskStatus.READY)
|
|
341
|
+
|
|
342
|
+
return blocker
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def resolve(workspace: Workspace, blocker_id: str) -> Blocker:
|
|
346
|
+
"""Mark a blocker as resolved.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
workspace: Target workspace
|
|
350
|
+
blocker_id: Blocker to resolve (can be partial ID)
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Updated Blocker
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
ValueError: If blocker not found or not answered
|
|
357
|
+
"""
|
|
358
|
+
blocker = get(workspace, blocker_id)
|
|
359
|
+
if not blocker:
|
|
360
|
+
raise ValueError(f"Blocker not found: {blocker_id}")
|
|
361
|
+
|
|
362
|
+
if blocker.status == BlockerStatus.OPEN:
|
|
363
|
+
raise ValueError(f"Blocker must be answered before resolving: {blocker_id}")
|
|
364
|
+
|
|
365
|
+
if blocker.status == BlockerStatus.RESOLVED:
|
|
366
|
+
raise ValueError(f"Blocker already resolved: {blocker_id}")
|
|
367
|
+
|
|
368
|
+
conn = get_db_connection(workspace)
|
|
369
|
+
cursor = conn.cursor()
|
|
370
|
+
|
|
371
|
+
cursor.execute(
|
|
372
|
+
"""
|
|
373
|
+
UPDATE blockers
|
|
374
|
+
SET status = ?
|
|
375
|
+
WHERE id = ?
|
|
376
|
+
""",
|
|
377
|
+
(BlockerStatus.RESOLVED.value, blocker.id),
|
|
378
|
+
)
|
|
379
|
+
conn.commit()
|
|
380
|
+
conn.close()
|
|
381
|
+
|
|
382
|
+
# Emit blocker resolved event
|
|
383
|
+
events.emit_for_workspace(
|
|
384
|
+
workspace,
|
|
385
|
+
events.EventType.BLOCKER_RESOLVED,
|
|
386
|
+
{"blocker_id": blocker.id, "task_id": blocker.task_id},
|
|
387
|
+
print_event=True,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
blocker.status = BlockerStatus.RESOLVED
|
|
391
|
+
return blocker
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def count_by_status(workspace: Workspace) -> dict[str, int]:
|
|
395
|
+
"""Count blockers by status.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
workspace: Workspace to query
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dict mapping status string to count
|
|
402
|
+
"""
|
|
403
|
+
conn = get_db_connection(workspace)
|
|
404
|
+
cursor = conn.cursor()
|
|
405
|
+
|
|
406
|
+
cursor.execute(
|
|
407
|
+
"""
|
|
408
|
+
SELECT status, COUNT(*) as count
|
|
409
|
+
FROM blockers
|
|
410
|
+
WHERE workspace_id = ?
|
|
411
|
+
GROUP BY status
|
|
412
|
+
""",
|
|
413
|
+
(workspace.id,),
|
|
414
|
+
)
|
|
415
|
+
rows = cursor.fetchall()
|
|
416
|
+
conn.close()
|
|
417
|
+
|
|
418
|
+
return {row[0]: row[1] for row in rows}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _row_to_blocker(row: tuple) -> Blocker:
|
|
422
|
+
"""Convert a database row to a Blocker object."""
|
|
423
|
+
return Blocker(
|
|
424
|
+
id=row[0],
|
|
425
|
+
workspace_id=row[1],
|
|
426
|
+
task_id=row[2],
|
|
427
|
+
question=row[3],
|
|
428
|
+
answer=row[4],
|
|
429
|
+
status=BlockerStatus(row[5]),
|
|
430
|
+
created_at=datetime.fromisoformat(row[6]),
|
|
431
|
+
answered_at=datetime.fromisoformat(row[7]) if row[7] else None,
|
|
432
|
+
created_by=BlockerOrigin(row[8]),
|
|
433
|
+
)
|