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,232 @@
|
|
|
1
|
+
"""Opt-in anonymous telemetry + crash reporting (issue #616).
|
|
2
|
+
|
|
3
|
+
Headless core module — no FastAPI/UI imports. The CLI layer decides *when* to
|
|
4
|
+
record; this module owns consent resolution, payload construction, and
|
|
5
|
+
transport.
|
|
6
|
+
|
|
7
|
+
Privacy guarantees (see PRIVACY.md):
|
|
8
|
+
- Default OFF; nothing is ever sent unless the user opts in.
|
|
9
|
+
- Resolution order: ``CODEFRAME_TELEMETRY`` env var > ``DO_NOT_TRACK`` >
|
|
10
|
+
``~/.codeframe/telemetry.json`` > default off.
|
|
11
|
+
- Events carry only: command name (validated against the registered command
|
|
12
|
+
tree by the caller), duration, exit code, version, OS, Python version, and a
|
|
13
|
+
random anonymous id. Never args, paths, prompts, or project content.
|
|
14
|
+
- Crash reports carry the exception class and traceback frames *inside the
|
|
15
|
+
codeframe package only* (paths relativized); exception messages are omitted
|
|
16
|
+
because they frequently embed user file paths.
|
|
17
|
+
- Sends are fire-and-forget with a short timeout; failures are always silent.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import threading
|
|
27
|
+
import traceback
|
|
28
|
+
import uuid
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
DEFAULT_ENDPOINT = "https://telemetry.codeframe.dev/v1/events"
|
|
39
|
+
CONFIG_FILENAME = "telemetry.json"
|
|
40
|
+
SCHEMA_VERSION = 1
|
|
41
|
+
SEND_TIMEOUT_SECONDS = 3.0
|
|
42
|
+
|
|
43
|
+
_TRUE_VALUES = {"on", "1", "true", "yes"}
|
|
44
|
+
_FALSE_VALUES = {"off", "0", "false", "no"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class TelemetryConfig:
|
|
49
|
+
"""Machine-wide telemetry consent state, stored at ~/.codeframe/telemetry.json."""
|
|
50
|
+
|
|
51
|
+
enabled: bool = False
|
|
52
|
+
prompted: bool = False
|
|
53
|
+
anonymous_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
54
|
+
endpoint: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_storage_dir() -> Path:
|
|
58
|
+
"""Machine-wide config dir (same convention as CredentialManager)."""
|
|
59
|
+
return Path.home() / ".codeframe"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def config_path(storage_dir: Optional[Path] = None) -> Path:
|
|
63
|
+
return (storage_dir or default_storage_dir()) / CONFIG_FILENAME
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_config(storage_dir: Optional[Path] = None) -> TelemetryConfig:
|
|
67
|
+
"""Load config; a missing or corrupted file yields safe defaults (off)."""
|
|
68
|
+
path = config_path(storage_dir)
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(path.read_text())
|
|
71
|
+
return TelemetryConfig(
|
|
72
|
+
enabled=bool(data.get("enabled", False)),
|
|
73
|
+
prompted=bool(data.get("prompted", False)),
|
|
74
|
+
anonymous_id=str(data.get("anonymous_id") or uuid.uuid4()),
|
|
75
|
+
endpoint=data.get("endpoint") or None,
|
|
76
|
+
)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
return TelemetryConfig()
|
|
79
|
+
except (OSError, ValueError, TypeError):
|
|
80
|
+
logger.debug("Unreadable telemetry config at %s; using defaults", path)
|
|
81
|
+
return TelemetryConfig()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def save_config(config: TelemetryConfig, storage_dir: Optional[Path] = None) -> None:
|
|
85
|
+
"""Atomically persist config (unique temp file + os.replace)."""
|
|
86
|
+
import tempfile
|
|
87
|
+
|
|
88
|
+
path = config_path(storage_dir)
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
payload = {
|
|
91
|
+
"enabled": config.enabled,
|
|
92
|
+
"prompted": config.prompted,
|
|
93
|
+
"anonymous_id": config.anonymous_id,
|
|
94
|
+
}
|
|
95
|
+
if config.endpoint:
|
|
96
|
+
payload["endpoint"] = config.endpoint
|
|
97
|
+
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
98
|
+
try:
|
|
99
|
+
with os.fdopen(fd, "w") as f:
|
|
100
|
+
f.write(json.dumps(payload, indent=2))
|
|
101
|
+
os.replace(tmp_name, path)
|
|
102
|
+
except Exception:
|
|
103
|
+
try:
|
|
104
|
+
os.unlink(tmp_name)
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ensure_config(storage_dir: Optional[Path] = None) -> TelemetryConfig:
|
|
111
|
+
"""Load config, persisting defaults on first call so the anonymous id is stable."""
|
|
112
|
+
config = load_config(storage_dir)
|
|
113
|
+
if not config_path(storage_dir).exists():
|
|
114
|
+
save_config(config, storage_dir)
|
|
115
|
+
return config
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def env_override() -> Optional[bool]:
|
|
119
|
+
"""Explicit CODEFRAME_TELEMETRY=on|off override, or None if unset/unrecognized."""
|
|
120
|
+
value = os.environ.get("CODEFRAME_TELEMETRY", "").strip().lower()
|
|
121
|
+
if value in _TRUE_VALUES:
|
|
122
|
+
return True
|
|
123
|
+
if value in _FALSE_VALUES:
|
|
124
|
+
return False
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_enabled(storage_dir: Optional[Path] = None) -> bool:
|
|
129
|
+
"""Resolve consent: env var > DO_NOT_TRACK > config file > default off."""
|
|
130
|
+
override = env_override()
|
|
131
|
+
if override is not None:
|
|
132
|
+
return override
|
|
133
|
+
dnt = os.environ.get("DO_NOT_TRACK", "").strip().lower()
|
|
134
|
+
if dnt not in ("", "0", "false"):
|
|
135
|
+
return False
|
|
136
|
+
return load_config(storage_dir).enabled
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def resolve_endpoint(storage_dir: Optional[Path] = None) -> str:
|
|
140
|
+
"""Resolve collector URL: env var > config file > built-in default."""
|
|
141
|
+
env_endpoint = os.environ.get("CODEFRAME_TELEMETRY_ENDPOINT", "").strip()
|
|
142
|
+
if env_endpoint:
|
|
143
|
+
return env_endpoint
|
|
144
|
+
return load_config(storage_dir).endpoint or DEFAULT_ENDPOINT
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _base_event(event: str, anonymous_id: str) -> dict:
|
|
148
|
+
from codeframe import __version__
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"event": event,
|
|
152
|
+
"schema": SCHEMA_VERSION,
|
|
153
|
+
"anonymous_id": anonymous_id,
|
|
154
|
+
"version": __version__,
|
|
155
|
+
"os": platform.system().lower(),
|
|
156
|
+
"python": platform.python_version(),
|
|
157
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def build_command_event(
|
|
162
|
+
command: str, duration_ms: int, exit_code: int, anonymous_id: str
|
|
163
|
+
) -> dict:
|
|
164
|
+
"""Usage event: command name, duration, outcome — nothing else."""
|
|
165
|
+
payload = _base_event("command", anonymous_id)
|
|
166
|
+
payload.update(
|
|
167
|
+
{
|
|
168
|
+
"command": command,
|
|
169
|
+
"duration_ms": duration_ms,
|
|
170
|
+
"exit_code": exit_code,
|
|
171
|
+
"success": exit_code == 0,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
return payload
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _sanitize_frames(exc: BaseException) -> list[dict]:
|
|
178
|
+
"""Keep only traceback frames inside the codeframe package, relativized.
|
|
179
|
+
|
|
180
|
+
Exception messages and out-of-package frames (user code, stdlib, deps) are
|
|
181
|
+
dropped entirely — they can embed user file paths.
|
|
182
|
+
"""
|
|
183
|
+
package_root = Path(__file__).resolve().parent.parent # .../codeframe
|
|
184
|
+
base = package_root.parent
|
|
185
|
+
frames = []
|
|
186
|
+
for frame in traceback.extract_tb(exc.__traceback__):
|
|
187
|
+
try:
|
|
188
|
+
relative = Path(frame.filename).resolve().relative_to(base)
|
|
189
|
+
except ValueError:
|
|
190
|
+
continue
|
|
191
|
+
if relative.parts and relative.parts[0] == package_root.name:
|
|
192
|
+
frames.append(
|
|
193
|
+
{"file": str(relative), "line": frame.lineno, "function": frame.name}
|
|
194
|
+
)
|
|
195
|
+
return frames
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_crash_event(exc: BaseException, anonymous_id: str) -> dict:
|
|
199
|
+
"""Crash report: exception class + sanitized in-package frames only."""
|
|
200
|
+
payload = _base_event("crash", anonymous_id)
|
|
201
|
+
payload.update(
|
|
202
|
+
{"exception_type": type(exc).__name__, "frames": _sanitize_frames(exc)}
|
|
203
|
+
)
|
|
204
|
+
return payload
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def send_events(
|
|
208
|
+
events: list[dict],
|
|
209
|
+
endpoint: str,
|
|
210
|
+
timeout: float = SEND_TIMEOUT_SECONDS,
|
|
211
|
+
client: Optional[httpx.Client] = None,
|
|
212
|
+
) -> bool:
|
|
213
|
+
"""POST a batch of events. Never raises — telemetry must not break the CLI."""
|
|
214
|
+
try:
|
|
215
|
+
if client is not None:
|
|
216
|
+
response = client.post(endpoint, json={"events": events})
|
|
217
|
+
else:
|
|
218
|
+
with httpx.Client(timeout=timeout) as own_client:
|
|
219
|
+
response = own_client.post(endpoint, json={"events": events})
|
|
220
|
+
return 200 <= response.status_code < 300
|
|
221
|
+
except Exception:
|
|
222
|
+
logger.debug("Telemetry send to %s failed", endpoint, exc_info=True)
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def send_events_background(events: list[dict], endpoint: str) -> threading.Thread:
|
|
227
|
+
"""Fire-and-forget send on a daemon thread. Caller may join() with a bound."""
|
|
228
|
+
thread = threading.Thread(
|
|
229
|
+
target=send_events, args=(events, endpoint), daemon=True, name="telemetry-send"
|
|
230
|
+
)
|
|
231
|
+
thread.start()
|
|
232
|
+
return thread
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Task template management for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
This module provides v2-compatible wrappers around the template functionality.
|
|
4
|
+
It bridges v2 Workspace/Task models with the TaskTemplateManager.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Literal, Optional
|
|
12
|
+
|
|
13
|
+
from codeframe.core.workspace import Workspace
|
|
14
|
+
from codeframe.core import tasks
|
|
15
|
+
from codeframe.core.state_machine import TaskStatus
|
|
16
|
+
from codeframe.planning.task_templates import TaskTemplateManager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# V2-Compatible Data Classes
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TemplateInfo:
|
|
28
|
+
"""Summary information about a template (v2 compatible)."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
name: str
|
|
32
|
+
description: str
|
|
33
|
+
category: str
|
|
34
|
+
total_estimated_hours: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TemplateTaskInfo:
|
|
39
|
+
"""Information about a task within a template (v2 compatible)."""
|
|
40
|
+
|
|
41
|
+
title: str
|
|
42
|
+
description: str
|
|
43
|
+
estimated_hours: float
|
|
44
|
+
complexity_score: int
|
|
45
|
+
uncertainty_level: Literal["low", "medium", "high"]
|
|
46
|
+
depends_on_indices: list[int]
|
|
47
|
+
tags: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class TemplateDetails:
|
|
52
|
+
"""Full details of a template including tasks (v2 compatible)."""
|
|
53
|
+
|
|
54
|
+
id: str
|
|
55
|
+
name: str
|
|
56
|
+
description: str
|
|
57
|
+
category: str
|
|
58
|
+
tags: list[str]
|
|
59
|
+
total_estimated_hours: float
|
|
60
|
+
tasks: list[TemplateTaskInfo]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ApplyTemplateResult:
|
|
65
|
+
"""Result of applying a template (v2 compatible)."""
|
|
66
|
+
|
|
67
|
+
template_id: str
|
|
68
|
+
tasks_created: int
|
|
69
|
+
task_ids: list[str] # v2 uses string UUIDs
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# Template Functions
|
|
74
|
+
# ============================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_templates(category: Optional[str] = None) -> list[TemplateInfo]:
|
|
78
|
+
"""List all available task templates.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
category: Optional category filter
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of TemplateInfo with basic info
|
|
85
|
+
"""
|
|
86
|
+
manager = TaskTemplateManager()
|
|
87
|
+
templates = manager.list_templates(category=category)
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
TemplateInfo(
|
|
91
|
+
id=t.id,
|
|
92
|
+
name=t.name,
|
|
93
|
+
description=t.description,
|
|
94
|
+
category=t.category,
|
|
95
|
+
total_estimated_hours=t.total_estimated_hours,
|
|
96
|
+
)
|
|
97
|
+
for t in templates
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_template(template_id: str) -> Optional[TemplateDetails]:
|
|
102
|
+
"""Get details for a specific template.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
template_id: Template ID
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
TemplateDetails with full information, or None if not found
|
|
109
|
+
"""
|
|
110
|
+
manager = TaskTemplateManager()
|
|
111
|
+
template = manager.get_template(template_id)
|
|
112
|
+
|
|
113
|
+
if not template:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
return TemplateDetails(
|
|
117
|
+
id=template.id,
|
|
118
|
+
name=template.name,
|
|
119
|
+
description=template.description,
|
|
120
|
+
category=template.category,
|
|
121
|
+
tags=template.tags,
|
|
122
|
+
total_estimated_hours=template.total_estimated_hours,
|
|
123
|
+
tasks=[
|
|
124
|
+
TemplateTaskInfo(
|
|
125
|
+
title=task.title,
|
|
126
|
+
description=task.description,
|
|
127
|
+
estimated_hours=task.estimated_hours,
|
|
128
|
+
complexity_score=task.complexity_score,
|
|
129
|
+
uncertainty_level=task.uncertainty_level,
|
|
130
|
+
depends_on_indices=task.depends_on_indices,
|
|
131
|
+
tags=task.tags,
|
|
132
|
+
)
|
|
133
|
+
for task in template.tasks
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_categories() -> list[str]:
|
|
139
|
+
"""List all template categories.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of category names
|
|
143
|
+
"""
|
|
144
|
+
manager = TaskTemplateManager()
|
|
145
|
+
return manager.get_categories()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def apply_template(
|
|
149
|
+
workspace: Workspace,
|
|
150
|
+
template_id: str,
|
|
151
|
+
issue_number: str = "1",
|
|
152
|
+
context: Optional[dict[str, Any]] = None,
|
|
153
|
+
) -> ApplyTemplateResult:
|
|
154
|
+
"""Apply a template to create tasks in a workspace.
|
|
155
|
+
|
|
156
|
+
Creates tasks from the template and adds them to the workspace,
|
|
157
|
+
including dependency relationships.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
workspace: Target workspace
|
|
161
|
+
template_id: Template ID to apply
|
|
162
|
+
issue_number: Parent issue number for task numbering
|
|
163
|
+
context: Optional context dict for template variables
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ApplyTemplateResult with created task IDs
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If template not found
|
|
170
|
+
"""
|
|
171
|
+
manager = TaskTemplateManager()
|
|
172
|
+
template = manager.get_template(template_id)
|
|
173
|
+
|
|
174
|
+
if not template:
|
|
175
|
+
raise ValueError(f"Template not found: {template_id}")
|
|
176
|
+
|
|
177
|
+
# Apply template to get task definitions
|
|
178
|
+
task_dicts = manager.apply_template(
|
|
179
|
+
template_id=template_id,
|
|
180
|
+
context=context or {},
|
|
181
|
+
issue_number=issue_number,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Create tasks using v2 API
|
|
185
|
+
created_tasks: list[tuple[tasks.Task, list[int]]] = []
|
|
186
|
+
for task_dict in task_dicts:
|
|
187
|
+
task = tasks.create(
|
|
188
|
+
workspace,
|
|
189
|
+
title=task_dict["title"],
|
|
190
|
+
description=task_dict["description"],
|
|
191
|
+
status=TaskStatus.BACKLOG,
|
|
192
|
+
estimated_hours=task_dict.get("estimated_hours"),
|
|
193
|
+
complexity_score=task_dict.get("complexity_score"),
|
|
194
|
+
uncertainty_level=task_dict.get("uncertainty_level"),
|
|
195
|
+
)
|
|
196
|
+
created_tasks.append((task, task_dict.get("depends_on_indices", [])))
|
|
197
|
+
|
|
198
|
+
# Wire up dependencies using indices -> actual task IDs
|
|
199
|
+
for task, dep_indices in created_tasks:
|
|
200
|
+
if dep_indices:
|
|
201
|
+
# Map 0-based indices to actual task IDs
|
|
202
|
+
depends_on_ids = [
|
|
203
|
+
created_tasks[idx][0].id
|
|
204
|
+
for idx in dep_indices
|
|
205
|
+
if 0 <= idx < len(created_tasks)
|
|
206
|
+
]
|
|
207
|
+
if depends_on_ids:
|
|
208
|
+
tasks.update_depends_on(workspace, task.id, depends_on_ids)
|
|
209
|
+
|
|
210
|
+
created_task_ids = [task.id for task, _ in created_tasks]
|
|
211
|
+
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Applied template '{template_id}' to workspace {workspace.id}: "
|
|
214
|
+
f"{len(created_task_ids)} tasks created"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return ApplyTemplateResult(
|
|
218
|
+
template_id=template_id,
|
|
219
|
+
tasks_created=len(created_task_ids),
|
|
220
|
+
task_ids=created_task_ids,
|
|
221
|
+
)
|