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.
- claude_task_master/__init__.py +1 -1
- claude_task_master/api/models.py +309 -0
- claude_task_master/api/routes.py +229 -0
- claude_task_master/api/routes_repo.py +317 -0
- claude_task_master/bin/claudetm +1 -1
- claude_task_master/cli.py +3 -1
- claude_task_master/cli_commands/mailbox.py +295 -0
- claude_task_master/cli_commands/workflow.py +37 -0
- claude_task_master/core/__init__.py +5 -0
- claude_task_master/core/agent_phases.py +1 -1
- claude_task_master/core/config.py +3 -3
- claude_task_master/core/orchestrator.py +432 -9
- claude_task_master/core/parallel.py +4 -4
- claude_task_master/core/plan_updater.py +199 -0
- claude_task_master/core/pr_context.py +176 -62
- claude_task_master/core/prompts.py +4 -0
- claude_task_master/core/prompts_plan_update.py +148 -0
- claude_task_master/core/prompts_planning.py +6 -2
- claude_task_master/core/state.py +5 -1
- claude_task_master/core/task_runner.py +73 -34
- claude_task_master/core/workflow_stages.py +229 -22
- claude_task_master/github/client_pr.py +86 -20
- claude_task_master/mailbox/__init__.py +23 -0
- claude_task_master/mailbox/merger.py +163 -0
- claude_task_master/mailbox/models.py +95 -0
- claude_task_master/mailbox/storage.py +209 -0
- claude_task_master/mcp/server.py +183 -0
- claude_task_master/mcp/tools.py +921 -0
- claude_task_master/webhooks/events.py +356 -2
- {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/METADATA +223 -4
- {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/RECORD +34 -26
- {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/WHEEL +1 -1
- {claude_task_master-0.1.4.dist-info → claude_task_master-0.1.6.dist-info}/entry_points.txt +0 -0
- {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
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|