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
claude_task_master/mcp/tools.py
CHANGED
|
@@ -117,6 +117,72 @@ class UpdateConfigResult(BaseModel):
|
|
|
117
117
|
error: str | None = None
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
class SendMessageResult(BaseModel):
|
|
121
|
+
"""Result from send_message mailbox tool."""
|
|
122
|
+
|
|
123
|
+
success: bool
|
|
124
|
+
message_id: str | None = None
|
|
125
|
+
message: str | None = None
|
|
126
|
+
error: str | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MailboxStatusResult(BaseModel):
|
|
130
|
+
"""Result from check_mailbox tool."""
|
|
131
|
+
|
|
132
|
+
success: bool
|
|
133
|
+
count: int = 0
|
|
134
|
+
previews: list[dict[str, Any]] = []
|
|
135
|
+
last_checked: str | None = None
|
|
136
|
+
total_messages_received: int = 0
|
|
137
|
+
error: str | None = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ClearMailboxResult(BaseModel):
|
|
141
|
+
"""Result from clear_mailbox tool."""
|
|
142
|
+
|
|
143
|
+
success: bool
|
|
144
|
+
messages_cleared: int = 0
|
|
145
|
+
message: str | None = None
|
|
146
|
+
error: str | None = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class CloneRepoResult(BaseModel):
|
|
150
|
+
"""Result from clone_repo tool."""
|
|
151
|
+
|
|
152
|
+
success: bool
|
|
153
|
+
message: str
|
|
154
|
+
repo_url: str | None = None
|
|
155
|
+
target_dir: str | None = None
|
|
156
|
+
branch: str | None = None
|
|
157
|
+
error: str | None = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SetupRepoResult(BaseModel):
|
|
161
|
+
"""Result from setup_repo tool."""
|
|
162
|
+
|
|
163
|
+
success: bool
|
|
164
|
+
message: str
|
|
165
|
+
work_dir: str | None = None
|
|
166
|
+
steps_completed: list[str] = []
|
|
167
|
+
venv_path: str | None = None
|
|
168
|
+
dependencies_installed: bool = False
|
|
169
|
+
setup_scripts_run: list[str] = []
|
|
170
|
+
error: str | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class PlanRepoResult(BaseModel):
|
|
174
|
+
"""Result from plan_repo tool."""
|
|
175
|
+
|
|
176
|
+
success: bool
|
|
177
|
+
message: str
|
|
178
|
+
work_dir: str | None = None
|
|
179
|
+
goal: str | None = None
|
|
180
|
+
plan: str | None = None
|
|
181
|
+
criteria: str | None = None
|
|
182
|
+
run_id: str | None = None
|
|
183
|
+
error: str | None = None
|
|
184
|
+
|
|
185
|
+
|
|
120
186
|
# =============================================================================
|
|
121
187
|
# Tool Implementations
|
|
122
188
|
# =============================================================================
|
|
@@ -838,3 +904,858 @@ def resource_context(work_dir: Path) -> str:
|
|
|
838
904
|
return state_manager.load_context()
|
|
839
905
|
except Exception:
|
|
840
906
|
return "Error loading context"
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# =============================================================================
|
|
910
|
+
# Mailbox Tool Implementations
|
|
911
|
+
# =============================================================================
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def send_message(
|
|
915
|
+
work_dir: Path,
|
|
916
|
+
content: str,
|
|
917
|
+
sender: str = "anonymous",
|
|
918
|
+
priority: int = 1,
|
|
919
|
+
state_dir: str | None = None,
|
|
920
|
+
) -> dict[str, Any]:
|
|
921
|
+
"""Send a message to the claudetm mailbox.
|
|
922
|
+
|
|
923
|
+
Messages in the mailbox will be processed after the current task completes.
|
|
924
|
+
Multiple messages are merged into a single change request that updates
|
|
925
|
+
the plan before continuing work.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
work_dir: Working directory for the server.
|
|
929
|
+
content: The message content describing the change request.
|
|
930
|
+
sender: Identifier of the sender (default: "anonymous").
|
|
931
|
+
priority: Message priority (0=low, 1=normal, 2=high, 3=urgent).
|
|
932
|
+
state_dir: Optional custom state directory path.
|
|
933
|
+
|
|
934
|
+
Returns:
|
|
935
|
+
Dictionary containing the message_id on success, or error info.
|
|
936
|
+
"""
|
|
937
|
+
from claude_task_master.mailbox import MailboxStorage
|
|
938
|
+
|
|
939
|
+
state_path = Path(state_dir) if state_dir else work_dir / ".claude-task-master"
|
|
940
|
+
|
|
941
|
+
# Validate content
|
|
942
|
+
if not content or not content.strip():
|
|
943
|
+
return SendMessageResult(
|
|
944
|
+
success=False,
|
|
945
|
+
error="Message content cannot be empty",
|
|
946
|
+
).model_dump()
|
|
947
|
+
|
|
948
|
+
# Validate priority range
|
|
949
|
+
if priority < 0 or priority > 3:
|
|
950
|
+
return SendMessageResult(
|
|
951
|
+
success=False,
|
|
952
|
+
error="Priority must be between 0 (low) and 3 (urgent)",
|
|
953
|
+
).model_dump()
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
mailbox = MailboxStorage(state_dir=state_path)
|
|
957
|
+
message_id = mailbox.add_message(
|
|
958
|
+
content=content.strip(),
|
|
959
|
+
sender=sender,
|
|
960
|
+
priority=priority,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
return SendMessageResult(
|
|
964
|
+
success=True,
|
|
965
|
+
message_id=message_id,
|
|
966
|
+
message=f"Message sent successfully (id: {message_id})",
|
|
967
|
+
).model_dump()
|
|
968
|
+
except Exception as e:
|
|
969
|
+
return SendMessageResult(
|
|
970
|
+
success=False,
|
|
971
|
+
error=f"Failed to send message: {e}",
|
|
972
|
+
).model_dump()
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def check_mailbox(
|
|
976
|
+
work_dir: Path,
|
|
977
|
+
state_dir: str | None = None,
|
|
978
|
+
) -> dict[str, Any]:
|
|
979
|
+
"""Check the status of the claudetm mailbox.
|
|
980
|
+
|
|
981
|
+
Returns the number of pending messages and previews of each.
|
|
982
|
+
|
|
983
|
+
Args:
|
|
984
|
+
work_dir: Working directory for the server.
|
|
985
|
+
state_dir: Optional custom state directory path.
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
Dictionary containing mailbox status information.
|
|
989
|
+
"""
|
|
990
|
+
from claude_task_master.mailbox import MailboxStorage
|
|
991
|
+
|
|
992
|
+
state_path = Path(state_dir) if state_dir else work_dir / ".claude-task-master"
|
|
993
|
+
|
|
994
|
+
try:
|
|
995
|
+
mailbox = MailboxStorage(state_dir=state_path)
|
|
996
|
+
status = mailbox.get_status()
|
|
997
|
+
|
|
998
|
+
return MailboxStatusResult(
|
|
999
|
+
success=True,
|
|
1000
|
+
count=status["count"],
|
|
1001
|
+
previews=status["previews"],
|
|
1002
|
+
last_checked=status["last_checked"],
|
|
1003
|
+
total_messages_received=status["total_messages_received"],
|
|
1004
|
+
).model_dump()
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
return MailboxStatusResult(
|
|
1007
|
+
success=False,
|
|
1008
|
+
error=f"Failed to check mailbox: {e}",
|
|
1009
|
+
).model_dump()
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def clear_mailbox(
|
|
1013
|
+
work_dir: Path,
|
|
1014
|
+
state_dir: str | None = None,
|
|
1015
|
+
) -> dict[str, Any]:
|
|
1016
|
+
"""Clear all messages from the claudetm mailbox.
|
|
1017
|
+
|
|
1018
|
+
Args:
|
|
1019
|
+
work_dir: Working directory for the server.
|
|
1020
|
+
state_dir: Optional custom state directory path.
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
Dictionary indicating success and number of messages cleared.
|
|
1024
|
+
"""
|
|
1025
|
+
from claude_task_master.mailbox import MailboxStorage
|
|
1026
|
+
|
|
1027
|
+
state_path = Path(state_dir) if state_dir else work_dir / ".claude-task-master"
|
|
1028
|
+
|
|
1029
|
+
try:
|
|
1030
|
+
mailbox = MailboxStorage(state_dir=state_path)
|
|
1031
|
+
count = mailbox.clear()
|
|
1032
|
+
|
|
1033
|
+
return ClearMailboxResult(
|
|
1034
|
+
success=True,
|
|
1035
|
+
messages_cleared=count,
|
|
1036
|
+
message=f"Cleared {count} message(s) from mailbox",
|
|
1037
|
+
).model_dump()
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
return ClearMailboxResult(
|
|
1040
|
+
success=False,
|
|
1041
|
+
error=f"Failed to clear mailbox: {e}",
|
|
1042
|
+
).model_dump()
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
# =============================================================================
|
|
1046
|
+
# Repo Setup Tool Implementations
|
|
1047
|
+
# =============================================================================
|
|
1048
|
+
|
|
1049
|
+
# Default workspace base for repo setup
|
|
1050
|
+
DEFAULT_WORKSPACE_BASE = Path.home() / "workspace" / "claude-task-master"
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _extract_repo_name(url: str) -> str:
|
|
1054
|
+
"""Extract repository name from a git URL.
|
|
1055
|
+
|
|
1056
|
+
Supports both HTTPS and SSH URLs:
|
|
1057
|
+
- https://github.com/user/repo.git -> repo
|
|
1058
|
+
- git@github.com:user/repo.git -> repo
|
|
1059
|
+
- https://github.com/user/repo -> repo
|
|
1060
|
+
|
|
1061
|
+
Args:
|
|
1062
|
+
url: Git repository URL.
|
|
1063
|
+
|
|
1064
|
+
Returns:
|
|
1065
|
+
Repository name without .git suffix.
|
|
1066
|
+
"""
|
|
1067
|
+
# Remove trailing .git if present
|
|
1068
|
+
clean_url = url.rstrip("/")
|
|
1069
|
+
if clean_url.endswith(".git"):
|
|
1070
|
+
clean_url = clean_url[:-4]
|
|
1071
|
+
|
|
1072
|
+
# Extract repo name from path
|
|
1073
|
+
# For SSH: git@github.com:user/repo
|
|
1074
|
+
if ":" in clean_url and "@" in clean_url:
|
|
1075
|
+
repo_name = clean_url.split("/")[-1]
|
|
1076
|
+
else:
|
|
1077
|
+
# For HTTPS: https://github.com/user/repo
|
|
1078
|
+
repo_name = clean_url.split("/")[-1]
|
|
1079
|
+
|
|
1080
|
+
return repo_name
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def clone_repo(
|
|
1084
|
+
url: str,
|
|
1085
|
+
target_dir: str | None = None,
|
|
1086
|
+
branch: str | None = None,
|
|
1087
|
+
) -> dict[str, Any]:
|
|
1088
|
+
"""Clone a git repository to the workspace.
|
|
1089
|
+
|
|
1090
|
+
Clones the repository to ~/workspace/claude-task-master/{project-name}
|
|
1091
|
+
by default, or to a custom target directory if specified.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
url: Git repository URL (HTTPS or SSH).
|
|
1095
|
+
target_dir: Optional custom target directory path. If not provided,
|
|
1096
|
+
defaults to ~/workspace/claude-task-master/{repo-name}.
|
|
1097
|
+
branch: Optional branch to checkout after cloning.
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Dictionary containing clone result with success status and details.
|
|
1101
|
+
"""
|
|
1102
|
+
import subprocess
|
|
1103
|
+
|
|
1104
|
+
# Validate URL
|
|
1105
|
+
if not url or not url.strip():
|
|
1106
|
+
return CloneRepoResult(
|
|
1107
|
+
success=False,
|
|
1108
|
+
message="Repository URL is required",
|
|
1109
|
+
error="Repository URL cannot be empty",
|
|
1110
|
+
).model_dump()
|
|
1111
|
+
|
|
1112
|
+
url = url.strip()
|
|
1113
|
+
|
|
1114
|
+
# Basic URL validation
|
|
1115
|
+
if not (
|
|
1116
|
+
url.startswith("https://")
|
|
1117
|
+
or url.startswith("git@")
|
|
1118
|
+
or url.startswith("git://")
|
|
1119
|
+
or url.startswith("ssh://")
|
|
1120
|
+
):
|
|
1121
|
+
return CloneRepoResult(
|
|
1122
|
+
success=False,
|
|
1123
|
+
message="Invalid repository URL format",
|
|
1124
|
+
repo_url=url,
|
|
1125
|
+
error="URL must start with https://, git@, git://, or ssh://",
|
|
1126
|
+
).model_dump()
|
|
1127
|
+
|
|
1128
|
+
# Determine target directory
|
|
1129
|
+
repo_name = _extract_repo_name(url)
|
|
1130
|
+
if target_dir:
|
|
1131
|
+
target_path = Path(target_dir).expanduser().resolve()
|
|
1132
|
+
else:
|
|
1133
|
+
# Default to ~/workspace/claude-task-master/{repo-name}
|
|
1134
|
+
target_path = DEFAULT_WORKSPACE_BASE / repo_name
|
|
1135
|
+
|
|
1136
|
+
# Check if target already exists
|
|
1137
|
+
if target_path.exists():
|
|
1138
|
+
return CloneRepoResult(
|
|
1139
|
+
success=False,
|
|
1140
|
+
message=f"Target directory already exists: {target_path}",
|
|
1141
|
+
repo_url=url,
|
|
1142
|
+
target_dir=str(target_path),
|
|
1143
|
+
error="Target directory already exists. Remove it first or specify a different target.",
|
|
1144
|
+
).model_dump()
|
|
1145
|
+
|
|
1146
|
+
# Ensure parent directory exists
|
|
1147
|
+
try:
|
|
1148
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1149
|
+
except PermissionError as e:
|
|
1150
|
+
return CloneRepoResult(
|
|
1151
|
+
success=False,
|
|
1152
|
+
message=f"Permission denied creating parent directory: {target_path.parent}",
|
|
1153
|
+
repo_url=url,
|
|
1154
|
+
target_dir=str(target_path),
|
|
1155
|
+
error=str(e),
|
|
1156
|
+
).model_dump()
|
|
1157
|
+
except OSError as e:
|
|
1158
|
+
return CloneRepoResult(
|
|
1159
|
+
success=False,
|
|
1160
|
+
message=f"Failed to create parent directory: {target_path.parent}",
|
|
1161
|
+
repo_url=url,
|
|
1162
|
+
target_dir=str(target_path),
|
|
1163
|
+
error=str(e),
|
|
1164
|
+
).model_dump()
|
|
1165
|
+
|
|
1166
|
+
# Build git clone command
|
|
1167
|
+
cmd = ["git", "clone"]
|
|
1168
|
+
if branch:
|
|
1169
|
+
cmd.extend(["--branch", branch])
|
|
1170
|
+
cmd.extend([url, str(target_path)])
|
|
1171
|
+
|
|
1172
|
+
# Execute git clone
|
|
1173
|
+
try:
|
|
1174
|
+
result = subprocess.run(
|
|
1175
|
+
cmd,
|
|
1176
|
+
capture_output=True,
|
|
1177
|
+
text=True,
|
|
1178
|
+
check=False,
|
|
1179
|
+
timeout=300, # 5 minute timeout for large repos
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
if result.returncode != 0:
|
|
1183
|
+
# Clean up partial clone if it exists
|
|
1184
|
+
if target_path.exists():
|
|
1185
|
+
shutil.rmtree(target_path, ignore_errors=True)
|
|
1186
|
+
|
|
1187
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
1188
|
+
return CloneRepoResult(
|
|
1189
|
+
success=False,
|
|
1190
|
+
message=f"Git clone failed: {error_msg}",
|
|
1191
|
+
repo_url=url,
|
|
1192
|
+
target_dir=str(target_path),
|
|
1193
|
+
branch=branch,
|
|
1194
|
+
error=error_msg,
|
|
1195
|
+
).model_dump()
|
|
1196
|
+
|
|
1197
|
+
return CloneRepoResult(
|
|
1198
|
+
success=True,
|
|
1199
|
+
message=f"Successfully cloned {repo_name} to {target_path}",
|
|
1200
|
+
repo_url=url,
|
|
1201
|
+
target_dir=str(target_path),
|
|
1202
|
+
branch=branch,
|
|
1203
|
+
).model_dump()
|
|
1204
|
+
|
|
1205
|
+
except subprocess.TimeoutExpired:
|
|
1206
|
+
# Clean up partial clone
|
|
1207
|
+
if target_path.exists():
|
|
1208
|
+
shutil.rmtree(target_path, ignore_errors=True)
|
|
1209
|
+
|
|
1210
|
+
return CloneRepoResult(
|
|
1211
|
+
success=False,
|
|
1212
|
+
message="Git clone timed out (5 minute limit exceeded)",
|
|
1213
|
+
repo_url=url,
|
|
1214
|
+
target_dir=str(target_path),
|
|
1215
|
+
branch=branch,
|
|
1216
|
+
error="Clone operation timed out - repository may be too large or network is slow",
|
|
1217
|
+
).model_dump()
|
|
1218
|
+
|
|
1219
|
+
except FileNotFoundError:
|
|
1220
|
+
return CloneRepoResult(
|
|
1221
|
+
success=False,
|
|
1222
|
+
message="Git is not installed or not in PATH",
|
|
1223
|
+
repo_url=url,
|
|
1224
|
+
target_dir=str(target_path),
|
|
1225
|
+
branch=branch,
|
|
1226
|
+
error="git command not found - please install git",
|
|
1227
|
+
).model_dump()
|
|
1228
|
+
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
# Clean up partial clone
|
|
1231
|
+
if target_path.exists():
|
|
1232
|
+
shutil.rmtree(target_path, ignore_errors=True)
|
|
1233
|
+
|
|
1234
|
+
return CloneRepoResult(
|
|
1235
|
+
success=False,
|
|
1236
|
+
message=f"Clone failed: {e}",
|
|
1237
|
+
repo_url=url,
|
|
1238
|
+
target_dir=str(target_path),
|
|
1239
|
+
branch=branch,
|
|
1240
|
+
error=str(e),
|
|
1241
|
+
).model_dump()
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def setup_repo(
|
|
1245
|
+
work_dir: str | Path,
|
|
1246
|
+
) -> dict[str, Any]:
|
|
1247
|
+
"""Set up a cloned repository for development.
|
|
1248
|
+
|
|
1249
|
+
Detects the project type and performs appropriate setup:
|
|
1250
|
+
- Creates virtual environment (for Python projects)
|
|
1251
|
+
- Installs dependencies (pip, npm, etc.)
|
|
1252
|
+
- Runs setup scripts (setup-hooks.sh, etc.)
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
work_dir: Path to the cloned repository directory.
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
Dictionary containing setup result with success status and details.
|
|
1259
|
+
"""
|
|
1260
|
+
import subprocess
|
|
1261
|
+
import sys
|
|
1262
|
+
|
|
1263
|
+
work_path = Path(work_dir).expanduser().resolve()
|
|
1264
|
+
steps_completed: list[str] = []
|
|
1265
|
+
setup_scripts_run: list[str] = []
|
|
1266
|
+
venv_path: str | None = None
|
|
1267
|
+
dependencies_installed = False
|
|
1268
|
+
|
|
1269
|
+
# Validate work directory
|
|
1270
|
+
if not work_path.exists():
|
|
1271
|
+
return SetupRepoResult(
|
|
1272
|
+
success=False,
|
|
1273
|
+
message=f"Directory does not exist: {work_path}",
|
|
1274
|
+
work_dir=str(work_path),
|
|
1275
|
+
error="Work directory not found",
|
|
1276
|
+
).model_dump()
|
|
1277
|
+
|
|
1278
|
+
if not work_path.is_dir():
|
|
1279
|
+
return SetupRepoResult(
|
|
1280
|
+
success=False,
|
|
1281
|
+
message=f"Path is not a directory: {work_path}",
|
|
1282
|
+
work_dir=str(work_path),
|
|
1283
|
+
error="Path is not a directory",
|
|
1284
|
+
).model_dump()
|
|
1285
|
+
|
|
1286
|
+
# Detect project type based on manifest files
|
|
1287
|
+
is_python = (work_path / "pyproject.toml").exists() or (work_path / "setup.py").exists()
|
|
1288
|
+
is_node = (work_path / "package.json").exists()
|
|
1289
|
+
has_requirements = (work_path / "requirements.txt").exists()
|
|
1290
|
+
has_uv_lock = (work_path / "uv.lock").exists()
|
|
1291
|
+
|
|
1292
|
+
# Detect setup scripts
|
|
1293
|
+
setup_scripts: list[Path] = []
|
|
1294
|
+
scripts_dir = work_path / "scripts"
|
|
1295
|
+
if scripts_dir.exists():
|
|
1296
|
+
# Look for common setup scripts
|
|
1297
|
+
for script_name in ["setup-hooks.sh", "setup.sh", "install.sh", "bootstrap.sh"]:
|
|
1298
|
+
script_path = scripts_dir / script_name
|
|
1299
|
+
if script_path.exists() and script_path.is_file():
|
|
1300
|
+
setup_scripts.append(script_path)
|
|
1301
|
+
|
|
1302
|
+
# Also check root directory for setup scripts
|
|
1303
|
+
for script_name in ["setup.sh", "install.sh", "bootstrap.sh"]:
|
|
1304
|
+
script_path = work_path / script_name
|
|
1305
|
+
if script_path.exists() and script_path.is_file():
|
|
1306
|
+
setup_scripts.append(script_path)
|
|
1307
|
+
|
|
1308
|
+
try:
|
|
1309
|
+
# === Python Project Setup ===
|
|
1310
|
+
if is_python:
|
|
1311
|
+
steps_completed.append("Detected Python project")
|
|
1312
|
+
|
|
1313
|
+
# Check for uv (preferred) or fall back to standard venv + pip
|
|
1314
|
+
has_uv = shutil.which("uv") is not None
|
|
1315
|
+
|
|
1316
|
+
if has_uv:
|
|
1317
|
+
# Use uv for virtual environment and dependency management
|
|
1318
|
+
steps_completed.append("Using uv for dependency management")
|
|
1319
|
+
|
|
1320
|
+
# Create venv with uv if not exists
|
|
1321
|
+
venv_dir = work_path / ".venv"
|
|
1322
|
+
if not venv_dir.exists():
|
|
1323
|
+
result = subprocess.run(
|
|
1324
|
+
["uv", "venv", str(venv_dir)],
|
|
1325
|
+
cwd=work_path,
|
|
1326
|
+
capture_output=True,
|
|
1327
|
+
text=True,
|
|
1328
|
+
check=False,
|
|
1329
|
+
timeout=60,
|
|
1330
|
+
)
|
|
1331
|
+
if result.returncode != 0:
|
|
1332
|
+
return SetupRepoResult(
|
|
1333
|
+
success=False,
|
|
1334
|
+
message=f"Failed to create venv with uv: {result.stderr}",
|
|
1335
|
+
work_dir=str(work_path),
|
|
1336
|
+
steps_completed=steps_completed,
|
|
1337
|
+
error=result.stderr,
|
|
1338
|
+
).model_dump()
|
|
1339
|
+
steps_completed.append("Created virtual environment with uv")
|
|
1340
|
+
else:
|
|
1341
|
+
steps_completed.append("Virtual environment already exists")
|
|
1342
|
+
|
|
1343
|
+
venv_path = str(venv_dir)
|
|
1344
|
+
|
|
1345
|
+
# Install dependencies with uv
|
|
1346
|
+
# Prefer uv sync for uv-managed projects, otherwise uv pip install
|
|
1347
|
+
if has_uv_lock or (work_path / "pyproject.toml").exists():
|
|
1348
|
+
# Use uv sync for projects with pyproject.toml
|
|
1349
|
+
sync_cmd = ["uv", "sync"]
|
|
1350
|
+
# Try to install all extras if available
|
|
1351
|
+
if (work_path / "pyproject.toml").exists():
|
|
1352
|
+
sync_cmd.append("--all-extras")
|
|
1353
|
+
|
|
1354
|
+
result = subprocess.run(
|
|
1355
|
+
sync_cmd,
|
|
1356
|
+
cwd=work_path,
|
|
1357
|
+
capture_output=True,
|
|
1358
|
+
text=True,
|
|
1359
|
+
check=False,
|
|
1360
|
+
timeout=300,
|
|
1361
|
+
)
|
|
1362
|
+
if result.returncode == 0:
|
|
1363
|
+
dependencies_installed = True
|
|
1364
|
+
steps_completed.append("Installed dependencies with uv sync")
|
|
1365
|
+
else:
|
|
1366
|
+
# Fall back to basic uv sync without extras
|
|
1367
|
+
result = subprocess.run(
|
|
1368
|
+
["uv", "sync"],
|
|
1369
|
+
cwd=work_path,
|
|
1370
|
+
capture_output=True,
|
|
1371
|
+
text=True,
|
|
1372
|
+
check=False,
|
|
1373
|
+
timeout=300,
|
|
1374
|
+
)
|
|
1375
|
+
if result.returncode == 0:
|
|
1376
|
+
dependencies_installed = True
|
|
1377
|
+
steps_completed.append("Installed dependencies with uv sync (basic)")
|
|
1378
|
+
else:
|
|
1379
|
+
steps_completed.append(f"Warning: uv sync failed: {result.stderr}")
|
|
1380
|
+
elif has_requirements:
|
|
1381
|
+
result = subprocess.run(
|
|
1382
|
+
["uv", "pip", "install", "-r", "requirements.txt"],
|
|
1383
|
+
cwd=work_path,
|
|
1384
|
+
capture_output=True,
|
|
1385
|
+
text=True,
|
|
1386
|
+
check=False,
|
|
1387
|
+
timeout=300,
|
|
1388
|
+
)
|
|
1389
|
+
if result.returncode == 0:
|
|
1390
|
+
dependencies_installed = True
|
|
1391
|
+
steps_completed.append("Installed dependencies from requirements.txt")
|
|
1392
|
+
else:
|
|
1393
|
+
steps_completed.append(f"Warning: pip install failed: {result.stderr}")
|
|
1394
|
+
else:
|
|
1395
|
+
# Fall back to standard Python venv + pip
|
|
1396
|
+
steps_completed.append("Using standard venv + pip")
|
|
1397
|
+
|
|
1398
|
+
venv_dir = work_path / ".venv"
|
|
1399
|
+
if not venv_dir.exists():
|
|
1400
|
+
result = subprocess.run(
|
|
1401
|
+
["python3", "-m", "venv", str(venv_dir)],
|
|
1402
|
+
cwd=work_path,
|
|
1403
|
+
capture_output=True,
|
|
1404
|
+
text=True,
|
|
1405
|
+
check=False,
|
|
1406
|
+
timeout=120,
|
|
1407
|
+
)
|
|
1408
|
+
if result.returncode != 0:
|
|
1409
|
+
return SetupRepoResult(
|
|
1410
|
+
success=False,
|
|
1411
|
+
message=f"Failed to create venv: {result.stderr}",
|
|
1412
|
+
work_dir=str(work_path),
|
|
1413
|
+
steps_completed=steps_completed,
|
|
1414
|
+
error=result.stderr,
|
|
1415
|
+
).model_dump()
|
|
1416
|
+
steps_completed.append("Created virtual environment")
|
|
1417
|
+
else:
|
|
1418
|
+
steps_completed.append("Virtual environment already exists")
|
|
1419
|
+
|
|
1420
|
+
venv_path = str(venv_dir)
|
|
1421
|
+
# Use platform-appropriate path for pip
|
|
1422
|
+
pip_path = (
|
|
1423
|
+
venv_dir
|
|
1424
|
+
/ ("Scripts" if sys.platform == "win32" else "bin")
|
|
1425
|
+
/ ("pip.exe" if sys.platform == "win32" else "pip")
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
# Install dependencies with pip
|
|
1429
|
+
if (work_path / "pyproject.toml").exists():
|
|
1430
|
+
# Install project in editable mode with dev extras
|
|
1431
|
+
result = subprocess.run(
|
|
1432
|
+
[str(pip_path), "install", "-e", ".[dev]"],
|
|
1433
|
+
cwd=work_path,
|
|
1434
|
+
capture_output=True,
|
|
1435
|
+
text=True,
|
|
1436
|
+
check=False,
|
|
1437
|
+
timeout=300,
|
|
1438
|
+
)
|
|
1439
|
+
if result.returncode == 0:
|
|
1440
|
+
dependencies_installed = True
|
|
1441
|
+
steps_completed.append("Installed project with pip (editable + dev)")
|
|
1442
|
+
else:
|
|
1443
|
+
# Try without dev extras
|
|
1444
|
+
result = subprocess.run(
|
|
1445
|
+
[str(pip_path), "install", "-e", "."],
|
|
1446
|
+
cwd=work_path,
|
|
1447
|
+
capture_output=True,
|
|
1448
|
+
text=True,
|
|
1449
|
+
check=False,
|
|
1450
|
+
timeout=300,
|
|
1451
|
+
)
|
|
1452
|
+
if result.returncode == 0:
|
|
1453
|
+
dependencies_installed = True
|
|
1454
|
+
steps_completed.append("Installed project with pip (editable)")
|
|
1455
|
+
else:
|
|
1456
|
+
steps_completed.append(f"Warning: pip install failed: {result.stderr}")
|
|
1457
|
+
elif has_requirements:
|
|
1458
|
+
result = subprocess.run(
|
|
1459
|
+
[str(pip_path), "install", "-r", "requirements.txt"],
|
|
1460
|
+
cwd=work_path,
|
|
1461
|
+
capture_output=True,
|
|
1462
|
+
text=True,
|
|
1463
|
+
check=False,
|
|
1464
|
+
timeout=300,
|
|
1465
|
+
)
|
|
1466
|
+
if result.returncode == 0:
|
|
1467
|
+
dependencies_installed = True
|
|
1468
|
+
steps_completed.append("Installed dependencies from requirements.txt")
|
|
1469
|
+
else:
|
|
1470
|
+
steps_completed.append(f"Warning: pip install failed: {result.stderr}")
|
|
1471
|
+
|
|
1472
|
+
# === Node.js Project Setup ===
|
|
1473
|
+
if is_node:
|
|
1474
|
+
steps_completed.append("Detected Node.js project")
|
|
1475
|
+
|
|
1476
|
+
# Check for package managers (lock files indicate preferred manager)
|
|
1477
|
+
has_pnpm_lock = (work_path / "pnpm-lock.yaml").exists()
|
|
1478
|
+
has_yarn_lock = (work_path / "yarn.lock").exists()
|
|
1479
|
+
has_bun_lock = (work_path / "bun.lockb").exists()
|
|
1480
|
+
|
|
1481
|
+
# Determine which package manager to use
|
|
1482
|
+
if has_bun_lock and shutil.which("bun"):
|
|
1483
|
+
pkg_manager = "bun"
|
|
1484
|
+
install_cmd = ["bun", "install"]
|
|
1485
|
+
elif has_pnpm_lock and shutil.which("pnpm"):
|
|
1486
|
+
pkg_manager = "pnpm"
|
|
1487
|
+
install_cmd = ["pnpm", "install"]
|
|
1488
|
+
elif has_yarn_lock and shutil.which("yarn"):
|
|
1489
|
+
pkg_manager = "yarn"
|
|
1490
|
+
install_cmd = ["yarn", "install"]
|
|
1491
|
+
elif shutil.which("npm"):
|
|
1492
|
+
pkg_manager = "npm"
|
|
1493
|
+
install_cmd = ["npm", "install"]
|
|
1494
|
+
else:
|
|
1495
|
+
steps_completed.append("Warning: No Node.js package manager found")
|
|
1496
|
+
pkg_manager = None
|
|
1497
|
+
install_cmd = None
|
|
1498
|
+
|
|
1499
|
+
if install_cmd:
|
|
1500
|
+
result = subprocess.run(
|
|
1501
|
+
install_cmd,
|
|
1502
|
+
cwd=work_path,
|
|
1503
|
+
capture_output=True,
|
|
1504
|
+
text=True,
|
|
1505
|
+
check=False,
|
|
1506
|
+
timeout=300,
|
|
1507
|
+
)
|
|
1508
|
+
if result.returncode == 0:
|
|
1509
|
+
dependencies_installed = True
|
|
1510
|
+
steps_completed.append(f"Installed dependencies with {pkg_manager}")
|
|
1511
|
+
else:
|
|
1512
|
+
steps_completed.append(
|
|
1513
|
+
f"Warning: {pkg_manager} install failed: {result.stderr}"
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
# === Run Setup Scripts ===
|
|
1517
|
+
for script in setup_scripts:
|
|
1518
|
+
try:
|
|
1519
|
+
# Make script executable (skip on Windows where chmod is not needed)
|
|
1520
|
+
if sys.platform != "win32":
|
|
1521
|
+
script.chmod(script.stat().st_mode | 0o755)
|
|
1522
|
+
|
|
1523
|
+
result = subprocess.run(
|
|
1524
|
+
[str(script)],
|
|
1525
|
+
cwd=work_path,
|
|
1526
|
+
capture_output=True,
|
|
1527
|
+
text=True,
|
|
1528
|
+
check=False,
|
|
1529
|
+
timeout=120,
|
|
1530
|
+
)
|
|
1531
|
+
if result.returncode == 0:
|
|
1532
|
+
setup_scripts_run.append(str(script.relative_to(work_path)))
|
|
1533
|
+
steps_completed.append(f"Ran setup script: {script.name}")
|
|
1534
|
+
else:
|
|
1535
|
+
steps_completed.append(
|
|
1536
|
+
f"Warning: Setup script {script.name} failed: {result.stderr}"
|
|
1537
|
+
)
|
|
1538
|
+
except Exception as e:
|
|
1539
|
+
steps_completed.append(f"Warning: Could not run {script.name}: {e}")
|
|
1540
|
+
|
|
1541
|
+
# Determine overall success
|
|
1542
|
+
# Success if we detected a project type and either installed deps or ran scripts
|
|
1543
|
+
success = len(steps_completed) > 0 and (
|
|
1544
|
+
dependencies_installed or len(setup_scripts_run) > 0
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
if not is_python and not is_node:
|
|
1548
|
+
steps_completed.append("No recognized project type (Python or Node.js)")
|
|
1549
|
+
if setup_scripts_run:
|
|
1550
|
+
success = True # Still success if we ran setup scripts
|
|
1551
|
+
else:
|
|
1552
|
+
success = False
|
|
1553
|
+
|
|
1554
|
+
message = (
|
|
1555
|
+
f"Setup completed for {work_path.name}"
|
|
1556
|
+
if success
|
|
1557
|
+
else f"Setup incomplete for {work_path.name}"
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
return SetupRepoResult(
|
|
1561
|
+
success=success,
|
|
1562
|
+
message=message,
|
|
1563
|
+
work_dir=str(work_path),
|
|
1564
|
+
steps_completed=steps_completed,
|
|
1565
|
+
venv_path=venv_path,
|
|
1566
|
+
dependencies_installed=dependencies_installed,
|
|
1567
|
+
setup_scripts_run=setup_scripts_run,
|
|
1568
|
+
).model_dump()
|
|
1569
|
+
|
|
1570
|
+
except subprocess.TimeoutExpired as e:
|
|
1571
|
+
return SetupRepoResult(
|
|
1572
|
+
success=False,
|
|
1573
|
+
message=f"Setup timed out: {e}",
|
|
1574
|
+
work_dir=str(work_path),
|
|
1575
|
+
steps_completed=steps_completed,
|
|
1576
|
+
venv_path=venv_path,
|
|
1577
|
+
dependencies_installed=dependencies_installed,
|
|
1578
|
+
setup_scripts_run=setup_scripts_run,
|
|
1579
|
+
error="Operation timed out",
|
|
1580
|
+
).model_dump()
|
|
1581
|
+
|
|
1582
|
+
except Exception as e:
|
|
1583
|
+
return SetupRepoResult(
|
|
1584
|
+
success=False,
|
|
1585
|
+
message=f"Setup failed: {e}",
|
|
1586
|
+
work_dir=str(work_path),
|
|
1587
|
+
steps_completed=steps_completed,
|
|
1588
|
+
venv_path=venv_path,
|
|
1589
|
+
dependencies_installed=dependencies_installed,
|
|
1590
|
+
setup_scripts_run=setup_scripts_run,
|
|
1591
|
+
error=str(e),
|
|
1592
|
+
).model_dump()
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def plan_repo(
|
|
1596
|
+
work_dir: str | Path,
|
|
1597
|
+
goal: str,
|
|
1598
|
+
model: str = "opus",
|
|
1599
|
+
) -> dict[str, Any]:
|
|
1600
|
+
"""Create a plan for a repository without executing any work.
|
|
1601
|
+
|
|
1602
|
+
This is a plan-only mode that reads the codebase using read-only tools
|
|
1603
|
+
(Read, Glob, Grep, Bash) and outputs a structured plan with tasks and
|
|
1604
|
+
success criteria. No changes are made to the repository.
|
|
1605
|
+
|
|
1606
|
+
Use this after `clone_repo` and `setup_repo` to plan work before execution,
|
|
1607
|
+
or to get a plan for a new goal in an existing repository.
|
|
1608
|
+
|
|
1609
|
+
Args:
|
|
1610
|
+
work_dir: Path to the repository directory to plan for.
|
|
1611
|
+
goal: The goal/task description to plan for.
|
|
1612
|
+
model: Model to use for planning (default: "opus" for best quality).
|
|
1613
|
+
|
|
1614
|
+
Returns:
|
|
1615
|
+
Dictionary containing planning result with success status, plan, and criteria.
|
|
1616
|
+
"""
|
|
1617
|
+
work_path = Path(work_dir).expanduser().resolve()
|
|
1618
|
+
|
|
1619
|
+
# Validate work directory
|
|
1620
|
+
if not work_path.exists():
|
|
1621
|
+
return PlanRepoResult(
|
|
1622
|
+
success=False,
|
|
1623
|
+
message=f"Directory does not exist: {work_path}",
|
|
1624
|
+
work_dir=str(work_path),
|
|
1625
|
+
goal=goal,
|
|
1626
|
+
error="Work directory not found",
|
|
1627
|
+
).model_dump()
|
|
1628
|
+
|
|
1629
|
+
if not work_path.is_dir():
|
|
1630
|
+
return PlanRepoResult(
|
|
1631
|
+
success=False,
|
|
1632
|
+
message=f"Path is not a directory: {work_path}",
|
|
1633
|
+
work_dir=str(work_path),
|
|
1634
|
+
goal=goal,
|
|
1635
|
+
error="Path is not a directory",
|
|
1636
|
+
).model_dump()
|
|
1637
|
+
|
|
1638
|
+
# Validate goal
|
|
1639
|
+
if not goal or not goal.strip():
|
|
1640
|
+
return PlanRepoResult(
|
|
1641
|
+
success=False,
|
|
1642
|
+
message="Goal is required",
|
|
1643
|
+
work_dir=str(work_path),
|
|
1644
|
+
error="Goal cannot be empty",
|
|
1645
|
+
).model_dump()
|
|
1646
|
+
|
|
1647
|
+
goal = goal.strip()
|
|
1648
|
+
|
|
1649
|
+
# Initialize state manager for this repo
|
|
1650
|
+
state_path = work_path / ".claude-task-master"
|
|
1651
|
+
state_manager = StateManager(state_dir=state_path)
|
|
1652
|
+
|
|
1653
|
+
# Check if task already exists
|
|
1654
|
+
if state_manager.exists():
|
|
1655
|
+
# Load existing state to check if we can replan
|
|
1656
|
+
try:
|
|
1657
|
+
existing_state = state_manager.load_state()
|
|
1658
|
+
# If task is in progress, don't overwrite
|
|
1659
|
+
if existing_state.status in ("planning", "working"):
|
|
1660
|
+
return PlanRepoResult(
|
|
1661
|
+
success=False,
|
|
1662
|
+
message=f"Task already in progress (status: {existing_state.status})",
|
|
1663
|
+
work_dir=str(work_path),
|
|
1664
|
+
goal=goal,
|
|
1665
|
+
run_id=existing_state.run_id,
|
|
1666
|
+
error="Cannot create new plan while task is active. Use clean_task first.",
|
|
1667
|
+
).model_dump()
|
|
1668
|
+
except Exception:
|
|
1669
|
+
pass # State exists but couldn't be loaded - will be overwritten
|
|
1670
|
+
|
|
1671
|
+
try:
|
|
1672
|
+
# Import credentials and agent here to avoid circular imports
|
|
1673
|
+
from claude_task_master.core.agent import AgentWrapper
|
|
1674
|
+
from claude_task_master.core.agent_models import ModelType
|
|
1675
|
+
from claude_task_master.core.credentials import CredentialManager
|
|
1676
|
+
|
|
1677
|
+
# Get valid access token
|
|
1678
|
+
cred_manager = CredentialManager()
|
|
1679
|
+
access_token = cred_manager.get_valid_token()
|
|
1680
|
+
|
|
1681
|
+
# Map model string to ModelType
|
|
1682
|
+
model_map = {
|
|
1683
|
+
"opus": ModelType.OPUS,
|
|
1684
|
+
"sonnet": ModelType.SONNET,
|
|
1685
|
+
"haiku": ModelType.HAIKU,
|
|
1686
|
+
}
|
|
1687
|
+
model_type = model_map.get(model.lower(), ModelType.OPUS)
|
|
1688
|
+
|
|
1689
|
+
# Initialize task state
|
|
1690
|
+
options = TaskOptions(
|
|
1691
|
+
auto_merge=False, # Plan-only mode
|
|
1692
|
+
max_sessions=1,
|
|
1693
|
+
pause_on_pr=True,
|
|
1694
|
+
)
|
|
1695
|
+
state = state_manager.initialize(goal=goal, model=model, options=options)
|
|
1696
|
+
|
|
1697
|
+
# Update status to planning
|
|
1698
|
+
state.status = "planning"
|
|
1699
|
+
state_manager.save_state(state)
|
|
1700
|
+
|
|
1701
|
+
# Create agent wrapper
|
|
1702
|
+
agent = AgentWrapper(
|
|
1703
|
+
access_token=access_token,
|
|
1704
|
+
model=model_type,
|
|
1705
|
+
working_dir=str(work_path),
|
|
1706
|
+
enable_safety_hooks=True,
|
|
1707
|
+
)
|
|
1708
|
+
|
|
1709
|
+
# Run planning phase (read-only)
|
|
1710
|
+
result = agent.run_planning_phase(goal=goal, context="")
|
|
1711
|
+
|
|
1712
|
+
# Extract plan and criteria
|
|
1713
|
+
plan = result.get("plan", "")
|
|
1714
|
+
criteria = result.get("criteria", "")
|
|
1715
|
+
|
|
1716
|
+
# Save plan and criteria to state
|
|
1717
|
+
if plan:
|
|
1718
|
+
state_manager.save_plan(plan)
|
|
1719
|
+
if criteria:
|
|
1720
|
+
state_manager.save_criteria(criteria)
|
|
1721
|
+
|
|
1722
|
+
# Update state to paused (plan complete, ready for work)
|
|
1723
|
+
state.status = "paused"
|
|
1724
|
+
state_manager.save_state(state)
|
|
1725
|
+
|
|
1726
|
+
return PlanRepoResult(
|
|
1727
|
+
success=True,
|
|
1728
|
+
message=f"Successfully created plan for: {goal}",
|
|
1729
|
+
work_dir=str(work_path),
|
|
1730
|
+
goal=goal,
|
|
1731
|
+
plan=plan,
|
|
1732
|
+
criteria=criteria,
|
|
1733
|
+
run_id=state.run_id,
|
|
1734
|
+
).model_dump()
|
|
1735
|
+
|
|
1736
|
+
except ImportError as e:
|
|
1737
|
+
return PlanRepoResult(
|
|
1738
|
+
success=False,
|
|
1739
|
+
message="Failed to import required modules",
|
|
1740
|
+
work_dir=str(work_path),
|
|
1741
|
+
goal=goal,
|
|
1742
|
+
error=f"Import error: {e}. Ensure claude-agent-sdk is installed.",
|
|
1743
|
+
).model_dump()
|
|
1744
|
+
|
|
1745
|
+
except Exception as e:
|
|
1746
|
+
# Try to update state to failed if possible
|
|
1747
|
+
try:
|
|
1748
|
+
if state_manager.exists():
|
|
1749
|
+
state = state_manager.load_state()
|
|
1750
|
+
state.status = "blocked"
|
|
1751
|
+
state_manager.save_state(state)
|
|
1752
|
+
except Exception:
|
|
1753
|
+
pass # State update failed, continue with error return
|
|
1754
|
+
|
|
1755
|
+
return PlanRepoResult(
|
|
1756
|
+
success=False,
|
|
1757
|
+
message=f"Planning failed: {e}",
|
|
1758
|
+
work_dir=str(work_path),
|
|
1759
|
+
goal=goal,
|
|
1760
|
+
error=str(e),
|
|
1761
|
+
).model_dump()
|