titan-cli 0.1.0__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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search JIRA issues using saved query from utils registry
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_cli.ui.tui.widgets import Panel, Table
|
|
7
|
+
from ..exceptions import JiraAPIError
|
|
8
|
+
from ..messages import msg
|
|
9
|
+
from ..utils import SAVED_QUERIES, IssueSorter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
13
|
+
"""
|
|
14
|
+
Search JIRA issues using a saved query.
|
|
15
|
+
|
|
16
|
+
Queries are predefined in utils.SAVED_QUERIES (from utils/saved_queries.py).
|
|
17
|
+
Projects can override or add custom queries in .titan/config.toml under [jira.saved_queries].
|
|
18
|
+
|
|
19
|
+
Inputs (from ctx.data):
|
|
20
|
+
query_name (str): Name of saved query (e.g., "my_bugs", "team_bugs")
|
|
21
|
+
project (str, optional): Project key for parameterized queries (e.g., "ECAPP")
|
|
22
|
+
max_results (int, optional): Maximum number of results (default: 50)
|
|
23
|
+
|
|
24
|
+
Outputs (saved to ctx.data):
|
|
25
|
+
jira_issues (list): List of JiraTicket objects
|
|
26
|
+
jira_issue_count (int): Number of issues found
|
|
27
|
+
used_query_name (str): Name of the query that was used
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Success: Issues found
|
|
31
|
+
Error: Query not found or search failed
|
|
32
|
+
|
|
33
|
+
Available predefined queries (from utils):
|
|
34
|
+
Personal: my_open_issues, my_bugs, my_in_review, my_in_progress
|
|
35
|
+
Team: current_sprint, team_open, team_bugs, team_in_review
|
|
36
|
+
Priority: critical_issues, high_priority, blocked_issues
|
|
37
|
+
Time: updated_today, created_this_week, recent_bugs
|
|
38
|
+
Status: todo_issues, in_progress_all, done_recently
|
|
39
|
+
|
|
40
|
+
Example usage in workflow:
|
|
41
|
+
```yaml
|
|
42
|
+
- id: search
|
|
43
|
+
plugin: jira
|
|
44
|
+
step: search_saved_query
|
|
45
|
+
params:
|
|
46
|
+
query_name: "my_bugs"
|
|
47
|
+
|
|
48
|
+
# For queries with {project} parameter:
|
|
49
|
+
- id: search_team
|
|
50
|
+
plugin: jira
|
|
51
|
+
step: search_saved_query
|
|
52
|
+
params:
|
|
53
|
+
query_name: "team_bugs"
|
|
54
|
+
project: "ECAPP"
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
if not ctx.textual:
|
|
58
|
+
return Error("Textual UI context is not available for this step.")
|
|
59
|
+
|
|
60
|
+
if not ctx.jira:
|
|
61
|
+
ctx.textual.mount(Panel(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, panel_type="error"))
|
|
62
|
+
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
63
|
+
|
|
64
|
+
# Get query name
|
|
65
|
+
query_name = ctx.get("query_name")
|
|
66
|
+
if not query_name:
|
|
67
|
+
ctx.textual.mount(Panel(msg.Steps.Search.QUERY_NAME_REQUIRED, panel_type="error"))
|
|
68
|
+
return Error(msg.Steps.Search.QUERY_NAME_REQUIRED)
|
|
69
|
+
|
|
70
|
+
# Get all predefined queries from utils
|
|
71
|
+
predefined_queries = SAVED_QUERIES.get_all()
|
|
72
|
+
|
|
73
|
+
# Get custom queries from config (if any)
|
|
74
|
+
custom_queries = {}
|
|
75
|
+
try:
|
|
76
|
+
if hasattr(ctx, 'plugin_manager') and ctx.plugin_manager is not None:
|
|
77
|
+
jira_plugin = ctx.plugin_manager.get_plugin('jira')
|
|
78
|
+
if jira_plugin and hasattr(jira_plugin, '_config') and jira_plugin._config is not None:
|
|
79
|
+
custom_queries = jira_plugin._config.saved_queries or {}
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Merge queries (custom queries override predefined)
|
|
84
|
+
all_queries = {**predefined_queries, **custom_queries}
|
|
85
|
+
|
|
86
|
+
# Look up the query
|
|
87
|
+
if query_name not in all_queries:
|
|
88
|
+
# Build helpful error message
|
|
89
|
+
predefined_list = list(predefined_queries.keys())
|
|
90
|
+
custom_list = list(custom_queries.keys())
|
|
91
|
+
|
|
92
|
+
error_msg = msg.Steps.Search.QUERY_NOT_FOUND.format(query_name=query_name) + "\n\n"
|
|
93
|
+
error_msg += msg.Steps.Search.AVAILABLE_PREDEFINED + "\n "
|
|
94
|
+
error_msg += "\n ".join(predefined_list[:15])
|
|
95
|
+
if len(predefined_list) > 15:
|
|
96
|
+
error_msg += "\n" + msg.Steps.Search.MORE_QUERIES.format(count=len(predefined_list) - 15)
|
|
97
|
+
|
|
98
|
+
if custom_list:
|
|
99
|
+
error_msg += "\n\n" + msg.Steps.Search.CUSTOM_QUERIES_HEADER + "\n "
|
|
100
|
+
error_msg += "\n ".join(custom_list)
|
|
101
|
+
else:
|
|
102
|
+
error_msg += "\n\n" + msg.Steps.Search.ADD_CUSTOM_HINT + "\n"
|
|
103
|
+
error_msg += msg.Steps.Search.CUSTOM_QUERY_EXAMPLE
|
|
104
|
+
|
|
105
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
106
|
+
return Error(error_msg)
|
|
107
|
+
|
|
108
|
+
jql = all_queries[query_name]
|
|
109
|
+
|
|
110
|
+
# Get parameters for query formatting
|
|
111
|
+
project = ctx.get("project")
|
|
112
|
+
|
|
113
|
+
# Format query if it has parameters
|
|
114
|
+
if "{project}" in jql:
|
|
115
|
+
if not project:
|
|
116
|
+
# Try to use default project from JIRA client
|
|
117
|
+
if ctx.jira and hasattr(ctx.jira, 'project_key'):
|
|
118
|
+
project = ctx.jira.project_key
|
|
119
|
+
|
|
120
|
+
if not project:
|
|
121
|
+
error_msg = msg.Steps.Search.PROJECT_REQUIRED.format(query_name=query_name, jql=jql)
|
|
122
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
123
|
+
return Error(error_msg)
|
|
124
|
+
|
|
125
|
+
jql = jql.format(project=project)
|
|
126
|
+
|
|
127
|
+
# Show which query is being used
|
|
128
|
+
is_custom = query_name in custom_queries
|
|
129
|
+
source_label = "Custom" if is_custom else "Predefined"
|
|
130
|
+
|
|
131
|
+
ctx.textual.text("")
|
|
132
|
+
ctx.textual.text(f"Using {source_label} Query: {query_name}", markup="bold")
|
|
133
|
+
ctx.textual.text(f" JQL: {jql}", markup="dim")
|
|
134
|
+
ctx.textual.text("")
|
|
135
|
+
|
|
136
|
+
# Get max results
|
|
137
|
+
max_results = ctx.get("max_results", 50)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Execute search with loading indicator
|
|
141
|
+
with ctx.textual.loading("Searching JIRA issues..."):
|
|
142
|
+
issues = ctx.jira.search_tickets(jql=jql, max_results=max_results)
|
|
143
|
+
|
|
144
|
+
if not issues:
|
|
145
|
+
ctx.textual.mount(
|
|
146
|
+
Panel(
|
|
147
|
+
text=f"No issues found for query: {query_name}",
|
|
148
|
+
panel_type="info"
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
return Success(
|
|
152
|
+
"No issues found",
|
|
153
|
+
metadata={
|
|
154
|
+
"jira_issues": [],
|
|
155
|
+
"jira_issue_count": 0,
|
|
156
|
+
"used_query_name": query_name
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Show results
|
|
161
|
+
ctx.textual.mount(
|
|
162
|
+
Panel(
|
|
163
|
+
text=f"Found {len(issues)} issues",
|
|
164
|
+
panel_type="success"
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
ctx.textual.text("")
|
|
168
|
+
|
|
169
|
+
# Show detailed table
|
|
170
|
+
ctx.textual.text("Found Issues:", markup="bold")
|
|
171
|
+
ctx.textual.text("")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Sort issues intelligently
|
|
175
|
+
sorter = IssueSorter()
|
|
176
|
+
sorted_issues = sorter.sort(issues)
|
|
177
|
+
|
|
178
|
+
# Prepare table data with row numbers for selection
|
|
179
|
+
headers = ["#", "Key", "Status", "Summary", "Assignee", "Type", "Priority"]
|
|
180
|
+
rows = []
|
|
181
|
+
for i, issue in enumerate(sorted_issues, 1):
|
|
182
|
+
assignee = issue.assignee or "Unassigned"
|
|
183
|
+
status = issue.status or "Unknown"
|
|
184
|
+
priority = issue.priority or "Unknown"
|
|
185
|
+
issue_type = issue.issue_type or "Unknown"
|
|
186
|
+
summary = (issue.summary or "No summary")[:60]
|
|
187
|
+
|
|
188
|
+
rows.append([
|
|
189
|
+
str(i),
|
|
190
|
+
issue.key,
|
|
191
|
+
status,
|
|
192
|
+
summary,
|
|
193
|
+
assignee,
|
|
194
|
+
issue_type,
|
|
195
|
+
priority
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
# Render table using textual widget
|
|
199
|
+
ctx.textual.mount(
|
|
200
|
+
Table(
|
|
201
|
+
headers=headers,
|
|
202
|
+
rows=rows,
|
|
203
|
+
title=f"Issues (sorted by {sorter.get_sort_description()})"
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Use sorted issues for downstream steps
|
|
208
|
+
issues = sorted_issues
|
|
209
|
+
except Exception as e:
|
|
210
|
+
# If table rendering fails, show error but continue with raw issue list
|
|
211
|
+
ctx.textual.text(f"Error rendering table: {e}", markup="red")
|
|
212
|
+
ctx.textual.text(f"Found {len(issues)} issues (showing raw data)", markup="cyan")
|
|
213
|
+
for i, issue in enumerate(issues, 1):
|
|
214
|
+
ctx.textual.text(f"{i}. {issue.key} - {getattr(issue, 'summary', 'N/A')}")
|
|
215
|
+
ctx.textual.text("")
|
|
216
|
+
|
|
217
|
+
return Success(
|
|
218
|
+
f"Found {len(issues)} issues using query: {query_name}",
|
|
219
|
+
metadata={
|
|
220
|
+
"jira_issues": issues,
|
|
221
|
+
"jira_issue_count": len(issues),
|
|
222
|
+
"used_query_name": query_name
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except JiraAPIError as e:
|
|
227
|
+
error_msg = f"JIRA search failed: {e}"
|
|
228
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
229
|
+
return Error(error_msg)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
import traceback
|
|
232
|
+
error_detail = traceback.format_exc()
|
|
233
|
+
error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
|
|
234
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
235
|
+
return Error(error_msg)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = ["search_saved_query_step"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Issue Sorter Utility
|
|
3
|
+
|
|
4
|
+
Provides intelligent sorting for JIRA issues based on status, priority, and key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Dict
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class IssueSortConfig:
|
|
13
|
+
"""Configuration for issue sorting priorities."""
|
|
14
|
+
|
|
15
|
+
status_order: Dict[str, int]
|
|
16
|
+
priority_order: Dict[str, int]
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def default(cls) -> "IssueSortConfig":
|
|
20
|
+
"""
|
|
21
|
+
Returns default sorting configuration.
|
|
22
|
+
|
|
23
|
+
Status order: To Do → In Progress → Done
|
|
24
|
+
Priority order: Critical → High → Medium → Low
|
|
25
|
+
"""
|
|
26
|
+
return cls(
|
|
27
|
+
status_order={
|
|
28
|
+
# To Do / Open states
|
|
29
|
+
"to do": 0,
|
|
30
|
+
"open": 0,
|
|
31
|
+
"backlog": 0,
|
|
32
|
+
# In Progress states
|
|
33
|
+
"in progress": 1,
|
|
34
|
+
"in review": 1,
|
|
35
|
+
"code review": 1,
|
|
36
|
+
"review": 1,
|
|
37
|
+
# Done / Closed states
|
|
38
|
+
"done": 2,
|
|
39
|
+
"closed": 2,
|
|
40
|
+
"resolved": 2,
|
|
41
|
+
"completed": 2
|
|
42
|
+
},
|
|
43
|
+
priority_order={
|
|
44
|
+
# Critical/Highest
|
|
45
|
+
"critical": 0,
|
|
46
|
+
"highest": 0,
|
|
47
|
+
"blocker": 0,
|
|
48
|
+
# High
|
|
49
|
+
"high": 1,
|
|
50
|
+
# Medium/Normal
|
|
51
|
+
"medium": 2,
|
|
52
|
+
"normal": 2,
|
|
53
|
+
# Low/Lowest
|
|
54
|
+
"low": 3,
|
|
55
|
+
"lowest": 3,
|
|
56
|
+
"trivial": 3
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IssueSorter:
|
|
62
|
+
"""
|
|
63
|
+
Sorts JIRA issues intelligently based on status, priority, and key.
|
|
64
|
+
|
|
65
|
+
Sorting criteria (in order):
|
|
66
|
+
1. Status (To Do → In Progress → Done)
|
|
67
|
+
2. Priority (Critical → High → Medium → Low)
|
|
68
|
+
3. Key (alphabetical)
|
|
69
|
+
|
|
70
|
+
Unknown statuses/priorities are placed at the end.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> from titan_plugin_jira.models import JiraTicket
|
|
74
|
+
>>> sorter = IssueSorter()
|
|
75
|
+
>>> sorted_issues = sorter.sort(issues)
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, config: IssueSortConfig = None):
|
|
79
|
+
"""
|
|
80
|
+
Initialize the sorter with optional custom configuration.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: IssueSortConfig instance. If None, uses default configuration.
|
|
84
|
+
"""
|
|
85
|
+
self.config = config or IssueSortConfig.default()
|
|
86
|
+
self._unknown_value = 99 # Value for unknown statuses/priorities
|
|
87
|
+
|
|
88
|
+
def sort(self, issues: List) -> List:
|
|
89
|
+
"""
|
|
90
|
+
Sort issues based on status, priority, and key.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
issues: List of JiraTicket objects
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Sorted list of JiraTicket objects
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> sorter = IssueSorter()
|
|
100
|
+
>>> sorted_issues = sorter.sort(my_issues)
|
|
101
|
+
"""
|
|
102
|
+
return sorted(issues, key=self._sort_key)
|
|
103
|
+
|
|
104
|
+
def _sort_key(self, issue) -> tuple:
|
|
105
|
+
"""
|
|
106
|
+
Generate sort key for an issue.
|
|
107
|
+
|
|
108
|
+
Returns tuple of (status_order, priority_order, key) for sorting.
|
|
109
|
+
"""
|
|
110
|
+
status_value = self._get_status_order(issue.status)
|
|
111
|
+
priority_value = self._get_priority_order(issue.priority)
|
|
112
|
+
|
|
113
|
+
return (status_value, priority_value, issue.key)
|
|
114
|
+
|
|
115
|
+
def _get_status_order(self, status: str) -> int:
|
|
116
|
+
"""Get sort order for a status (case-insensitive)."""
|
|
117
|
+
if not status:
|
|
118
|
+
return self._unknown_value
|
|
119
|
+
return self.config.status_order.get(
|
|
120
|
+
status.lower(),
|
|
121
|
+
self._unknown_value
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _get_priority_order(self, priority: str) -> int:
|
|
125
|
+
"""Get sort order for a priority (case-insensitive)."""
|
|
126
|
+
if not priority:
|
|
127
|
+
return self._unknown_value
|
|
128
|
+
return self.config.priority_order.get(
|
|
129
|
+
priority.lower(),
|
|
130
|
+
self._unknown_value
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def get_sort_description(self) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Get human-readable description of the sort order.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Description string like "Status → Priority → Key"
|
|
139
|
+
"""
|
|
140
|
+
return "Status → Priority → Key"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Predefined JIRA saved queries registry.
|
|
3
|
+
|
|
4
|
+
Common JQL queries that can be used across projects.
|
|
5
|
+
Projects can extend these with custom queries in .titan/config.toml
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SavedQueries:
|
|
12
|
+
"""
|
|
13
|
+
Registry of predefined JQL queries.
|
|
14
|
+
|
|
15
|
+
These are common queries that work with any JIRA instance.
|
|
16
|
+
Projects can override or extend these via config.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ==================== PERSONAL QUERIES ====================
|
|
20
|
+
|
|
21
|
+
OPEN_ISSUES = 'project = {project} AND status IN ("Open", "Ready to Dev") ORDER BY updated DESC'
|
|
22
|
+
"""All issues that are Open or Ready to Dev in the specified project (regardless of assignee), ordered by last updated"""
|
|
23
|
+
|
|
24
|
+
MY_OPEN_ISSUES = 'project = {project} AND assignee = currentUser() AND status IN ("Open", "Ready to Dev") ORDER BY updated DESC'
|
|
25
|
+
"""Issues assigned to you that are Open or Ready to Dev in the specified project"""
|
|
26
|
+
|
|
27
|
+
MY_ISSUES = "assignee = currentUser() ORDER BY updated DESC"
|
|
28
|
+
"""All issues assigned to you (including Done)"""
|
|
29
|
+
|
|
30
|
+
MY_BUGS = "assignee = currentUser() AND type = Bug AND status != Done"
|
|
31
|
+
"""Open bugs assigned to you"""
|
|
32
|
+
|
|
33
|
+
MY_IN_REVIEW = "assignee = currentUser() AND status = 'In Review'"
|
|
34
|
+
"""Issues you have in review status"""
|
|
35
|
+
|
|
36
|
+
MY_IN_PROGRESS = "assignee = currentUser() AND status = 'In Progress'"
|
|
37
|
+
"""Issues you're currently working on"""
|
|
38
|
+
|
|
39
|
+
REPORTED_BY_ME = "reporter = currentUser() ORDER BY created DESC"
|
|
40
|
+
"""Issues you created"""
|
|
41
|
+
|
|
42
|
+
# ==================== TEAM QUERIES ====================
|
|
43
|
+
|
|
44
|
+
CURRENT_SPRINT = "sprint in openSprints() AND project = {project}"
|
|
45
|
+
"""Issues in current sprint (requires project parameter)"""
|
|
46
|
+
|
|
47
|
+
TEAM_OPEN = "project = {project} AND status != Done"
|
|
48
|
+
"""All open issues in project"""
|
|
49
|
+
|
|
50
|
+
TEAM_BUGS = "project = {project} AND type = Bug AND status != Done"
|
|
51
|
+
"""Open bugs in project"""
|
|
52
|
+
|
|
53
|
+
TEAM_IN_REVIEW = "project = {project} AND status = 'In Review'"
|
|
54
|
+
"""Issues in review for project"""
|
|
55
|
+
|
|
56
|
+
TEAM_READY_FOR_QA = "project = {project} AND status = 'Ready for QA'"
|
|
57
|
+
"""Issues ready for QA testing"""
|
|
58
|
+
|
|
59
|
+
# ==================== PRIORITY QUERIES ====================
|
|
60
|
+
|
|
61
|
+
CRITICAL_ISSUES = "priority = Highest AND status != Done ORDER BY created ASC"
|
|
62
|
+
"""All critical priority issues"""
|
|
63
|
+
|
|
64
|
+
HIGH_PRIORITY = "priority IN (Highest, High) AND status != Done ORDER BY priority DESC"
|
|
65
|
+
"""High and highest priority issues"""
|
|
66
|
+
|
|
67
|
+
CRITICAL_MY_PROJECT = "priority = Highest AND status != Done AND project = {project}"
|
|
68
|
+
"""Critical issues in specific project"""
|
|
69
|
+
|
|
70
|
+
BLOCKED_ISSUES = "status = Blocked ORDER BY updated DESC"
|
|
71
|
+
"""All blocked issues"""
|
|
72
|
+
|
|
73
|
+
# ==================== TIME-BASED QUERIES ====================
|
|
74
|
+
|
|
75
|
+
UPDATED_TODAY = "updated >= startOfDay() ORDER BY updated DESC"
|
|
76
|
+
"""Issues updated today"""
|
|
77
|
+
|
|
78
|
+
UPDATED_THIS_WEEK = "updated >= startOfWeek() ORDER BY updated DESC"
|
|
79
|
+
"""Issues updated this week"""
|
|
80
|
+
|
|
81
|
+
CREATED_TODAY = "created >= startOfDay() ORDER BY created DESC"
|
|
82
|
+
"""Issues created today"""
|
|
83
|
+
|
|
84
|
+
CREATED_THIS_WEEK = "created >= startOfWeek() ORDER BY created DESC"
|
|
85
|
+
"""Issues created this week"""
|
|
86
|
+
|
|
87
|
+
RECENT_BUGS = "type = Bug AND created >= -7d ORDER BY created DESC"
|
|
88
|
+
"""Bugs created in last 7 days"""
|
|
89
|
+
|
|
90
|
+
# ==================== STATUS QUERIES ====================
|
|
91
|
+
|
|
92
|
+
TODO_ISSUES = "status = 'To Do' ORDER BY priority DESC, created ASC"
|
|
93
|
+
"""All issues in To Do status"""
|
|
94
|
+
|
|
95
|
+
IN_PROGRESS_ALL = "status = 'In Progress' ORDER BY updated DESC"
|
|
96
|
+
"""All issues currently in progress"""
|
|
97
|
+
|
|
98
|
+
IN_REVIEW_ALL = "status = 'In Review' ORDER BY updated DESC"
|
|
99
|
+
"""All issues in review"""
|
|
100
|
+
|
|
101
|
+
DONE_RECENTLY = "status = Done AND updated >= -7d ORDER BY updated DESC"
|
|
102
|
+
"""Issues completed in last 7 days"""
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def get_all(cls) -> Dict[str, str]:
|
|
106
|
+
"""
|
|
107
|
+
Get all predefined queries as a dictionary.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dict mapping query names to JQL strings
|
|
111
|
+
"""
|
|
112
|
+
queries = {}
|
|
113
|
+
for attr in dir(cls):
|
|
114
|
+
if attr.isupper() and not attr.startswith('_'):
|
|
115
|
+
value = getattr(cls, attr)
|
|
116
|
+
if isinstance(value, str):
|
|
117
|
+
# Convert attribute name to lowercase with underscores
|
|
118
|
+
key = attr.lower()
|
|
119
|
+
queries[key] = value
|
|
120
|
+
return queries
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def format(cls, query_name: str, **params) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Format a query with parameters.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
query_name: Name of the query (lowercase with underscores)
|
|
129
|
+
**params: Parameters to format into query
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Formatted JQL query
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> SavedQueries.format('current_sprint', project='ECAPP')
|
|
136
|
+
'sprint in openSprints() AND project = ECAPP'
|
|
137
|
+
"""
|
|
138
|
+
queries = cls.get_all()
|
|
139
|
+
if query_name not in queries:
|
|
140
|
+
raise ValueError(f"Query '{query_name}' not found")
|
|
141
|
+
|
|
142
|
+
query = queries[query_name]
|
|
143
|
+
return query.format(**params)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Singleton instance
|
|
147
|
+
SAVED_QUERIES = SavedQueries()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = ["SavedQueries", "SAVED_QUERIES"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: "Analyze JIRA Open and Ready to Dev Issues"
|
|
2
|
+
description: "List all JIRA issues in Open or Ready to Dev status and analyze selected issue with AI"
|
|
3
|
+
|
|
4
|
+
params:
|
|
5
|
+
# Can override which saved query to use
|
|
6
|
+
query_name: "open_issues"
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
- id: search_open_issues
|
|
10
|
+
name: "Search Open Issues"
|
|
11
|
+
plugin: jira
|
|
12
|
+
step: search_saved_query
|
|
13
|
+
params:
|
|
14
|
+
query_name: "open_issues"
|
|
15
|
+
max_results: 20
|
|
16
|
+
|
|
17
|
+
- id: prompt_select_issue
|
|
18
|
+
name: "Select Issue to Analyze"
|
|
19
|
+
plugin: jira
|
|
20
|
+
step: prompt_select_issue
|
|
21
|
+
|
|
22
|
+
- id: get_issue_details
|
|
23
|
+
name: "Get Full Issue Details"
|
|
24
|
+
plugin: jira
|
|
25
|
+
step: get_issue
|
|
26
|
+
requires:
|
|
27
|
+
- jira_issue_key
|
|
28
|
+
|
|
29
|
+
- id: ai_analyze_issue
|
|
30
|
+
name: "AI Analyze Issue"
|
|
31
|
+
plugin: jira
|
|
32
|
+
step: ai_analyze_issue_requirements
|
|
33
|
+
requires:
|
|
34
|
+
- jira_issue
|