mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,338 @@
1
+ """Unified milestone management tools (v2.0.0).
2
+
3
+ This module implements milestone management through a single unified `milestone()` interface.
4
+
5
+ Version 2.0.0 changes:
6
+ - Single `milestone()` function is the exposed MCP tool
7
+ - All operations accessible via milestone(action="create"|"get"|"list"|"update"|"delete"|"get_issues")
8
+ - Follows the pattern from ticket() and hierarchy() unified tools
9
+ """
10
+
11
+ import logging
12
+ from datetime import datetime
13
+ from typing import Any, Literal
14
+
15
+ from ....core.adapter import BaseAdapter
16
+ from ..server_sdk import get_adapter, mcp
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Sentinel value to distinguish between "parameter not provided" and "explicitly None"
21
+ _UNSET = object()
22
+
23
+
24
+ def _build_adapter_metadata(
25
+ adapter: BaseAdapter,
26
+ milestone_id: str | None = None,
27
+ ) -> dict[str, Any]:
28
+ """Build adapter metadata for MCP responses.
29
+
30
+ Args:
31
+ adapter: The adapter that handled the operation
32
+ milestone_id: Optional milestone ID to include in metadata
33
+
34
+ Returns:
35
+ Dictionary with adapter metadata fields
36
+
37
+ """
38
+ metadata = {
39
+ "adapter": adapter.adapter_type,
40
+ "adapter_name": adapter.adapter_display_name,
41
+ }
42
+
43
+ if milestone_id:
44
+ metadata["milestone_id"] = milestone_id
45
+
46
+ return metadata
47
+
48
+
49
+ @mcp.tool()
50
+ async def milestone(
51
+ action: Literal["create", "get", "list", "update", "delete", "get_issues"],
52
+ # Entity identification
53
+ milestone_id: str | None = None,
54
+ # Creation/Update parameters
55
+ name: str | None = None,
56
+ target_date: str | None = None,
57
+ labels: list[str] | None = None,
58
+ description: str = "",
59
+ state: str | None = None,
60
+ # List/filter parameters
61
+ project_id: str | None = None,
62
+ ) -> dict[str, Any]:
63
+ """Unified milestone management tool for cross-platform milestone support.
64
+
65
+ A milestone is a list of labels with target dates, into which issues can be grouped.
66
+
67
+ Consolidates all milestone operations into a single interface with progress tracking.
68
+
69
+ Args:
70
+ action: Operation to perform:
71
+ - "create": Create new milestone
72
+ - "get": Get milestone by ID with progress
73
+ - "list": List milestones (optionally filtered)
74
+ - "update": Update milestone properties
75
+ - "delete": Delete milestone
76
+ - "get_issues": Get issues in milestone
77
+ milestone_id: Milestone ID (required for get, update, delete, get_issues)
78
+ name: Milestone name (required for create)
79
+ target_date: Target completion date (ISO format: YYYY-MM-DD)
80
+ labels: Labels that define this milestone (user's definition)
81
+ description: Milestone description
82
+ state: Milestone state (open, active, completed, closed)
83
+ project_id: Project/repository filter for list operations
84
+
85
+ Returns:
86
+ Operation results with status, data, and metadata
87
+
88
+ Raises:
89
+ ValueError: If action is invalid or required parameters missing
90
+
91
+ Examples:
92
+ # Create milestone
93
+ await milestone(
94
+ action="create",
95
+ name="v2.1.0 Release",
96
+ target_date="2025-12-31",
97
+ labels=["v2.1", "release"]
98
+ )
99
+
100
+ # Get milestone with progress
101
+ await milestone(action="get", milestone_id="milestone-123")
102
+
103
+ # List active milestones
104
+ await milestone(action="list", state="active")
105
+
106
+ # Update milestone
107
+ await milestone(
108
+ action="update",
109
+ milestone_id="milestone-123",
110
+ state="completed"
111
+ )
112
+
113
+ # Get issues in milestone
114
+ await milestone(action="get_issues", milestone_id="milestone-123")
115
+
116
+ # Delete milestone
117
+ await milestone(action="delete", milestone_id="milestone-123")
118
+
119
+ """
120
+ try:
121
+ # Get adapter from registry
122
+ adapter = get_adapter()
123
+
124
+ # Validate action
125
+ valid_actions = ["create", "get", "list", "update", "delete", "get_issues"]
126
+ if action not in valid_actions:
127
+ return {
128
+ "status": "error",
129
+ "error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
130
+ }
131
+
132
+ # Route to appropriate handler
133
+ if action == "create":
134
+ return await _handle_create(
135
+ adapter, name, target_date, labels, description, project_id
136
+ )
137
+ elif action == "get":
138
+ return await _handle_get(adapter, milestone_id)
139
+ elif action == "list":
140
+ return await _handle_list(adapter, project_id, state)
141
+ elif action == "update":
142
+ return await _handle_update(
143
+ adapter, milestone_id, name, target_date, state, labels, description
144
+ )
145
+ elif action == "delete":
146
+ return await _handle_delete(adapter, milestone_id)
147
+ elif action == "get_issues":
148
+ return await _handle_get_issues(adapter, milestone_id, state)
149
+
150
+ except Exception as e:
151
+ logger.exception("Milestone operation failed")
152
+ return {
153
+ "status": "error",
154
+ "error": f"Milestone operation failed: {str(e)}",
155
+ "action": action,
156
+ "milestone_id": milestone_id,
157
+ }
158
+
159
+
160
+ async def _handle_create(
161
+ adapter: BaseAdapter,
162
+ name: str | None,
163
+ target_date: str | None,
164
+ labels: list[str] | None,
165
+ description: str,
166
+ project_id: str | None,
167
+ ) -> dict[str, Any]:
168
+ """Handle milestone creation."""
169
+ if not name:
170
+ return {
171
+ "status": "error",
172
+ "error": "name is required for create action",
173
+ }
174
+
175
+ # Parse target_date if provided (expect date object, not string)
176
+ parsed_date = None
177
+ if target_date:
178
+ try:
179
+ # Try parsing as ISO date string
180
+ parsed_date = datetime.fromisoformat(target_date)
181
+ except ValueError:
182
+ return {
183
+ "status": "error",
184
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
185
+ }
186
+
187
+ milestone_obj = await adapter.milestone_create(
188
+ name=name,
189
+ target_date=parsed_date,
190
+ labels=labels or [],
191
+ description=description,
192
+ project_id=project_id,
193
+ )
194
+
195
+ return {
196
+ "status": "completed",
197
+ "message": f"Milestone '{name}' created successfully",
198
+ "milestone": milestone_obj.model_dump(),
199
+ "metadata": _build_adapter_metadata(adapter, milestone_obj.id),
200
+ }
201
+
202
+
203
+ async def _handle_get(adapter: BaseAdapter, milestone_id: str | None) -> dict[str, Any]:
204
+ """Handle milestone retrieval with progress calculation."""
205
+ if not milestone_id:
206
+ return {
207
+ "status": "error",
208
+ "error": "milestone_id is required for get action",
209
+ }
210
+
211
+ milestone_obj = await adapter.milestone_get(milestone_id)
212
+
213
+ if not milestone_obj:
214
+ return {
215
+ "status": "error",
216
+ "error": f"Milestone '{milestone_id}' not found",
217
+ }
218
+
219
+ return {
220
+ "status": "completed",
221
+ "milestone": milestone_obj.model_dump(),
222
+ "metadata": _build_adapter_metadata(adapter, milestone_id),
223
+ }
224
+
225
+
226
+ async def _handle_list(
227
+ adapter: BaseAdapter,
228
+ project_id: str | None,
229
+ state: str | None,
230
+ ) -> dict[str, Any]:
231
+ """Handle milestone listing with optional filters."""
232
+ milestones = await adapter.milestone_list(project_id=project_id, state=state)
233
+
234
+ return {
235
+ "status": "completed",
236
+ "message": f"Found {len(milestones)} milestone(s)",
237
+ "milestones": [m.model_dump() for m in milestones],
238
+ "count": len(milestones),
239
+ "metadata": _build_adapter_metadata(adapter),
240
+ }
241
+
242
+
243
+ async def _handle_update(
244
+ adapter: BaseAdapter,
245
+ milestone_id: str | None,
246
+ name: str | None,
247
+ target_date: str | None,
248
+ state: str | None,
249
+ labels: list[str] | None,
250
+ description: str | None,
251
+ ) -> dict[str, Any]:
252
+ """Handle milestone update."""
253
+ if not milestone_id:
254
+ return {
255
+ "status": "error",
256
+ "error": "milestone_id is required for update action",
257
+ }
258
+
259
+ # Parse target_date if provided
260
+ parsed_date = None
261
+ if target_date:
262
+ try:
263
+ parsed_date = datetime.fromisoformat(target_date)
264
+ except ValueError:
265
+ return {
266
+ "status": "error",
267
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
268
+ }
269
+
270
+ milestone_obj = await adapter.milestone_update(
271
+ milestone_id=milestone_id,
272
+ name=name,
273
+ target_date=parsed_date,
274
+ state=state,
275
+ labels=labels,
276
+ description=description,
277
+ )
278
+
279
+ if not milestone_obj:
280
+ return {
281
+ "status": "error",
282
+ "error": f"Failed to update milestone '{milestone_id}'",
283
+ }
284
+
285
+ return {
286
+ "status": "completed",
287
+ "message": f"Milestone '{milestone_id}' updated successfully",
288
+ "milestone": milestone_obj.model_dump(),
289
+ "metadata": _build_adapter_metadata(adapter, milestone_id),
290
+ }
291
+
292
+
293
+ async def _handle_delete(
294
+ adapter: BaseAdapter, milestone_id: str | None
295
+ ) -> dict[str, Any]:
296
+ """Handle milestone deletion."""
297
+ if not milestone_id:
298
+ return {
299
+ "status": "error",
300
+ "error": "milestone_id is required for delete action",
301
+ }
302
+
303
+ success = await adapter.milestone_delete(milestone_id)
304
+
305
+ if success:
306
+ return {
307
+ "status": "completed",
308
+ "message": f"Milestone '{milestone_id}' deleted successfully",
309
+ "metadata": _build_adapter_metadata(adapter, milestone_id),
310
+ }
311
+ else:
312
+ return {
313
+ "status": "error",
314
+ "error": f"Failed to delete milestone '{milestone_id}'",
315
+ }
316
+
317
+
318
+ async def _handle_get_issues(
319
+ adapter: BaseAdapter,
320
+ milestone_id: str | None,
321
+ state: str | None,
322
+ ) -> dict[str, Any]:
323
+ """Handle getting issues in milestone."""
324
+ if not milestone_id:
325
+ return {
326
+ "status": "error",
327
+ "error": "milestone_id is required for get_issues action",
328
+ }
329
+
330
+ issues = await adapter.milestone_get_issues(milestone_id, state=state)
331
+
332
+ return {
333
+ "status": "completed",
334
+ "message": f"Found {len(issues)} issue(s) in milestone",
335
+ "issues": [issue.model_dump() for issue in issues],
336
+ "count": len(issues),
337
+ "metadata": _build_adapter_metadata(adapter, milestone_id),
338
+ }
@@ -7,10 +7,9 @@ in all adapters.
7
7
 
8
8
  from typing import Any
9
9
 
10
- from ..server_sdk import get_adapter, mcp
10
+ from ..server_sdk import get_adapter
11
11
 
12
12
 
13
- @mcp.tool()
14
13
  async def ticket_create_pr(
15
14
  ticket_id: str,
16
15
  title: str,
@@ -58,7 +57,7 @@ async def ticket_create_pr(
58
57
  source_branch = f"feature/{ticket_id}"
59
58
 
60
59
  # Create PR via adapter
61
- pr_data = await adapter.create_pull_request( # type: ignore
60
+ pr_data = await adapter.create_pull_request(
62
61
  ticket_id=ticket_id,
63
62
  title=title,
64
63
  description=description,
@@ -86,7 +85,6 @@ async def ticket_create_pr(
86
85
  }
87
86
 
88
87
 
89
- @mcp.tool()
90
88
  async def ticket_link_pr(
91
89
  ticket_id: str,
92
90
  pr_url: str,
@@ -118,9 +116,7 @@ async def ticket_link_pr(
118
116
 
119
117
  # Check if adapter has specialized PR linking
120
118
  if hasattr(adapter, "link_pull_request"):
121
- result = await adapter.link_pull_request( # type: ignore
122
- ticket_id=ticket_id, pr_url=pr_url
123
- )
119
+ result = await adapter.link_pull_request(ticket_id=ticket_id, pr_url=pr_url)
124
120
  return {
125
121
  "status": "completed",
126
122
  "ticket_id": ticket_id,
@@ -0,0 +1,158 @@
1
+ """MCP tools for project status analysis and work planning.
2
+
3
+ This module provides PM-focused tools to analyze project health and
4
+ generate intelligent work plans with recommendations.
5
+
6
+ Tools:
7
+ - project_status: Comprehensive project/epic analysis with health assessment
8
+ """
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+ from ....analysis.project_status import StatusAnalyzer
14
+ from ....core.project_config import ConfigResolver
15
+ from ..server_sdk import get_adapter, mcp
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @mcp.tool()
21
+ async def project_status(project_id: str | None = None) -> dict[str, Any]:
22
+ """Analyze project/epic status and generate work plan with recommendations.
23
+
24
+ Provides comprehensive project analysis including:
25
+ - Health assessment (on_track, at_risk, off_track)
26
+ - Status breakdown by state and priority
27
+ - Dependency analysis and critical path
28
+ - Top 3 recommended tickets to start next
29
+ - Blocker identification
30
+ - Work distribution by assignee
31
+ - Actionable recommendations for project managers
32
+
33
+ Args:
34
+ project_id: ID of the project/epic to analyze (optional, uses default_project if not provided)
35
+
36
+ Returns:
37
+ Complete project status analysis with recommendations, or error information
38
+
39
+ Example:
40
+ # Analyze specific project
41
+ result = await project_status(project_id="eac28953c267")
42
+
43
+ # Analyze default project
44
+ result = await project_status()
45
+
46
+ Example Response:
47
+ {
48
+ "status": "success",
49
+ "project_id": "eac28953c267",
50
+ "project_name": "MCP Ticketer",
51
+ "health": "at_risk",
52
+ "summary": {
53
+ "total": 4,
54
+ "open": 3,
55
+ "in_progress": 1,
56
+ "done": 0
57
+ },
58
+ "recommended_next": [
59
+ {
60
+ "ticket_id": "1M-317",
61
+ "title": "Fix project organization",
62
+ "priority": "critical",
63
+ "reason": "Critical priority, Unblocks 2 tickets",
64
+ "blocks": ["1M-315", "1M-316"]
65
+ }
66
+ ],
67
+ "recommendations": [
68
+ "Resolve 1M-317 first (critical) - Unblocks 2 tickets",
69
+ "1 critical priority ticket needs attention"
70
+ ]
71
+ }
72
+
73
+ """
74
+ try:
75
+ adapter = get_adapter()
76
+
77
+ # Use default project if not provided
78
+ if not project_id:
79
+ resolver = ConfigResolver()
80
+ config = resolver.resolve()
81
+ project_id = config.default_project
82
+
83
+ if not project_id:
84
+ return {
85
+ "status": "error",
86
+ "error": "No project_id provided and no default_project configured",
87
+ "message": "Use config_set_project to set a default project, or provide project_id parameter",
88
+ }
89
+
90
+ # Read the epic/project to get name
91
+ try:
92
+ epic = await adapter.read(project_id)
93
+ if epic is None:
94
+ return {
95
+ "status": "error",
96
+ "error": f"Project/Epic {project_id} not found",
97
+ }
98
+ project_name = epic.title or project_id
99
+ except Exception as e:
100
+ logger.warning(
101
+ f"Failed to read project {project_id} for name: {e}. Using ID as name."
102
+ )
103
+ project_name = project_id
104
+
105
+ # Get all child issues
106
+ child_issue_ids = getattr(epic, "child_issues", [])
107
+
108
+ if not child_issue_ids:
109
+ return {
110
+ "status": "success",
111
+ "project_id": project_id,
112
+ "project_name": project_name,
113
+ "health": "on_track",
114
+ "summary": {"total": 0},
115
+ "message": "Project has no tickets yet",
116
+ "recommended_next": [],
117
+ "recommendations": ["Project is empty - Create tickets to get started"],
118
+ }
119
+
120
+ # Fetch each child issue
121
+ tickets = []
122
+ for issue_id in child_issue_ids:
123
+ try:
124
+ issue = await adapter.read(issue_id)
125
+ if issue:
126
+ tickets.append(issue)
127
+ except Exception as e:
128
+ logger.warning(f"Failed to read issue {issue_id}: {e}")
129
+ continue
130
+
131
+ if not tickets:
132
+ return {
133
+ "status": "success",
134
+ "project_id": project_id,
135
+ "project_name": project_name,
136
+ "health": "at_risk",
137
+ "summary": {"total": 0},
138
+ "message": f"Could not load tickets for project (found {len(child_issue_ids)} IDs but couldn't read them)",
139
+ "recommended_next": [],
140
+ "recommendations": ["Check ticket IDs and permissions"],
141
+ }
142
+
143
+ # Perform status analysis
144
+ analyzer = StatusAnalyzer()
145
+ result = analyzer.analyze(project_id, project_name, tickets)
146
+
147
+ # Convert to dict and add success status
148
+ result_dict = result.model_dump()
149
+ result_dict["status"] = "success"
150
+
151
+ return result_dict
152
+
153
+ except Exception as e:
154
+ logger.error(f"Error analyzing project status: {e}", exc_info=True)
155
+ return {
156
+ "status": "error",
157
+ "error": f"Failed to analyze project status: {str(e)}",
158
+ }