ralph-hero-mcp-server 2.4.39 → 2.4.41
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.
|
@@ -22,7 +22,7 @@ const REMAINING_PHASES = {
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
// Oversized estimate detection
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
|
-
const OVERSIZED_ESTIMATES = new Set(["M", "L", "XL"]);
|
|
25
|
+
export const OVERSIZED_ESTIMATES = new Set(["M", "L", "XL"]);
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Detection logic
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
@@ -56,8 +56,8 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary) {
|
|
|
56
56
|
blocking: [],
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
// Step 1: Check for oversized issues needing split
|
|
60
|
-
const oversized = issues.filter((i) => i.estimate !== null && OVERSIZED_ESTIMATES.has(i.estimate));
|
|
59
|
+
// Step 1: Check for oversized issues needing split (skip already-split issues)
|
|
60
|
+
const oversized = issues.filter((i) => i.estimate !== null && OVERSIZED_ESTIMATES.has(i.estimate) && i.subIssueCount === 0);
|
|
61
61
|
if (oversized.length > 0) {
|
|
62
62
|
return buildResult("SPLIT", `${oversized.length} issue(s) need splitting (estimate: ${oversized.map((i) => `#${i.number}=${i.estimate}`).join(", ")})`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
|
|
63
63
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { paginateConnection } from "../lib/pagination.js";
|
|
9
9
|
import { detectGroup } from "../lib/group-detection.js";
|
|
10
|
-
import { detectPipelinePosition, } from "../lib/pipeline-detection.js";
|
|
10
|
+
import { detectPipelinePosition, OVERSIZED_ESTIMATES, } from "../lib/pipeline-detection.js";
|
|
11
11
|
import { isValidState, VALID_STATES, LOCK_STATES, } from "../lib/workflow-states.js";
|
|
12
12
|
import { resolveState } from "../lib/state-resolution.js";
|
|
13
13
|
import { parseDateMath } from "../lib/date-math.js";
|
|
@@ -880,8 +880,28 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
880
880
|
title: ticket.title,
|
|
881
881
|
workflowState: state.workflowState || "unknown",
|
|
882
882
|
estimate: state.estimate || null,
|
|
883
|
+
subIssueCount: 0,
|
|
883
884
|
};
|
|
884
885
|
}));
|
|
886
|
+
// Fetch sub-issue counts for oversized issues (targeted query, not all issues)
|
|
887
|
+
const oversizedNumbers = issueStates
|
|
888
|
+
.filter((i) => i.estimate !== null && OVERSIZED_ESTIMATES.has(i.estimate))
|
|
889
|
+
.map((i) => i.number);
|
|
890
|
+
if (oversizedNumbers.length > 0) {
|
|
891
|
+
await Promise.all(oversizedNumbers.map(async (num) => {
|
|
892
|
+
const subResult = await client.query(`query($owner: String!, $repo: String!, $issueNum: Int!) {
|
|
893
|
+
repository(owner: $owner, name: $repo) {
|
|
894
|
+
issue(number: $issueNum) {
|
|
895
|
+
subIssuesSummary { total }
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}`, { owner, repo, issueNum: num });
|
|
899
|
+
const issueState = issueStates.find((i) => i.number === num);
|
|
900
|
+
if (issueState && subResult.repository?.issue?.subIssuesSummary) {
|
|
901
|
+
issueState.subIssueCount = subResult.repository.issue.subIssuesSummary.total;
|
|
902
|
+
}
|
|
903
|
+
}));
|
|
904
|
+
}
|
|
885
905
|
// Detect pipeline position
|
|
886
906
|
const position = detectPipelinePosition(issueStates, group.isGroup, group.groupPrimary.number);
|
|
887
907
|
return toolSuccess(position);
|
|
@@ -13,6 +13,53 @@ import { isValidState, isEarlierState, VALID_STATES, PARENT_GATE_STATES, isParen
|
|
|
13
13
|
import { toolSuccess, toolError } from "../types.js";
|
|
14
14
|
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, syncStatusField, } from "../lib/helpers.js";
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
+
// Sub-issue tree helpers (exported for testing)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* Recursively build the GraphQL selection set for nested sub-issues.
|
|
20
|
+
* At the leaf level (currentDepth >= maxDepth), returns only base fields.
|
|
21
|
+
* At inner levels, includes subIssuesSummary and nested subIssues.
|
|
22
|
+
*/
|
|
23
|
+
export function buildSubIssueFragment(currentDepth, maxDepth) {
|
|
24
|
+
const base = "id number title state";
|
|
25
|
+
if (currentDepth >= maxDepth)
|
|
26
|
+
return base;
|
|
27
|
+
return `${base}
|
|
28
|
+
subIssuesSummary { total completed percentCompleted }
|
|
29
|
+
subIssues(first: 50) {
|
|
30
|
+
nodes { ${buildSubIssueFragment(currentDepth + 1, maxDepth)} }
|
|
31
|
+
}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Recursively map raw GraphQL sub-issue nodes into typed SubIssueNode[].
|
|
35
|
+
* Adds subIssues/subIssuesSummary fields when currentDepth < maxDepth.
|
|
36
|
+
*/
|
|
37
|
+
export function mapSubIssueNodes(
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
nodes, currentDepth, maxDepth) {
|
|
40
|
+
return nodes.map((node) => {
|
|
41
|
+
const mapped = {
|
|
42
|
+
id: node.id,
|
|
43
|
+
number: node.number,
|
|
44
|
+
title: node.title,
|
|
45
|
+
state: node.state,
|
|
46
|
+
};
|
|
47
|
+
if (currentDepth < maxDepth && node.subIssues?.nodes) {
|
|
48
|
+
mapped.subIssues = mapSubIssueNodes(node.subIssues.nodes, currentDepth + 1, maxDepth);
|
|
49
|
+
mapped.subIssuesSummary = node.subIssuesSummary || {
|
|
50
|
+
total: node.subIssues.nodes.length,
|
|
51
|
+
completed: node.subIssues.nodes.filter((si) => si.state === "CLOSED").length,
|
|
52
|
+
percentCompleted: node.subIssues.nodes.length > 0
|
|
53
|
+
? Math.round((node.subIssues.nodes.filter((si) => si.state === "CLOSED").length /
|
|
54
|
+
node.subIssues.nodes.length) *
|
|
55
|
+
100)
|
|
56
|
+
: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return mapped;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
16
63
|
// Register relationship tools
|
|
17
64
|
// ---------------------------------------------------------------------------
|
|
18
65
|
export function registerRelationshipTools(server, client, fieldCache) {
|
|
@@ -73,7 +120,7 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
73
120
|
// -------------------------------------------------------------------------
|
|
74
121
|
// ralph_hero__list_sub_issues
|
|
75
122
|
// -------------------------------------------------------------------------
|
|
76
|
-
server.tool("ralph_hero__list_sub_issues", "List all sub-issues (children) of a parent GitHub issue, with completion summary", {
|
|
123
|
+
server.tool("ralph_hero__list_sub_issues", "List all sub-issues (children) of a parent GitHub issue, with completion summary. Use depth parameter (1-3) to fetch nested sub-issue trees in a single call. Default depth=1 returns direct children only.", {
|
|
77
124
|
owner: z
|
|
78
125
|
.string()
|
|
79
126
|
.optional()
|
|
@@ -83,10 +130,17 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
83
130
|
.optional()
|
|
84
131
|
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
85
132
|
number: z.coerce.number().describe("Parent issue number"),
|
|
133
|
+
depth: z.coerce
|
|
134
|
+
.number()
|
|
135
|
+
.optional()
|
|
136
|
+
.default(1)
|
|
137
|
+
.describe("How many levels of sub-issues to fetch (1=direct children, 2=children+grandchildren, max 3)"),
|
|
86
138
|
}, async (args) => {
|
|
87
139
|
try {
|
|
88
140
|
const { owner, repo } = resolveConfig(client, args);
|
|
89
|
-
const
|
|
141
|
+
const depth = Math.min(Math.max(args.depth, 1), 3);
|
|
142
|
+
const subIssueFields = buildSubIssueFragment(1, depth);
|
|
143
|
+
const queryStr = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
90
144
|
repository(owner: $owner, name: $repo) {
|
|
91
145
|
issue(number: $number) {
|
|
92
146
|
id
|
|
@@ -94,34 +148,31 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
94
148
|
title
|
|
95
149
|
subIssuesSummary { total completed percentCompleted }
|
|
96
150
|
subIssues(first: 50) {
|
|
97
|
-
nodes {
|
|
151
|
+
nodes { ${subIssueFields} }
|
|
98
152
|
pageInfo { hasNextPage endCursor }
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
155
|
}
|
|
102
|
-
}
|
|
156
|
+
}`;
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
const result = await client.query(queryStr, { owner, repo, number: args.number });
|
|
103
159
|
const issue = result.repository?.issue;
|
|
104
160
|
if (!issue) {
|
|
105
161
|
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
106
162
|
}
|
|
163
|
+
const mappedSubIssues = mapSubIssueNodes(issue.subIssues.nodes, 1, depth);
|
|
107
164
|
return toolSuccess({
|
|
108
165
|
parent: {
|
|
109
166
|
id: issue.id,
|
|
110
167
|
number: issue.number,
|
|
111
168
|
title: issue.title,
|
|
112
169
|
},
|
|
113
|
-
subIssues:
|
|
114
|
-
id: si.id,
|
|
115
|
-
number: si.number,
|
|
116
|
-
title: si.title,
|
|
117
|
-
state: si.state,
|
|
118
|
-
})),
|
|
170
|
+
subIssues: mappedSubIssues,
|
|
119
171
|
summary: issue.subIssuesSummary || {
|
|
120
172
|
total: issue.subIssues.nodes.length,
|
|
121
173
|
completed: issue.subIssues.nodes.filter((si) => si.state === "CLOSED").length,
|
|
122
174
|
percentCompleted: issue.subIssues.nodes.length > 0
|
|
123
|
-
? Math.round((issue.subIssues.nodes.filter((si) => si.state === "CLOSED")
|
|
124
|
-
.length /
|
|
175
|
+
? Math.round((issue.subIssues.nodes.filter((si) => si.state === "CLOSED").length /
|
|
125
176
|
issue.subIssues.nodes.length) *
|
|
126
177
|
100)
|
|
127
178
|
: 0,
|