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,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
+ )