mcp-orbit 0.2.0__tar.gz
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.
- mcp_orbit-0.2.0/.gitignore +18 -0
- mcp_orbit-0.2.0/LICENSE +21 -0
- mcp_orbit-0.2.0/PKG-INFO +51 -0
- mcp_orbit-0.2.0/README.md +21 -0
- mcp_orbit-0.2.0/pyproject.toml +50 -0
- mcp_orbit-0.2.0/src/mcp_orbit/__init__.py +3 -0
- mcp_orbit-0.2.0/src/mcp_orbit/app.py +5 -0
- mcp_orbit-0.2.0/src/mcp_orbit/config.py +25 -0
- mcp_orbit-0.2.0/src/mcp_orbit/db.py +35 -0
- mcp_orbit-0.2.0/src/mcp_orbit/errors.py +85 -0
- mcp_orbit-0.2.0/src/mcp_orbit/helpers.py +194 -0
- mcp_orbit-0.2.0/src/mcp_orbit/iteration_log.py +333 -0
- mcp_orbit-0.2.0/src/mcp_orbit/models.py +131 -0
- mcp_orbit-0.2.0/src/mcp_orbit/orbit.py +461 -0
- mcp_orbit-0.2.0/src/mcp_orbit/server.py +31 -0
- mcp_orbit-0.2.0/src/mcp_orbit/templates/__init__.py +1 -0
- mcp_orbit-0.2.0/src/mcp_orbit/templates/context.md +33 -0
- mcp_orbit-0.2.0/src/mcp_orbit/templates/plan.md +39 -0
- mcp_orbit-0.2.0/src/mcp_orbit/templates/tasks.md +33 -0
- mcp_orbit-0.2.0/src/mcp_orbit/tools_docs.py +289 -0
- mcp_orbit-0.2.0/src/mcp_orbit/tools_iteration.py +167 -0
- mcp_orbit-0.2.0/src/mcp_orbit/tools_planning.py +473 -0
- mcp_orbit-0.2.0/src/mcp_orbit/tools_tasks.py +521 -0
- mcp_orbit-0.2.0/src/mcp_orbit/tools_tracking.py +287 -0
- mcp_orbit-0.2.0/tests/conftest.py +171 -0
- mcp_orbit-0.2.0/tests/test_errors.py +67 -0
- mcp_orbit-0.2.0/tests/test_helpers.py +38 -0
- mcp_orbit-0.2.0/tests/test_iteration_log.py +117 -0
- mcp_orbit-0.2.0/tests/test_models.py +63 -0
- mcp_orbit-0.2.0/tests/test_orbit_files.py +130 -0
- mcp_orbit-0.2.0/tests/test_orbit_pure.py +145 -0
- mcp_orbit-0.2.0/uv.lock +759 -0
mcp_orbit-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tomer Brami
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mcp_orbit-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-orbit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for the orbit Claude Code plugin: project management with time tracking
|
|
5
|
+
Project-URL: Homepage, https://github.com/tomerbr1/claude-orbit
|
|
6
|
+
Project-URL: Repository, https://github.com/tomerbr1/claude-orbit
|
|
7
|
+
Project-URL: Issues, https://github.com/tomerbr1/claude-orbit/issues
|
|
8
|
+
Author-email: Tomer Brami <tomerbrami@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude-code,mcp,model-context-protocol,orbit,task-tracking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: mcp>=1.0.0
|
|
23
|
+
Requires-Dist: orbit-db>=1.0.0
|
|
24
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# MCP Orbit Server
|
|
32
|
+
|
|
33
|
+
MCP server for orbit project management with time tracking.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
mcp-orbit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or via uvx:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx --from ~/.claude/plugins/local/orbit/mcp-server mcp-orbit
|
|
51
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MCP Orbit Server
|
|
2
|
+
|
|
3
|
+
MCP server for orbit project management with time tracking.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
mcp-orbit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or via uvx:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uvx --from ~/.claude/plugins/local/orbit/mcp-server mcp-orbit
|
|
21
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-orbit"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "MCP server for the orbit Claude Code plugin: project management with time tracking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Tomer Brami", email = "tomerbrami@gmail.com"}
|
|
11
|
+
]
|
|
12
|
+
keywords = ["claude-code", "orbit", "mcp", "model-context-protocol", "task-tracking"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"orbit-db>=1.0.0",
|
|
26
|
+
"mcp>=1.0.0",
|
|
27
|
+
"pydantic>=2.0.0",
|
|
28
|
+
"pydantic-settings>=2.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7.0.0",
|
|
34
|
+
"pytest-asyncio>=0.21.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/tomerbr1/claude-orbit"
|
|
39
|
+
Repository = "https://github.com/tomerbr1/claude-orbit"
|
|
40
|
+
Issues = "https://github.com/tomerbr1/claude-orbit/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
mcp-orbit = "mcp_orbit.server:main"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["hatchling"]
|
|
47
|
+
build-backend = "hatchling.build"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/mcp_orbit"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Configuration for the orbit MCP server."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
"""Server configuration from environment variables."""
|
|
10
|
+
|
|
11
|
+
# Path to the task database
|
|
12
|
+
db_path: Path = Path.home() / ".claude" / "tasks.db"
|
|
13
|
+
|
|
14
|
+
# Centralized orbit root directory
|
|
15
|
+
orbit_root: Path = Path.home() / ".claude" / "orbit"
|
|
16
|
+
|
|
17
|
+
# Active and completed subdirectory names
|
|
18
|
+
active_dir_name: str = "active"
|
|
19
|
+
completed_dir_name: str = "completed"
|
|
20
|
+
|
|
21
|
+
class Config:
|
|
22
|
+
env_prefix = "ORBIT_"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
settings = Settings()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Database wrapper for orbit_db."""
|
|
2
|
+
|
|
3
|
+
from orbit_db import Repository, Task, TaskDB, TaskStatus
|
|
4
|
+
|
|
5
|
+
from .config import settings
|
|
6
|
+
|
|
7
|
+
# Module-level singleton
|
|
8
|
+
_db: TaskDB | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_db() -> TaskDB:
|
|
12
|
+
"""Get or create the TaskDB singleton."""
|
|
13
|
+
global _db
|
|
14
|
+
if _db is None:
|
|
15
|
+
_db = TaskDB(db_path=settings.db_path)
|
|
16
|
+
_db.initialize()
|
|
17
|
+
return _db
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def repo_to_dict(repo: Repository) -> dict:
|
|
21
|
+
"""Convert Repository dataclass to dict."""
|
|
22
|
+
from dataclasses import asdict
|
|
23
|
+
|
|
24
|
+
return asdict(repo)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Re-export for convenience
|
|
28
|
+
__all__ = [
|
|
29
|
+
"get_db",
|
|
30
|
+
"repo_to_dict",
|
|
31
|
+
"Task",
|
|
32
|
+
"Repository",
|
|
33
|
+
"TaskDB",
|
|
34
|
+
"TaskStatus",
|
|
35
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Error codes and handling for the orbit MCP server."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ErrorCode(str, Enum):
|
|
8
|
+
"""Standard error codes for structured error responses."""
|
|
9
|
+
|
|
10
|
+
TASK_NOT_FOUND = "TASK_NOT_FOUND"
|
|
11
|
+
REPO_NOT_FOUND = "REPO_NOT_FOUND"
|
|
12
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
13
|
+
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
14
|
+
DB_ERROR = "DB_ERROR"
|
|
15
|
+
PERMISSION_ERROR = "PERMISSION_ERROR"
|
|
16
|
+
INVALID_STATE = "INVALID_STATE"
|
|
17
|
+
OPERATION_FAILED = "OPERATION_FAILED"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OrbitError(Exception):
|
|
21
|
+
"""Base exception for orbit errors with structured response."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, code: ErrorCode, message: str, details: dict[str, Any] | None = None
|
|
25
|
+
):
|
|
26
|
+
self.code = code
|
|
27
|
+
self.message = message
|
|
28
|
+
self.details = details or {}
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
"""Convert to dictionary for MCP response."""
|
|
33
|
+
return {
|
|
34
|
+
"error": True,
|
|
35
|
+
"code": self.code.value,
|
|
36
|
+
"message": self.message,
|
|
37
|
+
"details": self.details,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TaskNotFoundError(OrbitError):
|
|
42
|
+
"""Task not found in database."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, task_id: int | str, message: str | None = None):
|
|
45
|
+
super().__init__(
|
|
46
|
+
ErrorCode.TASK_NOT_FOUND,
|
|
47
|
+
message or f"Task not found: {task_id}",
|
|
48
|
+
{"task_id": task_id},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OrbitFileNotFoundError(OrbitError):
|
|
53
|
+
"""File not found on filesystem."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, path: str, message: str | None = None):
|
|
56
|
+
super().__init__(
|
|
57
|
+
ErrorCode.FILE_NOT_FOUND,
|
|
58
|
+
message or f"File not found: {path}",
|
|
59
|
+
{"path": path},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ValidationError(OrbitError):
|
|
64
|
+
"""Input validation failed."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, message: str, field: str | None = None):
|
|
67
|
+
details = {"field": field} if field else {}
|
|
68
|
+
super().__init__(ErrorCode.VALIDATION_ERROR, message, details)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InvalidStateError(OrbitError):
|
|
72
|
+
"""Operation invalid for current state."""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
message: str,
|
|
77
|
+
current_state: str | None = None,
|
|
78
|
+
expected_state: str | None = None,
|
|
79
|
+
):
|
|
80
|
+
details = {}
|
|
81
|
+
if current_state:
|
|
82
|
+
details["current_state"] = current_state
|
|
83
|
+
if expected_state:
|
|
84
|
+
details["expected_state"] = expected_state
|
|
85
|
+
super().__init__(ErrorCode.INVALID_STATE, message, details)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Shared helper functions used across tool modules."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import orbit
|
|
7
|
+
from .config import settings
|
|
8
|
+
from .db import Task, get_db
|
|
9
|
+
from .errors import TaskNotFoundError, ValidationError
|
|
10
|
+
from .models import TaskDetail, TaskProgress, TaskSummary
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_path(
|
|
16
|
+
path: str, field_name: str = "path", must_be_under: Path | None = None
|
|
17
|
+
) -> Path:
|
|
18
|
+
"""Validate and resolve a filesystem path.
|
|
19
|
+
|
|
20
|
+
Checks for empty strings and null bytes, then resolves the path.
|
|
21
|
+
If must_be_under is provided, verifies the resolved path is contained
|
|
22
|
+
within that directory.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValidationError: If path is empty, contains null bytes, or resolves
|
|
26
|
+
outside the required root directory.
|
|
27
|
+
"""
|
|
28
|
+
if not path or not path.strip():
|
|
29
|
+
raise ValidationError(f"{field_name} cannot be empty", field=field_name)
|
|
30
|
+
if "\x00" in path:
|
|
31
|
+
raise ValidationError(f"{field_name} contains null bytes", field=field_name)
|
|
32
|
+
resolved = Path(path).resolve()
|
|
33
|
+
if must_be_under is not None:
|
|
34
|
+
root = must_be_under.resolve()
|
|
35
|
+
if resolved != root and not str(resolved).startswith(str(root) + "/"):
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
f"{field_name} must be within {root}", field=field_name
|
|
38
|
+
)
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_task_dir(
|
|
43
|
+
db, task_id: int | None, task_name: str | None
|
|
44
|
+
) -> tuple[Path, str]:
|
|
45
|
+
"""Resolve task directory and name from task_id or task_name.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (task_dir, task_name).
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
TaskNotFoundError: If task cannot be found.
|
|
52
|
+
"""
|
|
53
|
+
task = None
|
|
54
|
+
if task_id:
|
|
55
|
+
task = db.get_task(task_id)
|
|
56
|
+
elif task_name:
|
|
57
|
+
task = db.get_task_by_name(task_name)
|
|
58
|
+
|
|
59
|
+
if not task:
|
|
60
|
+
identifier = task_id if task_id is not None else (task_name or "unknown")
|
|
61
|
+
raise TaskNotFoundError(identifier)
|
|
62
|
+
|
|
63
|
+
task_dir = settings.orbit_root / task.full_path
|
|
64
|
+
return task_dir, task.name
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _task_to_summary(
|
|
68
|
+
task: Task, db=None, time_seconds: int | None = None
|
|
69
|
+
) -> TaskSummary:
|
|
70
|
+
"""Convert a Task to TaskSummary with time info.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
task: Task object to convert.
|
|
74
|
+
db: Database instance (auto-resolved if None).
|
|
75
|
+
time_seconds: Pre-fetched time in seconds. If None, fetches from DB.
|
|
76
|
+
"""
|
|
77
|
+
if db is None:
|
|
78
|
+
db = get_db()
|
|
79
|
+
|
|
80
|
+
# Get time tracking info (use pre-fetched if available)
|
|
81
|
+
if time_seconds is None:
|
|
82
|
+
time_seconds = db.get_task_time(task.id)
|
|
83
|
+
time_formatted = db.format_duration(time_seconds)
|
|
84
|
+
|
|
85
|
+
# Get effective last updated (uses file mtime if more recent)
|
|
86
|
+
effective_last = db.get_effective_last_updated(task)
|
|
87
|
+
last_worked_ago = db.format_time_ago(effective_last)
|
|
88
|
+
|
|
89
|
+
# Get repo info if available
|
|
90
|
+
repo_name = None
|
|
91
|
+
repo_path = None
|
|
92
|
+
if task.repo_id:
|
|
93
|
+
repo = db.get_repo(task.repo_id)
|
|
94
|
+
if repo:
|
|
95
|
+
repo_name = repo.short_name
|
|
96
|
+
repo_path = repo.path
|
|
97
|
+
|
|
98
|
+
# Check if orbit files exist
|
|
99
|
+
has_orbit_files = False
|
|
100
|
+
if task.full_path:
|
|
101
|
+
task_dir = settings.orbit_root / task.full_path
|
|
102
|
+
has_orbit_files = task_dir.exists() and any(
|
|
103
|
+
(task_dir / f).exists()
|
|
104
|
+
for f in [
|
|
105
|
+
f"{task.name}-context.md",
|
|
106
|
+
f"{task.name}-tasks.md",
|
|
107
|
+
f"{task.name}-plan.md",
|
|
108
|
+
"context.md",
|
|
109
|
+
"tasks.md",
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return TaskSummary(
|
|
114
|
+
id=task.id,
|
|
115
|
+
name=task.name,
|
|
116
|
+
status=task.status,
|
|
117
|
+
type=task.task_type,
|
|
118
|
+
repo_name=repo_name,
|
|
119
|
+
repo_path=repo_path,
|
|
120
|
+
jira_key=task.jira_key,
|
|
121
|
+
tags=task.tags,
|
|
122
|
+
time_total_seconds=time_seconds,
|
|
123
|
+
time_formatted=time_formatted,
|
|
124
|
+
last_worked_on=effective_last,
|
|
125
|
+
last_worked_ago=last_worked_ago,
|
|
126
|
+
has_orbit_files=has_orbit_files,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _task_to_detail(
|
|
131
|
+
task: Task, include_subtasks: bool = True, include_updates: bool = True
|
|
132
|
+
) -> TaskDetail:
|
|
133
|
+
"""Convert a Task to TaskDetail with full information."""
|
|
134
|
+
db = get_db()
|
|
135
|
+
|
|
136
|
+
# Get base summary fields
|
|
137
|
+
summary = _task_to_summary(task, db)
|
|
138
|
+
|
|
139
|
+
# Parse progress from orbit files
|
|
140
|
+
progress = None
|
|
141
|
+
if task.full_path:
|
|
142
|
+
progress = _parse_task_progress(
|
|
143
|
+
settings.orbit_root / task.full_path, task.name
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get subtasks if this is a parent task
|
|
147
|
+
subtasks = []
|
|
148
|
+
if include_subtasks:
|
|
149
|
+
hierarchy = db.get_active_tasks_hierarchical(task.repo_id)
|
|
150
|
+
if task.id in hierarchy.get("children", {}):
|
|
151
|
+
for subtask in hierarchy["children"][task.id]:
|
|
152
|
+
subtasks.append(_task_to_summary(subtask, db))
|
|
153
|
+
|
|
154
|
+
# Get recent updates for non-coding tasks
|
|
155
|
+
recent_updates = []
|
|
156
|
+
if include_updates and task.task_type == "non-coding":
|
|
157
|
+
recent_updates = db.get_task_updates(task.id, limit=5)
|
|
158
|
+
|
|
159
|
+
return TaskDetail(
|
|
160
|
+
**summary.model_dump(by_alias=True),
|
|
161
|
+
full_path=task.full_path,
|
|
162
|
+
parent_id=task.parent_id,
|
|
163
|
+
branch=task.branch,
|
|
164
|
+
pr_url=task.pr_url,
|
|
165
|
+
created_at=task.created_at,
|
|
166
|
+
updated_at=task.updated_at,
|
|
167
|
+
completed_at=task.completed_at,
|
|
168
|
+
progress=progress,
|
|
169
|
+
subtasks=subtasks,
|
|
170
|
+
recent_updates=recent_updates,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _parse_task_progress(task_dir: Path, task_name: str) -> TaskProgress | None:
|
|
175
|
+
"""Parse progress from the tasks.md file."""
|
|
176
|
+
if not task_dir.exists():
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# Try both naming conventions
|
|
180
|
+
tasks_files = [
|
|
181
|
+
task_dir / f"{task_name}-tasks.md",
|
|
182
|
+
task_dir / "tasks.md",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
for tasks_file in tasks_files:
|
|
186
|
+
if tasks_file.exists():
|
|
187
|
+
try:
|
|
188
|
+
content = tasks_file.read_text()
|
|
189
|
+
return orbit.parse_task_progress(content)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.warning(f"Failed to parse {tasks_file}: {e}")
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
return None
|