Uh oh!
There was an error while loading. Please reload this page.
[AISOS-2294] Add decompose draft review step before Jira task creation - #242
[AISOS-2294] Add decompose draft review step before Jira task creation#242ekuris-redhat wants to merge 90 commits into
Conversation
ekuris-redhat
left a comment
There was a problem hiding this comment.
! Please address the following issues found during review:
- Missing empty-draft guard in epic decomposition (non-YOLO path)
In src/forge/workflow/nodes/task_generation.py, the non-YOLO path correctly checks if not proposed_tasks_list: and returns a retry state
before touching Jira. The equivalent path in src/forge/workflow/nodes/epic_decomposition.py has no such check. If the agent returns
zero epics in non-YOLO mode, the code creates an empty ForgeDecompositionDraft, uploads it, posts a comment with an empty table, and
pauses the workflow with nothing for the human to approve. Please add the same guard before the draft creation block in decompose_epics:
if epics_data is empty after the LLM call, return a retry state with an appropriate last_error, matching the YOLO path's else branch.
- edit_comment skips retry logic
In src/forge/integrations/jira/client.py, the edit_comment method uses client = await self._get_client() and calls client.put(...)
directly. Every other new Jira helper added in this PR (download_attachment, delete_attachment, get_attachments, add_attachment) routes
through _request_with_retry. The PR explicitly advertises rate-limit and retry logic as a feature, but edit_comment will fail
immediately on transient 429 responses. Please refactor it to use _request_with_retry like the other helpers.
- Revision comment detection inconsistency
In src/forge/orchestrator/worker.py, the revision comment check uses comment_body.startswith("!"). The classify_comment function in
comment_classifier.py uses _REVISION_PATTERN = re.compile(r"^\s*!") which allows leading whitespace. A comment with a leading space
would be classified as FEEDBACK by the classifier but not caught as a revision comment by the worker, falling through to trigger full
regeneration instead of draft JSON revision. Please change the worker check to use bool(re.match(r"^\s*!", comment_body)) to match the
classifier.
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
5 similar comments
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
Hey, a few things I'd like addressed before we move forward with this one.
First, and most importantly: the .mypy_cache/3.11/ directory got committed — 18 binary cache files are showing up in the diff.
These are local mypy artifacts that should never be tracked. Please remove all of them from the branch and also add
.mypy_cache/ to .gitignore since it's currently missing from there.
Duplicate code in the worker: In worker.py, the block that finds the original review comment and edits it appears twice in a
row — once in the forge command handler and once in the revision comment handler. These are nearly identical. Please extract
that logic into a small helper so we don't have to maintain two copies.
format_review_comment duplication: The stories and tasks branches in draft_manager.py are about 95% identical. The only things
that differ are the header text, the field label, and the approval label. Please collapse them into a single helper that
accepts those as parameters.
Unnecessary LangChain wrapping: In revise_draft_with_feedback, load_prompt already returns a fully formatted string. Wrapping
it in PromptTemplate.from_template("{prompt_text}") just to pass it through is extra ceremony that doesn't add anything. A
direct call to model.ainvoke(prompt_text) would do the same thing with fewer moving parts.
A few smaller things:
The nested from datetime import datetime inside the worker function body should move to the top of the file alongside the
existing from datetime import UTC.
available_repos: Any = set() in epic_decomposition.py should use the concrete type set[str] instead of Any.
In get_attachments, each attachment dict gets both a content and a content_url key pointing to the same value. One is enough.
The _validate_item_params method in DraftManager manually replicates the type checks that Pydantic's DraftItem.model_validate
would already enforce. Consider leaning on Pydantic for this instead.
Finally, provision_epics_from_draft and provision_tasks_from_draft are called from both the worker and the route functions. The
guard on epic_keys prevents double execution, but the split ownership is confusing. Please add a comment explaining why both
call sites need to exist, or consolidate them.
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
1 similar comment
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
A few concrete issues I want to flag before merging:
- format_review_comment ignores the module's own constants
draft_manager.py defines FORGE_STORIES_DRAFT_FILENAME and FORGE_TASKS_DRAFT_FILENAME at module level, but format_review_comment
hardcodes the literal strings "forge-stories-draft.json" and "forge-tasks-draft.json" in two places. If the filenames ever
change, this method will silently diverge. Replace with the constants.
- getattr(settings, "yolo_mode", False) is stale — the field exists
This PR adds yolo_mode: bool = Field(default=False) to config.py, which means the getattr fallback in epic_decomposition.py and
task_generation.py is unnecessary. Use settings.yolo_mode directly.
- Duplicate ValidationError import inside _validate_item_params
from pydantic import ValidationError is already imported at the top of draft_manager.py. The same import inside
_validate_item_params's body is redundant — remove it.
- YOLO detection copy-pasted 4 times
The same three-component check — "forge:yolo" in labels or getattr(settings, "yolo_mode", False) or state.get("yolo_mode",
False) — appears identically in epic_decomposition.py, task_generation.py, plan_approval.py, and task_approval.py. This should
be a shared helper so all four sites stay in sync.
- provision_epics_from_draft and provision_tasks_from_draft use Any for typed parameters
Both functions are declared (state: Any, jira: Any). They should use WorkflowState and JiraClient.
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
1 similar comment
ekuris-redhat
commented
Jul 30, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
Please address the following issues found in the review:
- Unguarded split outside the non-blocking try block (update_docs_repo.py:65)
current_repo.split("/", 1) runs before the try block that starts at line 100. If current_repo is empty or missing a /, the
unpacking raises ValueError that is not caught by the non-blocking handler at line 197 — breaking the non-blocking guarantee.
Add a guard before line 65:
if not current_repo or "/" not in current_repo:
logger.warning(f"current_repo is missing or malformed for {ticket_key}, skipping docs repo update")
return state
- import subprocess inside function body (update_docs_repo.py:258)
Move the import subprocess statement to the top of the module with the other imports.
- Bare except Exception silently swallows errors in _branch_has_commits (update_docs_repo.py:269)
When the git command fails for any reason, the function silently returns False, causing PR creation to be skipped even when the
container committed documentation changes. Add a log statement:
except Exception as e:
logger.warning(f"Could not check for commits in {workspace_path}: {e}")
return False
- _branch_has_commits bypasses GitOperations abstraction (update_docs_repo.py:256-270)
All other git operations in this codebase go through GitOperations. Move this logic into a GitOperations method (e.g.,
has_commits_ahead(base_branch: str) -> bool) and call it through docs_git instead of using subprocess directly.
- Duplicate default-branch fetch pattern (update_docs_repo.py:69-90)
Lines 69-77 and 82-90 are identical: create a GitHubClient, call get_repository, extract default_branch, log, close. Extract to
a shared helper:
async def _get_repo_default_branch(settings: Settings, owner: str, repo_name: str) -> str:
github = GitHubClient(settings)
try:
data = await github.get_repository(owner, repo_name)
return data.get("default_branch", "main")
except Exception as e:
logger.warning(f"Could not fetch default branch for {owner}/{repo_name}, defaulting to 'main': {e}")
return "main"
finally:
await github.close()
- Dead branch in guardrails slice (update_docs_repo.py:160)
guardrails is already defaulted to "" at line 92 via .get("guardrails", ""). The if guardrails else "" branch is unreachable —
""[:2000] and "" are identical. Simplify to guardrails[:2000].
- Missing tests
Add tests for:
- current_repo empty or missing / — verifies the guard added in item 1 returns state without crashing
- GitError fallback path (lines 121-145) — branch deleted after merge, code falls back to fetching merge commit SHA via
get_pull_request - _create_docs_pr directly — verify fork creation, fork sync, push, PR creation, and Jira comment are all called with the
correct arguments
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
1 similar comment
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
Two issues that need addressing:
- JSON boundary extraction uses wrong end delimiter (src/forge/integrations/agents/agent.py)
When the LLM response has no markdown code block, the fallback uses max(rfind("}"), rfind("]")) to find the end of the JSON.
This is wrong when the types are mismatched — if the JSON starts with { but there's a trailing ] after the closing } (common in
LLM responses with postamble), max picks the ] and the extracted slice is invalid JSON.
Fix: match the end delimiter to the opening delimiter:
if start_idx == start_brace:
end_idx = cleaned_text.rfind("}")
else:
end_idx = cleaned_text.rfind("]")
- Pipe characters in item summary or repo break the Jira markdown table
(src/forge/workflow/utils/draft_manager.py:format_review_comment)
item.summary and item.repo are interpolated directly into table cells without escaping. A summary like "Support A | B toggle"
produces a broken 4-column row instead of 3. Escape pipe characters before interpolation:
def _escape_cell(text: str) -> str:
return text.replace("|", "\|")
table += f"| {item.id} | {_escape_cell(item.summary)} | {_escape_cell(item.repo or 'unknown')} |\n"
Apply the same escaping in the condensed table path.
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
1 similar comment
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
| return validated_json_str | ||
| except json.JSONDecodeError as e: | ||
| logger.error(f"Failed to parse LLM response as valid JSON: {e}\nResponse: {response}") | ||
| raise ValueError( |
There was a problem hiding this comment.
Security: The raw LLM response is included in this ValueError (Response: {response}). This exception propagates to the worker where it gets posted as a Jira comment via f"Forge command/revision failed: {str(e)}". The LLM response may contain prompt internals or system instructions that shouldn't be visible to users.
Suggestion: log the full response at ERROR level but raise with a sanitized message like "Failed to parse revised draft as valid JSON".
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
| exc_info=True, | ||
| ) | ||
| error_comment_text = f"❌ Forge command/revision failed: {str(e)}" |
There was a problem hiding this comment.
Security:str(e) for HTTP exceptions can contain request URLs, auth headers, or API tokens. Same issue on lines 1780 and 1805 for provisioning errors. Consider sanitizing or using a generic user-facing message while logging the full exception separately (which you're already doing with exc_info=True above).
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
| try: | ||
| epic_keys = await provision_epics_from_draft(state, jira) | ||
| # Store the newly created keys | ||
| state["epic_keys"] = epic_keys |
There was a problem hiding this comment.
Correctness: Mutating state["epic_keys"] directly inside a routing function is risky — LangGraph checkpoints state before routing, so these mutations aren't captured. If the process crashes after provision_epics_from_draft creates Jira tickets and deletes the draft but before the next node checkpoints, on restart: epic_keys won't be in the checkpoint, the draft is already deleted, and provisioning would either fail (no draft) or create duplicates.
The worker path (line ~1773) has the same pattern but at least runs outside the LangGraph graph. Consider moving provisioning entirely into the worker or into a dedicated node that checkpoints the created keys before deleting the draft.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
| jira = JiraClient() | ||
| try: | ||
| epic_keys = await provision_epics_from_draft(cast(Any, updated_state), jira) | ||
| updated_state["epic_keys"] = epic_keys |
There was a problem hiding this comment.
Correctness (minor): Both label-based approval (adding forge:plan-approved) and command-based approval (/forge approve) can trigger provisioning. If both arrive as near-simultaneous webhook events, two workers could each pass the not updated_state.get("epic_keys") guard since each loads the same checkpoint independently. The window is small but could create duplicate tickets. A Jira-side guard (check if children already exist before creating) would make this idempotent.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
There was a problem hiding this comment.
Forge implemented this feedback in the latest pushed revision.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
There was a problem hiding this comment.
This feedback has already been successfully addressed on the branch via JQL-based Jira-side idempotency guards.
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
1 similar comment
ekuris-redhat
commented
Aug 2, 2026
Forge is addressing PR review feedback now. This status update is informational. |
eranco74
commented
Aug 2, 2026
/lgtm |
ekuris-redhat
commented
Aug 2, 2026
thanks. I am now testing it and I will share the results when I have them. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
15 similar comments
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
temp io files under |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
5 similar comments
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 18, 2026
Forge is addressing PR review feedback now. |
forgeSmith-bot
commented
Aug 19, 2026
Forge is addressing PR review feedback now. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
temp io files under .mypy_cache/3.11 such as .mypy_cache/3.11/cache.0.db have been committed. These need to be squashed out . We also need to delete pipfile.
forgeSmith-bot
commented
Aug 19, 2026
Forge is addressing PR review feedback now. |
1 similar comment
forgeSmith-bot
commented
Aug 19, 2026
Forge is addressing PR review feedback now. |
ekuris-redhat
left a comment
There was a problem hiding this comment.
temp io files under .mypy_cache/3.11 such as .mypy_cache/3.11/cache.0.db have been committed. These need to be squashed out . We also need to delete pipfile.
forgeSmith-bot
commented
Aug 19, 2026
Forge is addressing PR review feedback now. |
1 similar comment
forgeSmith-bot
commented
Aug 19, 2026
Forge is addressing PR review feedback now. |
Detailed description: - Removed the accidentally committed .mypy_cache/3.11/ binary and database files from the repository index and filesystem. - Deleted the Pipfile from the root repository. - Avoided modifying .gitignore per repository and workflow instructions. Closes: AISOS-2294-review-fix
Summary
This pull request implements a streamlined planning review and approval flow for epic decomposition and task generation workflows. It leverages the LangGraph
FeatureStateas the single atomic source of truth to manage proposed items, supplemented by derived, one-way writes of updated drafts to Jira attachments to keep backups in sync, and attaching sliced task drafts to individual Epic tickets. Transition gates are resolved through label-based approvals (forge:plan-approvedandforge:task-approved) or comment-based commands (such as/forge approve), while revision requests and natural language feedback are captured via leading whitespace-tolerant comments starting with!. This simplifies the human-in-the-loop validation and regeneration loops, directly provisioning Epic and Task tickets in Jira under structured orchestration.Changes
Workflow State Management
FeatureStateas the single atomic source of truth for planning and review states, supplemented by derived, one-way writes of updated drafts to Jira attachments to keep backups in sync.epic_keys_to_clean, and renaming duplicated variables in orchestrator worker resume blocks).Natural Language Feedback & Comment Classification
src/forge/workflow/utils/comment_classifier.pyto identify comments starting with a!prefix asCommentType.FEEDBACK, comments with a?prefix asCommentType.QUESTION, and/forgecommands asCommentType.COMMAND(supportingremove,exclude,approve,add, andupdatecommands).forge:plan-approved,forge:task-approved) and comment-based commands (such as/forge approve) to trigger direct ticket provisioning.Orchestration & State Machine Integration
src/forge/workflow/nodes/epic_decomposition.pyandsrc/forge/workflow/nodes/task_generation.pyto conditionally enter the plan/task approval gates, pausing execution atPENDING_APPROVALto wait for webhook events.epic_key.update_docs_repo.pyand reverted type-safety changes indocs_updater.pyto match the main branch state.checkout_commitandhas_commits_aheadmethods fromGitOperationsinsrc/forge/workspace/git_ops.pyto clean up dead code.forge:direct-modelabel, which slices task drafts per Epic and attaches epic-specificforge-tasks-draft.jsonfiles to individual Epic tickets, and automatically deletes them upon provisioning.Pipfileand.mypy_cache/directory in Git to prevent cache pollution and index bloat, preservingpyproject.tomlanduv.lockas the correct package definition files.Documentation Updates
CLAUDE.md,docs/guide/labels.md,docs/guide/feature-workflow.md, anddocs/developer-guide.mdto reflect comment-based commands, label-based approvals, revision comments, and updated workflow stage transitions.Implementation Notes
FeatureState): By utilizing the LangGraph state checkpointing mechanism, the plan status and generated epics/tasks are managed as part of the state context. For synchronicity, a derived, one-way write of updated drafts is made back to Jira attachments during interactive edits.forge:plan-approved,forge:task-approved) or comment-based commands (such as/forge approve).checkout_commitandhas_commits_aheadgit methods, and simplifies the docs updater implementation by removing separate docs repository handling and inlining workspace exclude logic to maintain alignment with the main branch.forge:yololabel to automatically skip the planning approval gates and provision issues immediately, while cleaning up globalyolo_modesettings checks. Additionally, supports aforge:direct-modelabel for direct ticket creation without full YOLO mode.Testing
tests/integration/orchestrator/test_workflow_execution.pyto the pluggable workflows and configured proper mocks.tests/unit/models/test_draft.pytests/unit/workflow/test_comment_classifier.pytests/unit/workflow/utils/test_draft_manager.pytests/workflow/utils/test_comment_command.pytests/unit/integrations/jira/test_client_attachments.pytests/unit/integrations/agents/test_agent.pytests/unit/orchestrator/gates/test_plan_approval.pytests/unit/orchestrator/gates/test_task_approval.pytests/unit/workflow/nodes/test_epic_decomposition.pytests/unit/workflow/nodes/test_task_generation.pytests/unit/orchestrator/test_worker.pytests/integration/orchestrator/test_workflow_execution.pytests/workflow/test_draft_review_flow.pytests/sandbox/test_task_execution.pyRelated Tickets
FixedAdd decompose draft review step before Jira task creation #218
Generated by Forge SDLC Orchestrator