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,400 @@
|
|
|
1
|
+
"""Recursive task decomposition and tree operations.
|
|
2
|
+
|
|
3
|
+
Provides functions to classify, decompose, and recursively build task trees
|
|
4
|
+
using LLM-powered analysis. Also handles tree display and status propagation.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from codeframe.adapters.llm.base import Purpose
|
|
14
|
+
from codeframe.core import tasks as task_module
|
|
15
|
+
from codeframe.core.state_machine import TaskStatus
|
|
16
|
+
from codeframe.core.workspace import Workspace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CLASSIFY_SYSTEM_PROMPT = (
|
|
20
|
+
"You are a task decomposition expert. Classify whether a task is 'atomic' "
|
|
21
|
+
"(can be done in 1-2 hours by one developer) or 'composite' (should be broken "
|
|
22
|
+
"into subtasks). When in doubt, choose 'atomic'. Return ONLY the word 'atomic' "
|
|
23
|
+
"or 'composite'."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DECOMPOSE_SYSTEM_PROMPT = (
|
|
27
|
+
"Break this task into 2-7 concrete subtasks. Each should be actionable and "
|
|
28
|
+
"testable. Return a JSON array of objects with 'title' and 'description' fields."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Status display icons
|
|
32
|
+
_STATUS_ICONS = {
|
|
33
|
+
TaskStatus.DONE: "\u2713",
|
|
34
|
+
TaskStatus.IN_PROGRESS: "\u25cf",
|
|
35
|
+
TaskStatus.FAILED: "\u2717",
|
|
36
|
+
TaskStatus.BLOCKED: "\u2298",
|
|
37
|
+
TaskStatus.BACKLOG: "\u25cb",
|
|
38
|
+
TaskStatus.READY: "\u25cb",
|
|
39
|
+
TaskStatus.MERGED: "\u2713",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def classify_task(
|
|
44
|
+
provider, description: str, lineage: list[str]
|
|
45
|
+
) -> str:
|
|
46
|
+
"""Classify a task as 'atomic' or 'composite' using LLM.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
provider: LLM provider instance
|
|
50
|
+
description: Task description to classify
|
|
51
|
+
lineage: List of ancestor task descriptions for context
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
'atomic' or 'composite'
|
|
55
|
+
"""
|
|
56
|
+
lineage_context = ""
|
|
57
|
+
if lineage:
|
|
58
|
+
lineage_context = "\n\nParent context:\n" + "\n".join(
|
|
59
|
+
f"- {desc}" for desc in lineage
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
user_message = f"Task: {description}{lineage_context}"
|
|
63
|
+
|
|
64
|
+
response = provider.complete(
|
|
65
|
+
messages=[{"role": "user", "content": user_message}],
|
|
66
|
+
purpose=Purpose.PLANNING,
|
|
67
|
+
system=CLASSIFY_SYSTEM_PROMPT,
|
|
68
|
+
max_tokens=50,
|
|
69
|
+
temperature=0.0,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
result = response.content.strip().lower()
|
|
73
|
+
if result in ("atomic", "composite"):
|
|
74
|
+
return result
|
|
75
|
+
return "atomic"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def decompose_task(
|
|
79
|
+
provider, description: str, lineage: list[str]
|
|
80
|
+
) -> list[dict]:
|
|
81
|
+
"""Decompose a task into 2-7 subtasks using LLM.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
provider: LLM provider instance
|
|
85
|
+
description: Task description to decompose
|
|
86
|
+
lineage: List of ancestor task descriptions for context
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of dicts with 'title' and 'description' keys (2-7 items)
|
|
90
|
+
"""
|
|
91
|
+
lineage_context = ""
|
|
92
|
+
if lineage:
|
|
93
|
+
lineage_context = "\n\nParent context:\n" + "\n".join(
|
|
94
|
+
f"- {desc}" for desc in lineage
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
user_message = f"Task to decompose: {description}{lineage_context}"
|
|
98
|
+
|
|
99
|
+
response = provider.complete(
|
|
100
|
+
messages=[{"role": "user", "content": user_message}],
|
|
101
|
+
purpose=Purpose.PLANNING,
|
|
102
|
+
system=DECOMPOSE_SYSTEM_PROMPT,
|
|
103
|
+
max_tokens=2048,
|
|
104
|
+
temperature=0.0,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
subtasks = _parse_subtasks(response.content)
|
|
108
|
+
|
|
109
|
+
# Clamp to 2-7 items
|
|
110
|
+
if len(subtasks) > 7:
|
|
111
|
+
subtasks = subtasks[:7]
|
|
112
|
+
while len(subtasks) < 2:
|
|
113
|
+
subtasks.append({
|
|
114
|
+
"title": f"Part {len(subtasks) + 1} of: {description[:60]}",
|
|
115
|
+
"description": f"Additional subtask for: {description}",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return subtasks
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_subtasks(content: str) -> list[dict]:
|
|
122
|
+
"""Parse LLM response into subtask list.
|
|
123
|
+
|
|
124
|
+
Handles JSON arrays directly or wrapped in markdown code blocks.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
content: Raw LLM response
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of dicts with 'title' and 'description' keys
|
|
131
|
+
"""
|
|
132
|
+
# Try markdown-wrapped JSON first
|
|
133
|
+
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
|
|
134
|
+
if json_match:
|
|
135
|
+
json_str = json_match.group(1)
|
|
136
|
+
else:
|
|
137
|
+
# Try raw JSON array
|
|
138
|
+
json_match = re.search(r"\[[\s\S]*\]", content)
|
|
139
|
+
if json_match:
|
|
140
|
+
json_str = json_match.group(0)
|
|
141
|
+
else:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
raw = json.loads(json_str)
|
|
146
|
+
except json.JSONDecodeError:
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
if not isinstance(raw, list):
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
result = []
|
|
153
|
+
for item in raw:
|
|
154
|
+
if isinstance(item, dict) and "title" in item:
|
|
155
|
+
result.append({
|
|
156
|
+
"title": str(item["title"])[:200],
|
|
157
|
+
"description": str(item.get("description", ""))[:2000],
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def generate_task_tree(
|
|
164
|
+
provider,
|
|
165
|
+
description: str,
|
|
166
|
+
lineage: Optional[list[str]] = None,
|
|
167
|
+
depth: int = 0,
|
|
168
|
+
max_depth: int = 3,
|
|
169
|
+
) -> dict:
|
|
170
|
+
"""Recursively generate a task tree using LLM classification and decomposition.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
provider: LLM provider instance
|
|
174
|
+
description: Task description
|
|
175
|
+
lineage: Ancestor task descriptions (accumulates through recursion)
|
|
176
|
+
depth: Current recursion depth
|
|
177
|
+
max_depth: Maximum recursion depth before forcing leaf nodes
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tree dict with keys: title, description, is_leaf, children, lineage
|
|
181
|
+
"""
|
|
182
|
+
lineage = lineage or []
|
|
183
|
+
|
|
184
|
+
# Force leaf at max depth
|
|
185
|
+
if depth >= max_depth:
|
|
186
|
+
return {
|
|
187
|
+
"title": description[:80],
|
|
188
|
+
"description": description,
|
|
189
|
+
"is_leaf": True,
|
|
190
|
+
"children": [],
|
|
191
|
+
"lineage": lineage,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
kind = classify_task(provider, description, lineage)
|
|
195
|
+
|
|
196
|
+
if kind == "atomic":
|
|
197
|
+
return {
|
|
198
|
+
"title": description[:80],
|
|
199
|
+
"description": description,
|
|
200
|
+
"is_leaf": True,
|
|
201
|
+
"children": [],
|
|
202
|
+
"lineage": lineage,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Composite: decompose and recurse
|
|
206
|
+
subtasks = decompose_task(provider, description, lineage)
|
|
207
|
+
children = []
|
|
208
|
+
child_lineage = lineage + [description]
|
|
209
|
+
|
|
210
|
+
for sub in subtasks:
|
|
211
|
+
child_tree = generate_task_tree(
|
|
212
|
+
provider,
|
|
213
|
+
sub["description"] or sub["title"],
|
|
214
|
+
lineage=child_lineage,
|
|
215
|
+
depth=depth + 1,
|
|
216
|
+
max_depth=max_depth,
|
|
217
|
+
)
|
|
218
|
+
# Use the subtask title if it's better than the truncated description
|
|
219
|
+
child_tree["title"] = sub["title"][:80]
|
|
220
|
+
children.append(child_tree)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"title": description[:80],
|
|
224
|
+
"description": description,
|
|
225
|
+
"is_leaf": False,
|
|
226
|
+
"children": children,
|
|
227
|
+
"lineage": lineage,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def flatten_task_tree(
|
|
232
|
+
tree: dict,
|
|
233
|
+
workspace: Workspace,
|
|
234
|
+
prd_id: Optional[str] = None,
|
|
235
|
+
parent_id: Optional[str] = None,
|
|
236
|
+
position: int = 1,
|
|
237
|
+
prefix: str = "",
|
|
238
|
+
) -> list:
|
|
239
|
+
"""Walk the tree and create task records in the workspace.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
tree: Tree dict from generate_task_tree()
|
|
243
|
+
workspace: Target workspace
|
|
244
|
+
prd_id: Optional PRD ID to associate tasks with
|
|
245
|
+
parent_id: Parent task ID (for children)
|
|
246
|
+
position: Position among siblings (1-based)
|
|
247
|
+
prefix: Hierarchical ID prefix (e.g., "1.2")
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Flat list of all created Task objects
|
|
251
|
+
"""
|
|
252
|
+
h_id = f"{prefix}{position}" if prefix else str(position)
|
|
253
|
+
|
|
254
|
+
task = task_module.create(
|
|
255
|
+
workspace=workspace,
|
|
256
|
+
title=tree["title"],
|
|
257
|
+
description=tree.get("description", ""),
|
|
258
|
+
prd_id=prd_id,
|
|
259
|
+
parent_id=parent_id,
|
|
260
|
+
lineage=tree.get("lineage", []),
|
|
261
|
+
is_leaf=tree["is_leaf"],
|
|
262
|
+
hierarchical_id=h_id,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
result = [task]
|
|
266
|
+
|
|
267
|
+
for i, child in enumerate(tree.get("children", []), start=1):
|
|
268
|
+
child_tasks = flatten_task_tree(
|
|
269
|
+
child,
|
|
270
|
+
workspace,
|
|
271
|
+
prd_id=prd_id,
|
|
272
|
+
parent_id=task.id,
|
|
273
|
+
position=i,
|
|
274
|
+
prefix=f"{h_id}.",
|
|
275
|
+
)
|
|
276
|
+
result.extend(child_tasks)
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def display_task_tree(workspace: Workspace) -> str:
|
|
282
|
+
"""Build an ASCII tree display of all tasks in a workspace.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
workspace: Workspace to display tasks from
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Formatted ASCII tree string
|
|
289
|
+
"""
|
|
290
|
+
all_tasks = task_module.list_tasks(workspace)
|
|
291
|
+
|
|
292
|
+
if not all_tasks:
|
|
293
|
+
return "No tasks found."
|
|
294
|
+
|
|
295
|
+
# Build children map and find roots
|
|
296
|
+
children_map: dict[str, list] = {}
|
|
297
|
+
|
|
298
|
+
for t in all_tasks:
|
|
299
|
+
pid = t.parent_id
|
|
300
|
+
if pid not in children_map:
|
|
301
|
+
children_map[pid] = []
|
|
302
|
+
children_map[pid].append(t)
|
|
303
|
+
|
|
304
|
+
# Sort children by hierarchical_id if available, else by title
|
|
305
|
+
for pid in children_map:
|
|
306
|
+
children_map[pid].sort(
|
|
307
|
+
key=lambda t: t.hierarchical_id or t.title
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
roots = children_map.get(None, [])
|
|
311
|
+
|
|
312
|
+
if not roots:
|
|
313
|
+
# No tree structure — display flat list
|
|
314
|
+
lines = []
|
|
315
|
+
for t in all_tasks:
|
|
316
|
+
icon = _STATUS_ICONS.get(t.status, "\u25cb")
|
|
317
|
+
label = t.hierarchical_id or t.id[:8]
|
|
318
|
+
kind = "composite" if not t.is_leaf else "atomic"
|
|
319
|
+
lines.append(f"{label}. {t.title} [{kind}] {icon}")
|
|
320
|
+
return "\n".join(lines)
|
|
321
|
+
|
|
322
|
+
lines = []
|
|
323
|
+
for root in roots:
|
|
324
|
+
_render_node(root, children_map, lines, indent="", is_last=True)
|
|
325
|
+
|
|
326
|
+
return "\n".join(lines)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _render_node(
|
|
330
|
+
task,
|
|
331
|
+
children_map: dict,
|
|
332
|
+
lines: list[str],
|
|
333
|
+
indent: str,
|
|
334
|
+
is_last: bool,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Recursively render a task node in ASCII tree format."""
|
|
337
|
+
icon = _STATUS_ICONS.get(task.status, "\u25cb")
|
|
338
|
+
label = task.hierarchical_id or task.id[:8]
|
|
339
|
+
kind = "composite" if not task.is_leaf else "atomic"
|
|
340
|
+
|
|
341
|
+
connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
|
|
342
|
+
if not indent:
|
|
343
|
+
# Root node — no connector
|
|
344
|
+
lines.append(f"{label}. {task.title} [{kind}] {icon}")
|
|
345
|
+
else:
|
|
346
|
+
lines.append(f"{indent}{connector}{label}. {task.title} [{kind}] {icon}")
|
|
347
|
+
|
|
348
|
+
children = children_map.get(task.id, [])
|
|
349
|
+
child_indent = indent + (" " if is_last else "\u2502 ")
|
|
350
|
+
|
|
351
|
+
for i, child in enumerate(children):
|
|
352
|
+
_render_node(
|
|
353
|
+
child, children_map, lines, child_indent, is_last=(i == len(children) - 1)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def propagate_status(workspace: Workspace, task_id: str) -> None:
|
|
358
|
+
"""Propagate status changes from a child task up to its parent(s).
|
|
359
|
+
|
|
360
|
+
When a child task changes status, the parent's status may need to update:
|
|
361
|
+
- All children DONE -> parent DONE
|
|
362
|
+
- Any child FAILED -> parent FAILED
|
|
363
|
+
- Any child IN_PROGRESS -> parent IN_PROGRESS
|
|
364
|
+
- Otherwise -> no change
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
workspace: Workspace containing the tasks
|
|
368
|
+
task_id: ID of the task whose status just changed
|
|
369
|
+
"""
|
|
370
|
+
task = task_module.get(workspace, task_id)
|
|
371
|
+
if not task or not task.parent_id:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
parent = task_module.get(workspace, task.parent_id)
|
|
375
|
+
if not parent or parent.is_leaf:
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# Load all children of the parent
|
|
379
|
+
all_tasks = task_module.list_tasks(workspace)
|
|
380
|
+
children = [t for t in all_tasks if t.parent_id == parent.id]
|
|
381
|
+
|
|
382
|
+
if not children:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
child_statuses = [c.status for c in children]
|
|
386
|
+
|
|
387
|
+
# Determine new parent status
|
|
388
|
+
new_status = None
|
|
389
|
+
if all(s == TaskStatus.DONE for s in child_statuses):
|
|
390
|
+
new_status = TaskStatus.DONE
|
|
391
|
+
elif any(s == TaskStatus.FAILED for s in child_statuses):
|
|
392
|
+
new_status = TaskStatus.FAILED
|
|
393
|
+
elif any(s == TaskStatus.IN_PROGRESS for s in child_statuses):
|
|
394
|
+
new_status = TaskStatus.IN_PROGRESS
|
|
395
|
+
|
|
396
|
+
if new_status and new_status != parent.status:
|
|
397
|
+
task_module.update_status(workspace, parent.id, new_status)
|
|
398
|
+
|
|
399
|
+
# Recursively propagate upward
|
|
400
|
+
propagate_status(workspace, parent.id)
|