Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
import com.github.istin.dmtools.atlassian.jira.BasicJiraClient;
import com.github.istin.dmtools.context.ContextOrchestrator;
import com.github.istin.dmtools.presentation.PresentationMakerOrchestrator;
import com.github.istin.dmtools.projectsetup.agent.FinalStatusDetectionAgent;
import com.github.istin.dmtools.projectsetup.agent.ProjectSetupAnalysisAgent;
import com.github.istin.dmtools.projectsetup.agent.StoryDescriptionWritingRulesAgent;
import com.github.istin.dmtools.projectsetup.agent.TestCaseWritingRulesAgent;
import com.github.istin.dmtools.projectsetup.agent.WorkflowAnalysisAgent;
import com.github.istin.dmtools.prompt.IPromptTemplateReader;
import com.github.istin.dmtools.search.ConfluenceSearchOrchestrator;
import com.github.istin.dmtools.search.TrackerSearchOrchestrator;
Expand Down Expand Up @@ -139,4 +144,29 @@ ToolSelectorAgent provideToolSelectorAgent() {
MermaidDiagramGeneratorAgent provideMermaidDiagramGeneratorAgent() {
return new MermaidDiagramGeneratorAgent();
}

@Provides
FinalStatusDetectionAgent provideFinalStatusDetectionAgent() {
return new FinalStatusDetectionAgent();
}

@Provides
ProjectSetupAnalysisAgent provideProjectSetupAnalysisAgent() {
return new ProjectSetupAnalysisAgent();
}

@Provides
WorkflowAnalysisAgent provideWorkflowAnalysisAgent() {
return new WorkflowAnalysisAgent();
}

@Provides
StoryDescriptionWritingRulesAgent provideStoryDescriptionWritingRulesAgent() {
return new StoryDescriptionWritingRulesAgent();
}

@Provides
TestCaseWritingRulesAgent provideTestCaseWritingRulesAgent() {
return new TestCaseWritingRulesAgent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.istin.dmtools.di;

import com.github.istin.dmtools.projectsetup.agent.FinalStatusDetectionAgent;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(modules = {ConfigurationModule.class, AIComponentsModule.class})
public interface FinalStatusDetectionAgentComponent {
void inject(FinalStatusDetectionAgent finalStatusDetectionAgent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.istin.dmtools.di;

import com.github.istin.dmtools.projectsetup.agent.ProjectSetupAnalysisAgent;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(modules = {ConfigurationModule.class, AIComponentsModule.class})
public interface ProjectSetupAnalysisAgentComponent {
void inject(ProjectSetupAnalysisAgent projectSetupAnalysisAgent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.istin.dmtools.di;

import com.github.istin.dmtools.projectsetup.agent.StoryDescriptionWritingRulesAgent;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(modules = {ConfigurationModule.class, AIComponentsModule.class})
public interface StoryDescriptionWritingRulesAgentComponent {
void inject(StoryDescriptionWritingRulesAgent storyDescriptionWritingRulesAgent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.istin.dmtools.di;

import com.github.istin.dmtools.projectsetup.agent.TestCaseWritingRulesAgent;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(modules = {ConfigurationModule.class, AIComponentsModule.class})
public interface TestCaseWritingRulesAgentComponent {
void inject(TestCaseWritingRulesAgent testCaseWritingRulesAgent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.istin.dmtools.di;

import com.github.istin.dmtools.projectsetup.agent.WorkflowAnalysisAgent;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(modules = {ConfigurationModule.class, AIComponentsModule.class})
public interface WorkflowAnalysisAgentComponent {
void inject(WorkflowAnalysisAgent workflowAnalysisAgent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package com.github.istin.dmtools.projectsetup;

import com.github.istin.dmtools.atlassian.jira.JiraClient;
import com.github.istin.dmtools.common.model.ITicket;
import com.github.istin.dmtools.common.model.ToText;
import com.github.istin.dmtools.common.tracker.TrackerClient;
import com.github.istin.dmtools.di.ServerManagedIntegrationsModule;
import com.github.istin.dmtools.job.AbstractJob;
import com.github.istin.dmtools.projectsetup.agent.*;
import lombok.Getter;
import org.json.JSONArray;
import org.json.JSONObject;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import dagger.Component;

public class ProjectSetupAnalysisJob extends AbstractJob<ProjectSetupAnalysisJobParams, JSONObject> {

@Inject
TrackerClient<? extends ITicket> trackerClient;

@Inject
FinalStatusDetectionAgent finalStatusDetectionAgent;

@Inject
ProjectSetupAnalysisAgent projectSetupAnalysisAgent;

@Inject
WorkflowAnalysisAgent workflowAnalysisAgent;

@Inject
StoryDescriptionWritingRulesAgent storyDescriptionWritingRulesAgent;

@Inject
TestCaseWritingRulesAgent testCaseWritingRulesAgent;

@Getter
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method overrides AbstractJob<ProjectSetupAnalysisJobParams,JSONObject>.getAi; it is advisable to add an Override annotation.

Suggested change
@Getter
@Getter(onMethod_ = @Override)

Copilot uses AI. Check for mistakes.
@Inject
com.github.istin.dmtools.ai.AI ai;

@Singleton
@Component(modules = {com.github.istin.dmtools.di.ConfigurationModule.class,
com.github.istin.dmtools.di.JiraModule.class,
com.github.istin.dmtools.di.AIComponentsModule.class,
com.github.istin.dmtools.di.ConfluenceModule.class,
com.github.istin.dmtools.di.AIAgentsModule.class})
public interface ProjectSetupAnalysisJobComponent {
void inject(ProjectSetupAnalysisJob projectSetupAnalysisJob);
}

@Singleton
@Component(modules = {ServerManagedIntegrationsModule.class, com.github.istin.dmtools.di.AIAgentsModule.class})
public interface ServerManagedProjectSetupAnalysisJobComponent {
void inject(ProjectSetupAnalysisJob projectSetupAnalysisJob);
}

public ProjectSetupAnalysisJob() {
}

@Override
protected void initializeStandalone() {
DaggerProjectSetupAnalysisJob_ProjectSetupAnalysisJobComponent.create().inject(this);
}

@Override
protected void initializeServerManaged(org.json.JSONObject resolvedIntegrations) {
try {
ServerManagedIntegrationsModule module = new ServerManagedIntegrationsModule(resolvedIntegrations);
ServerManagedProjectSetupAnalysisJobComponent component =
DaggerProjectSetupAnalysisJob_ServerManagedProjectSetupAnalysisJobComponent.builder()
.serverManagedIntegrationsModule(module)
.build();
component.inject(this);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize ProjectSetupAnalysisJob in server-managed mode", e);
}
}

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation for the public method executeJob. Add documentation explaining the method's purpose, parameters, return value, and exceptions:

/**
 * Executes the project setup analysis job for the given project.
 * 
 * @param params The job parameters containing the project key
 * @return A JSONObject containing aggregated analysis results from all agents
 * @throws IllegalArgumentException if projectKey is null or empty
 * @throws Exception if any agent execution or data retrieval fails
 */
@Override
protected JSONObject executeJob(ProjectSetupAnalysisJobParams params) throws Exception {
Suggested change
/**
* Executes the project setup analysis job for the given project.
*
* @param params The job parameters containing the project key.
* @return A {@link JSONObject} containing aggregated analysis results from all agents.
* @throws IllegalArgumentException if the project key is null or empty.
* @throws Exception if any agent execution or data retrieval fails.
*/

Copilot uses AI. Check for mistakes.
@Override
protected JSONObject executeJob(ProjectSetupAnalysisJobParams params) throws Exception {
String projectKey = params.getProjectKey();
if (projectKey == null || projectKey.trim().isEmpty()) {
throw new IllegalArgumentException("projectKey is required");
}

// Step 1: Get workflow metadata for final status detection
String workflowMetadata = getWorkflowMetadata(projectKey);

// Step 2: Detect final statuses
JSONArray finalStatuses = finalStatusDetectionAgent.run(
new FinalStatusDetectionAgent.Params(projectKey, workflowMetadata)
);

// Step 3: Get issue types and fields
// Cast TrackerClient to JiraClient to access getIssueTypes and getFields methods
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast from TrackerClient to JiraClient is unsafe without a type check. If the trackerClient implementation is not actually a JiraClient, this will throw a ClassCastException at runtime. Add a type check before casting:

if (!(trackerClient instanceof JiraClient)) {
    throw new IllegalStateException("TrackerClient must be an instance of JiraClient for this job");
}
JiraClient<? extends ITicket> jiraClient = (JiraClient<? extends ITicket>) trackerClient;
Suggested change
// Cast TrackerClient to JiraClient to access getIssueTypes and getFields methods
// Cast TrackerClient to JiraClient to access getIssueTypes and getFields methods
if (!(trackerClient instanceof JiraClient)) {
throw new IllegalStateException("TrackerClient must be an instance of JiraClient for this job");
}

Copilot uses AI. Check for mistakes.
JiraClient<? extends ITicket> jiraClient = (JiraClient<? extends ITicket>) trackerClient;
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import statement for JiraClient. The code uses JiraClient at line 101 but doesn't import it. Add the import:

import com.github.istin.dmtools.atlassian.jira.JiraClient;

Copilot uses AI. Check for mistakes.
List<com.github.istin.dmtools.atlassian.jira.model.IssueType> issueTypes = jiraClient.getIssueTypes(projectKey);
String fieldsJson = jiraClient.getFields(projectKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassCastException when using non-Jira trackers

High Severity

The job unconditionally casts trackerClient to JiraClient to call getIssueTypes() and getFields(). In server-managed mode, ServerManagedIntegrationsModule.provideTrackerClient() can return AzureDevOpsClient or RallyClient instances, which do not extend JiraClient. This cast throws a ClassCastException at runtime when the configured tracker is Azure DevOps or Rally, making the job incompatible with non-Jira trackers.

Fix in Cursor Fix in Web


// Step 4: Analyze project setup
JSONObject projectSetupResult = projectSetupAnalysisAgent.run(
new ProjectSetupAnalysisAgent.Params(
projectKey,
new JSONArray(issueTypes).toString(),
fieldsJson
)
Comment on lines +109 to +111
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor new JSONArray(issueTypes) will not correctly serialize a List of IssueType objects. The JSONArray constructor that takes a Collection will call toString() on each element, which may not produce valid JSON structure. Convert the list properly:

JSONArray issueTypesArray = new JSONArray();
for (IssueType issueType : issueTypes) {
    issueTypesArray.put(issueType.getJSONObject());
}
String issueTypesJson = issueTypesArray.toString();

Then pass issueTypesJson to the Params constructor.

Copilot uses AI. Check for mistakes.
);

// Step 5: Get completed tickets (last 50)
List<? extends ITicket> completedTickets = getCompletedTickets(projectKey, finalStatuses);
String completedTicketsData = formatTicketsForAnalysis(completedTickets);

// Step 6: Analyze workflow
JSONObject workflowAnalysisResult = workflowAnalysisAgent.run(
new WorkflowAnalysisAgent.Params(
projectKey,
finalStatuses,
completedTicketsData
)
);

// Step 7: Extract story descriptions
String storyDescriptionsData = extractStoryDescriptions(completedTickets);

// Step 8: Generate story description writing rules
JSONObject storyDescriptionRules = storyDescriptionWritingRulesAgent.run(
new StoryDescriptionWritingRulesAgent.Params(
projectKey,
storyDescriptionsData
)
);

// Step 9: Extract test case data
String testCaseData = extractTestCaseData(completedTickets);

// Step 10: Generate test case writing rules
JSONObject testCaseRules = testCaseWritingRulesAgent.run(
new TestCaseWritingRulesAgent.Params(
projectKey,
testCaseData
)
);

// Step 11: Aggregate all results
JSONObject result = new JSONObject();
result.put("projectKey", projectKey);
result.put("projectIssueTypes", projectSetupResult.optJSONArray("issueTypes") != null
? projectSetupResult.getJSONArray("issueTypes")
: new JSONArray(issueTypes));
result.put("projectFields", projectSetupResult.optJSONObject("fields") != null
? projectSetupResult.getJSONObject("fields")
: new JSONObject().put("raw", fieldsJson));
result.put("finalStatuses", finalStatuses);
result.put("workflowAnalysis", workflowAnalysisResult);
result.put("storyDescriptionRules", storyDescriptionRules);
result.put("testCaseRules", testCaseRules);

return result;
}

Comment thread
IstiN marked this conversation as resolved.
private String getWorkflowMetadata(String projectKey) throws Exception {
// Try to get workflow metadata from Jira API
// For now, return a placeholder - this would need to call Jira workflow API
try {
// Attempt to get workflow information
// This is a simplified approach - in production, you'd call the Jira workflow API
return "{\"projectKey\":\"" + projectKey + "\",\"note\":\"Workflow metadata retrieval needs Jira workflow API integration\"}";
} catch (Exception e) {
return "{\"projectKey\":\"" + projectKey + "\",\"error\":\"" + e.getMessage() + "\"}";
}
}

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation. Add documentation:

/**
 * Retrieves completed tickets for the specified project based on final statuses.
 * Returns the last 50 tickets ordered by updated date descending.
 * 
 * @param projectKey The Jira project key
 * @param finalStatuses The array of final status names to query, or null to use defaults
 * @return A list of completed tickets, limited to the most recent 50
 * @throws Exception if ticket retrieval fails
 */
private List<? extends ITicket> getCompletedTickets(String projectKey, JSONArray finalStatuses) throws Exception {
Suggested change
/**
* Retrieves completed tickets for the specified project based on final statuses.
* Returns the last 50 tickets ordered by updated date descending.
*
* @param projectKey The Jira project key
* @param finalStatuses The array of final status names to query, or null to use defaults
* @return A list of completed tickets, limited to the most recent 50
* @throws Exception if ticket retrieval fails
*/

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Verified via automated test of github_add_inline_comment MCP tool (auto-fetched commitId).

private List<? extends ITicket> getCompletedTickets(String projectKey, JSONArray finalStatuses) throws Exception {
// Build JQL query for tickets in final statuses
StringBuilder jql = new StringBuilder("project = ").append(projectKey);

if (finalStatuses != null && finalStatuses.length() > 0) {
jql.append(" AND status IN (");
for (int i = 0; i < finalStatuses.length(); i++) {
if (i > 0) jql.append(", ");
String status = finalStatuses.getString(i);
jql.append("\"").append(status).append("\"");
}
jql.append(")");
} else {
// Fallback to common final statuses if detection failed
jql.append(" AND status IN (\"Done\", \"Closed\", \"Resolved\", \"Completed\")");
}

jql.append(" ORDER BY updated DESC");

// Get last 50 tickets
List<? extends ITicket> allTickets = trackerClient.searchAndPerform(
jql.toString(),
trackerClient.getExtendedQueryFields()
);

return allTickets.stream()
.limit(50)
Comment on lines +203 to +204
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 50 for limiting tickets should be defined as a named constant for better maintainability:

private static final int MAX_COMPLETED_TICKETS = 50;

Then use:

return allTickets.stream()
    .limit(MAX_COMPLETED_TICKETS)
    .collect(Collectors.toList());

Copilot uses AI. Check for mistakes.
.collect(Collectors.toList());
}

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation for private helper methods. Add documentation for better code maintainability:

/**
 * Formats a list of tickets into a text representation suitable for AI analysis.
 * Falls back to basic ticket information if full ticket text conversion fails.
 * 
 * @param tickets The list of tickets to format
 * @return A formatted string with ticket data separated by delimiters
 */
private String formatTicketsForAnalysis(List<? extends ITicket> tickets) {
Suggested change
/**
* Formats a list of tickets into a text representation suitable for AI analysis.
* Attempts to use the ticket's {@code toText()} method; if that fails, falls back to
* basic ticket information (key, title, description). If all else fails, uses only the ticket key.
*
* @param tickets the list of tickets to format
* @return a formatted string with ticket data separated by delimiters
*/

Copilot uses AI. Check for mistakes.
private String formatTicketsForAnalysis(List<? extends ITicket> tickets) {
List<String> ticketTexts = new ArrayList<>();
for (ITicket ticket : tickets) {
try {
ticketTexts.add(ticket.toText());
} catch (Exception e) {
// Fallback to basic ticket info if toText() fails
try {
String title = ticket.getTicketTitle();
String description = ticket.getTicketDescription();
ticketTexts.add("Issue: " + ticket.getTicketKey() + "\nSummary: " +
(title != null ? title : "") +
"\nDescription: " + (description != null ? description : ""));
} catch (Exception ex) {
// If even fallback fails, just use the key
ticketTexts.add("Issue: " + ticket.getTicketKey());
}
}
}
return String.join("\n\n---\n\n", ticketTexts);
}

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation. Add documentation:

/**
 * Extracts story descriptions from the provided tickets across all issue types.
 * 
 * @param tickets The list of tickets to extract descriptions from
 * @return A formatted string containing ticket descriptions separated by delimiters
 */
private String extractStoryDescriptions(List<? extends ITicket> tickets) {
Suggested change
/**
* Extracts story descriptions from the provided tickets across all issue types.
*
* @param tickets The list of tickets to extract descriptions from
* @return A formatted string containing ticket descriptions separated by delimiters
*/

Copilot uses AI. Check for mistakes.
private String extractStoryDescriptions(List<? extends ITicket> tickets) {
List<String> descriptions = new ArrayList<>();
for (ITicket ticket : tickets) {
try {
// Include all issue types, not just Story (per DMC-778)
String description = ticket.getTicketDescription();
if (description != null && !description.trim().isEmpty()) {
descriptions.add("Issue: " + ticket.getTicketKey() + "\nType: " + ticket.getIssueType() + "\nDescription: " + description);
}
} catch (Exception e) {
// Skip tickets that fail to retrieve description
}
}
return String.join("\n\n---\n\n", descriptions);
}

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation. Add documentation:

/**
 * Extracts test case data from the provided tickets across all issue types.
 * 
 * @param tickets The list of tickets to extract test case data from
 * @return A formatted string containing ticket summaries and descriptions separated by delimiters
 */
private String extractTestCaseData(List<? extends ITicket> tickets) {
Suggested change
/**
* Extracts test case data from the provided tickets across all issue types.
*
* @param tickets The list of tickets to extract test case data from
* @return A formatted string containing ticket summaries and descriptions separated by delimiters
*/

Copilot uses AI. Check for mistakes.
private String extractTestCaseData(List<? extends ITicket> tickets) {
List<String> testCaseData = new ArrayList<>();
for (ITicket ticket : tickets) {
try {
// Include all issue types for test case analysis (per DMC-778)
String description = ticket.getTicketDescription();
String summary = ticket.getTicketTitle();
if ((description != null && !description.trim().isEmpty()) ||
(summary != null && !summary.trim().isEmpty())) {
testCaseData.add("Issue: " + ticket.getTicketKey() +
"\nType: " + ticket.getIssueType() +
"\nSummary: " + (summary != null ? summary : "") +
"\nDescription: " + (description != null ? description : ""));
}
} catch (Exception e) {
// Skip tickets that fail to retrieve data
}
}
return String.join("\n\n---\n\n", testCaseData);
}
Comment thread
IstiN marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.istin.dmtools.projectsetup;

import com.github.istin.dmtools.job.Params;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Data
@EqualsAndHashCode(callSuper = false)
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method overrides Params.canEqual; it is advisable to add an Override annotation.

Copilot uses AI. Check for mistakes.
@NoArgsConstructor
@AllArgsConstructor
public class ProjectSetupAnalysisJobParams extends Params {

public static final String PROJECT_KEY = "projectKey";

@SerializedName(PROJECT_KEY)
private String projectKey;
}
Loading
Loading