feat: Add foundation for Jira issue creation workflow#166
feat: Add foundation for Jira issue creation workflow#166
Conversation
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
plugins/titan-plugin-jira/titan_plugin_jira/steps/ai_enhance_issue_description_step.py
Show resolved
Hide resolved
plugins/titan-plugin-jira/tests/operations/test_issue_operations.py
Outdated
Show resolved
Hide resolved
plugins/titan-plugin-jira/titan_plugin_jira/clients/jira_client.py
Outdated
Show resolved
Hide resolved
|
|
||
| def find_ready_to_dev_transition( | ||
| jira_client: "JiraClient", issue_key: str | ||
| ) -> ClientResult["UITransition"]: |
There was a problem hiding this comment.
This should not return the ClientResult, just the UITransition
|
|
||
| def transition_issue_to_ready_for_dev( | ||
| jira_client: "JiraClient", issue_key: str | ||
| ) -> ClientResult[None]: |
There was a problem hiding this comment.
ClientResult, should not be returned in operations. It's just to Client/Service. The step that calls this should call try/except
There was a problem hiding this comment.
Already fixed. The function now returns None and raises exceptions instead of returning ClientResult[None], following the operations layer pattern.
There was a problem hiding this comment.
Already resolved. Function returns UITransition (UI model) and raises exceptions, following the operations pattern correctly.
plugins/titan-plugin-jira/titan_plugin_jira/utils/input_validation.py
Outdated
Show resolved
Hide resolved
plugins/titan-plugin-jira/titan_plugin_jira/steps/select_issue_priority_step.py
Outdated
Show resolved
Hide resolved
|
|
||
| selected_type = issue_types[index] | ||
|
|
||
| if not selected_type: |
There was a problem hiding this comment.
It should never be false here
| match transition_result: | ||
| case ClientSuccess(): | ||
| # Get transition details to show user | ||
| find_result = ctx.jira.get_transitions(issue_key) |
There was a problem hiding this comment.
You are calling the api twice. In line 128 you have already called the api. You need to add the necessary data to the clientSuccess.message, or in the data model
| value = int(selection) | ||
| index = value - 1 | ||
|
|
||
| if index < 0 or index >= max_value: |
There was a problem hiding this comment.
You should validate min_value as well
plugins/titan-plugin-jira/titan_plugin_jira/steps/prompt_issue_description_step.py
Outdated
Show resolved
Hide resolved
plugins/titan-plugin-jira/titan_plugin_jira/steps/select_issue_type_step.py
Outdated
Show resolved
Hide resolved
| """ | ||
| ctx.textual.begin_step(StepTitles.PRIORITY) | ||
|
|
||
| ctx.textual.markdown("## 🔥 Priority") |
There was a problem hiding this comment.
These should not be a markdown. Check the rest of the steps, cause the title of something's should not be a markdown,
There was a problem hiding this comment.
Already fixed in ccf3519. All step headers now use bold_text() instead of markdown():
ctx.textual.bold_text("🔥 Priority")Changed across all step files (select_issue_priority_step, select_issue_type_step, prompt_issue_description_step).
finxo
left a comment
There was a problem hiding this comment.
Also testing the PR I found this bug:
────────────────────────────────────────────────────────────────────────────────
SESSION START 2026-03-02 10:14:41 UTC PID 85720
────────────────────────────────────────────────────────────────────────────────
{"version": "0.1.11", "mode": "development", "log_level": "WARNING", "pid": 85720, "log_file": "/home/alex/.local/state/titan/logs/titan.log", "event": "session_started", "level": "info", "logger": "titan", "timestamp": "2026-03-02T10:14:41.119017Z"}
{"name": "git", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.150177Z"}
{"name": "jira", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.226858Z"}
{"name": "github", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.591243Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.015, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:41.623199Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.022, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:42.668828Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.021, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:45.829840Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.021, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:15:00.475315Z"}
{"workflow": "Create Jira Issue", "source": "plugin", "total_steps": 7, "is_nested": false, "event": "workflow_started", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:00.581044Z"}
{"workflow": "Create Jira Issue", "step_id": "description", "message": "Brief description captured: 87 characters", "duration": 16.898, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:17.485003Z"}
{"message": "Found 21 issue types", "result_type": "list", "duration": 0.164, "event": "get_issue_types_success", "level": "info", "logger": "titan_plugin_jira.clients.services.metadata_service", "timestamp": "2026-03-02T10:15:17.657802Z"}
{"workflow": "Create Jira Issue", "step_id": "issue_type", "message": "Issue type selected: Epic", "duration": 13.333, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:30.846453Z"}
{"workflow": "Create Jira Issue", "step_id": "priority", "message": "Priority selected: Highest", "duration": 10.62, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:41.478548Z"}
{"event": "HTTP Request: POST https://llm.tools.cloud.masorange.es/v1/messages \"HTTP/1.1 200 OK\"", "timestamp": "2026-03-02T10:15:58.747430Z"}
{"workflow": "Create Jira Issue", "step_id": "generate_with_ai", "error": "Error executing step 'ai_enhance_issue_description' from plugin 'jira': object of type 'NoneType' has no len()", "on_error": "fail", "duration": 17.276, "event": "step_failed", "level": "error", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:58.755240Z"}
{"workflow": "Create Jira Issue", "failed_at_step": "generate_with_ai", "error": "Error executing step 'ai_enhance_issue_description' from plugin 'jira': object of type 'NoneType' has no len()", "steps_completed": 4, "duration": 58.186, "event": "workflow_failed", "level": "error", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:58.761589Z"}
● Encontré el bug. La cadena del problema:
1. _parse_ai_response inicializa "title": ""
2. En el cleanup final: si queda vacío, lo pone a None → sections["title"] = None
3. parsed.pop("title", DEFAULT_TITLE) devuelve None (la clave existe pero vale None — el default solo aplica si la clave no existe)
4. len(None) → TypeError
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
|
|
| ) | ||
|
|
||
| except Exception as e: | ||
| import traceback |
There was a problem hiding this comment.
imports goes up in the file not here
| @@ -0,0 +1,167 @@ | |||
| # Plantillas Personalizadas para Issues | |||
| result = transition_issue_to_ready_for_dev(mock_client, "TEST-123") | ||
|
|
||
| # Assert | ||
| assert result is None |
There was a problem hiding this comment.
The function transition_issue_to_ready_to_dev returns UIJiraTransition not none. This is wrong test
| result = transition_issue_to_ready_for_dev(mock_client, "TEST-123") | ||
|
|
||
| # Assert | ||
| assert result is None |
There was a problem hiding this comment.
The function transition_issue_to_ready_to_dev returns UIJiraTransition not none. This is wrong test
| error_code="CREATE_SUBTASK_ERROR" | ||
| ) | ||
|
|
||
| # ==================== INTERNAL HELPERS ==================== |
There was a problem hiding this comment.
Remove this kind of comments
|
|
||
|
|
||
| def find_ready_to_dev_transition( | ||
| jira_client: "JiraClient", issue_key: str |
There was a problem hiding this comment.
this is the problem of type_checking, you are returning a string not a class. SAme in the rest of the file
| # Call AI | ||
| with ctx.textual.loading("Generating description with AI..."): | ||
| try: | ||
| from titan_cli.ai.models import AIMessage |
There was a problem hiding this comment.
imports goes up in the file
| from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step | ||
| from .steps.list_versions_step import list_versions_step | ||
|
|
||
| # Technical Specification Workflow steps (COMMENTED - steps don't exist) |
There was a problem hiding this comment.
If this does not exist remove them
| "ai_analyze_issue_requirements": ai_analyze_issue_requirements_step, | ||
| "list_versions": list_versions_step, | ||
|
|
||
| # Technical Specification Workflow steps (COMMENTED - steps don't exist) |
There was a problem hiding this comment.
If this steps does not exist remove them
There was a problem hiding this comment.
This should return a UIModel not Network one
There was a problem hiding this comment.
This should return a UIModel not a dict
There was a problem hiding this comment.
This should return an UI Model not dict
There was a problem hiding this comment.
This should return a UIModel not List[dict]
By: finxo
By: finxo
By: finxo
|
|
1 similar comment
|
|
| priority=priority | ||
| ) | ||
| # Find issue type (delegated to operation) | ||
| issue_type_result = find_issue_type_by_name(self, project_key, issue_type) |
There was a problem hiding this comment.
Client is calling an operation and passing itself as a parameter. This is backwards - operations should call the client, not vice versa. This violates the 5-layer architecture where Client (layer 3) should never call Operations (layer 2).
| description=description | ||
| ) | ||
| # Find subtask issue type (delegated to operation) | ||
| subtask_result = find_subtask_issue_type(self, self.project_key) |
There was a problem hiding this comment.
Client is calling an operation and passing itself as a parameter. This is backwards - operations should call the client, not vice versa. This violates the 5-layer architecture where Client (layer 3) should never call Operations (layer 2).
| ) | ||
|
|
||
| except Exception as e: | ||
| import traceback |
There was a problem hiding this comment.
imports go up in the file, not in the exception handler. Move this to the top of the file with other imports.
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
By: finxo
Refactor create_issue to follow 5-layer architecture: - Move issue type search logic from Client to Service - Move Epic name preparation from Client to Service - Create IssueService.create_issue_with_type_search() method - Simplify JiraClient.create_issue() to pure delegation Resolves PR #166 Issue #1 (CRITICAL): Business logic in Client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace ctx.textual.markdown() with bold_text() for simple title headers following Textual TUI conventions. Changes: - Remove markdown syntax (**) from PREVIEW_LABEL and GENERATED_DESC_LABEL constants - Replace markdown() with bold_text() in 4 step files: * ai_enhance_issue_description_step.py (lines 52, 125) * confirm_auto_assign_step.py (line 32) * create_generic_issue_step.py (line 60) * review_issue_description_step.py (lines 40, 44) - Maintain i18n by using constants instead of hardcoded strings Markdown should only be used for complex formatted content (AI analysis, descriptions, previews), not simple headers/labels. Resolves PR #166 Issue #2 (MINOR): Markdown for simple titles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Issue #2 (MINOR) - Markdown for simple titles: Fixed in ccf3519 Replaced Changes:
Files modified:
Pattern applied: # Before
ctx.textual.markdown("## 🤖 Title")
ctx.textual.markdown(InfoMessages.PREVIEW_LABEL) # "**Preview:**"
# After
ctx.textual.bold_text("🤖 Title")
ctx.textual.bold_text(InfoMessages.PREVIEW_LABEL) # "Preview:" (constant updated)Markdown now only used for complex formatted content (AI analysis, descriptions, previews), not simple headers. ✅ |
finxo
left a comment
There was a problem hiding this comment.
plugins/titan-plugin-jira/titan_plugin_jira/operations/issue_operations.py (line 0):
Operations layer methods are returning ClientResult[T] when they should return plain UIModel objects or domain data. This violates the 5-layer architecture pattern (Network → Models → Services handle ClientResult → Operations → Steps). ClientResult should only exist at the client/service boundary.
Change operation signatures to return UITransition, UIJiraIssue, etc. directly. Let the service layer wrap these in ClientResult. Example: def transition_issue(...) -> UITransition: not -> ClientResult[UITransition]:
plugins/titan-plugin-jira/titan_plugin_jira/steps/select_issue_type_step.py (line 1):
Assertions should never be used for runtime validation in production code. If this condition "should never be false," assertions are silently disabled in optimized Python (python -O), leaving no error handling.
Replace with proper error handling: if not condition: return Error("Detailed error message") or raise a specific exception with context.
|
Thanks for the review! Both issues have already been addressed: Issue #1 (CRITICAL) - Operations returning
|
**Issue #1 - Add StrEnum for Jira priorities**: - Created JiraPriority StrEnum in models/enums.py with values: Highest, High, Medium, Low, Lowest - Added icon and label properties to enum for type-safe priority handling - Updated defaults.py to use JiraPriority enum instead of string literals - Maintains string compatibility via StrEnum while providing type safety **Issue #2 - Remove docstring examples**: - Removed all doctest examples (>>>) from production code docstrings - Replaced examples with clear textual descriptions - Affected files: jira_client.py, jira_network.py, prompts.py, markdown_formatter.py, saved_queries.py, formatting.py, issue_formatting_operations.py - Follows project guidelines: no examples in docstrings, only clear documentation **Benefits**: - Type safety: IDE autocomplete and type checking for priorities - Cleaner docs: Focus on what the code does, not examples that can become stale - Consistency: All docstrings follow same documentation pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
**Issue**: metadata_service.py had quoted type annotations (forward references) using TYPE_CHECKING, but there were no circular imports. **Changes**: - Removed TYPE_CHECKING guard (no circular imports exist) - Moved UI model imports from TYPE_CHECKING block to regular imports - Removed quotes from all type annotations: - ClientResult[List["UIJiraIssueType"]] → ClientResult[List[UIJiraIssueType]] - ClientResult[List["UIJiraStatus"]] → ClientResult[List[UIJiraStatus]] - ClientResult["UIJiraUser"] → ClientResult[UIJiraUser] - ClientResult[List["UIJiraVersion"]] → ClientResult[List[UIJiraVersion]] - ClientResult[List["UIPriority"]] → ClientResult[List[UIPriority]] - ClientResult["UIJiraIssueType"] → ClientResult[UIJiraIssueType] **Benefits**: - Proper type checking at runtime - Better IDE autocomplete and type hints - Cleaner code without unnecessary forward references - Follows Python typing best practices Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixed: StrEnum for Priorities + Remove Docstring Examples ✅Commit: 4fd6d68 Issue #1 - Add StrEnum for Jira PrioritiesCreated class JiraPriority(StrEnum):
HIGHEST = "Highest"
HIGH = "High"
MEDIUM = "Medium"
LOW = "Low"
LOWEST = "Lowest"
@property
def icon(self) -> str: ...
@property
def label(self) -> str: ...Benefits:
Updated:
Issue #2 - Remove Docstring ExamplesRemoved all doctest examples (
Replaced with: Clear textual descriptions of behavior Follows project guidelines: No examples in docstrings, only documentation |
Fixed: Quoted Type Annotations ✅Commit: e7c30f6 Issue - Types as Strings in AnnotationsFound and fixed 6 methods in Before: def get_issue_types(...) -> ClientResult[List["UIJiraIssueType"]]:
def list_statuses(...) -> ClientResult[List["UIJiraStatus"]]:
def get_current_user(...) -> ClientResult["UIJiraUser"]:
def list_project_versions(...) -> ClientResult[List["UIJiraVersion"]]:
def get_priorities(...) -> ClientResult[List["UIPriority"]]:
def find_subtask_issue_type(...) -> ClientResult["UIJiraIssueType"]:After: def get_issue_types(...) -> ClientResult[List[UIJiraIssueType]]:
def list_statuses(...) -> ClientResult[List[UIJiraStatus]]:
def get_current_user(...) -> ClientResult[UIJiraUser]:
def list_project_versions(...) -> ClientResult[List[UIJiraVersion]]:
def get_priorities(...) -> ClientResult[List[UIPriority]]:
def find_subtask_issue_type(...) -> ClientResult[UIJiraIssueType]:Changes:
Benefits:
|
**Changes**: 1. **Extended JiraPriority enum**: - Added `get_icon()` class method for any priority name - Handles standard priorities + aliases (Blocker, Critical, Major, Minor, Trivial) - Removed redundant PRIORITY_ICONS dict from priority_mapper.py 2. **Created JiraIssueType enum**: - Standard issue types: Bug, Story, Task, Epic, Sub-task, Improvement, etc. - Added `icon` property and `get_icon()` class method - Replaced ISSUE_TYPE_ICONS dict in issue_type_mapper.py 3. **Created JiraStatusCategory enum**: - Status categories: TO_DO, IN_PROGRESS, DONE - Added `icon` property and `get_icon()` class method - Handles both category keys and names (case-insensitive) - Replaced STATUS_CATEGORY_ICONS dict in status_mapper.py **Benefits**: - ✅ Type safety: IDE autocomplete and type checking - ✅ Single source of truth: Icons defined once in enums - ✅ Maintainability: Centralized logic, easier to extend - ✅ Consistency: All mappers use same enum pattern **Migration**: - All mappers now use enum class methods instead of dict lookups - Backward compatible: Handles unknown values gracefully with default icons Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request
📝 Summary
This PR lays the groundwork for integrating a Jira issue creation workflow. It enables the Jira plugin in the configuration and refactors the
create_branch_stepto use a more robustResultobject pattern for handling git operations. This shift from exception-based error handling to amatch/casestructure improves code clarity and resilience.🔧 Changes Made
[plugins.jira]in.titan/config.toml.create_branch_step.pyto useClientSuccessandClientErrorresult objects instead of raisingGitErrorexceptions.try/exceptblocks withmatchstatements for handling the outcomes of all git commands (get_branches,checkout,delete_branch,create_branch).resulttypes fromtitan_cli.core.result.🧪 Testing
poetry run pytest)make test)titan-devThe refactoring touches core git functionality. Existing unit tests for
create_branch_stepshould be updated to validate the newResult-based logic, ensuring all success and error paths are handled correctly.📊 Logs
✅ Checklist