scopemate 0.1.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.
- scopemate/__init__.py +16 -0
- scopemate/__main__.py +10 -0
- scopemate/breakdown.py +466 -0
- scopemate/cli.py +174 -0
- scopemate/core.py +23 -0
- scopemate/engine.py +226 -0
- scopemate/interaction.py +292 -0
- scopemate/llm.py +343 -0
- scopemate/models.py +157 -0
- scopemate/storage.py +106 -0
- scopemate/task_analysis.py +357 -0
- scopemate-0.1.0.dist-info/METADATA +410 -0
- scopemate-0.1.0.dist-info/RECORD +17 -0
- scopemate-0.1.0.dist-info/WHEEL +5 -0
- scopemate-0.1.0.dist-info/entry_points.txt +2 -0
- scopemate-0.1.0.dist-info/licenses/LICENSE +21 -0
- scopemate-0.1.0.dist-info/top_level.txt +1 -0
scopemate/engine.py
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
scopemate Engine - Main application logic for scopemate
|
4
|
+
|
5
|
+
This module contains the TaskEngine class which coordinates all the functionality
|
6
|
+
of scopemate, handling the workflow for creating, breaking down, and saving tasks.
|
7
|
+
"""
|
8
|
+
import os
|
9
|
+
from typing import List, Optional, Dict, Any
|
10
|
+
|
11
|
+
from .models import ScopeMateTask
|
12
|
+
from .storage import (
|
13
|
+
save_checkpoint, save_plan, load_plan,
|
14
|
+
checkpoint_exists, delete_checkpoint, CHECKPOINT_FILE
|
15
|
+
)
|
16
|
+
from .task_analysis import (
|
17
|
+
check_and_update_parent_estimates, find_long_duration_leaf_tasks,
|
18
|
+
should_decompose_task, _initialize_task_depths,
|
19
|
+
get_task_depth, is_leaf_task
|
20
|
+
)
|
21
|
+
from .breakdown import suggest_breakdown
|
22
|
+
from .interaction import prompt_user, build_root_task, print_summary
|
23
|
+
|
24
|
+
|
25
|
+
class TaskEngine:
|
26
|
+
"""
|
27
|
+
Main engine for scopemate that coordinates task creation, breakdown, and management.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self):
|
31
|
+
"""Initialize the TaskEngine."""
|
32
|
+
self.tasks: List[ScopeMateTask] = []
|
33
|
+
self.task_depths: Dict[str, int] = {}
|
34
|
+
self.max_depth: int = 5 # Maximum depth of task hierarchy
|
35
|
+
|
36
|
+
def load_from_checkpoint(self) -> bool:
|
37
|
+
"""
|
38
|
+
Load tasks from checkpoint file if it exists.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
True if checkpoint was loaded, False otherwise
|
42
|
+
"""
|
43
|
+
if checkpoint_exists():
|
44
|
+
resume = prompt_user(
|
45
|
+
f"Found checkpoint '{CHECKPOINT_FILE}'. Resume?",
|
46
|
+
default="y",
|
47
|
+
choices=["y","n"]
|
48
|
+
)
|
49
|
+
if resume.lower() == "y":
|
50
|
+
self.tasks = load_plan(CHECKPOINT_FILE)
|
51
|
+
return True
|
52
|
+
else:
|
53
|
+
delete_checkpoint()
|
54
|
+
|
55
|
+
return False
|
56
|
+
|
57
|
+
def load_from_file(self, default_filename: str = "scopemate_plan.json") -> bool:
|
58
|
+
"""
|
59
|
+
Load tasks from a user-specified file.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
default_filename: Default filename to suggest
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
True if file was loaded, False otherwise
|
66
|
+
"""
|
67
|
+
choice = prompt_user("Load existing plan?", default="n", choices=["y","n"])
|
68
|
+
if choice.lower() == "y":
|
69
|
+
fname = prompt_user("Enter filename to load", default=default_filename)
|
70
|
+
try:
|
71
|
+
self.tasks = load_plan(fname)
|
72
|
+
return True
|
73
|
+
except FileNotFoundError:
|
74
|
+
print(f"File not found: {fname}")
|
75
|
+
|
76
|
+
return False
|
77
|
+
|
78
|
+
def create_new_task(self) -> None:
|
79
|
+
"""Create a new root task interactively."""
|
80
|
+
self.tasks.append(build_root_task())
|
81
|
+
save_checkpoint(self.tasks)
|
82
|
+
|
83
|
+
def breakdown_complex_tasks(self) -> None:
|
84
|
+
"""Process all tasks and break down complex ones."""
|
85
|
+
# Initialize depth tracking
|
86
|
+
self.task_depths = _initialize_task_depths(self.tasks)
|
87
|
+
|
88
|
+
# Maintain a list of tasks to process (for recursive handling)
|
89
|
+
tasks_to_process = list(self.tasks)
|
90
|
+
|
91
|
+
# Process tasks with checkpointing
|
92
|
+
while tasks_to_process:
|
93
|
+
# Get the next task to process
|
94
|
+
current_task = tasks_to_process.pop(0)
|
95
|
+
|
96
|
+
# Calculate depth for this task if not already tracked
|
97
|
+
current_depth = get_task_depth(current_task, self.task_depths, self.tasks)
|
98
|
+
|
99
|
+
# Check if this task is a leaf (has no children)
|
100
|
+
is_leaf = is_leaf_task(current_task.id, self.tasks)
|
101
|
+
|
102
|
+
# Only decompose if criteria met
|
103
|
+
if should_decompose_task(current_task, current_depth, self.max_depth, is_leaf):
|
104
|
+
print(f"\nDecomposing task {current_task.id} at depth {current_depth}...")
|
105
|
+
print(f" Size: {current_task.scope.size}, Time: {current_task.scope.time_estimate}")
|
106
|
+
|
107
|
+
# Get subtask breakdown from LLM with user interaction
|
108
|
+
subtasks = suggest_breakdown(current_task)
|
109
|
+
|
110
|
+
if subtasks:
|
111
|
+
# Set depth for new subtasks
|
112
|
+
for sub in subtasks:
|
113
|
+
self.task_depths[sub.id] = current_depth + 1
|
114
|
+
|
115
|
+
# Add subtasks to the task list
|
116
|
+
self.tasks.extend(subtasks)
|
117
|
+
|
118
|
+
# Add the newly created subtasks to the processing queue
|
119
|
+
tasks_to_process.extend(subtasks)
|
120
|
+
|
121
|
+
save_checkpoint(self.tasks)
|
122
|
+
|
123
|
+
print(f"Created {len(subtasks)} subtasks for {current_task.id}")
|
124
|
+
|
125
|
+
# Check and update parent estimates if needed
|
126
|
+
self.tasks = check_and_update_parent_estimates(self.tasks)
|
127
|
+
save_checkpoint(self.tasks)
|
128
|
+
|
129
|
+
def handle_long_duration_tasks(self) -> None:
|
130
|
+
"""Find and handle long duration leaf tasks."""
|
131
|
+
# Find long duration leaf tasks
|
132
|
+
long_duration_leaf_tasks = find_long_duration_leaf_tasks(self.tasks)
|
133
|
+
|
134
|
+
if long_duration_leaf_tasks:
|
135
|
+
print("\n=== Found Long-Duration Leaf Tasks ===")
|
136
|
+
print("These tasks have long durations but no subtasks:")
|
137
|
+
|
138
|
+
for i, task in enumerate(long_duration_leaf_tasks):
|
139
|
+
print(f"{i+1}. [{task.id}] {task.title} - {task.scope.time_estimate}")
|
140
|
+
|
141
|
+
print("\nDo you want to break down any of these tasks?")
|
142
|
+
choice = prompt_user("Enter task numbers to break down (comma-separated) or 'n' to skip", default="n")
|
143
|
+
|
144
|
+
if choice.lower() != "n":
|
145
|
+
try:
|
146
|
+
# Parse the choices
|
147
|
+
selected_indices = [int(idx.strip()) - 1 for idx in choice.split(",")]
|
148
|
+
for idx in selected_indices:
|
149
|
+
if 0 <= idx < len(long_duration_leaf_tasks):
|
150
|
+
task_to_breakdown = long_duration_leaf_tasks[idx]
|
151
|
+
print(f"\nBreaking down: [{task_to_breakdown.id}] {task_to_breakdown.title}")
|
152
|
+
|
153
|
+
# Get subtask suggestions
|
154
|
+
suggested_subtasks = suggest_breakdown(task_to_breakdown)
|
155
|
+
|
156
|
+
if suggested_subtasks:
|
157
|
+
# Set depth for new subtasks
|
158
|
+
for sub in suggested_subtasks:
|
159
|
+
if task_to_breakdown.id in self.task_depths:
|
160
|
+
self.task_depths[sub.id] = self.task_depths[task_to_breakdown.id] + 1
|
161
|
+
else:
|
162
|
+
self.task_depths[sub.id] = 1
|
163
|
+
|
164
|
+
self.tasks.extend(suggested_subtasks)
|
165
|
+
save_checkpoint(self.tasks)
|
166
|
+
print(f"Created {len(suggested_subtasks)} subtasks for {task_to_breakdown.id}")
|
167
|
+
|
168
|
+
# Check and update parent estimates
|
169
|
+
self.tasks = check_and_update_parent_estimates(self.tasks)
|
170
|
+
save_checkpoint(self.tasks)
|
171
|
+
except ValueError:
|
172
|
+
print("Invalid selection, skipping breakdown.")
|
173
|
+
|
174
|
+
def finalize_plan(self) -> None:
|
175
|
+
"""Review and save the final plan."""
|
176
|
+
# Final check of parent-child estimate consistency
|
177
|
+
self.tasks = check_and_update_parent_estimates(self.tasks)
|
178
|
+
|
179
|
+
# Review and final save
|
180
|
+
print_summary(self.tasks)
|
181
|
+
proceed = prompt_user("Save final plan?", default="y", choices=["y","n"])
|
182
|
+
if proceed.lower() == 'y':
|
183
|
+
fname = prompt_user("Save plan to file", default="scopemate_plan.json")
|
184
|
+
save_plan(self.tasks, fname)
|
185
|
+
delete_checkpoint()
|
186
|
+
else:
|
187
|
+
print(f"Plan left in checkpoint '{CHECKPOINT_FILE}'. Run again to resume.")
|
188
|
+
|
189
|
+
def run(self) -> None:
|
190
|
+
"""Run the full interactive workflow."""
|
191
|
+
# Display introduction
|
192
|
+
print("=== scopemate Action Plan Builder ===")
|
193
|
+
print("This tool helps break down complex tasks and maintain consistent time estimates.")
|
194
|
+
print("Now with interactive task breakdown - choose from alternative approaches and customize subtasks!")
|
195
|
+
print("Note: Parent tasks will be automatically adjusted if child tasks take longer.\n")
|
196
|
+
|
197
|
+
# Try to load existing checkpoint
|
198
|
+
if not self.load_from_checkpoint():
|
199
|
+
# If no checkpoint, try loading from file or create new
|
200
|
+
if not self.load_from_file():
|
201
|
+
self.create_new_task()
|
202
|
+
|
203
|
+
# Process the tasks
|
204
|
+
self.breakdown_complex_tasks()
|
205
|
+
|
206
|
+
# Check for leaf tasks with long durations
|
207
|
+
self.handle_long_duration_tasks()
|
208
|
+
|
209
|
+
# Finalize the plan
|
210
|
+
self.finalize_plan()
|
211
|
+
|
212
|
+
def run_interactive(self) -> None:
|
213
|
+
"""Run the interactive mode of the application."""
|
214
|
+
print("=== scopemate Action Plan Builder ===")
|
215
|
+
|
216
|
+
|
217
|
+
def interactive_builder():
|
218
|
+
"""
|
219
|
+
Legacy function for backward compatibility that runs the TaskEngine.
|
220
|
+
"""
|
221
|
+
engine = TaskEngine()
|
222
|
+
try:
|
223
|
+
engine.run()
|
224
|
+
except KeyboardInterrupt:
|
225
|
+
print("\nOperation cancelled. Progress saved in checkpoint.")
|
226
|
+
save_checkpoint(engine.tasks)
|
scopemate/interaction.py
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
scopemate Interaction - Functions for user interaction
|
4
|
+
|
5
|
+
This module handles all interactive aspects of scopemate, including
|
6
|
+
collecting user input and displaying information.
|
7
|
+
"""
|
8
|
+
from typing import List, Optional, Dict, Any
|
9
|
+
|
10
|
+
from .models import (
|
11
|
+
ScopeMateTask, Purpose, Scope, Outcome, Meta,
|
12
|
+
get_utc_now, VALID_URGENCY_TYPES, VALID_OUTCOME_TYPES,
|
13
|
+
VALID_SIZE_TYPES, VALID_TIME_ESTIMATES, VALID_CONFIDENCE_LEVELS,
|
14
|
+
VALID_TEAMS
|
15
|
+
)
|
16
|
+
|
17
|
+
# -------------------------------
|
18
|
+
# User Input Functions
|
19
|
+
# -------------------------------
|
20
|
+
def prompt_user(
|
21
|
+
prompt: str,
|
22
|
+
default: Optional[str] = None,
|
23
|
+
choices: Optional[List[str]] = None
|
24
|
+
) -> str:
|
25
|
+
"""
|
26
|
+
Prompt user for input with optional default and choices validation.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
prompt: The prompt text to display
|
30
|
+
default: Optional default value if user enters nothing
|
31
|
+
choices: Optional list of valid choices
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
User's validated input as a string
|
35
|
+
"""
|
36
|
+
while True:
|
37
|
+
suffix = f" [{default}]" if default is not None else ""
|
38
|
+
resp = input(f"{prompt}{suffix}: ").strip()
|
39
|
+
|
40
|
+
if not resp and default is not None:
|
41
|
+
resp = default
|
42
|
+
|
43
|
+
if choices:
|
44
|
+
low = resp.lower()
|
45
|
+
if low not in [c.lower() for c in choices]:
|
46
|
+
print(f"Please choose from {choices}.")
|
47
|
+
continue
|
48
|
+
|
49
|
+
if resp:
|
50
|
+
return resp
|
51
|
+
|
52
|
+
# If we get here with an empty response and no default, loop again
|
53
|
+
|
54
|
+
|
55
|
+
def generate_concise_title(parent_title: str, subtask_title: str) -> str:
|
56
|
+
"""
|
57
|
+
Generate a concise subtask title without repeating the parent title.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
parent_title: The title of the parent task
|
61
|
+
subtask_title: The proposed title for the subtask
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
A concise title for the subtask
|
65
|
+
"""
|
66
|
+
# If subtask title already contains parent title, extract the unique part
|
67
|
+
if parent_title and parent_title.lower() in subtask_title.lower():
|
68
|
+
# Try to find the part after the parent title
|
69
|
+
suffix = subtask_title[subtask_title.lower().find(parent_title.lower()) + len(parent_title):].strip()
|
70
|
+
if suffix:
|
71
|
+
# Remove any leading separators like "-" or ":"
|
72
|
+
return suffix.lstrip(" -:").strip()
|
73
|
+
|
74
|
+
# If parent title isn't in subtask or couldn't extract suffix, use the subtask title directly
|
75
|
+
return subtask_title
|
76
|
+
|
77
|
+
|
78
|
+
def build_custom_subtask(parent_task: ScopeMateTask) -> ScopeMateTask:
|
79
|
+
"""
|
80
|
+
Interactively gather information to create a custom subtask.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
parent_task: The parent ScopeMateTask
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
A new ScopeMateTask object as a subtask of the parent
|
87
|
+
"""
|
88
|
+
# This is a simplified version - the full version should include all the prompts
|
89
|
+
# from the original code in core.py
|
90
|
+
|
91
|
+
import uuid
|
92
|
+
|
93
|
+
print(f"\n=== Creating Custom Subtask for: {parent_task.title} ===")
|
94
|
+
|
95
|
+
title = prompt_user("Give a short TITLE for this subtask")
|
96
|
+
title = generate_concise_title(parent_task.title, title)
|
97
|
+
|
98
|
+
summary = prompt_user("What is the primary PURPOSE of this subtask?")
|
99
|
+
outcome_def = prompt_user("Define the desired OUTCOME")
|
100
|
+
|
101
|
+
# Ask for team assignment
|
102
|
+
print("\nTEAM options:")
|
103
|
+
print("- Product: Product management team")
|
104
|
+
print("- Design: Design and user experience team")
|
105
|
+
print("- Frontend: Frontend development team")
|
106
|
+
print("- Backend: Backend development team")
|
107
|
+
print("- ML: Machine learning team")
|
108
|
+
print("- Infra: Infrastructure and DevOps team")
|
109
|
+
print("- Testing: QA and testing team")
|
110
|
+
print("- Other: Any other team")
|
111
|
+
team = prompt_user(
|
112
|
+
"TEAM responsible",
|
113
|
+
default=parent_task.meta.team,
|
114
|
+
choices=VALID_TEAMS
|
115
|
+
)
|
116
|
+
|
117
|
+
# Create the subtask with sensible defaults inheriting from parent
|
118
|
+
subtask = ScopeMateTask(
|
119
|
+
id=f"TASK-{uuid.uuid4().hex[:6]}",
|
120
|
+
title=title,
|
121
|
+
purpose=Purpose(
|
122
|
+
detailed_description=summary,
|
123
|
+
alignment=parent_task.purpose.alignment.copy(),
|
124
|
+
urgency=parent_task.purpose.urgency
|
125
|
+
),
|
126
|
+
scope=Scope(
|
127
|
+
size="straightforward",
|
128
|
+
time_estimate="days",
|
129
|
+
dependencies=[],
|
130
|
+
risks=[]
|
131
|
+
),
|
132
|
+
outcome=Outcome(
|
133
|
+
type=parent_task.outcome.type,
|
134
|
+
detailed_outcome_definition=outcome_def,
|
135
|
+
acceptance_criteria=[],
|
136
|
+
metric=None,
|
137
|
+
validation_method=None
|
138
|
+
),
|
139
|
+
meta=Meta(
|
140
|
+
status="backlog",
|
141
|
+
priority=None,
|
142
|
+
created=get_utc_now(),
|
143
|
+
updated=get_utc_now(),
|
144
|
+
due_date=None,
|
145
|
+
confidence="medium",
|
146
|
+
team=team
|
147
|
+
),
|
148
|
+
parent_id=parent_task.id
|
149
|
+
)
|
150
|
+
|
151
|
+
return subtask
|
152
|
+
|
153
|
+
|
154
|
+
def build_root_task() -> ScopeMateTask:
|
155
|
+
"""
|
156
|
+
Interactively gather information to create a new root task.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
A new ScopeMateTask object
|
160
|
+
"""
|
161
|
+
# This is a simplified version - the full version should include all the prompts
|
162
|
+
# from the original code in core.py
|
163
|
+
|
164
|
+
import uuid
|
165
|
+
from .llm import estimate_scope
|
166
|
+
|
167
|
+
print("=== scopemate Action Plan Builder ===")
|
168
|
+
|
169
|
+
title = prompt_user("Give a short TITLE for this task")
|
170
|
+
summary = prompt_user("What is the primary PURPOSE of this task?")
|
171
|
+
outcome_def = prompt_user("Define the desired OUTCOME")
|
172
|
+
|
173
|
+
# Ask for team assignment
|
174
|
+
print("\nTEAM options:")
|
175
|
+
print("- Product: Product management team")
|
176
|
+
print("- Design: Design and user experience team")
|
177
|
+
print("- Frontend: Frontend development team")
|
178
|
+
print("- Backend: Backend development team")
|
179
|
+
print("- ML: Machine learning team")
|
180
|
+
print("- Infra: Infrastructure and DevOps team")
|
181
|
+
print("- Testing: QA and testing team")
|
182
|
+
print("- Other: Any other team")
|
183
|
+
team = prompt_user(
|
184
|
+
"TEAM responsible",
|
185
|
+
default="Product",
|
186
|
+
choices=VALID_TEAMS
|
187
|
+
)
|
188
|
+
|
189
|
+
# Create root task with sensible defaults
|
190
|
+
root = ScopeMateTask(
|
191
|
+
id=f"TASK-{uuid.uuid4().hex[:6]}",
|
192
|
+
title=title,
|
193
|
+
purpose=Purpose(
|
194
|
+
detailed_description=summary,
|
195
|
+
alignment=[],
|
196
|
+
urgency="strategic"
|
197
|
+
),
|
198
|
+
scope=Scope(
|
199
|
+
size="straightforward",
|
200
|
+
time_estimate="week",
|
201
|
+
dependencies=[],
|
202
|
+
risks=[]
|
203
|
+
),
|
204
|
+
outcome=Outcome(
|
205
|
+
type="customer-facing",
|
206
|
+
detailed_outcome_definition=outcome_def,
|
207
|
+
acceptance_criteria=[],
|
208
|
+
metric=None,
|
209
|
+
validation_method=None
|
210
|
+
),
|
211
|
+
meta=Meta(
|
212
|
+
status="backlog",
|
213
|
+
priority=None,
|
214
|
+
created=get_utc_now(),
|
215
|
+
updated=get_utc_now(),
|
216
|
+
due_date=None,
|
217
|
+
confidence="medium",
|
218
|
+
team=team
|
219
|
+
)
|
220
|
+
)
|
221
|
+
|
222
|
+
# Use LLM to estimate scope
|
223
|
+
root.scope = estimate_scope(root)
|
224
|
+
return root
|
225
|
+
|
226
|
+
|
227
|
+
def print_summary(tasks: List[ScopeMateTask]) -> None:
|
228
|
+
"""
|
229
|
+
Print a hierarchical summary of tasks with complexity indicators and statistics.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
tasks: List of ScopeMateTask objects to summarize
|
233
|
+
"""
|
234
|
+
print("\n=== Task Summary ===")
|
235
|
+
|
236
|
+
# Build hierarchy maps
|
237
|
+
task_map = {t.id: t for t in tasks}
|
238
|
+
children_map = {}
|
239
|
+
|
240
|
+
for t in tasks:
|
241
|
+
if t.parent_id:
|
242
|
+
if t.parent_id not in children_map:
|
243
|
+
children_map[t.parent_id] = []
|
244
|
+
children_map[t.parent_id].append(t.id)
|
245
|
+
|
246
|
+
# Find root tasks (those without parents or with unknown parents)
|
247
|
+
root_tasks = [t.id for t in tasks if not t.parent_id or t.parent_id not in task_map]
|
248
|
+
|
249
|
+
# Print the hierarchy starting from root tasks
|
250
|
+
for root_id in root_tasks:
|
251
|
+
_print_task_hierarchy(root_id, task_map, children_map)
|
252
|
+
|
253
|
+
# Print some statistics about task complexity
|
254
|
+
complex_count = sum(1 for t in tasks if t.scope.size in ["complex", "uncertain", "pioneering"])
|
255
|
+
long_tasks = sum(1 for t in tasks if t.scope.time_estimate in ["sprint", "multi-sprint"])
|
256
|
+
leaf_tasks = sum(1 for t_id in task_map if t_id not in children_map)
|
257
|
+
|
258
|
+
print("\n=== Task Statistics ===")
|
259
|
+
print(f"Total tasks: {len(tasks)}")
|
260
|
+
print(f"Leaf tasks (no subtasks): {leaf_tasks}")
|
261
|
+
print(f"Complex+ tasks: {complex_count} ({(complex_count/len(tasks))*100:.1f}%)")
|
262
|
+
print(f"Sprint+ duration tasks: {long_tasks} ({(long_tasks/len(tasks))*100:.1f}%)")
|
263
|
+
|
264
|
+
|
265
|
+
def _print_task_hierarchy(
|
266
|
+
task_id: str,
|
267
|
+
task_map: Dict[str, ScopeMateTask],
|
268
|
+
children_map: Dict[str, List[str]],
|
269
|
+
level: int = 0
|
270
|
+
) -> None:
|
271
|
+
"""
|
272
|
+
Recursively print a task and its children with hierarchical indentation.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
task_id: ID of the task to print
|
276
|
+
task_map: Dictionary mapping task IDs to tasks
|
277
|
+
children_map: Dictionary mapping task IDs to lists of child IDs
|
278
|
+
level: Current indentation level
|
279
|
+
"""
|
280
|
+
if task_id not in task_map:
|
281
|
+
return
|
282
|
+
|
283
|
+
t = task_map[task_id]
|
284
|
+
indent = " " * level
|
285
|
+
|
286
|
+
print(f"{indent}{'└─' if level > 0 else ''} [{t.id}] {t.title}")
|
287
|
+
print(f"{indent} Scope: {t.scope.size} | Est: {t.scope.time_estimate} | Team: {t.meta.team or 'Not assigned'}")
|
288
|
+
|
289
|
+
# Print children recursively
|
290
|
+
if task_id in children_map:
|
291
|
+
for child_id in children_map[task_id]:
|
292
|
+
_print_task_hierarchy(child_id, task_map, children_map, level + 1)
|