claude-task-master 0.1.4__py3-none-any.whl → 0.1.6__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 (34) hide show
  1. claude_task_master/__init__.py +1 -1
  2. claude_task_master/api/models.py +309 -0
  3. claude_task_master/api/routes.py +229 -0
  4. claude_task_master/api/routes_repo.py +317 -0
  5. claude_task_master/bin/claudetm +1 -1
  6. claude_task_master/cli.py +3 -1
  7. claude_task_master/cli_commands/mailbox.py +295 -0
  8. claude_task_master/cli_commands/workflow.py +37 -0
  9. claude_task_master/core/__init__.py +5 -0
  10. claude_task_master/core/agent_phases.py +1 -1
  11. claude_task_master/core/config.py +3 -3
  12. claude_task_master/core/orchestrator.py +432 -9
  13. claude_task_master/core/parallel.py +4 -4
  14. claude_task_master/core/plan_updater.py +199 -0
  15. claude_task_master/core/pr_context.py +176 -62
  16. claude_task_master/core/prompts.py +4 -0
  17. claude_task_master/core/prompts_plan_update.py +148 -0
  18. claude_task_master/core/prompts_planning.py +6 -2
  19. claude_task_master/core/state.py +5 -1
  20. claude_task_master/core/task_runner.py +73 -34
  21. claude_task_master/core/workflow_stages.py +229 -22
  22. claude_task_master/github/client_pr.py +86 -20
  23. claude_task_master/mailbox/__init__.py +23 -0
  24. claude_task_master/mailbox/merger.py +163 -0
  25. claude_task_master/mailbox/models.py +95 -0
  26. claude_task_master/mailbox/storage.py +209 -0
  27. claude_task_master/mcp/server.py +183 -0
  28. claude_task_master/mcp/tools.py +921 -0
  29. claude_task_master/webhooks/events.py +356 -2
  30. {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/METADATA +223 -4
  31. {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/RECORD +34 -26
  32. {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/WHEEL +1 -1
  33. {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/entry_points.txt +0 -0
  34. {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,199 @@
1
+ """Plan Updater - Updates existing plans based on change requests.
2
+
3
+ This module handles the plan update workflow when a change request is
4
+ received via `claudetm resume "message"` or from the mailbox system.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from .agent_phases import run_async_with_cleanup
10
+ from .prompts_plan_update import build_plan_update_prompt
11
+
12
+ if TYPE_CHECKING:
13
+ from .agent import AgentWrapper
14
+ from .logger import TaskLogger
15
+ from .state import StateManager
16
+
17
+
18
+ class PlanUpdater:
19
+ """Handles updating existing plans based on change requests.
20
+
21
+ This class orchestrates the plan update workflow:
22
+ 1. Load the current plan
23
+ 2. Run Claude with plan update prompt
24
+ 3. Extract and save the updated plan
25
+ 4. Update progress tracking
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ agent: "AgentWrapper",
31
+ state_manager: "StateManager",
32
+ logger: "TaskLogger | None" = None,
33
+ ):
34
+ """Initialize the plan updater.
35
+
36
+ Args:
37
+ agent: The agent wrapper for running queries.
38
+ state_manager: The state manager for loading/saving plans.
39
+ logger: Optional logger for tracking operations.
40
+ """
41
+ self.agent = agent
42
+ self.state_manager = state_manager
43
+ self.logger = logger
44
+
45
+ def update_plan(self, change_request: str) -> dict[str, Any]:
46
+ """Update the plan based on a change request.
47
+
48
+ This method:
49
+ 1. Loads the current plan from state
50
+ 2. Loads optional goal and context
51
+ 3. Runs Claude to analyze and update the plan
52
+ 4. Saves the updated plan
53
+ 5. Returns the result
54
+
55
+ Args:
56
+ change_request: The change request message describing what to update.
57
+
58
+ Returns:
59
+ Dict with keys:
60
+ - 'success': bool - whether the update succeeded
61
+ - 'plan': str - the updated plan content
62
+ - 'raw_output': str - the raw response from Claude
63
+ - 'changes_made': bool - whether the plan was actually modified
64
+
65
+ Raises:
66
+ ValueError: If no current plan exists to update.
67
+ """
68
+ # Load current plan
69
+ current_plan = self.state_manager.load_plan()
70
+ if not current_plan:
71
+ raise ValueError("No plan exists to update. Use 'start' to create a new plan.")
72
+
73
+ # Load optional context
74
+ goal = self.state_manager.load_goal()
75
+ context = self.state_manager.load_context()
76
+
77
+ # Build the update prompt
78
+ prompt = build_plan_update_prompt(
79
+ current_plan=current_plan,
80
+ change_request=change_request,
81
+ goal=goal if goal else None,
82
+ context=context if context else None,
83
+ )
84
+
85
+ if self.logger:
86
+ self.logger.log_prompt(f"Plan update request: {change_request[:100]}...")
87
+
88
+ # Run the query using the agent's planning tools (read-only)
89
+ result = self._run_plan_update_query(prompt)
90
+
91
+ # Extract the updated plan from the result
92
+ updated_plan = self._extract_updated_plan(result)
93
+
94
+ # Check if plan actually changed
95
+ changes_made = updated_plan.strip() != current_plan.strip()
96
+
97
+ # Save the updated plan if changes were made
98
+ if changes_made:
99
+ self.state_manager.save_plan(updated_plan)
100
+ if self.logger:
101
+ self.logger.log_response("Plan updated and saved")
102
+ else:
103
+ if self.logger:
104
+ self.logger.log_response("No changes needed to plan")
105
+
106
+ return {
107
+ "success": True,
108
+ "plan": updated_plan,
109
+ "raw_output": result,
110
+ "changes_made": changes_made,
111
+ }
112
+
113
+ def _run_plan_update_query(self, prompt: str) -> str:
114
+ """Run the plan update query using the agent.
115
+
116
+ Args:
117
+ prompt: The complete plan update prompt.
118
+
119
+ Returns:
120
+ The raw response from Claude.
121
+ """
122
+ from .agent_models import ModelType
123
+
124
+ # Use the agent's query executor directly with planning tools
125
+ # Always use Opus for plan updates (requires strategic thinking)
126
+ result = run_async_with_cleanup(
127
+ self.agent._query_executor.run_query(
128
+ prompt=prompt,
129
+ tools=self.agent.get_tools_for_phase("planning"),
130
+ model_override=ModelType.OPUS,
131
+ get_model_name_func=self.agent._get_model_name,
132
+ get_agents_func=None, # No subagents for plan update
133
+ process_message_func=self.agent._message_processor.process_message,
134
+ )
135
+ )
136
+
137
+ return result
138
+
139
+ def _extract_updated_plan(self, result: str) -> str:
140
+ """Extract the updated plan from the Claude response.
141
+
142
+ Looks for the plan content between Task List header and the
143
+ PLAN UPDATE COMPLETE marker.
144
+
145
+ Args:
146
+ result: The raw response from Claude.
147
+
148
+ Returns:
149
+ The extracted plan content.
150
+ """
151
+ # Try to find the plan content
152
+ plan_content = result
153
+
154
+ # Remove the PLAN UPDATE COMPLETE marker if present
155
+ if "PLAN UPDATE COMPLETE" in plan_content:
156
+ plan_content = plan_content.split("PLAN UPDATE COMPLETE")[0]
157
+
158
+ # If response has Task List header, extract from there
159
+ if "## Task List" in plan_content:
160
+ # Find the start of the plan
161
+ start_idx = plan_content.find("## Task List")
162
+ plan_content = plan_content[start_idx:]
163
+
164
+ return plan_content.strip()
165
+
166
+ def update_plan_from_messages(self, messages: list[str]) -> dict[str, Any]:
167
+ """Update the plan from multiple messages (e.g., from mailbox).
168
+
169
+ Merges multiple messages into a single change request and updates
170
+ the plan accordingly.
171
+
172
+ Args:
173
+ messages: List of message strings to process.
174
+
175
+ Returns:
176
+ Dict with update results (same as update_plan).
177
+
178
+ Raises:
179
+ ValueError: If no messages provided or no plan exists.
180
+ """
181
+ if not messages:
182
+ raise ValueError("No messages provided for plan update")
183
+
184
+ # Merge messages into a single change request
185
+ if len(messages) == 1:
186
+ change_request = messages[0]
187
+ else:
188
+ # Format multiple messages with clear separation
189
+ merged_parts = []
190
+ for i, msg in enumerate(messages, 1):
191
+ merged_parts.append(f"### Change Request {i}\n{msg}")
192
+ change_request = "\n\n".join(merged_parts)
193
+ change_request = (
194
+ f"**Multiple change requests received ({len(messages)} total):**\n\n"
195
+ f"{change_request}\n\n"
196
+ f"**Please address ALL of these change requests in the plan update.**"
197
+ )
198
+
199
+ return self.update_plan(change_request)
@@ -41,10 +41,6 @@ class PRContextManager:
41
41
  if pr_number is None:
42
42
  return
43
43
 
44
- # Also save comments when saving CI failures (for complete context)
45
- if _also_save_comments:
46
- self.save_pr_comments(pr_number, _also_save_ci=False)
47
-
48
44
  # Clear old CI logs to avoid stale data
49
45
  try:
50
46
  pr_dir = self.state_manager.get_pr_dir(pr_number)
@@ -72,9 +68,17 @@ class PRContextManager:
72
68
  except Exception as e:
73
69
  console.warning(f"Could not save CI failures: {e}")
74
70
 
71
+ # Also save comments when saving CI failures (for complete context)
72
+ # Do this AFTER saving CI failures to ensure CI files exist first
73
+ if _also_save_comments:
74
+ self.save_pr_comments(pr_number, _also_save_ci=False)
75
+
75
76
  def save_pr_comments(self, pr_number: int | None, *, _also_save_ci: bool = True) -> int:
76
77
  """Fetch and save PR comments to files for Claude to read.
77
78
 
79
+ Uses REST API to get all comments (like tstc), then enriches with
80
+ resolved status from GraphQL.
81
+
78
82
  Args:
79
83
  pr_number: The PR number.
80
84
  _also_save_ci: Internal flag to also save CI failures (prevents recursion).
@@ -111,33 +115,97 @@ class PRContextManager:
111
115
  text=True,
112
116
  )
113
117
  repo_info = result.stdout.strip()
114
- owner, repo = repo_info.split("/")
115
118
 
116
- # GraphQL query to get structured comments with thread IDs
117
- query = """
118
- query($owner: String!, $repo: String!, $pr: Int!) {
119
- repository(owner: $owner, name: $repo) {
120
- pullRequest(number: $pr) {
121
- reviewThreads(first: 100) {
119
+ # Use REST API to get ALL PR review comments (like tstc)
120
+ result = subprocess.run(
121
+ ["gh", "api", "--paginate", f"repos/{repo_info}/pulls/{pr_number}/comments"],
122
+ check=True,
123
+ capture_output=True,
124
+ text=True,
125
+ )
126
+ all_comments = json.loads(result.stdout)
127
+
128
+ # Get resolved status from GraphQL
129
+ resolved_map = self._get_resolved_status_map(repo_info, pr_number)
130
+
131
+ # Get already-addressed comment IDs to skip them
132
+ addressed_threads = self.state_manager.get_addressed_threads(pr_number)
133
+
134
+ # Convert to list of comment dicts - filter unresolved, actionable
135
+ comments = []
136
+ for comment in all_comments:
137
+ comment_id = comment.get("id")
138
+ # Check if this comment's thread is resolved
139
+ is_resolved = resolved_map.get(comment_id, False)
140
+ if is_resolved:
141
+ continue # Skip resolved comments
142
+
143
+ # Get thread ID for this comment (for tracking addressed threads)
144
+ thread_id = self._get_thread_id_for_comment(comment_id, resolved_map)
145
+
146
+ # Skip threads we've already addressed (replied to)
147
+ if thread_id and thread_id in addressed_threads:
148
+ continue
149
+
150
+ body = comment.get("body", "")
151
+ author = comment.get("user", {}).get("login", "unknown")
152
+
153
+ # Skip non-actionable bot comments
154
+ if self._is_non_actionable_comment(author, body):
155
+ continue
156
+
157
+ comments.append(
158
+ {
159
+ "thread_id": thread_id or f"comment_{comment_id}",
160
+ "comment_id": str(comment_id),
161
+ "author": author,
162
+ "body": body,
163
+ "path": comment.get("path"),
164
+ "line": comment.get("line") or comment.get("original_line"),
165
+ "is_resolved": False,
166
+ }
167
+ )
168
+
169
+ # Save to files
170
+ self.state_manager.save_pr_comments(pr_number, comments)
171
+ return len(comments)
172
+
173
+ except Exception as e:
174
+ console.warning(f"Could not save PR comments: {e}")
175
+ return 0
176
+
177
+ def _get_resolved_status_map(self, repo_info: str, pr_number: int) -> dict[int, bool]:
178
+ """Get resolved status for all comments from GraphQL.
179
+
180
+ Returns a map of comment_id -> is_resolved.
181
+ Also stores thread_id for each comment for later lookup.
182
+ """
183
+ owner, repo = repo_info.split("/")
184
+
185
+ # Store thread info: comment_id -> (is_resolved, thread_id)
186
+ self._thread_info: dict[int, tuple[bool, str]] = {}
187
+
188
+ query = """
189
+ query($owner: String!, $repo: String!, $pr: Int!) {
190
+ repository(owner: $owner, name: $repo) {
191
+ pullRequest(number: $pr) {
192
+ reviewThreads(first: 100) {
193
+ nodes {
194
+ id
195
+ isResolved
196
+ comments(first: 100) {
122
197
  nodes {
123
- id
124
- isResolved
125
- comments(first: 10) {
126
- nodes {
127
- id
128
- author { login }
129
- body
130
- path
131
- line
132
- }
133
- }
198
+ databaseId
134
199
  }
135
200
  }
136
201
  }
137
202
  }
138
203
  }
139
- """
204
+ }
205
+ }
206
+ """
140
207
 
208
+ try:
141
209
  result = subprocess.run(
142
210
  [
143
211
  "gh",
@@ -160,47 +228,26 @@ class PRContextManager:
160
228
  data = json.loads(result.stdout)
161
229
  threads = data["data"]["repository"]["pullRequest"]["reviewThreads"]["nodes"]
162
230
 
163
- # Get already-addressed thread IDs to skip them
164
- addressed_threads = self.state_manager.get_addressed_threads(pr_number)
165
-
166
- # Convert to list of comment dicts - ONLY unresolved, actionable threads
167
- comments = []
231
+ resolved_map: dict[int, bool] = {}
168
232
  for thread in threads:
169
- if thread["isResolved"]:
170
- continue # Skip resolved threads
171
- thread_id = thread.get("id")
172
-
173
- # Skip threads we've already addressed (replied to)
174
- if thread_id in addressed_threads:
175
- continue
176
-
177
- for comment in thread["comments"]["nodes"]:
178
- body = comment["body"]
179
- author = comment["author"]["login"] if comment.get("author") else "unknown"
180
-
181
- # Skip non-actionable bot comments
182
- if self._is_non_actionable_comment(author, body):
183
- continue
184
-
185
- comments.append(
186
- {
187
- "thread_id": thread_id,
188
- "comment_id": comment.get("id"),
189
- "author": author,
190
- "body": body,
191
- "path": comment.get("path"),
192
- "line": comment.get("line"),
193
- "is_resolved": False,
194
- }
195
- )
233
+ thread_id = thread.get("id", "")
234
+ is_resolved = thread.get("isResolved", False)
235
+ for comment in thread.get("comments", {}).get("nodes", []):
236
+ db_id = comment.get("databaseId")
237
+ if db_id:
238
+ resolved_map[db_id] = is_resolved
239
+ self._thread_info[db_id] = (is_resolved, thread_id)
240
+
241
+ return resolved_map
242
+ except Exception:
243
+ return {}
196
244
 
197
- # Save to files
198
- self.state_manager.save_pr_comments(pr_number, comments)
199
- return len(comments)
200
-
201
- except Exception as e:
202
- console.warning(f"Could not save PR comments: {e}")
203
- return 0
245
+ def _get_thread_id_for_comment(
246
+ self, comment_id: int, resolved_map: dict[int, bool]
247
+ ) -> str | None:
248
+ """Get the thread ID for a comment."""
249
+ info = getattr(self, "_thread_info", {}).get(comment_id)
250
+ return info[1] if info else None
204
251
 
205
252
  def post_comment_replies(self, pr_number: int | None) -> None:
206
253
  """Post replies to comments based on resolve-comments.json.
@@ -454,3 +501,70 @@ class PRContextManager:
454
501
  return True
455
502
 
456
503
  return False
504
+
505
+ def has_pr_comments(self, pr_number: int | None) -> bool:
506
+ """Check if there are unresolved PR comments saved.
507
+
508
+ Args:
509
+ pr_number: The PR number.
510
+
511
+ Returns:
512
+ True if there are saved comment files.
513
+ """
514
+ if pr_number is None:
515
+ return False
516
+
517
+ try:
518
+ pr_dir = self.state_manager.get_pr_dir(pr_number)
519
+ comments_dir = pr_dir / "comments"
520
+ if not comments_dir.exists():
521
+ return False
522
+ comment_files = list(comments_dir.glob("*.txt"))
523
+ return len(comment_files) > 0
524
+ except Exception:
525
+ return False
526
+
527
+ def has_ci_failures(self, pr_number: int | None) -> bool:
528
+ """Check if there are CI failure logs saved.
529
+
530
+ Args:
531
+ pr_number: The PR number.
532
+
533
+ Returns:
534
+ True if there are saved CI failure files.
535
+ """
536
+ if pr_number is None:
537
+ return False
538
+
539
+ try:
540
+ pr_dir = self.state_manager.get_pr_dir(pr_number)
541
+ ci_dir = pr_dir / "ci"
542
+ if not ci_dir.exists():
543
+ return False
544
+ ci_files = list(ci_dir.glob("*.txt"))
545
+ return len(ci_files) > 0
546
+ except Exception:
547
+ return False
548
+
549
+ def get_combined_feedback(self, pr_number: int | None) -> tuple[bool, bool, str]:
550
+ """Get combined feedback context for CI failures and PR comments.
551
+
552
+ This method checks both CI failures and PR comments, returning information
553
+ about what types of feedback are present and paths to find them.
554
+
555
+ Args:
556
+ pr_number: The PR number.
557
+
558
+ Returns:
559
+ Tuple of (has_ci_failures, has_comments, pr_dir_path).
560
+ """
561
+ if pr_number is None:
562
+ return (False, False, "")
563
+
564
+ pr_dir = self.state_manager.get_pr_dir(pr_number)
565
+ pr_dir_path = str(pr_dir)
566
+
567
+ has_ci = self.has_ci_failures(pr_number)
568
+ has_comments = self.has_pr_comments(pr_number)
569
+
570
+ return (has_ci, has_comments, pr_dir_path)
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module provides structured prompt templates for different agent phases:
4
4
  - Planning: Initial codebase analysis and task creation
5
+ - Plan Update: Modifying existing plans based on change requests
5
6
  - Working: Task execution with verification
6
7
  - PR Review: Addressing code review feedback
7
8
  - Verification: Confirming success criteria
@@ -12,6 +13,7 @@ This module re-exports all prompt functions for backward compatibility.
12
13
  The actual implementations are in:
13
14
  - prompts_base.py: PromptSection, PromptBuilder
14
15
  - prompts_planning.py: build_planning_prompt
16
+ - prompts_plan_update.py: build_plan_update_prompt
15
17
  - prompts_working.py: build_work_prompt
16
18
  - prompts_verification.py: build_verification_prompt, build_task_completion_check_prompt,
17
19
  build_context_extraction_prompt, build_error_recovery_prompt
@@ -23,6 +25,7 @@ from __future__ import annotations
23
25
  from .prompts_base import PromptBuilder, PromptSection
24
26
 
25
27
  # Re-export planning prompts
28
+ from .prompts_plan_update import build_plan_update_prompt
26
29
  from .prompts_planning import build_planning_prompt
27
30
 
28
31
  # Re-export verification prompts
@@ -42,6 +45,7 @@ __all__ = [
42
45
  "PromptBuilder",
43
46
  # Planning
44
47
  "build_planning_prompt",
48
+ "build_plan_update_prompt",
45
49
  # Working
46
50
  "build_work_prompt",
47
51
  # Verification and utilities
@@ -0,0 +1,148 @@
1
+ """Plan Update Prompts for Claude Task Master.
2
+
3
+ This module contains prompts for updating an existing plan when
4
+ a change request is received (via `claudetm resume "message"` or mailbox).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .prompts_base import PromptBuilder
10
+
11
+
12
+ def build_plan_update_prompt(
13
+ current_plan: str,
14
+ change_request: str,
15
+ goal: str | None = None,
16
+ context: str | None = None,
17
+ ) -> str:
18
+ """Build the plan update prompt.
19
+
20
+ Args:
21
+ current_plan: The current plan markdown content.
22
+ change_request: The change request/message from the user.
23
+ goal: Optional original goal for context.
24
+ context: Optional accumulated context from previous sessions.
25
+
26
+ Returns:
27
+ Complete plan update prompt.
28
+ """
29
+ builder = PromptBuilder(
30
+ intro=f"""You are Claude Task Master in PLAN UPDATE MODE.
31
+
32
+ A change request has been received that may require updating the existing plan.
33
+
34
+ **Change Request:** {change_request}
35
+
36
+ Your mission: **Analyze the change request and update the plan accordingly.**
37
+
38
+ ## TOOL RESTRICTIONS (MANDATORY)
39
+
40
+ **ALLOWED TOOLS (use ONLY these):**
41
+ - `Read` - Read files to understand the codebase
42
+ - `Glob` - Find files by pattern
43
+ - `Grep` - Search for code patterns
44
+ - `Bash` - Run commands (git status, tests, lint checks, etc.)
45
+
46
+ **FORBIDDEN TOOLS (NEVER use during plan update):**
47
+ - `Write` - Do NOT write any files
48
+ - `Edit` - Do NOT edit any files
49
+ - `Task` - Do NOT launch any agents
50
+ - `TodoWrite` - Do NOT use todo tracking
51
+ - `WebFetch` - Do NOT fetch web pages
52
+ - `WebSearch` - Do NOT search the web
53
+
54
+ **WHY**: The orchestrator will save your updated plan to `plan.md` automatically.
55
+ You just need to OUTPUT the updated plan as TEXT in your response."""
56
+ )
57
+
58
+ # Original goal if available
59
+ if goal:
60
+ builder.add_section("Original Goal", goal)
61
+
62
+ # Current plan section
63
+ builder.add_section(
64
+ "Current Plan",
65
+ f"""Here is the current plan that may need updating:
66
+
67
+ ```markdown
68
+ {current_plan}
69
+ ```
70
+
71
+ **IMPORTANT:** Tasks marked with `[x]` are already completed and should NOT be removed or unchecked.
72
+ """,
73
+ )
74
+
75
+ # Context section if available
76
+ if context:
77
+ builder.add_section("Previous Context", context.strip())
78
+
79
+ # Instructions for updating
80
+ builder.add_section(
81
+ "Update Instructions",
82
+ """Analyze the change request and determine what updates are needed:
83
+
84
+ 1. **Understand the Change**: What is being requested? Is it:
85
+ - Adding new tasks/features?
86
+ - Modifying existing tasks?
87
+ - Removing/deprioritizing tasks?
88
+ - Changing the approach/architecture?
89
+ - Fixing a bug or issue?
90
+
91
+ 2. **Preserve Completed Work**:
92
+ - Keep all `[x]` (completed) tasks as-is
93
+ - Do NOT uncheck completed tasks
94
+ - Do NOT remove completed tasks unless explicitly requested
95
+
96
+ 3. **Update Uncompleted Tasks**:
97
+ - Modify `[ ]` tasks if the approach needs to change
98
+ - Add new `[ ]` tasks if new requirements are introduced
99
+ - Remove or mark as obsolete tasks that are no longer needed
100
+
101
+ 4. **Maintain PR Structure**:
102
+ - Keep the PR grouping structure (### PR N: Title)
103
+ - Add new PRs if needed for new features
104
+ - Reorder tasks within PRs if dependencies change
105
+
106
+ 5. **Use Proper Format**:
107
+ - Keep complexity tags: `[coding]`, `[quick]`, `[general]`
108
+ - Include file paths and symbols in task descriptions
109
+ - Maintain the success criteria section""",
110
+ )
111
+
112
+ # Output format
113
+ builder.add_section(
114
+ "Output Format",
115
+ """After analyzing the change request, OUTPUT the complete updated plan.
116
+
117
+ **CRITICAL:**
118
+ - Start your updated plan with `## Task List`
119
+ - Include ALL tasks (both completed and uncompleted)
120
+ - Keep the same markdown checkbox format
121
+ - End with `## Success Criteria` section
122
+ - Do NOT use Write tool - just OUTPUT the plan as text
123
+
124
+ **Example structure:**
125
+ ```markdown
126
+ ## Task List
127
+
128
+ ### PR 1: Infrastructure
129
+ - [x] `[quick]` Setup project structure (COMPLETED - keep as-is)
130
+ - [ ] `[coding]` Add new feature from change request
131
+
132
+ ### PR 2: New Requirements (from change request)
133
+ - [ ] `[coding]` Implement new requirement A
134
+ - [ ] `[general]` Add tests for new requirements
135
+
136
+ ## Success Criteria
137
+ 1. All tasks completed
138
+ 2. Tests pass
139
+ 3. ...
140
+ ```
141
+
142
+ End your response with:
143
+ ```
144
+ PLAN UPDATE COMPLETE
145
+ ```""",
146
+ )
147
+
148
+ return builder.build()
@@ -36,14 +36,18 @@ testing strategy, and how all pieces fit together. Plan for success.
36
36
  - `Glob` - Find files by pattern
37
37
  - `Grep` - Search for code patterns
38
38
  - `Bash` - Run commands (git status, tests, lint checks, etc.)
39
+ - `WebSearch` - Search the web for current information (use FIRST to find URLs)
40
+ - `WebFetch` - Fetch full content from URLs (only URLs from search results or user-provided)
41
+
42
+ **Web Research Workflow:** Use `WebSearch` first to find relevant documentation/articles,
43
+ then use `WebFetch` to retrieve full content from specific URLs in the search results.
44
+ WebFetch can also retrieve PDFs for technical documentation.
39
45
 
40
46
  **FORBIDDEN TOOLS (NEVER use during planning):**
41
47
  - ❌ `Write` - Do NOT write any files
42
48
  - ❌ `Edit` - Do NOT edit any files
43
49
  - ❌ `Task` - Do NOT launch any agents
44
50
  - ❌ `TodoWrite` - Do NOT use todo tracking
45
- - ❌ `WebFetch` - Do NOT fetch web pages
46
- - ❌ `WebSearch` - Do NOT search the web
47
51
 
48
52
  **WHY**: The orchestrator will save your plan to `plan.md` automatically.
49
53
  You just need to OUTPUT the plan as TEXT in your response.