ralph-hero-mcp-server 2.5.3 → 2.5.13
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.
- package/dist/lib/debug-logger.js +8 -3
- package/dist/lib/group-detection.js +83 -17
- package/dist/tools/decompose-tools.js +18 -16
- package/dist/tools/relationship-tools.js +140 -14
- package/package.json +1 -1
package/dist/lib/debug-logger.js
CHANGED
|
@@ -35,14 +35,19 @@ export function sanitize(obj) {
|
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
export class DebugLogger {
|
|
37
37
|
logPath = null;
|
|
38
|
+
logPathPromise = null;
|
|
38
39
|
logDir;
|
|
39
40
|
constructor(options) {
|
|
40
41
|
this.logDir =
|
|
41
42
|
options?.logDir ?? join(homedir(), ".ralph-hero", "logs");
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
if (this.
|
|
45
|
-
|
|
44
|
+
getLogPath() {
|
|
45
|
+
if (!this.logPathPromise) {
|
|
46
|
+
this.logPathPromise = this.initLogPath();
|
|
47
|
+
}
|
|
48
|
+
return this.logPathPromise;
|
|
49
|
+
}
|
|
50
|
+
async initLogPath() {
|
|
46
51
|
await mkdir(this.logDir, { recursive: true });
|
|
47
52
|
const now = new Date();
|
|
48
53
|
const ts = now
|
|
@@ -26,8 +26,8 @@ const SEED_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
|
26
26
|
number
|
|
27
27
|
title
|
|
28
28
|
state
|
|
29
|
-
blocking(first: 20) { nodes { number } }
|
|
30
|
-
blockedBy(first: 20) { nodes { number } }
|
|
29
|
+
blocking(first: 20) { nodes { number repository { owner { login } name } } }
|
|
30
|
+
blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -37,15 +37,15 @@ const SEED_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
|
37
37
|
number
|
|
38
38
|
title
|
|
39
39
|
state
|
|
40
|
-
blocking(first: 20) { nodes { number } }
|
|
41
|
-
blockedBy(first: 20) { nodes { number } }
|
|
40
|
+
blocking(first: 20) { nodes { number repository { owner { login } name } } }
|
|
41
|
+
blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
blocking(first: 20) {
|
|
45
|
-
nodes { id number title state }
|
|
45
|
+
nodes { id number title state repository { owner { login } name } }
|
|
46
46
|
}
|
|
47
47
|
blockedBy(first: 20) {
|
|
48
|
-
nodes { id number title state }
|
|
48
|
+
nodes { id number title state repository { owner { login } name } }
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -69,8 +69,8 @@ const EXPAND_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
|
69
69
|
number
|
|
70
70
|
title
|
|
71
71
|
state
|
|
72
|
-
blocking(first: 20) { nodes { number } }
|
|
73
|
-
blockedBy(first: 20) { nodes { number } }
|
|
72
|
+
blocking(first: 20) { nodes { number repository { owner { login } name } } }
|
|
73
|
+
blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -80,15 +80,15 @@ const EXPAND_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
|
80
80
|
number
|
|
81
81
|
title
|
|
82
82
|
state
|
|
83
|
-
blocking(first: 20) { nodes { number } }
|
|
84
|
-
blockedBy(first: 20) { nodes { number } }
|
|
83
|
+
blocking(first: 20) { nodes { number repository { owner { login } name } } }
|
|
84
|
+
blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
blocking(first: 20) {
|
|
88
|
-
nodes { id number title state }
|
|
88
|
+
nodes { id number title state repository { owner { login } name } }
|
|
89
89
|
}
|
|
90
90
|
blockedBy(first: 20) {
|
|
91
|
-
nodes { id number title state }
|
|
91
|
+
nodes { id number title state repository { owner { login } name } }
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -111,18 +111,36 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
111
111
|
const issueMap = new Map();
|
|
112
112
|
// Queue of issue numbers to expand
|
|
113
113
|
const expandQueue = [];
|
|
114
|
+
// Cross-repo info for dependency targets (number -> { owner, repo })
|
|
115
|
+
const depRepoInfo = new Map();
|
|
116
|
+
// Helper to store cross-repo info from dependency nodes
|
|
117
|
+
function trackDepRepoInfo(nodes) {
|
|
118
|
+
for (const dep of nodes) {
|
|
119
|
+
if (dep.repository) {
|
|
120
|
+
depRepoInfo.set(dep.number, {
|
|
121
|
+
owner: dep.repository.owner.login,
|
|
122
|
+
repo: dep.repository.name,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
// Step 1: Seed query
|
|
115
128
|
const seedResult = await client.query(SEED_QUERY, { owner, repo, number: seedNumber });
|
|
116
129
|
const seedIssue = seedResult.repository?.issue;
|
|
117
130
|
if (!seedIssue) {
|
|
118
131
|
throw new Error(`Issue #${seedNumber} not found in ${owner}/${repo}`);
|
|
119
132
|
}
|
|
133
|
+
// Track cross-repo info from seed's direct dependencies
|
|
134
|
+
trackDepRepoInfo(seedIssue.blocking.nodes);
|
|
135
|
+
trackDepRepoInfo(seedIssue.blockedBy.nodes);
|
|
120
136
|
// Process seed issue
|
|
121
137
|
addIssueToMap(issueMap, {
|
|
122
138
|
id: seedIssue.id,
|
|
123
139
|
number: seedIssue.number,
|
|
124
140
|
title: seedIssue.title,
|
|
125
141
|
state: seedIssue.state,
|
|
142
|
+
repoOwner: owner,
|
|
143
|
+
repoName: repo,
|
|
126
144
|
parentNumber: seedIssue.parent?.number ?? null,
|
|
127
145
|
subIssueNumbers: seedIssue.subIssues.nodes.map((n) => n.number),
|
|
128
146
|
blockingNumbers: seedIssue.blocking.nodes.map((n) => n.number),
|
|
@@ -136,17 +154,24 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
136
154
|
number: parent.number,
|
|
137
155
|
title: parent.title,
|
|
138
156
|
state: parent.state,
|
|
157
|
+
repoOwner: owner,
|
|
158
|
+
repoName: repo,
|
|
139
159
|
parentNumber: null,
|
|
140
160
|
subIssueNumbers: parent.subIssues.nodes.map((n) => n.number),
|
|
141
161
|
blockingNumbers: [],
|
|
142
162
|
blockedByNumbers: [],
|
|
143
163
|
});
|
|
144
164
|
for (const sibling of parent.subIssues.nodes) {
|
|
165
|
+
// Track cross-repo info from sibling dependencies
|
|
166
|
+
trackDepRepoInfo(sibling.blocking?.nodes ?? []);
|
|
167
|
+
trackDepRepoInfo(sibling.blockedBy?.nodes ?? []);
|
|
145
168
|
addIssueToMap(issueMap, {
|
|
146
169
|
id: sibling.id,
|
|
147
170
|
number: sibling.number,
|
|
148
171
|
title: sibling.title,
|
|
149
172
|
state: sibling.state,
|
|
173
|
+
repoOwner: owner,
|
|
174
|
+
repoName: repo,
|
|
150
175
|
parentNumber: parent.number,
|
|
151
176
|
subIssueNumbers: [],
|
|
152
177
|
blockingNumbers: sibling.blocking?.nodes.map((n) => n.number) ?? [],
|
|
@@ -156,11 +181,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
156
181
|
}
|
|
157
182
|
// Process sub-issues of seed
|
|
158
183
|
for (const child of seedIssue.subIssues.nodes) {
|
|
184
|
+
// Track cross-repo info from child dependencies
|
|
185
|
+
trackDepRepoInfo(child.blocking?.nodes ?? []);
|
|
186
|
+
trackDepRepoInfo(child.blockedBy?.nodes ?? []);
|
|
159
187
|
addIssueToMap(issueMap, {
|
|
160
188
|
id: child.id,
|
|
161
189
|
number: child.number,
|
|
162
190
|
title: child.title,
|
|
163
191
|
state: child.state,
|
|
192
|
+
repoOwner: owner,
|
|
193
|
+
repoName: repo,
|
|
164
194
|
parentNumber: seedIssue.number,
|
|
165
195
|
subIssueNumbers: [],
|
|
166
196
|
blockingNumbers: child.blocking?.nodes.map((n) => n.number) ?? [],
|
|
@@ -169,12 +199,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
169
199
|
}
|
|
170
200
|
// Process direct dependencies
|
|
171
201
|
for (const dep of seedIssue.blocking.nodes) {
|
|
202
|
+
const depOwner = dep.repository?.owner.login ?? owner;
|
|
203
|
+
const depRepo = dep.repository?.name ?? repo;
|
|
172
204
|
if (!issueMap.has(dep.number)) {
|
|
173
205
|
addIssueToMap(issueMap, {
|
|
174
206
|
id: dep.id,
|
|
175
207
|
number: dep.number,
|
|
176
208
|
title: dep.title,
|
|
177
209
|
state: dep.state,
|
|
210
|
+
repoOwner: depOwner,
|
|
211
|
+
repoName: depRepo,
|
|
178
212
|
parentNumber: null,
|
|
179
213
|
subIssueNumbers: [],
|
|
180
214
|
blockingNumbers: [],
|
|
@@ -184,12 +218,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
184
218
|
}
|
|
185
219
|
}
|
|
186
220
|
for (const dep of seedIssue.blockedBy.nodes) {
|
|
221
|
+
const depOwner = dep.repository?.owner.login ?? owner;
|
|
222
|
+
const depRepo = dep.repository?.name ?? repo;
|
|
187
223
|
if (!issueMap.has(dep.number)) {
|
|
188
224
|
addIssueToMap(issueMap, {
|
|
189
225
|
id: dep.id,
|
|
190
226
|
number: dep.number,
|
|
191
227
|
title: dep.title,
|
|
192
228
|
state: dep.state,
|
|
229
|
+
repoOwner: depOwner,
|
|
230
|
+
repoName: depRepo,
|
|
193
231
|
parentNumber: null,
|
|
194
232
|
subIssueNumbers: [],
|
|
195
233
|
blockingNumbers: [],
|
|
@@ -220,16 +258,35 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
220
258
|
continue;
|
|
221
259
|
}
|
|
222
260
|
expanded.add(num);
|
|
261
|
+
// Resolve owner/repo for this issue — check cross-repo info first
|
|
262
|
+
const crossRepoInfo = depRepoInfo.get(num);
|
|
263
|
+
const expandOwner = crossRepoInfo?.owner ?? owner;
|
|
264
|
+
const expandRepo = crossRepoInfo?.repo ?? repo;
|
|
223
265
|
try {
|
|
224
|
-
const expandResult = await client.query(EXPAND_QUERY, { owner, repo, number: num });
|
|
266
|
+
const expandResult = await client.query(EXPAND_QUERY, { owner: expandOwner, repo: expandRepo, number: num });
|
|
225
267
|
const expandedIssue = expandResult.repository?.issue;
|
|
226
268
|
if (!expandedIssue)
|
|
227
|
-
continue; //
|
|
269
|
+
continue; // Deleted issue, skip
|
|
270
|
+
// Track cross-repo info from expanded issue's dependency nodes
|
|
271
|
+
trackDepRepoInfo(expandedIssue.blocking.nodes);
|
|
272
|
+
trackDepRepoInfo(expandedIssue.blockedBy.nodes);
|
|
273
|
+
for (const child of expandedIssue.subIssues.nodes) {
|
|
274
|
+
trackDepRepoInfo(child.blocking?.nodes ?? []);
|
|
275
|
+
trackDepRepoInfo(child.blockedBy?.nodes ?? []);
|
|
276
|
+
}
|
|
277
|
+
if (expandedIssue.parent) {
|
|
278
|
+
for (const sibling of expandedIssue.parent.subIssues.nodes) {
|
|
279
|
+
trackDepRepoInfo(sibling.blocking?.nodes ?? []);
|
|
280
|
+
trackDepRepoInfo(sibling.blockedBy?.nodes ?? []);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
228
283
|
addIssueToMap(issueMap, {
|
|
229
284
|
id: expandedIssue.id,
|
|
230
285
|
number: expandedIssue.number,
|
|
231
286
|
title: expandedIssue.title,
|
|
232
287
|
state: expandedIssue.state,
|
|
288
|
+
repoOwner: expandOwner,
|
|
289
|
+
repoName: expandRepo,
|
|
233
290
|
parentNumber: expandedIssue.parent?.number ?? null,
|
|
234
291
|
subIssueNumbers: expandedIssue.subIssues.nodes.map((n) => n.number),
|
|
235
292
|
blockingNumbers: expandedIssue.blocking.nodes.map((n) => n.number),
|
|
@@ -247,7 +304,7 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
247
304
|
}
|
|
248
305
|
catch {
|
|
249
306
|
// Skip issues that can't be fetched (cross-repo, permissions, etc.)
|
|
250
|
-
console.error(`[group-detection] Could not fetch issue #${num}, skipping
|
|
307
|
+
console.error(`[group-detection] Could not fetch issue #${num} from ${expandOwner}/${expandRepo}, skipping`);
|
|
251
308
|
}
|
|
252
309
|
}
|
|
253
310
|
// Step 3: Topological sort
|
|
@@ -258,13 +315,18 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
258
315
|
// Step 4: Build result
|
|
259
316
|
const groupTickets = sorted.map((num, index) => {
|
|
260
317
|
const issue = issueMap.get(num);
|
|
261
|
-
|
|
318
|
+
const result = {
|
|
262
319
|
id: issue.id,
|
|
263
320
|
number: issue.number,
|
|
264
321
|
title: issue.title,
|
|
265
322
|
state: issue.state,
|
|
266
323
|
order: index + 1,
|
|
267
324
|
};
|
|
325
|
+
// Include repository only for cross-repo issues
|
|
326
|
+
if (issue.repoOwner !== owner || issue.repoName !== repo) {
|
|
327
|
+
result.repository = `${issue.repoOwner}/${issue.repoName}`;
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
268
330
|
});
|
|
269
331
|
const primary = groupTickets[0] || {
|
|
270
332
|
id: seedIssue.id,
|
|
@@ -301,13 +363,17 @@ function addIssueToMap(map, data) {
|
|
|
301
363
|
if (data.parentNumber !== null && existing.parentNumber === null) {
|
|
302
364
|
existing.parentNumber = data.parentNumber;
|
|
303
365
|
}
|
|
304
|
-
// Prefer non-empty id/title/state
|
|
366
|
+
// Prefer non-empty id/title/state/repo
|
|
305
367
|
if (!existing.id && data.id)
|
|
306
368
|
existing.id = data.id;
|
|
307
369
|
if (!existing.title && data.title)
|
|
308
370
|
existing.title = data.title;
|
|
309
371
|
if (!existing.state && data.state)
|
|
310
372
|
existing.state = data.state;
|
|
373
|
+
if (!existing.repoOwner && data.repoOwner)
|
|
374
|
+
existing.repoOwner = data.repoOwner;
|
|
375
|
+
if (!existing.repoName && data.repoName)
|
|
376
|
+
existing.repoName = data.repoName;
|
|
311
377
|
}
|
|
312
378
|
else {
|
|
313
379
|
map.set(data.number, { ...data });
|
|
@@ -259,49 +259,51 @@ export function registerDecomposeTools(server, client, fieldCache) {
|
|
|
259
259
|
projectItemId,
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
-
// Step 5: Wire dependencies (
|
|
263
|
-
// Parse "a -> b" edges
|
|
264
|
-
// may not be supported — catch and continue.
|
|
262
|
+
// Step 5: Wire dependencies (addBlockedBy for dependency-flow edges)
|
|
263
|
+
// Parse "a -> b" edges: a blocks b (b is blocked by a)
|
|
265
264
|
const wiringResults = [];
|
|
266
265
|
for (const edge of decomposition.dependency_chain) {
|
|
267
266
|
const match = edge.match(/^\s*(\S+)\s*->\s*(\S+)\s*$/);
|
|
268
267
|
if (!match) {
|
|
269
268
|
wiringResults.push({
|
|
270
269
|
edge,
|
|
270
|
+
type: "blockedBy",
|
|
271
271
|
status: "skipped",
|
|
272
272
|
reason: "Unrecognized edge format (expected 'a -> b')",
|
|
273
273
|
});
|
|
274
274
|
continue;
|
|
275
275
|
}
|
|
276
|
-
const [,
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
if (!
|
|
276
|
+
const [, blockingRepo, blockedRepo] = match;
|
|
277
|
+
const blockingIssue = createdIssues.find((i) => i.repoKey === blockingRepo);
|
|
278
|
+
const blockedIssue = createdIssues.find((i) => i.repoKey === blockedRepo);
|
|
279
|
+
if (!blockingIssue || !blockedIssue) {
|
|
280
280
|
wiringResults.push({
|
|
281
281
|
edge,
|
|
282
|
+
type: "blockedBy",
|
|
282
283
|
status: "skipped",
|
|
283
|
-
reason: `Could not find created issue for repo "${
|
|
284
|
+
reason: `Could not find created issue for repo "${blockingRepo}" or "${blockedRepo}"`,
|
|
284
285
|
});
|
|
285
286
|
continue;
|
|
286
287
|
}
|
|
287
288
|
try {
|
|
288
|
-
await client.mutate(`mutation($
|
|
289
|
-
|
|
290
|
-
issueId: $
|
|
291
|
-
|
|
289
|
+
await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
|
|
290
|
+
addBlockedBy(input: {
|
|
291
|
+
issueId: $blockedId,
|
|
292
|
+
blockingIssueId: $blockingId
|
|
292
293
|
}) {
|
|
293
294
|
issue { id }
|
|
294
|
-
|
|
295
|
+
blockingIssue { id }
|
|
295
296
|
}
|
|
296
|
-
}`, {
|
|
297
|
-
wiringResults.push({ edge, status: "ok" });
|
|
297
|
+
}`, { blockedId: blockedIssue.id, blockingId: blockingIssue.id });
|
|
298
|
+
wiringResults.push({ edge, type: "blockedBy", status: "ok" });
|
|
298
299
|
}
|
|
299
300
|
catch (err) {
|
|
300
301
|
const reason = err instanceof Error ? err.message : String(err);
|
|
301
302
|
wiringResults.push({
|
|
302
303
|
edge,
|
|
304
|
+
type: "blockedBy",
|
|
303
305
|
status: "skipped",
|
|
304
|
-
reason: `
|
|
306
|
+
reason: `addBlockedBy failed: ${reason}`,
|
|
305
307
|
});
|
|
306
308
|
}
|
|
307
309
|
}
|
|
@@ -193,33 +193,55 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
193
193
|
// -------------------------------------------------------------------------
|
|
194
194
|
// ralph_hero__add_dependency
|
|
195
195
|
// -------------------------------------------------------------------------
|
|
196
|
-
server.tool("ralph_hero__add_dependency", "Create a blocking dependency between two GitHub issues.
|
|
196
|
+
server.tool("ralph_hero__add_dependency", "Create a blocking dependency between two GitHub issues. Supports cross-repo: " +
|
|
197
|
+
"the blocked and blocking issues can be in different repositories. " +
|
|
198
|
+
"The 'blockingNumber' issue blocks the 'blockedNumber' issue.", {
|
|
197
199
|
owner: z
|
|
198
200
|
.string()
|
|
199
201
|
.optional()
|
|
200
|
-
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
202
|
+
.describe("Default GitHub owner for both issues. Defaults to GITHUB_OWNER env var"),
|
|
201
203
|
repo: z
|
|
202
204
|
.string()
|
|
203
205
|
.optional()
|
|
204
|
-
.describe("
|
|
206
|
+
.describe("Default repository for both issues. Defaults to GITHUB_REPO env var"),
|
|
205
207
|
blockedNumber: z
|
|
206
208
|
.number()
|
|
207
209
|
.describe("Issue number that IS blocked (cannot proceed until blocker is done)"),
|
|
210
|
+
blockedOwner: z
|
|
211
|
+
.string()
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("GitHub owner for the blocked issue. Defaults to 'owner' param"),
|
|
214
|
+
blockedRepo: z
|
|
215
|
+
.string()
|
|
216
|
+
.optional()
|
|
217
|
+
.describe("Repository for the blocked issue. Defaults to 'repo' param"),
|
|
208
218
|
blockingNumber: z
|
|
209
219
|
.number()
|
|
210
220
|
.describe("Issue number that IS the blocker (must be completed first)"),
|
|
221
|
+
blockingOwner: z
|
|
222
|
+
.string()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("GitHub owner for the blocking issue. Defaults to 'owner' param"),
|
|
225
|
+
blockingRepo: z
|
|
226
|
+
.string()
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Repository for the blocking issue. Defaults to 'repo' param"),
|
|
211
229
|
}, async (args) => {
|
|
212
230
|
try {
|
|
213
231
|
const { owner, repo } = resolveConfig(client, args);
|
|
214
|
-
const
|
|
215
|
-
const
|
|
232
|
+
const bOwner = args.blockedOwner || owner;
|
|
233
|
+
const bRepo = args.blockedRepo || repo;
|
|
234
|
+
const kOwner = args.blockingOwner || owner;
|
|
235
|
+
const kRepo = args.blockingRepo || repo;
|
|
236
|
+
const blockedId = await resolveIssueNodeId(client, bOwner, bRepo, args.blockedNumber);
|
|
237
|
+
const blockingId = await resolveIssueNodeId(client, kOwner, kRepo, args.blockingNumber);
|
|
216
238
|
const result = await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
|
|
217
239
|
addBlockedBy(input: {
|
|
218
240
|
issueId: $blockedId,
|
|
219
241
|
blockingIssueId: $blockingId
|
|
220
242
|
}) {
|
|
221
|
-
issue { id number title }
|
|
222
|
-
blockingIssue { id number title }
|
|
243
|
+
issue { id number title repository { nameWithOwner } }
|
|
244
|
+
blockingIssue { id number title repository { nameWithOwner } }
|
|
223
245
|
}
|
|
224
246
|
}`, { blockedId, blockingId });
|
|
225
247
|
return toolSuccess({
|
|
@@ -227,11 +249,13 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
227
249
|
id: result.addBlockedBy.issue.id,
|
|
228
250
|
number: result.addBlockedBy.issue.number,
|
|
229
251
|
title: result.addBlockedBy.issue.title,
|
|
252
|
+
repository: result.addBlockedBy.issue.repository.nameWithOwner,
|
|
230
253
|
},
|
|
231
254
|
blocking: {
|
|
232
255
|
id: result.addBlockedBy.blockingIssue.id,
|
|
233
256
|
number: result.addBlockedBy.blockingIssue.number,
|
|
234
257
|
title: result.addBlockedBy.blockingIssue.title,
|
|
258
|
+
repository: result.addBlockedBy.blockingIssue.repository.nameWithOwner,
|
|
235
259
|
},
|
|
236
260
|
});
|
|
237
261
|
}
|
|
@@ -243,29 +267,50 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
243
267
|
// -------------------------------------------------------------------------
|
|
244
268
|
// ralph_hero__remove_dependency
|
|
245
269
|
// -------------------------------------------------------------------------
|
|
246
|
-
server.tool("ralph_hero__remove_dependency", "Remove a blocking dependency between two GitHub issues"
|
|
270
|
+
server.tool("ralph_hero__remove_dependency", "Remove a blocking dependency between two GitHub issues. Supports cross-repo: " +
|
|
271
|
+
"the blocked and blocking issues can be in different repositories.", {
|
|
247
272
|
owner: z
|
|
248
273
|
.string()
|
|
249
274
|
.optional()
|
|
250
|
-
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
275
|
+
.describe("Default GitHub owner for both issues. Defaults to GITHUB_OWNER env var"),
|
|
251
276
|
repo: z
|
|
252
277
|
.string()
|
|
253
278
|
.optional()
|
|
254
|
-
.describe("
|
|
279
|
+
.describe("Default repository for both issues. Defaults to GITHUB_REPO env var"),
|
|
255
280
|
blockedNumber: z.coerce.number().describe("Issue number that was blocked"),
|
|
281
|
+
blockedOwner: z
|
|
282
|
+
.string()
|
|
283
|
+
.optional()
|
|
284
|
+
.describe("GitHub owner for the blocked issue. Defaults to 'owner' param"),
|
|
285
|
+
blockedRepo: z
|
|
286
|
+
.string()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe("Repository for the blocked issue. Defaults to 'repo' param"),
|
|
256
289
|
blockingNumber: z.coerce.number().describe("Issue number that was the blocker"),
|
|
290
|
+
blockingOwner: z
|
|
291
|
+
.string()
|
|
292
|
+
.optional()
|
|
293
|
+
.describe("GitHub owner for the blocking issue. Defaults to 'owner' param"),
|
|
294
|
+
blockingRepo: z
|
|
295
|
+
.string()
|
|
296
|
+
.optional()
|
|
297
|
+
.describe("Repository for the blocking issue. Defaults to 'repo' param"),
|
|
257
298
|
}, async (args) => {
|
|
258
299
|
try {
|
|
259
300
|
const { owner, repo } = resolveConfig(client, args);
|
|
260
|
-
const
|
|
261
|
-
const
|
|
301
|
+
const bOwner = args.blockedOwner || owner;
|
|
302
|
+
const bRepo = args.blockedRepo || repo;
|
|
303
|
+
const kOwner = args.blockingOwner || owner;
|
|
304
|
+
const kRepo = args.blockingRepo || repo;
|
|
305
|
+
const blockedId = await resolveIssueNodeId(client, bOwner, bRepo, args.blockedNumber);
|
|
306
|
+
const blockingId = await resolveIssueNodeId(client, kOwner, kRepo, args.blockingNumber);
|
|
262
307
|
const result = await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
|
|
263
308
|
removeBlockedBy(input: {
|
|
264
309
|
issueId: $blockedId,
|
|
265
310
|
blockingIssueId: $blockingId
|
|
266
311
|
}) {
|
|
267
|
-
issue { id number title }
|
|
268
|
-
blockingIssue { id number title }
|
|
312
|
+
issue { id number title repository { nameWithOwner } }
|
|
313
|
+
blockingIssue { id number title repository { nameWithOwner } }
|
|
269
314
|
}
|
|
270
315
|
}`, { blockedId, blockingId });
|
|
271
316
|
return toolSuccess({
|
|
@@ -273,11 +318,13 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
273
318
|
id: result.removeBlockedBy.issue.id,
|
|
274
319
|
number: result.removeBlockedBy.issue.number,
|
|
275
320
|
title: result.removeBlockedBy.issue.title,
|
|
321
|
+
repository: result.removeBlockedBy.issue.repository.nameWithOwner,
|
|
276
322
|
},
|
|
277
323
|
blocking: {
|
|
278
324
|
id: result.removeBlockedBy.blockingIssue.id,
|
|
279
325
|
number: result.removeBlockedBy.blockingIssue.number,
|
|
280
326
|
title: result.removeBlockedBy.blockingIssue.title,
|
|
327
|
+
repository: result.removeBlockedBy.blockingIssue.repository.nameWithOwner,
|
|
281
328
|
},
|
|
282
329
|
});
|
|
283
330
|
}
|
|
@@ -286,6 +333,85 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
286
333
|
return toolError(`Failed to remove dependency: ${message}`);
|
|
287
334
|
}
|
|
288
335
|
});
|
|
336
|
+
// -------------------------------------------------------------------------
|
|
337
|
+
// ralph_hero__list_dependencies
|
|
338
|
+
// -------------------------------------------------------------------------
|
|
339
|
+
server.tool("ralph_hero__list_dependencies", "List all blocking dependencies for a GitHub issue. Returns both 'blocking' " +
|
|
340
|
+
"(issues this issue blocks) and 'blockedBy' (issues blocking this issue) " +
|
|
341
|
+
"with full cross-repo context including repository name.", {
|
|
342
|
+
owner: z
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
346
|
+
repo: z
|
|
347
|
+
.string()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
350
|
+
number: z.coerce.number().describe("Issue number to query dependencies for"),
|
|
351
|
+
}, async (args) => {
|
|
352
|
+
try {
|
|
353
|
+
const { owner, repo } = resolveConfig(client, args);
|
|
354
|
+
const result = await client.query(`query($owner: String!, $repo: String!, $number: Int!) {
|
|
355
|
+
repository(owner: $owner, name: $repo) {
|
|
356
|
+
issue(number: $number) {
|
|
357
|
+
id
|
|
358
|
+
number
|
|
359
|
+
title
|
|
360
|
+
state
|
|
361
|
+
blocking(first: 50) {
|
|
362
|
+
nodes {
|
|
363
|
+
id number title state
|
|
364
|
+
repository { nameWithOwner }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
blockedBy(first: 50) {
|
|
368
|
+
nodes {
|
|
369
|
+
id number title state
|
|
370
|
+
repository { nameWithOwner }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}`, { owner, repo, number: args.number });
|
|
376
|
+
const issue = result.repository?.issue;
|
|
377
|
+
if (!issue) {
|
|
378
|
+
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
379
|
+
}
|
|
380
|
+
return toolSuccess({
|
|
381
|
+
issue: {
|
|
382
|
+
id: issue.id,
|
|
383
|
+
number: issue.number,
|
|
384
|
+
title: issue.title,
|
|
385
|
+
state: issue.state,
|
|
386
|
+
repository: `${owner}/${repo}`,
|
|
387
|
+
},
|
|
388
|
+
blocking: issue.blocking.nodes.map((n) => ({
|
|
389
|
+
id: n.id,
|
|
390
|
+
number: n.number,
|
|
391
|
+
title: n.title,
|
|
392
|
+
state: n.state,
|
|
393
|
+
repository: n.repository.nameWithOwner,
|
|
394
|
+
})),
|
|
395
|
+
blockedBy: issue.blockedBy.nodes.map((n) => ({
|
|
396
|
+
id: n.id,
|
|
397
|
+
number: n.number,
|
|
398
|
+
title: n.title,
|
|
399
|
+
state: n.state,
|
|
400
|
+
repository: n.repository.nameWithOwner,
|
|
401
|
+
})),
|
|
402
|
+
summary: {
|
|
403
|
+
blockingCount: issue.blocking.nodes.length,
|
|
404
|
+
blockedByCount: issue.blockedBy.nodes.length,
|
|
405
|
+
isBlocked: issue.blockedBy.nodes.length > 0,
|
|
406
|
+
isBlocking: issue.blocking.nodes.length > 0,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
return toolError(`Failed to list dependencies: ${message}`);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
289
415
|
// ralph_hero__advance_issue
|
|
290
416
|
// -------------------------------------------------------------------------
|
|
291
417
|
server.tool("ralph_hero__advance_issue", "Advance workflow state for related issues. " +
|