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,163 @@
1
+ """Message merger for combining multiple mailbox messages.
2
+
3
+ This module provides the MessageMerger class that consolidates multiple
4
+ messages into a single coherent change request for plan updates.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from .models import MailboxMessage
14
+
15
+
16
+ class MessageMerger:
17
+ """Merges multiple mailbox messages into a single change request.
18
+
19
+ The merger consolidates messages by:
20
+ 1. Sorting by priority (highest first)
21
+ 2. Grouping by sender (optional)
22
+ 3. Formatting into a structured change request
23
+
24
+ This is deterministic (no AI) to ensure consistent behavior.
25
+ """
26
+
27
+ def merge(self, messages: list[MailboxMessage]) -> str:
28
+ """Merge multiple messages into a single change request.
29
+
30
+ Args:
31
+ messages: List of MailboxMessage objects to merge.
32
+
33
+ Returns:
34
+ A formatted string suitable for plan update prompts.
35
+
36
+ Raises:
37
+ ValueError: If messages list is empty.
38
+ """
39
+ if not messages:
40
+ raise ValueError("Cannot merge empty message list")
41
+
42
+ if len(messages) == 1:
43
+ return self._format_single_message(messages[0])
44
+
45
+ return self._format_multiple_messages(messages)
46
+
47
+ def _format_single_message(self, message: MailboxMessage) -> str:
48
+ """Format a single message as a change request.
49
+
50
+ Args:
51
+ message: The message to format.
52
+
53
+ Returns:
54
+ Formatted change request string.
55
+ """
56
+ parts = [message.content]
57
+
58
+ # Add sender attribution if not anonymous
59
+ if message.sender != "anonymous":
60
+ parts.append(f"\n\n---\n*From: {message.sender}*")
61
+
62
+ return "".join(parts)
63
+
64
+ def _format_multiple_messages(self, messages: list[MailboxMessage]) -> str:
65
+ """Format multiple messages as a consolidated change request.
66
+
67
+ Args:
68
+ messages: List of messages to format (already sorted by priority).
69
+
70
+ Returns:
71
+ Formatted change request with all messages.
72
+ """
73
+ header = self._build_header(messages)
74
+ body = self._build_body(messages)
75
+ footer = self._build_footer(len(messages))
76
+
77
+ return f"{header}\n\n{body}\n\n{footer}"
78
+
79
+ def _build_header(self, messages: list[MailboxMessage]) -> str:
80
+ """Build the header section for merged messages.
81
+
82
+ Args:
83
+ messages: List of messages.
84
+
85
+ Returns:
86
+ Header string with count and timestamp.
87
+ """
88
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
89
+ return (
90
+ f"**Consolidated Change Requests ({len(messages)} messages)**\n"
91
+ f"*Processed at: {timestamp}*"
92
+ )
93
+
94
+ def _build_body(self, messages: list[MailboxMessage]) -> str:
95
+ """Build the body section with all message contents.
96
+
97
+ Args:
98
+ messages: List of messages to include.
99
+
100
+ Returns:
101
+ Formatted body with numbered messages.
102
+ """
103
+ parts = []
104
+
105
+ for i, msg in enumerate(messages, 1):
106
+ priority_label = self._get_priority_label(msg.priority)
107
+ sender_info = f" (from {msg.sender})" if msg.sender != "anonymous" else ""
108
+
109
+ parts.append(f"### Request {i}{priority_label}{sender_info}\n\n{msg.content}")
110
+
111
+ return "\n\n---\n\n".join(parts)
112
+
113
+ def _get_priority_label(self, priority: int) -> str:
114
+ """Get a human-readable priority label.
115
+
116
+ Args:
117
+ priority: Priority value (0-3).
118
+
119
+ Returns:
120
+ Priority label string or empty string for normal priority.
121
+ """
122
+ labels = {
123
+ 0: " [LOW]",
124
+ 1: "", # Normal - no label
125
+ 2: " [HIGH]",
126
+ 3: " [URGENT]",
127
+ }
128
+ return labels.get(priority, "")
129
+
130
+ def _build_footer(self, count: int) -> str:
131
+ """Build the footer with instructions.
132
+
133
+ Args:
134
+ count: Number of messages.
135
+
136
+ Returns:
137
+ Footer string with instructions.
138
+ """
139
+ return (
140
+ f"**Please address ALL {count} change requests above in the plan update.**\n"
141
+ f"Prioritize URGENT requests first, then HIGH, then others.\n"
142
+ f"If requests conflict, prefer higher-priority requests."
143
+ )
144
+
145
+ def merge_to_single_content(self, messages: list[MailboxMessage]) -> str:
146
+ """Merge messages to just the content strings combined.
147
+
148
+ A simpler alternative to merge() that just concatenates content.
149
+
150
+ Args:
151
+ messages: List of messages.
152
+
153
+ Returns:
154
+ Combined content string.
155
+ """
156
+ if not messages:
157
+ raise ValueError("Cannot merge empty message list")
158
+
159
+ if len(messages) == 1:
160
+ return messages[0].content
161
+
162
+ contents = [msg.content for msg in messages]
163
+ return "\n\n---\n\n".join(contents)
@@ -0,0 +1,95 @@
1
+ """Pydantic models for the mailbox system.
2
+
3
+ This module defines the data models used by the mailbox:
4
+ - MailboxMessage: A single message with metadata
5
+ - MailboxState: The overall mailbox state for persistence
6
+ - MessagePreview: A shortened version for status display
7
+ """
8
+
9
+ from datetime import datetime
10
+ from enum import IntEnum
11
+ from typing import Any
12
+ from uuid import uuid4
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ class Priority(IntEnum):
18
+ """Message priority levels.
19
+
20
+ Higher values indicate higher priority.
21
+ Messages with higher priority are processed first.
22
+ """
23
+
24
+ LOW = 0
25
+ NORMAL = 1
26
+ HIGH = 2
27
+ URGENT = 3
28
+
29
+
30
+ class MailboxMessage(BaseModel):
31
+ """A message in the mailbox.
32
+
33
+ Attributes:
34
+ id: Unique message identifier (auto-generated UUID).
35
+ sender: Identifier of the message sender.
36
+ content: The message content/body.
37
+ priority: Message priority level (higher = more important).
38
+ timestamp: When the message was created.
39
+ metadata: Optional additional metadata.
40
+ """
41
+
42
+ id: str = Field(default_factory=lambda: str(uuid4()))
43
+ sender: str = "anonymous"
44
+ content: str
45
+ priority: Priority = Priority.NORMAL
46
+ timestamp: datetime = Field(default_factory=datetime.now)
47
+ metadata: dict[str, Any] = Field(default_factory=dict)
48
+
49
+ def to_preview(self, max_length: int = 100) -> "MessagePreview":
50
+ """Convert to a preview for status display.
51
+
52
+ Args:
53
+ max_length: Maximum length of the content preview.
54
+
55
+ Returns:
56
+ MessagePreview with truncated content.
57
+ """
58
+ preview_content = self.content
59
+ if len(preview_content) > max_length:
60
+ preview_content = preview_content[: max_length - 3] + "..."
61
+
62
+ return MessagePreview(
63
+ id=self.id,
64
+ sender=self.sender,
65
+ content_preview=preview_content,
66
+ priority=self.priority,
67
+ timestamp=self.timestamp,
68
+ )
69
+
70
+
71
+ class MessagePreview(BaseModel):
72
+ """A shortened preview of a message.
73
+
74
+ Used in status responses to avoid sending full message content.
75
+ """
76
+
77
+ id: str
78
+ sender: str
79
+ content_preview: str
80
+ priority: Priority
81
+ timestamp: datetime
82
+
83
+
84
+ class MailboxState(BaseModel):
85
+ """Persistent state of the mailbox.
86
+
87
+ Attributes:
88
+ messages: List of messages currently in the mailbox.
89
+ last_checked: When the mailbox was last checked/processed.
90
+ total_messages_received: Total count of messages ever received.
91
+ """
92
+
93
+ messages: list[MailboxMessage] = Field(default_factory=list)
94
+ last_checked: datetime | None = None
95
+ total_messages_received: int = 0
@@ -0,0 +1,209 @@
1
+ """Mailbox storage for persistent message storage.
2
+
3
+ This module provides the MailboxStorage class that handles:
4
+ - Adding messages to the mailbox
5
+ - Retrieving messages (with optional clearing)
6
+ - Atomic file operations for safe concurrent access
7
+ - Persistence to .claude-task-master/mailbox.json
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import shutil
14
+ import tempfile
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ from pydantic import ValidationError
20
+
21
+ from .models import MailboxMessage, MailboxState, Priority
22
+
23
+ if TYPE_CHECKING:
24
+ from typing import Any
25
+
26
+
27
+ class MailboxStorageError(Exception):
28
+ """Base exception for mailbox storage errors."""
29
+
30
+ pass
31
+
32
+
33
+ class MailboxStorage:
34
+ """Manages mailbox message storage.
35
+
36
+ Persists messages to a JSON file with atomic writes for safety.
37
+ Supports concurrent access through file-based locking.
38
+
39
+ Attributes:
40
+ storage_path: Path to the mailbox.json file.
41
+ """
42
+
43
+ def __init__(self, state_dir: Path | None = None):
44
+ """Initialize mailbox storage.
45
+
46
+ Args:
47
+ state_dir: Directory for state files. Defaults to .claude-task-master.
48
+ """
49
+ self.state_dir = state_dir or Path(".claude-task-master")
50
+ self.storage_path = self.state_dir / "mailbox.json"
51
+
52
+ def _ensure_dir(self) -> None:
53
+ """Ensure the state directory exists."""
54
+ self.state_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ def _load_state(self) -> MailboxState:
57
+ """Load the current mailbox state from disk.
58
+
59
+ Returns:
60
+ MailboxState with current messages, or empty state if file doesn't exist.
61
+ """
62
+ if not self.storage_path.exists():
63
+ return MailboxState()
64
+
65
+ try:
66
+ with open(self.storage_path) as f:
67
+ data = json.load(f)
68
+ return MailboxState(**data)
69
+ except (json.JSONDecodeError, ValidationError):
70
+ # Corrupted file - return empty state
71
+ return MailboxState()
72
+
73
+ def _save_state(self, state: MailboxState) -> None:
74
+ """Save mailbox state atomically.
75
+
76
+ Uses temp file + rename for atomic writes.
77
+
78
+ Args:
79
+ state: The MailboxState to save.
80
+ """
81
+ self._ensure_dir()
82
+
83
+ # Write to temp file, then rename for atomicity
84
+ fd, temp_path = tempfile.mkstemp(dir=self.state_dir, prefix=".mailbox_", suffix=".json")
85
+ try:
86
+ with open(fd, "w") as f:
87
+ # Use model_dump with mode='json' for proper datetime serialization
88
+ json.dump(state.model_dump(mode="json"), f, indent=2)
89
+ shutil.move(temp_path, self.storage_path)
90
+ except Exception:
91
+ # Clean up temp file on error
92
+ try:
93
+ Path(temp_path).unlink()
94
+ except Exception:
95
+ pass
96
+ raise
97
+
98
+ def add_message(
99
+ self,
100
+ content: str,
101
+ sender: str = "anonymous",
102
+ priority: int | Priority = Priority.NORMAL,
103
+ metadata: dict[str, Any] | None = None,
104
+ ) -> str:
105
+ """Add a message to the mailbox.
106
+
107
+ Args:
108
+ content: The message content.
109
+ sender: Identifier of the sender (default: "anonymous").
110
+ priority: Message priority (0=low, 1=normal, 2=high, 3=urgent).
111
+ metadata: Optional additional metadata dict.
112
+
113
+ Returns:
114
+ The ID of the created message.
115
+ """
116
+ # Convert int to Priority enum if needed
117
+ if isinstance(priority, int):
118
+ priority = Priority(priority)
119
+
120
+ message = MailboxMessage(
121
+ content=content,
122
+ sender=sender,
123
+ priority=priority,
124
+ metadata=metadata or {},
125
+ )
126
+
127
+ state = self._load_state()
128
+ state.messages.append(message)
129
+ state.total_messages_received += 1
130
+ self._save_state(state)
131
+
132
+ return message.id
133
+
134
+ def get_messages(self) -> list[MailboxMessage]:
135
+ """Get all messages without removing them.
136
+
137
+ Returns:
138
+ List of messages sorted by priority (highest first), then timestamp.
139
+ """
140
+ state = self._load_state()
141
+ # Sort by priority (descending) then timestamp (ascending)
142
+ return sorted(state.messages, key=lambda m: (-m.priority, m.timestamp))
143
+
144
+ def get_and_clear(self) -> list[MailboxMessage]:
145
+ """Get all messages and remove them atomically.
146
+
147
+ This is the primary method for processing messages - it ensures
148
+ no message is lost or processed twice.
149
+
150
+ Returns:
151
+ List of messages sorted by priority (highest first), then timestamp.
152
+ """
153
+ state = self._load_state()
154
+ messages = sorted(state.messages, key=lambda m: (-m.priority, m.timestamp))
155
+
156
+ # Clear messages and update last_checked
157
+ state.messages = []
158
+ state.last_checked = datetime.now()
159
+ self._save_state(state)
160
+
161
+ return messages
162
+
163
+ def clear(self) -> int:
164
+ """Clear all messages from the mailbox.
165
+
166
+ Returns:
167
+ The number of messages that were removed.
168
+ """
169
+ state = self._load_state()
170
+ count = len(state.messages)
171
+
172
+ state.messages = []
173
+ state.last_checked = datetime.now()
174
+ self._save_state(state)
175
+
176
+ return count
177
+
178
+ def count(self) -> int:
179
+ """Get the number of messages in the mailbox.
180
+
181
+ Returns:
182
+ Number of pending messages.
183
+ """
184
+ state = self._load_state()
185
+ return len(state.messages)
186
+
187
+ def get_status(self) -> dict[str, Any]:
188
+ """Get mailbox status information.
189
+
190
+ Returns:
191
+ Dict with count, previews, last_checked, and total_received.
192
+ """
193
+ state = self._load_state()
194
+ messages = sorted(state.messages, key=lambda m: (-m.priority, m.timestamp))
195
+
196
+ return {
197
+ "count": len(messages),
198
+ "previews": [m.to_preview().model_dump(mode="json") for m in messages],
199
+ "last_checked": (state.last_checked.isoformat() if state.last_checked else None),
200
+ "total_messages_received": state.total_messages_received,
201
+ }
202
+
203
+ def exists(self) -> bool:
204
+ """Check if the mailbox storage file exists.
205
+
206
+ Returns:
207
+ True if mailbox.json exists.
208
+ """
209
+ return self.storage_path.exists()
@@ -63,6 +63,12 @@ PauseTaskResult = tools.PauseTaskResult
63
63
  StopTaskResult = tools.StopTaskResult
64
64
  ResumeTaskResult = tools.ResumeTaskResult
65
65
  UpdateConfigResult = tools.UpdateConfigResult
66
+ SendMessageResult = tools.SendMessageResult
67
+ MailboxStatusResult = tools.MailboxStatusResult
68
+ ClearMailboxResult = tools.ClearMailboxResult
69
+ CloneRepoResult = tools.CloneRepoResult
70
+ SetupRepoResult = tools.SetupRepoResult
71
+ PlanRepoResult = tools.PlanRepoResult
66
72
 
67
73
 
68
74
  # =============================================================================
@@ -348,6 +354,183 @@ def create_server(
348
354
  state_dir=state_dir,
349
355
  )
350
356
 
357
+ # =============================================================================
358
+ # Mailbox Tool Wrappers
359
+ # =============================================================================
360
+
361
+ @mcp.tool()
362
+ def send_message(
363
+ content: str,
364
+ sender: str = "anonymous",
365
+ priority: int = 1,
366
+ state_dir: str | None = None,
367
+ ) -> dict[str, Any]:
368
+ """Send a message to the claudetm mailbox.
369
+
370
+ Messages are processed after the current task completes. Multiple messages
371
+ are merged into a single change request that updates the plan before
372
+ continuing work.
373
+
374
+ Use this to send instructions, feedback, or change requests to a running
375
+ claudetm instance from external systems or other AI agents.
376
+
377
+ Args:
378
+ content: The message content describing the change request.
379
+ sender: Identifier of the sender (default: "anonymous").
380
+ priority: Message priority - 0=low, 1=normal, 2=high, 3=urgent.
381
+ state_dir: Optional custom state directory path.
382
+
383
+ Returns:
384
+ Dictionary containing the message_id on success, or error info.
385
+
386
+ Example:
387
+ send_message("Please also add unit tests for the new feature")
388
+ send_message("URGENT: Fix the security bug first", priority=3)
389
+ """
390
+ return tools.send_message(work_dir, content, sender, priority, state_dir)
391
+
392
+ @mcp.tool()
393
+ def check_mailbox(state_dir: str | None = None) -> dict[str, Any]:
394
+ """Check the status of the claudetm mailbox.
395
+
396
+ Returns the number of pending messages and previews of each.
397
+ Use this to see what messages are waiting to be processed.
398
+
399
+ Args:
400
+ state_dir: Optional custom state directory path.
401
+
402
+ Returns:
403
+ Dictionary containing mailbox status with message previews.
404
+ """
405
+ return tools.check_mailbox(work_dir, state_dir)
406
+
407
+ @mcp.tool()
408
+ def clear_mailbox(state_dir: str | None = None) -> dict[str, Any]:
409
+ """Clear all messages from the claudetm mailbox.
410
+
411
+ Use this to discard all pending messages without processing them.
412
+
413
+ Args:
414
+ state_dir: Optional custom state directory path.
415
+
416
+ Returns:
417
+ Dictionary indicating success and number of messages cleared.
418
+ """
419
+ return tools.clear_mailbox(work_dir, state_dir)
420
+
421
+ # =============================================================================
422
+ # Repo Setup Tool Wrappers
423
+ # =============================================================================
424
+
425
+ @mcp.tool()
426
+ def clone_repo(
427
+ url: str,
428
+ target_dir: str | None = None,
429
+ branch: str | None = None,
430
+ ) -> dict[str, Any]:
431
+ """Clone a git repository to the workspace.
432
+
433
+ Clones the repository to ~/workspace/claude-task-master/{project-name}
434
+ by default, or to a custom target directory if specified. This is the
435
+ first step in setting up a new development environment for AI developers.
436
+
437
+ Args:
438
+ url: Git repository URL (HTTPS or SSH format).
439
+ Examples: https://github.com/user/repo.git or git@github.com:user/repo.git
440
+ target_dir: Optional custom target directory path. If not provided,
441
+ defaults to ~/workspace/claude-task-master/{repo-name}.
442
+ branch: Optional branch to checkout after cloning.
443
+
444
+ Returns:
445
+ Dictionary containing:
446
+ - success: Whether clone was successful
447
+ - message: Human-readable result message
448
+ - repo_url: The cloned repository URL
449
+ - target_dir: The directory where repo was cloned
450
+ - branch: The branch checked out (if specified)
451
+ - error: Error details on failure
452
+
453
+ Example:
454
+ clone_repo("https://github.com/anthropics/claude-code")
455
+ clone_repo("git@github.com:user/project.git", branch="develop")
456
+ clone_repo("https://github.com/user/project", target_dir="/custom/path")
457
+ """
458
+ return tools.clone_repo(url, target_dir, branch)
459
+
460
+ @mcp.tool()
461
+ def setup_repo(
462
+ repo_dir: str,
463
+ ) -> dict[str, Any]:
464
+ """Set up a cloned repository for development.
465
+
466
+ Detects the project type and performs appropriate setup:
467
+ - Creates virtual environment (for Python projects)
468
+ - Installs dependencies (pip, npm, pnpm, yarn, bun)
469
+ - Runs setup scripts (setup-hooks.sh, setup.sh, etc.)
470
+
471
+ Supports Python projects (pyproject.toml, setup.py, requirements.txt)
472
+ and Node.js projects (package.json). Uses uv for Python dependency
473
+ management when available, falling back to standard venv + pip.
474
+
475
+ Args:
476
+ repo_dir: Path to the cloned repository directory to set up.
477
+
478
+ Returns:
479
+ Dictionary containing:
480
+ - success: Whether setup completed successfully
481
+ - message: Human-readable result message
482
+ - work_dir: The directory that was set up
483
+ - steps_completed: List of completed setup steps
484
+ - venv_path: Path to virtual environment (Python projects)
485
+ - dependencies_installed: Whether dependencies were installed
486
+ - setup_scripts_run: List of setup scripts that were executed
487
+ - error: Error details on failure
488
+
489
+ Example:
490
+ setup_repo("/home/user/workspace/claude-task-master/my-project")
491
+ setup_repo("~/workspace/claude-task-master/python-app")
492
+ """
493
+ return tools.setup_repo(repo_dir)
494
+
495
+ @mcp.tool()
496
+ def plan_repo(
497
+ repo_dir: str,
498
+ goal: str,
499
+ model: str = "opus",
500
+ ) -> dict[str, Any]:
501
+ """Create a plan for a repository without executing any work.
502
+
503
+ This is a plan-only mode that reads the codebase using read-only tools
504
+ (Read, Glob, Grep) and outputs a structured plan with tasks and success
505
+ criteria. No changes are made to the repository.
506
+
507
+ Use this after `clone_repo` and `setup_repo` to plan work before execution,
508
+ or to get a plan for a new goal in an existing repository.
509
+
510
+ Args:
511
+ repo_dir: Path to the repository directory to plan for.
512
+ goal: The goal/task description to plan for. Be specific about
513
+ what you want to accomplish.
514
+ model: Model to use for planning (default: "opus" for best quality).
515
+ Options: "opus", "sonnet", "haiku".
516
+
517
+ Returns:
518
+ Dictionary containing:
519
+ - success: Whether planning completed successfully
520
+ - message: Human-readable result message
521
+ - work_dir: The directory that was planned for
522
+ - goal: The goal that was planned for
523
+ - plan: The generated task plan (markdown with checkboxes)
524
+ - criteria: Success criteria for verifying completion
525
+ - run_id: Unique identifier for this planning session
526
+ - error: Error details on failure
527
+
528
+ Example:
529
+ plan_repo("/home/user/workspace/project", "Add user authentication")
530
+ plan_repo("~/workspace/my-app", "Fix the login bug", model="sonnet")
531
+ """
532
+ return tools.plan_repo(repo_dir, goal, model)
533
+
351
534
  # =============================================================================
352
535
  # Resource Wrappers
353
536
  # =============================================================================