scopemate 0.1.0__py3-none-any.whl → 0.1.2__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 CHANGED
@@ -4,7 +4,7 @@ This package provides tools for breaking down complex tasks using
4
4
  the Purpose/Scope/Outcome planning approach.
5
5
  """
6
6
 
7
- __version__ = "0.1.1"
7
+ __version__ = "0.1.2"
8
8
 
9
9
  # Public API
10
10
  from .models import (
scopemate/breakdown.py CHANGED
@@ -21,11 +21,39 @@ def suggest_breakdown(task: ScopeMateTask) -> List[ScopeMateTask]:
21
21
  """
22
22
  Use LLM to suggest a breakdown of a task into smaller subtasks.
23
23
 
24
+ This function is a critical part of the scopemate workflow. It uses a Large Language
25
+ Model to analyze a task and suggest appropriate subtasks that collectively accomplish
26
+ the parent task's goal. The function handles both complexity-based and time-based
27
+ breakdowns, ensuring that complex tasks are simplified and long-duration tasks are
28
+ broken into manageable timeframes.
29
+
30
+ The function works through these stages:
31
+ 1. Analyze if the breakdown is needed due to complexity or time duration
32
+ 2. Formulate a specialized prompt for the LLM with appropriate constraints
33
+ 3. Process the LLM's response to extract valid subtask definitions
34
+ 4. Convert the raw LLM output into proper ScopeMateTask objects
35
+ 5. Present the suggestions to the user through an interactive selection process
36
+
37
+ The subtasks generated are guaranteed to be:
38
+ - Smaller in scope than the parent task
39
+ - Less complex than the parent task
40
+ - Shorter in duration than the parent task
41
+ - Collectivey covering all aspects needed to accomplish the parent task
42
+
24
43
  Args:
25
- task: The ScopeMateTask to break down
44
+ task: The ScopeMateTask to break down into smaller subtasks
26
45
 
27
46
  Returns:
28
- List of ScopeMateTask objects representing subtasks
47
+ List of ScopeMateTask objects representing the subtasks, after user interaction
48
+
49
+ Example:
50
+ ```python
51
+ parent_task = get_task_by_id("TASK-123")
52
+ subtasks = suggest_breakdown(parent_task)
53
+ if subtasks:
54
+ print(f"Created {len(subtasks)} subtasks")
55
+ tasks.extend(subtasks)
56
+ ```
29
57
  """
30
58
  # Check if we're breaking down due to size complexity or time estimate
31
59
  is_complex = task.scope.size in ["complex", "uncertain", "pioneering"]
scopemate/cli.py CHANGED
@@ -9,12 +9,14 @@ outcome, and output file.
9
9
  import sys
10
10
  import argparse
11
11
  import uuid
12
+ import json
13
+ import os
12
14
  from typing import List
13
15
 
14
16
  from .models import (
15
17
  ScopeMateTask, Purpose, Scope, Outcome, Meta, get_utc_now
16
18
  )
17
- from .storage import save_plan
19
+ from .storage import save_plan, save_markdown_plan, generate_markdown_from_json
18
20
  from .llm import estimate_scope, generate_title_from_purpose_outcome
19
21
  from .breakdown import suggest_breakdown
20
22
  from .task_analysis import check_and_update_parent_estimates
@@ -101,36 +103,76 @@ def process_task_with_breakdown(task: ScopeMateTask) -> List[ScopeMateTask]:
101
103
 
102
104
 
103
105
  def command_line() -> None:
104
- """Process command line arguments and execute appropriate actions."""
106
+ """
107
+ Process command line arguments and execute appropriate actions.
108
+
109
+ This function is the primary entry point for the scopemate CLI, responsible
110
+ for parsing command-line arguments and routing execution to the appropriate
111
+ workflow based on those arguments. It supports two main modes of operation:
112
+
113
+ 1. Interactive mode (--interactive): Launches the full guided workflow with
114
+ the TaskEngine for an interactive task creation and breakdown experience.
115
+
116
+ 2. Non-interactive mode (--purpose and --outcome): Creates a task directly
117
+ from command-line arguments, generates subtasks using LLM, and saves the
118
+ resulting task hierarchy to a JSON file with an automatic Markdown version.
119
+
120
+ The function validates required arguments depending on the mode, provides
121
+ helpful error messages when arguments are missing, and handles the entire
122
+ lifecycle of task creation, breakdown, and saving in non-interactive mode.
123
+
124
+ Command line arguments:
125
+ --interactive: Flag to launch interactive workflow
126
+ --purpose: Text describing why the task matters (required in non-interactive mode)
127
+ --outcome: Text describing what will be delivered (required in non-interactive mode)
128
+ --output: Path to save the output JSON file (default: scopemate_plan.json)
129
+
130
+ Side Effects:
131
+ - Saves task data to a file on disk (both JSON and Markdown versions)
132
+ - Prints progress and error messages to stdout
133
+ - Exits with non-zero status code on errors
134
+
135
+ Example Usage:
136
+ ```bash
137
+ # Interactive mode
138
+ scopemate --interactive
139
+
140
+ # Non-interactive mode
141
+ scopemate --purpose "Improve website performance" \
142
+ --outcome "Page load time under 2 seconds" \
143
+ --output "perf_project.json"
144
+ ```
145
+
146
+ Note: Markdown files are automatically generated with the same base name
147
+ as the JSON file (e.g., "perf_project.md" for "perf_project.json").
148
+ """
105
149
  parser = argparse.ArgumentParser(
106
- description="🪜 scopemate v.0.1.0 - Break down complex projects with LLMs",
107
- epilog="Purpose: why it matters\n"
108
- "Outcome: what will change once it's done\n"
109
- "Scope: how will be delivered (this is where LLM can help)",
150
+ description="🪜 scopemate - Break down complex projects with LLMs",
110
151
  formatter_class=argparse.RawTextHelpFormatter
111
152
  )
112
153
 
113
154
  parser.add_argument(
114
155
  "--interactive",
115
156
  action="store_true",
116
- help="Launch guided workflow to define task, generate LLM-powered breakdowns, and estimate scope"
157
+ help="💡 Interactive mode"
117
158
  )
118
159
 
119
160
  parser.add_argument(
120
161
  "--outcome",
121
- help="🎯 Outcome: Clearly define what will be delivered and how success will be measured (asks: What will change once this is done?)"
162
+ help="🎯 What will change once this is done?"
122
163
  )
123
164
 
124
165
  parser.add_argument(
125
166
  "--purpose",
126
- help="🧭 Purpose: Clearly define why this project matters strategically (asks: Why does this matter now?)"
167
+ help="🧭 Why does this matter now?"
127
168
  )
128
169
 
129
170
  parser.add_argument(
130
171
  "--output",
131
172
  default="scopemate_plan.json",
132
- help="JSON file to save the task breakdown and scope estimates (default: scopemate_plan.json)"
173
+ help="🗂️ (default: scopemate_plan.json)"
133
174
  )
175
+
134
176
  args = parser.parse_args()
135
177
 
136
178
  # Check if running in interactive mode
@@ -154,8 +196,12 @@ def command_line() -> None:
154
196
  print("Generating subtasks...")
155
197
  all_tasks = process_task_with_breakdown(task)
156
198
 
157
- # Save plan to output file
199
+ # Save plan to output file (this will also create the MD version)
158
200
  save_plan(all_tasks, args.output)
201
+
202
+ # Show message about MD file creation
203
+ md_filename = os.path.splitext(args.output)[0] + ".md"
204
+ print(f"✅ Both JSON and Markdown versions have been saved. You can share {md_filename} with team members.")
159
205
 
160
206
 
161
207
  def main():
scopemate/engine.py CHANGED
@@ -25,10 +25,25 @@ from .interaction import prompt_user, build_root_task, print_summary
25
25
  class TaskEngine:
26
26
  """
27
27
  Main engine for scopemate that coordinates task creation, breakdown, and management.
28
+
29
+ The TaskEngine is the central coordinator of the scopemate application, managing the entire
30
+ lifecycle of tasks from creation to breakdown to final output. It handles loading and saving
31
+ task data, organizing the task hierarchy, and orchestrating the breakdown of complex tasks
32
+ into simpler subtasks.
33
+
34
+ Attributes:
35
+ tasks (List[ScopeMateTask]): List of all tasks in the current session
36
+ task_depths (Dict[str, int]): Mapping of task IDs to their depth in the hierarchy
37
+ max_depth (int): Maximum allowed depth for task nesting (default: 5)
28
38
  """
29
39
 
30
40
  def __init__(self):
31
- """Initialize the TaskEngine."""
41
+ """
42
+ Initialize the TaskEngine with empty task list and depth tracking.
43
+
44
+ Creates a new TaskEngine instance with an empty task list and depth tracking dictionary.
45
+ Sets the default maximum depth for task hierarchies to 5 levels.
46
+ """
32
47
  self.tasks: List[ScopeMateTask] = []
33
48
  self.task_depths: Dict[str, int] = {}
34
49
  self.max_depth: int = 5 # Maximum depth of task hierarchy
@@ -37,8 +52,21 @@ class TaskEngine:
37
52
  """
38
53
  Load tasks from checkpoint file if it exists.
39
54
 
55
+ Checks for the existence of a checkpoint file and prompts the user about whether
56
+ to resume from this checkpoint. If the user confirms, loads the tasks from the
57
+ checkpoint file into the engine.
58
+
40
59
  Returns:
41
- True if checkpoint was loaded, False otherwise
60
+ bool: True if checkpoint was loaded successfully, False otherwise
61
+
62
+ Example:
63
+ ```python
64
+ engine = TaskEngine()
65
+ if engine.load_from_checkpoint():
66
+ print(f"Loaded {len(engine.tasks)} tasks from checkpoint")
67
+ else:
68
+ print("Starting with a new task list")
69
+ ```
42
70
  """
43
71
  if checkpoint_exists():
44
72
  resume = prompt_user(
@@ -58,11 +86,24 @@ class TaskEngine:
58
86
  """
59
87
  Load tasks from a user-specified file.
60
88
 
89
+ Prompts the user about whether to load an existing plan. If confirmed,
90
+ asks for the filename and attempts to load tasks from that file.
91
+
61
92
  Args:
62
- default_filename: Default filename to suggest
93
+ default_filename (str): Default filename to suggest to the user
63
94
 
64
95
  Returns:
65
- True if file was loaded, False otherwise
96
+ bool: True if file was loaded successfully, False otherwise
97
+
98
+ Raises:
99
+ FileNotFoundError: Handled internally, prints error message if file not found
100
+
101
+ Example:
102
+ ```python
103
+ engine = TaskEngine()
104
+ if engine.load_from_file("my_project_plan.json"):
105
+ print(f"Loaded {len(engine.tasks)} tasks from file")
106
+ ```
66
107
  """
67
108
  choice = prompt_user("Load existing plan?", default="n", choices=["y","n"])
68
109
  if choice.lower() == "y":
@@ -76,12 +117,58 @@ class TaskEngine:
76
117
  return False
77
118
 
78
119
  def create_new_task(self) -> None:
79
- """Create a new root task interactively."""
120
+ """
121
+ Create a new root task interactively.
122
+
123
+ Initiates an interactive dialog to build a new root task, adds it to the task list,
124
+ and automatically saves a checkpoint. The dialog collects all necessary information
125
+ for a well-defined task including title, purpose, scope, and expected outcomes.
126
+
127
+ Side Effects:
128
+ - Appends new task to self.tasks
129
+ - Saves checkpoint to disk
130
+
131
+ Example:
132
+ ```python
133
+ engine = TaskEngine()
134
+ engine.create_new_task() # Interactively creates a new root task
135
+ ```
136
+ """
80
137
  self.tasks.append(build_root_task())
81
138
  save_checkpoint(self.tasks)
82
139
 
83
140
  def breakdown_complex_tasks(self) -> None:
84
- """Process all tasks and break down complex ones."""
141
+ """
142
+ Process all tasks and break down complex ones.
143
+
144
+ This is a core function that analyzes all tasks to identify those that are too complex
145
+ or have long durations, then interactively breaks them down into smaller subtasks.
146
+ It maintains the task hierarchy, updates parent-child relationships, and ensures
147
+ estimate consistency across the task tree.
148
+
149
+ The function uses a breadth-first approach to process tasks, breaking down parent tasks
150
+ before their children, and saving checkpoints after each breakdown.
151
+
152
+ Algorithm:
153
+ 1. Initialize task depths to track hierarchy
154
+ 2. Process each task and check if it needs breakdown
155
+ 3. For tasks needing breakdown, use LLM to suggest subtasks
156
+ 4. Add approved subtasks to the task list
157
+ 5. Update parent estimates based on subtask characteristics
158
+ 6. Save checkpoint after each task breakdown
159
+
160
+ Side Effects:
161
+ - Modifies self.tasks by adding subtasks
162
+ - Updates self.task_depths with new depth information
163
+ - Saves checkpoints to disk after each breakdown
164
+
165
+ Example:
166
+ ```python
167
+ engine = TaskEngine()
168
+ engine.load_from_file("my_project.json")
169
+ engine.breakdown_complex_tasks() # Interactively breaks down complex tasks
170
+ ```
171
+ """
85
172
  # Initialize depth tracking
86
173
  self.task_depths = _initialize_task_depths(self.tasks)
87
174
 
@@ -127,7 +214,28 @@ class TaskEngine:
127
214
  save_checkpoint(self.tasks)
128
215
 
129
216
  def handle_long_duration_tasks(self) -> None:
130
- """Find and handle long duration leaf tasks."""
217
+ """
218
+ Find and handle long duration leaf tasks.
219
+
220
+ Identifies leaf tasks (tasks with no children) that have long duration estimates,
221
+ presents them to the user, and offers the opportunity to break them down further.
222
+ This helps ensure that all tasks in the final plan are of manageable size.
223
+
224
+ The function uses task_analysis.find_long_duration_leaf_tasks to identify candidates
225
+ for further breakdown, then interactively processes user-selected tasks.
226
+
227
+ Side Effects:
228
+ - May modify self.tasks by adding subtasks for long-duration leaf tasks
229
+ - Updates parent estimates via check_and_update_parent_estimates
230
+ - Saves checkpoints to disk after each breakdown
231
+
232
+ Example:
233
+ ```python
234
+ engine = TaskEngine()
235
+ engine.load_from_file("my_project.json")
236
+ engine.handle_long_duration_tasks() # Interactively processes long-duration tasks
237
+ ```
238
+ """
131
239
  # Find long duration leaf tasks
132
240
  long_duration_leaf_tasks = find_long_duration_leaf_tasks(self.tasks)
133
241
 
@@ -172,7 +280,27 @@ class TaskEngine:
172
280
  print("Invalid selection, skipping breakdown.")
173
281
 
174
282
  def finalize_plan(self) -> None:
175
- """Review and save the final plan."""
283
+ """
284
+ Review and save the final plan.
285
+
286
+ Performs a final consistency check on all task estimates, displays a summary
287
+ of the entire task hierarchy to the user, and prompts for saving the finalized
288
+ plan to a permanent file. If saved, removes the temporary checkpoint file.
289
+
290
+ Side Effects:
291
+ - Ensures consistency in parent-child estimate relationships
292
+ - Saves final plan to user-specified file
293
+ - May delete checkpoint file if plan is saved
294
+
295
+ Example:
296
+ ```python
297
+ engine = TaskEngine()
298
+ engine.load_from_file("my_project.json")
299
+ engine.breakdown_complex_tasks()
300
+ engine.handle_long_duration_tasks()
301
+ engine.finalize_plan() # Review and save final plan
302
+ ```
303
+ """
176
304
  # Final check of parent-child estimate consistency
177
305
  self.tasks = check_and_update_parent_estimates(self.tasks)
178
306
 
@@ -187,7 +315,30 @@ class TaskEngine:
187
315
  print(f"Plan left in checkpoint '{CHECKPOINT_FILE}'. Run again to resume.")
188
316
 
189
317
  def run(self) -> None:
190
- """Run the full interactive workflow."""
318
+ """
319
+ Run the full interactive workflow.
320
+
321
+ Executes the complete scopemate workflow from start to finish:
322
+ 1. Displays introduction to the user
323
+ 2. Loads existing checkpoint or creates new task
324
+ 3. Processes and breaks down complex tasks
325
+ 4. Handles long-duration leaf tasks
326
+ 5. Finalizes and saves the plan
327
+
328
+ This is the main entry point for using the TaskEngine to build a complete
329
+ task breakdown plan interactively.
330
+
331
+ Side Effects:
332
+ - Interacts with the user via console
333
+ - Modifies task list based on user input
334
+ - Creates/updates files on disk for checkpoints and final plan
335
+
336
+ Example:
337
+ ```python
338
+ engine = TaskEngine()
339
+ engine.run() # Runs the complete interactive workflow
340
+ ```
341
+ """
191
342
  # Display introduction
192
343
  print("=== scopemate Action Plan Builder ===")
193
344
  print("This tool helps break down complex tasks and maintain consistent time estimates.")
@@ -210,13 +361,40 @@ class TaskEngine:
210
361
  self.finalize_plan()
211
362
 
212
363
  def run_interactive(self) -> None:
213
- """Run the interactive mode of the application."""
364
+ """
365
+ Run the interactive mode of the application.
366
+
367
+ This is a placeholder method for running an alternative interactive mode.
368
+ Currently just prints the header and would be extended in future versions.
369
+
370
+ Example:
371
+ ```python
372
+ engine = TaskEngine()
373
+ engine.run_interactive() # Would run an alternative interactive mode
374
+ ```
375
+ """
214
376
  print("=== scopemate Action Plan Builder ===")
215
377
 
216
378
 
217
379
  def interactive_builder():
218
380
  """
219
381
  Legacy function for backward compatibility that runs the TaskEngine.
382
+
383
+ Creates a TaskEngine instance and runs the full workflow, handling
384
+ KeyboardInterrupt exceptions by saving progress to a checkpoint.
385
+
386
+ This function provides backward compatibility with older versions
387
+ that used this entry point directly.
388
+
389
+ Side Effects:
390
+ - Creates and runs a TaskEngine instance
391
+ - Handles KeyboardInterrupt by saving checkpoint
392
+
393
+ Example:
394
+ ```python
395
+ from scopemate.engine import interactive_builder
396
+ interactive_builder() # Runs the complete workflow
397
+ ```
220
398
  """
221
399
  engine = TaskEngine()
222
400
  try:
scopemate/interaction.py CHANGED
@@ -25,13 +25,43 @@ def prompt_user(
25
25
  """
26
26
  Prompt user for input with optional default and choices validation.
27
27
 
28
+ This function is the primary method for all user interaction in scopemate. It
29
+ handles displaying prompts, validating input against constraints, and providing
30
+ default values when the user presses Enter without typing anything.
31
+
32
+ The function implements a validation loop that ensures users can only provide
33
+ valid input. If choices are specified, the function performs case-insensitive
34
+ validation against those choices and re-prompts when input is invalid.
35
+
36
+ Features:
37
+ - Displays a prompt with an optional default value in square brackets
38
+ - Supports empty input with a default fallback value
39
+ - Validates input against a predefined set of case-insensitive choices
40
+ - Provides clear error messages when input doesn't match required choices
41
+ - Loops until valid input is received
42
+
28
43
  Args:
29
- prompt: The prompt text to display
30
- default: Optional default value if user enters nothing
31
- choices: Optional list of valid choices
44
+ prompt (str): The prompt text to display to the user
45
+ default (Optional[str]): Default value to use if user submits empty input
46
+ choices (Optional[List[str]]): List of valid input choices for validation
32
47
 
33
48
  Returns:
34
- User's validated input as a string
49
+ str: The validated user input
50
+
51
+ Example:
52
+ ```python
53
+ # Simple prompt with no constraints
54
+ name = prompt_user("Enter your name")
55
+
56
+ # Prompt with a default value
57
+ team = prompt_user("Select team", default="Backend")
58
+
59
+ # Prompt with choices validation
60
+ response = prompt_user("Continue?", default="y", choices=["y", "n"])
61
+ if response.lower() == "y":
62
+ # Proceed with action
63
+ pass
64
+ ```
35
65
  """
36
66
  while True:
37
67
  suffix = f" [{default}]" if default is not None else ""
scopemate/llm.py CHANGED
@@ -26,12 +26,45 @@ def call_llm(prompt: str, model: str = DEFAULT_MODEL) -> dict:
26
26
  """
27
27
  Invoke LLM to get a structured JSON response.
28
28
 
29
+ This function is the core LLM integration point for scopemate, handling all
30
+ communication with the OpenAI API. It's designed to always return structured
31
+ JSON data that can be easily processed by the application.
32
+
33
+ The function:
34
+ 1. Creates an OpenAI client using the default API credentials
35
+ 2. Configures a system prompt that instructs the model to return valid JSON
36
+ 3. Sends the user's prompt with the task-specific instructions
37
+ 4. Sets response_format to force JSON output
38
+ 5. Parses and returns the JSON response
39
+
40
+ Error handling is built in to gracefully handle JSON parsing failures by
41
+ printing diagnostic information and returning an empty dictionary rather
42
+ than crashing.
43
+
29
44
  Args:
30
- prompt: The prompt to send to the LLM
31
- model: The model to use (defaults to DEFAULT_MODEL)
45
+ prompt (str): The prompt to send to the LLM, containing full instructions
46
+ and any task data needed for context
47
+ model (str): The OpenAI model identifier to use (defaults to DEFAULT_MODEL)
32
48
 
33
49
  Returns:
34
- A dictionary containing the parsed JSON response
50
+ dict: A dictionary containing the parsed JSON response from the LLM.
51
+ Returns an empty dict {} if parsing fails.
52
+
53
+ Example:
54
+ ```python
55
+ # Create a prompt asking for task breakdown
56
+ prompt = f"Break down this task into subtasks: {task.title}"
57
+
58
+ # Call the LLM and get structured data back
59
+ response = call_llm(prompt)
60
+
61
+ # Process the structured response
62
+ if "subtasks" in response:
63
+ for subtask_data in response["subtasks"]:
64
+ # Create a new subtask from the data
65
+ subtask = ScopeMateTask(**subtask_data)
66
+ tasks.append(subtask)
67
+ ```
35
68
  """
36
69
  client = OpenAI()
37
70
  response = client.chat.completions.create(
scopemate/models.py CHANGED
@@ -144,7 +144,69 @@ class Meta(BaseModel):
144
144
 
145
145
 
146
146
  class ScopeMateTask(BaseModel):
147
- """A Purpose/Context/Outcome task representing a unit of work."""
147
+ """
148
+ A Purpose/Context/Outcome task representing a unit of work.
149
+
150
+ ScopeMateTask is the core data model in scopemate, representing a single unit of work
151
+ with well-defined purpose, scope, and outcome. The model follows a comprehensive and
152
+ structured approach to task definition that ensures clarity in task planning and execution.
153
+
154
+ Each task has:
155
+ 1. Purpose - the "why" behind the task (detailed_description, alignment, urgency)
156
+ 2. Scope - the "how big" and "what's involved" (size, time_estimate, dependencies, risks)
157
+ 3. Outcome - the "what will be delivered" (type, definition, acceptance criteria, metrics)
158
+ 4. Meta - tracking information (status, priority, dates, confidence, team)
159
+
160
+ Tasks can form a hierarchical structure through the parent_id field, allowing complex
161
+ work to be broken down into manageable subtasks. The hierarchy supports:
162
+ - Parent tasks: higher-level tasks that can be decomposed
163
+ - Child tasks: more specific tasks that contribute to a parent
164
+ - Root tasks: top-level tasks with no parent
165
+ - Leaf tasks: tasks with no children
166
+
167
+ The model enforces validation rules through Pydantic, ensuring data integrity
168
+ across all fields (e.g., valid size values, time estimates, status, etc.).
169
+
170
+ Attributes:
171
+ id (str): Unique identifier for the task
172
+ title (str): Short descriptive title
173
+ purpose (Purpose): Why the task matters
174
+ scope (Scope): Size, time, dependencies and risks
175
+ outcome (Outcome): Delivered value and validation methods
176
+ meta (Meta): Status, timing, and tracking information
177
+ parent_id (Optional[str]): ID of parent task if this is a subtask
178
+
179
+ Example:
180
+ ```python
181
+ task = ScopeMateTask(
182
+ id="TASK-abc123",
183
+ title="Implement user authentication",
184
+ purpose=Purpose(
185
+ detailed_description="We need secure authentication for users",
186
+ alignment=["Security", "User experience"],
187
+ urgency="strategic"
188
+ ),
189
+ scope=Scope(
190
+ size="complex",
191
+ time_estimate="sprint",
192
+ dependencies=["API design", "Database setup"],
193
+ risks=["Security vulnerabilities", "Performance issues"]
194
+ ),
195
+ outcome=Outcome(
196
+ type="customer-facing",
197
+ detailed_outcome_definition="Complete authentication system with login/logout",
198
+ acceptance_criteria=["User can log in", "User can log out", "Password reset works"]
199
+ ),
200
+ meta=Meta(
201
+ status="backlog",
202
+ priority=1,
203
+ created=get_utc_now(),
204
+ updated=get_utc_now(),
205
+ team="Backend"
206
+ )
207
+ )
208
+ ```
209
+ """
148
210
  id: str
149
211
  title: str = Field(..., description="Short descriptive title")
150
212
  purpose: Purpose
scopemate/storage.py CHANGED
@@ -6,6 +6,7 @@ This module manages persistence of task data to disk and loading from files.
6
6
  """
7
7
  import os
8
8
  import json
9
+ from datetime import datetime
9
10
  from typing import List, Dict, Any
10
11
 
11
12
  from pydantic import ValidationError
@@ -37,28 +38,241 @@ def save_plan(tasks: List[ScopeMateTask], filename: str) -> None:
37
38
  """
38
39
  Save tasks to a plan file.
39
40
 
41
+ This function serializes a list of ScopeMateTask objects to JSON and writes them
42
+ to a file. The file format uses a consistent structure with a top-level "tasks"
43
+ array containing serialized task objects. This ensures compatibility with other
44
+ tooling and future versions of scopemate.
45
+
46
+ The function handles all serialization details including proper encoding and
47
+ indentation for readability. Each task is completely serialized with all its
48
+ nested structures (purpose, scope, outcome, meta) for complete persistence.
49
+
40
50
  Args:
41
- tasks: List of ScopeMateTask objects to save
51
+ tasks: List of ScopeMateTask objects to save to disk
42
52
  filename: Path to save the plan file
53
+
54
+ Side Effects:
55
+ - Writes to file system at the specified path
56
+ - Prints confirmation message upon successful save
57
+
58
+ Example:
59
+ ```python
60
+ tasks = [task1, task2, task3] # List of ScopeMateTask objects
61
+ save_plan(tasks, "project_alpha_plan.json")
62
+ # Saves all tasks to project_alpha_plan.json with proper formatting
63
+ ```
43
64
  """
44
65
  payload = {"tasks": [t.model_dump() for t in tasks]}
45
66
  with open(filename, "w", encoding="utf-8") as f:
46
67
  json.dump(payload, f, indent=2)
47
68
  print(f"✅ Plan saved to {filename}.")
69
+
70
+ # Automatically generate markdown version with the same basename
71
+ md_filename = os.path.splitext(filename)[0] + ".md"
72
+ save_markdown_plan(payload, md_filename)
73
+
74
+
75
+ def save_markdown_plan(data: Dict[str, Any], filename: str) -> None:
76
+ """
77
+ Save tasks to a Markdown file for human readability.
78
+
79
+ This function converts the JSON task data into a well-structured Markdown format
80
+ for easier reading and sharing with team members who may not use scopemate directly.
81
+
82
+ Args:
83
+ data: Dictionary containing the tasks data (with "tasks" key)
84
+ filename: Path to save the Markdown file
85
+ """
86
+ markdown = generate_markdown_from_json(data)
87
+ with open(filename, "w", encoding="utf-8") as f:
88
+ f.write(markdown)
89
+ print(f"✅ Markdown version saved to {filename}.")
90
+
91
+
92
+ def generate_markdown_from_json(data: Dict[str, Any]) -> str:
93
+ """
94
+ Convert scopemate JSON data to a well-structured Markdown format.
95
+
96
+ Args:
97
+ data: The scopemate JSON data as a dictionary
98
+
99
+ Returns:
100
+ A string containing the Markdown representation
101
+ """
102
+ # Start building markdown content
103
+ md = ["# Project Scope Plan\n"]
104
+ md.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
105
+
106
+ # Add summary section
107
+ tasks = data.get("tasks", [])
108
+ md.append(f"## Summary\n\n")
109
+ md.append(f"This document contains **{len(tasks)}** tasks.\n\n")
110
+
111
+ # Get counts by size complexity
112
+ size_counts = {}
113
+ for task in tasks:
114
+ if "scope" in task and "size" in task["scope"]:
115
+ size = task["scope"]["size"]
116
+ size_counts[size] = size_counts.get(size, 0) + 1
117
+
118
+ if size_counts:
119
+ md.append("**Complexity Breakdown:**\n\n")
120
+ for size, count in size_counts.items():
121
+ md.append(f"- {size.capitalize()}: {count} task(s)\n")
122
+ md.append("\n")
123
+
124
+ # Create hierarchical task structure
125
+ main_tasks = [t for t in tasks if not t.get("parent_id")]
126
+ child_tasks = {}
127
+ for task in tasks:
128
+ if task.get("parent_id"):
129
+ if task["parent_id"] not in child_tasks:
130
+ child_tasks[task["parent_id"]] = []
131
+ child_tasks[task["parent_id"]].append(task)
132
+
133
+ # Add detailed task section
134
+ md.append("## Task Details\n\n")
135
+
136
+ # Process main tasks with their children
137
+ for task in main_tasks:
138
+ md.extend(format_task_as_markdown(task, child_tasks, 0))
139
+
140
+ return "\n".join(md)
141
+
142
+
143
+ def format_task_as_markdown(task: Dict[str, Any], child_tasks: Dict[str, List[Dict[str, Any]]], level: int) -> List[str]:
144
+ """
145
+ Format a single task and its children as Markdown.
146
+
147
+ Args:
148
+ task: The task data
149
+ child_tasks: Dictionary mapping parent_id to list of child tasks
150
+ level: Current indentation level
151
+
152
+ Returns:
153
+ List of markdown formatted lines
154
+ """
155
+ md_lines = []
156
+
157
+ # Add task title with appropriate heading level
158
+ heading_level = "###" + "#" * level
159
+ task_id = task.get("id", "NO-ID")
160
+ title = task.get("title", "Untitled Task")
161
+ md_lines.append(f"{heading_level} {task_id}: {title}\n")
162
+
163
+ # Add purpose section
164
+ if "purpose" in task:
165
+ purpose = task["purpose"]
166
+ md_lines.append("**Purpose:**\n\n")
167
+ if "detailed_description" in purpose:
168
+ md_lines.append(f"{purpose['detailed_description']}\n\n")
169
+ if "alignment" in purpose and purpose["alignment"]:
170
+ md_lines.append("*Strategic Alignment:* ")
171
+ md_lines.append(", ".join(purpose["alignment"]))
172
+ md_lines.append("\n\n")
173
+ if "urgency" in purpose:
174
+ md_lines.append(f"*Urgency:* {purpose['urgency'].capitalize()}\n\n")
175
+
176
+ # Add scope section
177
+ if "scope" in task:
178
+ scope = task["scope"]
179
+ md_lines.append("**Scope:**\n\n")
180
+ if "size" in scope:
181
+ md_lines.append(f"*Size:* {scope['size'].capitalize()}\n\n")
182
+ if "time_estimate" in scope:
183
+ md_lines.append(f"*Time Estimate:* {scope['time_estimate'].capitalize()}\n\n")
184
+ if "dependencies" in scope and scope["dependencies"]:
185
+ md_lines.append("*Dependencies:*\n\n")
186
+ for dep in scope["dependencies"]:
187
+ md_lines.append(f"- {dep}\n")
188
+ md_lines.append("\n")
189
+ if "risks" in scope and scope["risks"]:
190
+ md_lines.append("*Risks:*\n\n")
191
+ for risk in scope["risks"]:
192
+ md_lines.append(f"- {risk}\n")
193
+ md_lines.append("\n")
194
+
195
+ # Add outcome section
196
+ if "outcome" in task:
197
+ outcome = task["outcome"]
198
+ md_lines.append("**Outcome:**\n\n")
199
+ if "type" in outcome:
200
+ md_lines.append(f"*Type:* {outcome['type'].capitalize().replace('-', ' ')}\n\n")
201
+ if "detailed_outcome_definition" in outcome:
202
+ md_lines.append(f"{outcome['detailed_outcome_definition']}\n\n")
203
+ if "acceptance_criteria" in outcome and outcome["acceptance_criteria"]:
204
+ md_lines.append("*Acceptance Criteria:*\n\n")
205
+ for ac in outcome["acceptance_criteria"]:
206
+ md_lines.append(f"- {ac}\n")
207
+ md_lines.append("\n")
208
+ if "metric" in outcome and outcome["metric"]:
209
+ md_lines.append(f"*Success Metric:* {outcome['metric']}\n\n")
210
+ if "validation_method" in outcome and outcome["validation_method"]:
211
+ md_lines.append(f"*Validation Method:* {outcome['validation_method']}\n\n")
212
+
213
+ # Add meta section
214
+ if "meta" in task:
215
+ meta = task["meta"]
216
+ md_lines.append("**Meta:**\n\n")
217
+ if "status" in meta:
218
+ md_lines.append(f"*Status:* {meta['status'].capitalize()}\n")
219
+ if "priority" in meta and meta["priority"] is not None:
220
+ md_lines.append(f"*Priority:* {meta['priority']}\n")
221
+ if "confidence" in meta:
222
+ md_lines.append(f"*Confidence:* {meta['confidence'].capitalize()}\n")
223
+ if "team" in meta and meta["team"]:
224
+ md_lines.append(f"*Team:* {meta['team']}\n")
225
+ md_lines.append("\n")
226
+
227
+ # Add separator line if not the last task
228
+ md_lines.append("---\n\n")
229
+
230
+ # Process children recursively
231
+ if task.get("id") in child_tasks:
232
+ for child in child_tasks[task["id"]]:
233
+ md_lines.extend(format_task_as_markdown(child, child_tasks, level + 1))
234
+
235
+ return md_lines
48
236
 
49
237
 
50
238
  def load_plan(filename: str) -> List[ScopeMateTask]:
51
239
  """
52
240
  Load tasks from a plan file.
53
241
 
242
+ This function reads a JSON file containing serialized tasks and deserializes them
243
+ into ScopeMateTask objects. It handles various backward compatibility issues and
244
+ performs validation on the loaded data to ensure integrity.
245
+
246
+ The function is robust against various common issues:
247
+ - It properly handles missing parent_id fields for backward compatibility
248
+ - It removes legacy fields that may exist in older files
249
+ - It skips invalid tasks with validation errors rather than failing entirely
250
+ - It provides clear warnings about skipped tasks
251
+
54
252
  Args:
55
- filename: Path to the plan file
253
+ filename: Path to the plan file to load
56
254
 
57
255
  Returns:
58
- List of ScopeMateTask objects
256
+ List of validated ScopeMateTask objects from the file
59
257
 
60
258
  Raises:
61
- FileNotFoundError: If the file doesn't exist
259
+ FileNotFoundError: If the specified file doesn't exist
260
+
261
+ Example:
262
+ ```python
263
+ try:
264
+ tasks = load_plan("project_alpha_plan.json")
265
+ print(f"Loaded {len(tasks)} tasks successfully")
266
+
267
+ # Process loaded tasks
268
+ for task in tasks:
269
+ if task.meta.status == "backlog":
270
+ # Do something with backlog tasks...
271
+ pass
272
+ except FileNotFoundError:
273
+ print("Plan file not found, starting with empty task list")
274
+ tasks = []
275
+ ```
62
276
  """
63
277
  if not os.path.exists(filename):
64
278
  raise FileNotFoundError(f"File not found: {filename}")
@@ -15,11 +15,30 @@ def check_and_update_parent_estimates(tasks: List[ScopeMateTask]) -> List[ScopeM
15
15
  """
16
16
  Check and update parent task estimates based on child task complexity.
17
17
 
18
+ This function ensures consistency in the task hierarchy by making sure that
19
+ parent tasks have appropriate size and time estimates relative to their children.
20
+ If a child task has a higher complexity or longer time estimate than its parent,
21
+ the parent's estimates are automatically increased to maintain logical consistency.
22
+
23
+ The function works by:
24
+ 1. Creating maps of task IDs to task objects and parent IDs
25
+ 2. Computing complexity values for all tasks based on their size and time estimates
26
+ 3. Identifying inconsistencies where child tasks have higher complexity than parents
27
+ 4. Updating parent estimates to match or exceed their children's complexity
28
+ 5. Recursively propagating updates up the task hierarchy to maintain consistency
29
+
18
30
  Args:
19
- tasks: List of ScopeMateTask objects
31
+ tasks: List of ScopeMateTask objects to analyze and update
20
32
 
21
33
  Returns:
22
- Updated list of ScopeMateTask objects
34
+ Updated list of ScopeMateTask objects with consistent parent-child estimates
35
+
36
+ Example:
37
+ ```python
38
+ # Before: parent task has "straightforward" size but child has "complex" size
39
+ updated_tasks = check_and_update_parent_estimates(tasks)
40
+ # After: parent task is updated to "complex" or higher to maintain consistency
41
+ ```
23
42
  """
24
43
  # Create a map of tasks by ID for easy access
25
44
  task_map = {t.id: t for t in tasks}
@@ -229,14 +248,36 @@ def should_decompose_task(task: ScopeMateTask, depth: int, max_depth: int, is_le
229
248
  """
230
249
  Determine if a task should be broken down based on complexity and time estimates.
231
250
 
251
+ This function applies a set of heuristics to decide whether a task needs further
252
+ decomposition. The decision is based on multiple factors:
253
+
254
+ 1. Task depth in the hierarchy - tasks at or beyond max_depth are never decomposed
255
+ 2. Task complexity - "complex", "uncertain", or "pioneering" tasks should be broken down
256
+ 3. Time estimate - tasks with long durations should be broken down into smaller units
257
+ 4. Leaf status - whether the task already has subtasks
258
+
259
+ The breakdown logic implements a graduated approach where:
260
+ - Very complex tasks are always broken down (unless at max depth)
261
+ - Long duration tasks are broken down, especially if they're leaf tasks
262
+ - Tasks with "week" duration are broken down up to max_depth
263
+ - Tasks at depth 2+ with "sprint" duration aren't broken down (unless they're "multi-sprint")
264
+
232
265
  Args:
233
266
  task: The ScopeMateTask to evaluate
234
- depth: Current depth in the task hierarchy
235
- max_depth: Maximum allowed depth
236
- is_leaf: Whether this is a leaf task (has no children)
267
+ depth: Current depth in the task hierarchy (0 for root tasks)
268
+ max_depth: Maximum allowed depth for the task hierarchy
269
+ is_leaf: Whether this task currently has no children
237
270
 
238
271
  Returns:
239
- True if the task should be broken down, False otherwise
272
+ True if the task should be broken down into subtasks, False otherwise
273
+
274
+ Example:
275
+ ```python
276
+ task = get_task_by_id("TASK-123")
277
+ depth = get_task_depth(task, task_depths, tasks)
278
+ if should_decompose_task(task, depth, max_depth=5, is_leaf=True):
279
+ subtasks = suggest_breakdown(task)
280
+ ```
240
281
  """
241
282
  # Always respect max depth limit
242
283
  if depth >= max_depth:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scopemate
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: 🪜 A CLI tool for Purpose/Scope/Outcome planning
5
5
  Author: Anoop Thomas Mathew
6
6
  Author-email: Anoop Thomas Mathew <atmb4u@gmail.com>
@@ -78,6 +78,7 @@ Scopemate is built around a three-part framework for strategic decision making:
78
78
  - Pydantic validation
79
79
  - Cross-platform support (Windows, macOS, Linux)
80
80
  - Works with Python 3.10 and above
81
+ - Automatic Markdown export of plans for easy sharing
81
82
 
82
83
  ## Requirements
83
84
 
@@ -171,11 +172,10 @@ scopemate --help
171
172
 
172
173
  # Generate a project plan with purpose and outcome
173
174
  scopemate --purpose="Build a REST API for user management" --outcome="A documented API with authentication and user CRUD operations" --output="project_plan.json"
174
-
175
- # Fix inconsistent estimates in an existing plan
176
- scopemate --fix-estimates --input="project_plan.json" --output="fixed_plan.json"
177
175
  ```
178
176
 
177
+ **Note:** A Markdown version of the output is automatically generated alongside the JSON file. For example, if you specify `--output="project_plan.json"`, a file named `project_plan.md` will also be created.
178
+
179
179
  ### Interactive Mode Workflow
180
180
 
181
181
  The interactive mode (`scopemate --interactive`) will guide you through:
@@ -205,7 +205,9 @@ The interactive mode (`scopemate --interactive`) will guide you through:
205
205
 
206
206
  ### Output Format
207
207
 
208
- scopemate generates a structured JSON output with the following format:
208
+ scopemate generates both JSON and Markdown output files:
209
+
210
+ 1. **JSON Output** - Structured data format with the following structure:
209
211
 
210
212
  ```json
211
213
  {
@@ -245,6 +247,14 @@ scopemate generates a structured JSON output with the following format:
245
247
  }
246
248
  ```
247
249
 
250
+ 2. **Markdown Output** - Human-readable format automatically generated with the same basename as the JSON file. The Markdown output includes:
251
+ - A summary of the plan with task counts and complexity breakdown
252
+ - Hierarchical task structure preserving parent-child relationships
253
+ - All relevant task details formatted for easy reading
254
+ - Properly formatted sections for purpose, scope, outcome, and metadata
255
+
256
+ This dual output approach makes it easy to both process the data programmatically (using the JSON) and share the plan with team members (using the Markdown).
257
+
248
258
  ### Integrating with Other Tools
249
259
 
250
260
  You can use scopemate's JSON output with other project management tools:
@@ -295,6 +305,23 @@ uv pip install -r requirements-dev.txt
295
305
  uv pip install -e .
296
306
  ```
297
307
 
308
+ #### Using pipx
309
+
310
+ [pipx](https://github.com/pypa/pipx) is useful for installing and running Python applications in isolated environments:
311
+
312
+ ```bash
313
+ # Install pipx if you don't have it
314
+ pip install pipx
315
+
316
+ # Clone the repository
317
+ git clone https://github.com/atmb4u/scopemate.git
318
+ cd scopemate
319
+
320
+ # Install the package in development mode with force flag
321
+ # This is useful when making changes and wanting to test the CLI immediately
322
+ pipx install --force .
323
+ ```
324
+
298
325
  ### Running Tests
299
326
 
300
327
  ```bash
@@ -0,0 +1,17 @@
1
+ scopemate/__init__.py,sha256=sMbeuIqGXqrO47d1LD4gk6r5cLHuZmRtVXR797c8K2s,472
2
+ scopemate/__main__.py,sha256=nPNZe_QEoOHQ_hXf17w72BHz1UFPKuW2g3whTLwuM8E,195
3
+ scopemate/breakdown.py,sha256=mwIDzf7m2GHVkDrRmMyeS8v2pNd99U3vlcPCtOajvs0,21048
4
+ scopemate/cli.py,sha256=qh6iFleQc8Bld0iptyUgRm5Ga3GtZsvE6Y-q6vbm3dk,6891
5
+ scopemate/core.py,sha256=wpXCpb5Kdpqul9edNCx2Da94137XCc1w-3KQc9-Tf3s,700
6
+ scopemate/engine.py,sha256=8yQoxSECJCGuNSIwS-qFoaOGM1iaZ-y4Lo7k5Q6v-mk,16992
7
+ scopemate/interaction.py,sha256=SeqQVME0MATK-m-M7T9nNHkcJ_VCRhlqydL_vQaSMWk,10893
8
+ scopemate/llm.py,sha256=k_1FQdg_TRt_51Yu_g_PjO0pBnmFmWHoK_-KEPGlASM,15526
9
+ scopemate/models.py,sha256=Q3SUoHu_4RejDAocEr83I00wGvxhDoJ1COVqjPsr4DQ,7738
10
+ scopemate/storage.py,sha256=uV1-7IdwJwBENeNoO9Y3WwUUXd-jA2NvKdiERGGhmV8,11642
11
+ scopemate/task_analysis.py,sha256=Mic0FOOy_BWI1_5TQmh__39miOcZBZ7mTUcCkv1DvkI,14967
12
+ scopemate-0.1.2.dist-info/licenses/LICENSE,sha256=4fqQFK5AkkXmg6FBG9Wr06gCR7BMQl02TvsPYt-YL6s,1076
13
+ scopemate-0.1.2.dist-info/METADATA,sha256=APn2Igy4cl3jhcP9gh6gwOHnBLMnN3CPsL4MrvDVabc,11774
14
+ scopemate-0.1.2.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
15
+ scopemate-0.1.2.dist-info/entry_points.txt,sha256=XXusGEDxI6NlrYmSBcPDtjV3QvsHWVWPSrt4zD4UcLg,49
16
+ scopemate-0.1.2.dist-info/top_level.txt,sha256=riMrI_jMCfZMb7-ecWBwqOBLdUsnPOxSu2Pgvqx7Too,10
17
+ scopemate-0.1.2.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- scopemate/__init__.py,sha256=3cyRjjI2wC2pG8VeXONVOrow_dJMKUCzre8EtYfrV0s,472
2
- scopemate/__main__.py,sha256=nPNZe_QEoOHQ_hXf17w72BHz1UFPKuW2g3whTLwuM8E,195
3
- scopemate/breakdown.py,sha256=lasv2Gt1yziE_GV4DgCBWVg3TIPuggT3Tf77ny5n7Iw,19650
4
- scopemate/cli.py,sha256=o05Od9NEP76sQRegc3dqs-d-DCpYip2KC1GXjriiCZQ,5059
5
- scopemate/core.py,sha256=wpXCpb5Kdpqul9edNCx2Da94137XCc1w-3KQc9-Tf3s,700
6
- scopemate/engine.py,sha256=wlE39lzKoJthi6twestlyEqvEjqNEXQYvNTOfT4aGZw,9521
7
- scopemate/interaction.py,sha256=qWFU3QM_KPwaGdh4Rw7ewCIeYiT6Wa_H9e6bnmNoJzw,9531
8
- scopemate/llm.py,sha256=hD37Mk54kchdECKYmCNF3yxg0U-vW-h4y8tpNghlS3Q,14031
9
- scopemate/models.py,sha256=ZvFn8iegMDgCgoLjvWxj7_C7XLDWrgkF8ySuktfRaqw,4962
10
- scopemate/storage.py,sha256=lloD_2f2E3q_inHLiL9Kp8F_tyeerG45_rSLKXvGh4Y,3102
11
- scopemate/task_analysis.py,sha256=I-tH62MfYAwlHbLonjlPKBGa-X_II9QqpWS_OsjLaxU,12644
12
- scopemate-0.1.0.dist-info/licenses/LICENSE,sha256=4fqQFK5AkkXmg6FBG9Wr06gCR7BMQl02TvsPYt-YL6s,1076
13
- scopemate-0.1.0.dist-info/METADATA,sha256=Wmb1wOEmCkigLUXIS0d1BujGzV0uTpx9IpNIvyrtIJI,10557
14
- scopemate-0.1.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
15
- scopemate-0.1.0.dist-info/entry_points.txt,sha256=XXusGEDxI6NlrYmSBcPDtjV3QvsHWVWPSrt4zD4UcLg,49
16
- scopemate-0.1.0.dist-info/top_level.txt,sha256=riMrI_jMCfZMb7-ecWBwqOBLdUsnPOxSu2Pgvqx7Too,10
17
- scopemate-0.1.0.dist-info/RECORD,,