ralph-hero-mcp-server 1.1.2 → 1.2.0
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/github-client.js +5 -3
- package/dist/index.js +48 -15
- package/dist/lib/cache.js +3 -1
- package/dist/lib/group-detection.js +13 -3
- package/dist/lib/pipeline-detection.js +20 -3
- package/dist/lib/state-resolution.js +12 -6
- package/dist/tools/issue-tools.js +260 -67
- package/dist/tools/project-tools.js +85 -19
- package/dist/tools/relationship-tools.js +81 -23
- package/dist/tools/view-tools.js +40 -9
- package/package.json +1 -1
package/dist/github-client.js
CHANGED
|
@@ -30,7 +30,8 @@ export function createGitHubClient(clientConfig) {
|
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
32
|
// Create a separate graphql instance for project operations if a different token is configured
|
|
33
|
-
const hasProjectToken = clientConfig.projectToken &&
|
|
33
|
+
const hasProjectToken = clientConfig.projectToken &&
|
|
34
|
+
clientConfig.projectToken !== clientConfig.token;
|
|
34
35
|
const projectGraphqlWithAuth = hasProjectToken
|
|
35
36
|
? graphql.defaults({
|
|
36
37
|
headers: {
|
|
@@ -55,7 +56,8 @@ export function createGitHubClient(clientConfig) {
|
|
|
55
56
|
const insertPos = fullQuery.indexOf("{", fullQuery.indexOf(match[0])) + 1;
|
|
56
57
|
fullQuery =
|
|
57
58
|
fullQuery.slice(0, insertPos) +
|
|
58
|
-
"\n " +
|
|
59
|
+
"\n " +
|
|
60
|
+
RATE_LIMIT_FRAGMENT +
|
|
59
61
|
fullQuery.slice(insertPos);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -77,7 +79,7 @@ export function createGitHubClient(clientConfig) {
|
|
|
77
79
|
"status" in error &&
|
|
78
80
|
error.status === 403) {
|
|
79
81
|
const retryAfter = error && typeof error === "object" && "headers" in error
|
|
80
|
-
?
|
|
82
|
+
? error.headers?.["retry-after"]
|
|
81
83
|
: undefined;
|
|
82
84
|
if (retryAfter) {
|
|
83
85
|
const waitMs = parseInt(retryAfter, 10) * 1000;
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,7 @@ function resolveEnv(name) {
|
|
|
27
27
|
}
|
|
28
28
|
function initGitHubClient() {
|
|
29
29
|
// Repo token: for repository operations (issues, PRs, comments)
|
|
30
|
-
const repoToken = resolveEnv("RALPH_GH_REPO_TOKEN") ||
|
|
31
|
-
resolveEnv("RALPH_HERO_GITHUB_TOKEN");
|
|
30
|
+
const repoToken = resolveEnv("RALPH_GH_REPO_TOKEN") || resolveEnv("RALPH_HERO_GITHUB_TOKEN");
|
|
32
31
|
// Project token: for Projects V2 operations (fields, workflow state)
|
|
33
32
|
// Falls back to repo token if not set
|
|
34
33
|
const projectToken = resolveEnv("RALPH_GH_PROJECT_TOKEN") || repoToken;
|
|
@@ -63,7 +62,9 @@ function initGitHubClient() {
|
|
|
63
62
|
"Project-level tools (workflow state, field updates) will not work.\n" +
|
|
64
63
|
"Run /ralph-setup to configure your GitHub Project.");
|
|
65
64
|
}
|
|
66
|
-
const repoTokenSource = resolveEnv("RALPH_GH_REPO_TOKEN")
|
|
65
|
+
const repoTokenSource = resolveEnv("RALPH_GH_REPO_TOKEN")
|
|
66
|
+
? "RALPH_GH_REPO_TOKEN"
|
|
67
|
+
: "RALPH_HERO_GITHUB_TOKEN";
|
|
67
68
|
console.error(`[ralph-hero] Repo token: ${repoTokenSource}`);
|
|
68
69
|
if (projectToken !== repoToken) {
|
|
69
70
|
console.error(`[ralph-hero] Project token: RALPH_GH_PROJECT_TOKEN (separate)`);
|
|
@@ -91,7 +92,10 @@ function registerCoreTools(server, client) {
|
|
|
91
92
|
checks.auth = { status: "ok", detail: `Authenticated as ${login}` };
|
|
92
93
|
}
|
|
93
94
|
catch (e) {
|
|
94
|
-
checks.auth = {
|
|
95
|
+
checks.auth = {
|
|
96
|
+
status: "fail",
|
|
97
|
+
detail: `Auth failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
98
|
+
};
|
|
95
99
|
}
|
|
96
100
|
// 2. Repo access check
|
|
97
101
|
if (client.config.owner && client.config.repo) {
|
|
@@ -99,14 +103,23 @@ function registerCoreTools(server, client) {
|
|
|
99
103
|
await client.query(`query($owner: String!, $repo: String!) {
|
|
100
104
|
repository(owner: $owner, name: $repo) { nameWithOwner }
|
|
101
105
|
}`, { owner: client.config.owner, repo: client.config.repo });
|
|
102
|
-
checks.repoAccess = {
|
|
106
|
+
checks.repoAccess = {
|
|
107
|
+
status: "ok",
|
|
108
|
+
detail: `${client.config.owner}/${client.config.repo}`,
|
|
109
|
+
};
|
|
103
110
|
}
|
|
104
111
|
catch (e) {
|
|
105
|
-
checks.repoAccess = {
|
|
112
|
+
checks.repoAccess = {
|
|
113
|
+
status: "fail",
|
|
114
|
+
detail: `Cannot access repo: ${e instanceof Error ? e.message : String(e)}. Token may lack 'repo' scope or org access.`,
|
|
115
|
+
};
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
else {
|
|
109
|
-
checks.repoAccess = {
|
|
119
|
+
checks.repoAccess = {
|
|
120
|
+
status: "skip",
|
|
121
|
+
detail: "RALPH_GH_OWNER/RALPH_GH_REPO not set",
|
|
122
|
+
};
|
|
110
123
|
}
|
|
111
124
|
// 3. Project access check (uses project token + project owner)
|
|
112
125
|
const projOwner = resolveProjectOwner(client.config);
|
|
@@ -139,28 +152,46 @@ function registerCoreTools(server, client) {
|
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
if (project) {
|
|
142
|
-
checks.projectAccess = {
|
|
155
|
+
checks.projectAccess = {
|
|
156
|
+
status: "ok",
|
|
157
|
+
detail: `${project.title} (#${projNum})`,
|
|
158
|
+
};
|
|
143
159
|
// 4. Required fields check
|
|
144
160
|
const requiredFields = ["Workflow State", "Priority", "Estimate"];
|
|
145
161
|
const fieldNames = project.fields.nodes.map((f) => f.name);
|
|
146
162
|
const missing = requiredFields.filter((f) => !fieldNames.includes(f));
|
|
147
163
|
if (missing.length === 0) {
|
|
148
|
-
checks.requiredFields = {
|
|
164
|
+
checks.requiredFields = {
|
|
165
|
+
status: "ok",
|
|
166
|
+
detail: "All required fields present",
|
|
167
|
+
};
|
|
149
168
|
}
|
|
150
169
|
else {
|
|
151
|
-
checks.requiredFields = {
|
|
170
|
+
checks.requiredFields = {
|
|
171
|
+
status: "fail",
|
|
172
|
+
detail: `Missing fields: ${missing.join(", ")}. Run /ralph-setup.`,
|
|
173
|
+
};
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
176
|
else {
|
|
155
|
-
checks.projectAccess = {
|
|
177
|
+
checks.projectAccess = {
|
|
178
|
+
status: "fail",
|
|
179
|
+
detail: `Project #${projNum} not found for owner "${projOwner}". Check RALPH_GH_PROJECT_OWNER.`,
|
|
180
|
+
};
|
|
156
181
|
}
|
|
157
182
|
}
|
|
158
183
|
catch (e) {
|
|
159
|
-
checks.projectAccess = {
|
|
184
|
+
checks.projectAccess = {
|
|
185
|
+
status: "fail",
|
|
186
|
+
detail: `Project access failed: ${e instanceof Error ? e.message : String(e)}. Token may lack 'project' scope.`,
|
|
187
|
+
};
|
|
160
188
|
}
|
|
161
189
|
}
|
|
162
190
|
else {
|
|
163
|
-
checks.projectAccess = {
|
|
191
|
+
checks.projectAccess = {
|
|
192
|
+
status: "skip",
|
|
193
|
+
detail: "RALPH_GH_PROJECT_NUMBER not set",
|
|
194
|
+
};
|
|
164
195
|
}
|
|
165
196
|
// Summary
|
|
166
197
|
const allOk = Object.values(checks).every((c) => c.status === "ok" || c.status === "skip");
|
|
@@ -172,8 +203,10 @@ function registerCoreTools(server, client) {
|
|
|
172
203
|
repo: client.config.repo || "(not set)",
|
|
173
204
|
projectOwner: resolveProjectOwner(client.config) || "(not set)",
|
|
174
205
|
projectNumber: client.config.projectNumber || "(not set)",
|
|
175
|
-
tokenMode: client.config.projectToken &&
|
|
176
|
-
|
|
206
|
+
tokenMode: client.config.projectToken &&
|
|
207
|
+
client.config.projectToken !== client.config.token
|
|
208
|
+
? "dual-token"
|
|
209
|
+
: "single-token",
|
|
177
210
|
},
|
|
178
211
|
});
|
|
179
212
|
});
|
package/dist/lib/cache.js
CHANGED
|
@@ -66,7 +66,9 @@ export class SessionCache {
|
|
|
66
66
|
*/
|
|
67
67
|
static queryKey(query, variables) {
|
|
68
68
|
const normalized = query.replace(/\s+/g, " ").trim();
|
|
69
|
-
const varsKey = variables
|
|
69
|
+
const varsKey = variables
|
|
70
|
+
? JSON.stringify(variables, Object.keys(variables).sort())
|
|
71
|
+
: "";
|
|
70
72
|
return `query:${normalized}:${varsKey}`;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
@@ -200,7 +200,10 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
200
200
|
}
|
|
201
201
|
// Check all discovered issues for dependency targets not yet in the set
|
|
202
202
|
for (const issue of issueMap.values()) {
|
|
203
|
-
for (const depNum of [
|
|
203
|
+
for (const depNum of [
|
|
204
|
+
...issue.blockingNumbers,
|
|
205
|
+
...issue.blockedByNumbers,
|
|
206
|
+
]) {
|
|
204
207
|
if (!issueMap.has(depNum)) {
|
|
205
208
|
expandQueue.push(depNum);
|
|
206
209
|
}
|
|
@@ -270,7 +273,11 @@ export async function detectGroup(client, owner, repo, seedNumber) {
|
|
|
270
273
|
};
|
|
271
274
|
return {
|
|
272
275
|
groupTickets,
|
|
273
|
-
groupPrimary: {
|
|
276
|
+
groupPrimary: {
|
|
277
|
+
id: primary.id,
|
|
278
|
+
number: primary.number,
|
|
279
|
+
title: primary.title,
|
|
280
|
+
},
|
|
274
281
|
isGroup: groupTickets.length > 1,
|
|
275
282
|
totalTickets: groupTickets.length,
|
|
276
283
|
};
|
|
@@ -359,7 +366,10 @@ function filterGroupMembers(issueMap, seedNumber) {
|
|
|
359
366
|
const issue = issueMap.get(num);
|
|
360
367
|
if (!issue)
|
|
361
368
|
continue;
|
|
362
|
-
for (const depNum of [
|
|
369
|
+
for (const depNum of [
|
|
370
|
+
...issue.blockingNumbers,
|
|
371
|
+
...issue.blockedByNumbers,
|
|
372
|
+
]) {
|
|
363
373
|
if (issueMap.has(depNum) && !members.has(depNum)) {
|
|
364
374
|
// Only add non-parent issues
|
|
365
375
|
const depIssue = issueMap.get(depNum);
|
|
@@ -84,8 +84,14 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary) {
|
|
|
84
84
|
required: isGroup,
|
|
85
85
|
met: false,
|
|
86
86
|
blocking: [
|
|
87
|
-
...needsResearch.map((i) => ({
|
|
88
|
-
|
|
87
|
+
...needsResearch.map((i) => ({
|
|
88
|
+
number: i.number,
|
|
89
|
+
state: i.workflowState,
|
|
90
|
+
})),
|
|
91
|
+
...inResearch.map((i) => ({
|
|
92
|
+
number: i.number,
|
|
93
|
+
state: i.workflowState,
|
|
94
|
+
})),
|
|
89
95
|
],
|
|
90
96
|
};
|
|
91
97
|
return buildResult("RESEARCH", `${needsResearch.length} need research, ${inResearch.length} in progress`, issues, isGroup, groupPrimary, convergence);
|
|
@@ -141,12 +147,23 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary) {
|
|
|
141
147
|
// Helpers
|
|
142
148
|
// ---------------------------------------------------------------------------
|
|
143
149
|
function buildResult(phase, reason, issues, isGroup, groupPrimary, convergence) {
|
|
150
|
+
// Derive recommendation from convergence state
|
|
151
|
+
let recommendation;
|
|
152
|
+
if (convergence.met) {
|
|
153
|
+
recommendation = "proceed";
|
|
154
|
+
}
|
|
155
|
+
else if (convergence.blocking.some((b) => b.state === "Human Needed")) {
|
|
156
|
+
recommendation = "escalate";
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
recommendation = "wait";
|
|
160
|
+
}
|
|
144
161
|
return {
|
|
145
162
|
phase,
|
|
146
163
|
reason,
|
|
147
164
|
remainingPhases: REMAINING_PHASES[phase],
|
|
148
165
|
issues,
|
|
149
|
-
convergence,
|
|
166
|
+
convergence: { ...convergence, recommendation },
|
|
150
167
|
isGroup,
|
|
151
168
|
groupPrimary,
|
|
152
169
|
};
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
// null means the intent is recognized but ambiguous for that command (multi-path)
|
|
9
9
|
// undefined (missing key) means the intent isn't mapped for that command
|
|
10
10
|
const SEMANTIC_INTENTS = {
|
|
11
|
-
|
|
11
|
+
__LOCK__: {
|
|
12
12
|
ralph_research: "Research in Progress",
|
|
13
13
|
ralph_plan: "Plan in Progress",
|
|
14
14
|
ralph_impl: "In Progress",
|
|
15
15
|
},
|
|
16
|
-
|
|
16
|
+
__COMPLETE__: {
|
|
17
17
|
ralph_triage: null, // multi-path: caller must use direct state
|
|
18
18
|
ralph_split: "Backlog",
|
|
19
19
|
ralph_research: "Ready for Plan",
|
|
@@ -21,13 +21,19 @@ const SEMANTIC_INTENTS = {
|
|
|
21
21
|
ralph_impl: "In Review",
|
|
22
22
|
ralph_review: "In Progress",
|
|
23
23
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
__ESCALATE__: { "*": "Human Needed" },
|
|
25
|
+
__CLOSE__: { "*": "Done" },
|
|
26
|
+
__CANCEL__: { "*": "Canceled" },
|
|
27
27
|
};
|
|
28
28
|
// --- Per-command allowed output states (valid_output_states ∪ {lock_state}) ---
|
|
29
29
|
const COMMAND_ALLOWED_STATES = {
|
|
30
|
-
ralph_triage: [
|
|
30
|
+
ralph_triage: [
|
|
31
|
+
"Research Needed",
|
|
32
|
+
"Ready for Plan",
|
|
33
|
+
"Done",
|
|
34
|
+
"Canceled",
|
|
35
|
+
"Human Needed",
|
|
36
|
+
],
|
|
31
37
|
ralph_split: ["Backlog"],
|
|
32
38
|
ralph_research: ["Research in Progress", "Ready for Plan", "Human Needed"],
|
|
33
39
|
ralph_plan: ["Plan in Progress", "Plan in Review", "Human Needed"],
|
|
@@ -7,8 +7,8 @@
|
|
|
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";
|
|
11
|
-
import { isValidState, VALID_STATES, LOCK_STATES } from "../lib/workflow-states.js";
|
|
10
|
+
import { detectPipelinePosition, } from "../lib/pipeline-detection.js";
|
|
11
|
+
import { isValidState, VALID_STATES, LOCK_STATES, } from "../lib/workflow-states.js";
|
|
12
12
|
import { resolveState } from "../lib/state-resolution.js";
|
|
13
13
|
import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -168,7 +168,8 @@ async function getCurrentFieldValue(client, fieldCache, owner, repo, issueNumber
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}`, { itemId: projectItemId });
|
|
171
|
-
const fieldValue = result.node?.fieldValues?.nodes?.find((fv) => fv.field?.name === fieldName &&
|
|
171
|
+
const fieldValue = result.node?.fieldValues?.nodes?.find((fv) => fv.field?.name === fieldName &&
|
|
172
|
+
fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
|
|
172
173
|
return fieldValue?.name;
|
|
173
174
|
}
|
|
174
175
|
function resolveConfig(client, args) {
|
|
@@ -199,17 +200,44 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
199
200
|
// -------------------------------------------------------------------------
|
|
200
201
|
// ralph_hero__list_issues
|
|
201
202
|
// -------------------------------------------------------------------------
|
|
202
|
-
server.tool("ralph_hero__list_issues", "List issues from a GitHub repository,
|
|
203
|
-
owner: z
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
server.tool("ralph_hero__list_issues", "List issues from a GitHub repository with optional filters. Returns: number, title, state, workflowState, estimate, priority, labels, assignees. Use workflowState filter to find issues in a specific phase. Recovery: if no results, broaden filters or check that issues exist in the project.", {
|
|
204
|
+
owner: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("GitHub owner (user or org). Defaults to GITHUB_OWNER env var"),
|
|
208
|
+
repo: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
212
|
+
workflowState: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe("Filter by Workflow State name"),
|
|
216
|
+
estimate: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Filter by Estimate (XS, S, M, L, XL)"),
|
|
220
|
+
priority: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("Filter by Priority (P0, P1, P2, P3)"),
|
|
208
224
|
label: z.string().optional().describe("Filter by label name"),
|
|
209
225
|
query: z.string().optional().describe("Additional search query string"),
|
|
210
|
-
state: z
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
state: z
|
|
227
|
+
.enum(["OPEN", "CLOSED"])
|
|
228
|
+
.optional()
|
|
229
|
+
.default("OPEN")
|
|
230
|
+
.describe("Issue state filter (default: OPEN)"),
|
|
231
|
+
orderBy: z
|
|
232
|
+
.enum(["CREATED_AT", "UPDATED_AT", "COMMENTS"])
|
|
233
|
+
.optional()
|
|
234
|
+
.default("CREATED_AT")
|
|
235
|
+
.describe("Order by field"),
|
|
236
|
+
limit: z
|
|
237
|
+
.number()
|
|
238
|
+
.optional()
|
|
239
|
+
.default(50)
|
|
240
|
+
.describe("Max items to return (default 50)"),
|
|
213
241
|
}, async (args) => {
|
|
214
242
|
try {
|
|
215
243
|
const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
@@ -282,7 +310,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
282
310
|
if (args.label) {
|
|
283
311
|
items = items.filter((item) => {
|
|
284
312
|
const content = item.content;
|
|
285
|
-
const labels = content?.labels?.nodes ||
|
|
313
|
+
const labels = content?.labels?.nodes ||
|
|
314
|
+
[];
|
|
286
315
|
return labels.some((l) => l.name === args.label);
|
|
287
316
|
});
|
|
288
317
|
}
|
|
@@ -336,10 +365,21 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
336
365
|
// -------------------------------------------------------------------------
|
|
337
366
|
// ralph_hero__get_issue
|
|
338
367
|
// -------------------------------------------------------------------------
|
|
339
|
-
server.tool("ralph_hero__get_issue", "Get a single GitHub issue with full context: properties, project field values, relationships (parent, sub-issues, blocking, blocked-by), and
|
|
340
|
-
owner: z
|
|
341
|
-
|
|
368
|
+
server.tool("ralph_hero__get_issue", "Get a single GitHub issue with full context: properties, project field values, relationships (parent, sub-issues, blocking, blocked-by), recent comments, and optional group detection. Returns group data by default so callers don't need a separate detect_group call. Key fields: number, title, workflowState, estimate, priority, parent, subIssues, blocking, blockedBy, comments, group.", {
|
|
369
|
+
owner: z
|
|
370
|
+
.string()
|
|
371
|
+
.optional()
|
|
372
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
373
|
+
repo: z
|
|
374
|
+
.string()
|
|
375
|
+
.optional()
|
|
376
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
342
377
|
number: z.number().describe("Issue number"),
|
|
378
|
+
includeGroup: z
|
|
379
|
+
.boolean()
|
|
380
|
+
.optional()
|
|
381
|
+
.default(true)
|
|
382
|
+
.describe("Include group detection results (default: true). Set to false to skip group detection and save API calls when group context is not needed."),
|
|
343
383
|
}, async (args) => {
|
|
344
384
|
try {
|
|
345
385
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -402,7 +442,9 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
402
442
|
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
403
443
|
}
|
|
404
444
|
// Cache the node ID
|
|
405
|
-
client
|
|
445
|
+
client
|
|
446
|
+
.getCache()
|
|
447
|
+
.set(`issue-node-id:${owner}/${repo}#${issue.number}`, issue.id, 30 * 60 * 1000);
|
|
406
448
|
// Extract project field values (find matching project if we know the project number)
|
|
407
449
|
let workflowState;
|
|
408
450
|
let estimate;
|
|
@@ -412,9 +454,12 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
412
454
|
: issue.projectItems.nodes[0]; // Use first project item if no project configured
|
|
413
455
|
if (projectItem) {
|
|
414
456
|
// Cache the project item ID
|
|
415
|
-
client
|
|
457
|
+
client
|
|
458
|
+
.getCache()
|
|
459
|
+
.set(`project-item-id:${owner}/${repo}#${issue.number}`, projectItem.id, 30 * 60 * 1000);
|
|
416
460
|
for (const fv of projectItem.fieldValues.nodes) {
|
|
417
|
-
if (fv.__typename === "ProjectV2ItemFieldSingleSelectValue" &&
|
|
461
|
+
if (fv.__typename === "ProjectV2ItemFieldSingleSelectValue" &&
|
|
462
|
+
fv.field) {
|
|
418
463
|
switch (fv.field.name) {
|
|
419
464
|
case "Workflow State":
|
|
420
465
|
workflowState = fv.name;
|
|
@@ -429,6 +474,32 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
429
474
|
}
|
|
430
475
|
}
|
|
431
476
|
}
|
|
477
|
+
// Optionally detect group context
|
|
478
|
+
let group = null;
|
|
479
|
+
if (args.includeGroup !== false) {
|
|
480
|
+
try {
|
|
481
|
+
const { owner: cfgOwner, repo: cfgRepo } = resolveConfig(client, args);
|
|
482
|
+
const groupResult = await detectGroup(client, cfgOwner, cfgRepo, args.number);
|
|
483
|
+
group = {
|
|
484
|
+
isGroup: groupResult.isGroup,
|
|
485
|
+
primary: {
|
|
486
|
+
number: groupResult.groupPrimary.number,
|
|
487
|
+
title: groupResult.groupPrimary.title,
|
|
488
|
+
},
|
|
489
|
+
members: groupResult.groupTickets.map((t) => ({
|
|
490
|
+
number: t.number,
|
|
491
|
+
title: t.title,
|
|
492
|
+
state: t.state,
|
|
493
|
+
order: t.order,
|
|
494
|
+
})),
|
|
495
|
+
totalTickets: groupResult.totalTickets,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// Group detection is best-effort; don't fail the whole request
|
|
500
|
+
group = null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
432
503
|
return toolSuccess({
|
|
433
504
|
number: issue.number,
|
|
434
505
|
id: issue.id,
|
|
@@ -446,7 +517,11 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
446
517
|
labels: issue.labels.nodes.map((l) => l.name),
|
|
447
518
|
assignees: issue.assignees.nodes.map((a) => a.login),
|
|
448
519
|
parent: issue.parent
|
|
449
|
-
? {
|
|
520
|
+
? {
|
|
521
|
+
number: issue.parent.number,
|
|
522
|
+
title: issue.parent.title,
|
|
523
|
+
state: issue.parent.state,
|
|
524
|
+
}
|
|
450
525
|
: null,
|
|
451
526
|
subIssuesSummary: issue.subIssuesSummary,
|
|
452
527
|
subIssues: issue.subIssues.nodes.map((si) => ({
|
|
@@ -470,6 +545,7 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
470
545
|
author: c.author?.login || "unknown",
|
|
471
546
|
createdAt: c.createdAt,
|
|
472
547
|
})),
|
|
548
|
+
group,
|
|
473
549
|
});
|
|
474
550
|
}
|
|
475
551
|
catch (error) {
|
|
@@ -480,14 +556,26 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
480
556
|
// -------------------------------------------------------------------------
|
|
481
557
|
// ralph_hero__create_issue
|
|
482
558
|
// -------------------------------------------------------------------------
|
|
483
|
-
server.tool("ralph_hero__create_issue", "Create a GitHub issue and add it to the project with optional field values
|
|
484
|
-
owner: z
|
|
485
|
-
|
|
559
|
+
server.tool("ralph_hero__create_issue", "Create a GitHub issue and add it to the project with optional field values. Returns: number, id, title, url, projectItemId, fieldsSet. Recovery: if field value fails, verify the option name matches exactly (case-sensitive).", {
|
|
560
|
+
owner: z
|
|
561
|
+
.string()
|
|
562
|
+
.optional()
|
|
563
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
564
|
+
repo: z
|
|
565
|
+
.string()
|
|
566
|
+
.optional()
|
|
567
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
486
568
|
title: z.string().describe("Issue title"),
|
|
487
569
|
body: z.string().optional().describe("Issue body (Markdown)"),
|
|
488
570
|
labels: z.array(z.string()).optional().describe("Label names to apply"),
|
|
489
|
-
assignees: z
|
|
490
|
-
|
|
571
|
+
assignees: z
|
|
572
|
+
.array(z.string())
|
|
573
|
+
.optional()
|
|
574
|
+
.describe("GitHub usernames to assign"),
|
|
575
|
+
workflowState: z
|
|
576
|
+
.string()
|
|
577
|
+
.optional()
|
|
578
|
+
.describe("Initial Workflow State name"),
|
|
491
579
|
estimate: z.string().optional().describe("Estimate (XS, S, M, L, XL)"),
|
|
492
580
|
priority: z.string().optional().describe("Priority (P0, P1, P2, P3)"),
|
|
493
581
|
}, async (args) => {
|
|
@@ -543,7 +631,9 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
543
631
|
});
|
|
544
632
|
const issue = createResult.createIssue.issue;
|
|
545
633
|
// Cache the node ID
|
|
546
|
-
client
|
|
634
|
+
client
|
|
635
|
+
.getCache()
|
|
636
|
+
.set(`issue-node-id:${owner}/${repo}#${issue.number}`, issue.id, 30 * 60 * 1000);
|
|
547
637
|
// Step 4: Add to project
|
|
548
638
|
const projectId = fieldCache.getProjectId();
|
|
549
639
|
if (!projectId) {
|
|
@@ -559,7 +649,9 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
559
649
|
}`, { projectId, contentId: issue.id });
|
|
560
650
|
const projectItemId = addResult.addProjectV2ItemById.item.id;
|
|
561
651
|
// Cache project item ID
|
|
562
|
-
client
|
|
652
|
+
client
|
|
653
|
+
.getCache()
|
|
654
|
+
.set(`project-item-id:${owner}/${repo}#${issue.number}`, projectItemId, 30 * 60 * 1000);
|
|
563
655
|
// Step 5: Set field values
|
|
564
656
|
if (args.workflowState) {
|
|
565
657
|
await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.workflowState);
|
|
@@ -591,14 +683,26 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
591
683
|
// -------------------------------------------------------------------------
|
|
592
684
|
// ralph_hero__update_issue
|
|
593
685
|
// -------------------------------------------------------------------------
|
|
594
|
-
server.tool("ralph_hero__update_issue", "Update a GitHub issue's basic properties (title, body, labels, assignees)", {
|
|
595
|
-
owner: z
|
|
596
|
-
|
|
686
|
+
server.tool("ralph_hero__update_issue", "Update a GitHub issue's basic properties (title, body, labels, assignees). Returns: number, title, url. Use update_workflow_state for state changes, update_estimate for estimates, update_priority for priorities.", {
|
|
687
|
+
owner: z
|
|
688
|
+
.string()
|
|
689
|
+
.optional()
|
|
690
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
691
|
+
repo: z
|
|
692
|
+
.string()
|
|
693
|
+
.optional()
|
|
694
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
597
695
|
number: z.number().describe("Issue number"),
|
|
598
696
|
title: z.string().optional().describe("New issue title"),
|
|
599
697
|
body: z.string().optional().describe("New issue body (Markdown)"),
|
|
600
|
-
labels: z
|
|
601
|
-
|
|
698
|
+
labels: z
|
|
699
|
+
.array(z.string())
|
|
700
|
+
.optional()
|
|
701
|
+
.describe("Label names (replaces existing labels)"),
|
|
702
|
+
assignees: z
|
|
703
|
+
.array(z.string())
|
|
704
|
+
.optional()
|
|
705
|
+
.describe("GitHub usernames to assign (replaces existing)"),
|
|
602
706
|
}, async (args) => {
|
|
603
707
|
try {
|
|
604
708
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -653,13 +757,23 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
653
757
|
// -------------------------------------------------------------------------
|
|
654
758
|
// ralph_hero__update_workflow_state
|
|
655
759
|
// -------------------------------------------------------------------------
|
|
656
|
-
server.tool("ralph_hero__update_workflow_state", "Change an issue's Workflow State
|
|
657
|
-
owner: z
|
|
658
|
-
|
|
760
|
+
server.tool("ralph_hero__update_workflow_state", "Change an issue's Workflow State using semantic intents or direct state names. Returns: number, previousState, newState, command. Semantic intents: __LOCK__ (lock for processing), __COMPLETE__ (mark done), __ESCALATE__ (needs human), __CLOSE__, __CANCEL__. Recovery: if state transition fails, verify the issue is in the project and the state name is valid.", {
|
|
761
|
+
owner: z
|
|
762
|
+
.string()
|
|
763
|
+
.optional()
|
|
764
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
765
|
+
repo: z
|
|
766
|
+
.string()
|
|
767
|
+
.optional()
|
|
768
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
659
769
|
number: z.number().describe("Issue number"),
|
|
660
|
-
state: z
|
|
770
|
+
state: z
|
|
771
|
+
.string()
|
|
772
|
+
.describe("Target state: semantic intent (__LOCK__, __COMPLETE__, __ESCALATE__, __CLOSE__, __CANCEL__) " +
|
|
661
773
|
"or direct state name (e.g., 'Research Needed', 'In Progress')"),
|
|
662
|
-
command: z
|
|
774
|
+
command: z
|
|
775
|
+
.string()
|
|
776
|
+
.describe("Ralph command making this transition (e.g., 'ralph_research', 'ralph_plan'). " +
|
|
663
777
|
"Required for validation and semantic intent resolution."),
|
|
664
778
|
}, async (args) => {
|
|
665
779
|
try {
|
|
@@ -693,9 +807,15 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
693
807
|
// -------------------------------------------------------------------------
|
|
694
808
|
// ralph_hero__update_estimate
|
|
695
809
|
// -------------------------------------------------------------------------
|
|
696
|
-
server.tool("ralph_hero__update_estimate", "Change an issue's Estimate in the project
|
|
697
|
-
owner: z
|
|
698
|
-
|
|
810
|
+
server.tool("ralph_hero__update_estimate", "Change an issue's Estimate in the project. Returns: number, estimate. Valid values: XS, S, M, L, XL. Recovery: if the issue is not in the project, add it first via create_issue.", {
|
|
811
|
+
owner: z
|
|
812
|
+
.string()
|
|
813
|
+
.optional()
|
|
814
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
815
|
+
repo: z
|
|
816
|
+
.string()
|
|
817
|
+
.optional()
|
|
818
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
699
819
|
number: z.number().describe("Issue number"),
|
|
700
820
|
estimate: z.string().describe("Estimate value (XS, S, M, L, XL)"),
|
|
701
821
|
}, async (args) => {
|
|
@@ -717,9 +837,15 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
717
837
|
// -------------------------------------------------------------------------
|
|
718
838
|
// ralph_hero__update_priority
|
|
719
839
|
// -------------------------------------------------------------------------
|
|
720
|
-
server.tool("ralph_hero__update_priority", "Change an issue's Priority in the project
|
|
721
|
-
owner: z
|
|
722
|
-
|
|
840
|
+
server.tool("ralph_hero__update_priority", "Change an issue's Priority in the project. Returns: number, priority. Valid values: P0, P1, P2, P3. Recovery: if the issue is not in the project, add it first via create_issue.", {
|
|
841
|
+
owner: z
|
|
842
|
+
.string()
|
|
843
|
+
.optional()
|
|
844
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
845
|
+
repo: z
|
|
846
|
+
.string()
|
|
847
|
+
.optional()
|
|
848
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
723
849
|
number: z.number().describe("Issue number"),
|
|
724
850
|
priority: z.string().describe("Priority value (P0, P1, P2, P3)"),
|
|
725
851
|
}, async (args) => {
|
|
@@ -741,9 +867,15 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
741
867
|
// -------------------------------------------------------------------------
|
|
742
868
|
// ralph_hero__create_comment
|
|
743
869
|
// -------------------------------------------------------------------------
|
|
744
|
-
server.tool("ralph_hero__create_comment", "Add a comment to a GitHub issue", {
|
|
745
|
-
owner: z
|
|
746
|
-
|
|
870
|
+
server.tool("ralph_hero__create_comment", "Add a comment to a GitHub issue. Returns: commentId, issueNumber. Recovery: if issue not found, verify the issue number exists in the repository.", {
|
|
871
|
+
owner: z
|
|
872
|
+
.string()
|
|
873
|
+
.optional()
|
|
874
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
875
|
+
repo: z
|
|
876
|
+
.string()
|
|
877
|
+
.optional()
|
|
878
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
747
879
|
number: z.number().describe("Issue number"),
|
|
748
880
|
body: z.string().describe("Comment body (Markdown)"),
|
|
749
881
|
}, async (args) => {
|
|
@@ -773,9 +905,15 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
773
905
|
// -------------------------------------------------------------------------
|
|
774
906
|
// ralph_hero__detect_pipeline_position
|
|
775
907
|
// -------------------------------------------------------------------------
|
|
776
|
-
server.tool("ralph_hero__detect_pipeline_position", "Determine
|
|
777
|
-
owner: z
|
|
778
|
-
|
|
908
|
+
server.tool("ralph_hero__detect_pipeline_position", "Determine which workflow phase to execute next for an issue or its group. Returns: phase (SPLIT/TRIAGE/RESEARCH/PLAN/REVIEW/IMPLEMENT/COMPLETE/HUMAN_GATE/TERMINAL), convergence status with recommendation (proceed/wait/escalate), all group member states, and remaining phases. Call this INSTEAD of separate detect_group + check_convergence calls. Recovery: if issue not found, verify the issue number and that it has been added to the project.", {
|
|
909
|
+
owner: z
|
|
910
|
+
.string()
|
|
911
|
+
.optional()
|
|
912
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
913
|
+
repo: z
|
|
914
|
+
.string()
|
|
915
|
+
.optional()
|
|
916
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
779
917
|
number: z.number().describe("Issue number (seed for group detection)"),
|
|
780
918
|
}, async (args) => {
|
|
781
919
|
try {
|
|
@@ -812,11 +950,19 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
812
950
|
// -------------------------------------------------------------------------
|
|
813
951
|
// ralph_hero__check_convergence
|
|
814
952
|
// -------------------------------------------------------------------------
|
|
815
|
-
server.tool("ralph_hero__check_convergence", "Check if all issues in a group have reached the required state for the next phase. Returns
|
|
816
|
-
owner: z
|
|
817
|
-
|
|
953
|
+
server.tool("ralph_hero__check_convergence", "Check if all issues in a group have reached the required state for the next phase. Returns: converged, targetState, total, ready, blocking (with distanceToTarget), recommendation (proceed/wait/escalate). Note: detect_pipeline_position already includes convergence data; use this only when checking convergence against a specific target state not covered by pipeline detection.", {
|
|
954
|
+
owner: z
|
|
955
|
+
.string()
|
|
956
|
+
.optional()
|
|
957
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
958
|
+
repo: z
|
|
959
|
+
.string()
|
|
960
|
+
.optional()
|
|
961
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
818
962
|
number: z.number().describe("Issue number (any issue in the group)"),
|
|
819
|
-
targetState: z
|
|
963
|
+
targetState: z
|
|
964
|
+
.string()
|
|
965
|
+
.describe("The state all issues must be in (e.g., 'Ready for Plan')"),
|
|
820
966
|
}, async (args) => {
|
|
821
967
|
try {
|
|
822
968
|
// Validate target state
|
|
@@ -839,12 +985,16 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
839
985
|
targetState: args.targetState,
|
|
840
986
|
total: 1,
|
|
841
987
|
ready: atTarget ? 1 : 0,
|
|
842
|
-
blocking: atTarget
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
988
|
+
blocking: atTarget
|
|
989
|
+
? []
|
|
990
|
+
: [
|
|
991
|
+
{
|
|
992
|
+
number: args.number,
|
|
993
|
+
title: group.groupPrimary.title,
|
|
994
|
+
currentState: state.workflowState || "unknown",
|
|
995
|
+
distanceToTarget: computeDistance(state.workflowState || "unknown", args.targetState),
|
|
996
|
+
},
|
|
997
|
+
],
|
|
848
998
|
recommendation: atTarget ? "proceed" : "wait",
|
|
849
999
|
});
|
|
850
1000
|
}
|
|
@@ -895,11 +1045,23 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
895
1045
|
// -------------------------------------------------------------------------
|
|
896
1046
|
// ralph_hero__pick_actionable_issue
|
|
897
1047
|
// -------------------------------------------------------------------------
|
|
898
|
-
server.tool("ralph_hero__pick_actionable_issue", "Find the highest-priority issue
|
|
899
|
-
owner: z
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1048
|
+
server.tool("ralph_hero__pick_actionable_issue", "Find the highest-priority issue matching a workflow state that is not blocked or locked. Returns: found, issue (with number, title, workflowState, estimate, priority, group context), alternatives count. Used by dispatch loop to find work for idle teammates. Recovery: if no issues found, try a different workflowState or increase maxEstimate.", {
|
|
1049
|
+
owner: z
|
|
1050
|
+
.string()
|
|
1051
|
+
.optional()
|
|
1052
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
1053
|
+
repo: z
|
|
1054
|
+
.string()
|
|
1055
|
+
.optional()
|
|
1056
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
1057
|
+
workflowState: z
|
|
1058
|
+
.string()
|
|
1059
|
+
.describe("Target workflow state (e.g., 'Research Needed', 'Ready for Plan')"),
|
|
1060
|
+
maxEstimate: z
|
|
1061
|
+
.string()
|
|
1062
|
+
.optional()
|
|
1063
|
+
.default("S")
|
|
1064
|
+
.describe("Maximum estimate to include (XS, S, M, L, XL). Default: S"),
|
|
903
1065
|
}, async (args) => {
|
|
904
1066
|
try {
|
|
905
1067
|
// Validate workflow state
|
|
@@ -999,7 +1161,12 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
999
1161
|
return !blockedBy.nodes.some((dep) => dep.state === "OPEN");
|
|
1000
1162
|
});
|
|
1001
1163
|
// Sort by priority (P0 > P1 > P2 > P3 > none)
|
|
1002
|
-
const priorityOrder = {
|
|
1164
|
+
const priorityOrder = {
|
|
1165
|
+
P0: 0,
|
|
1166
|
+
P1: 1,
|
|
1167
|
+
P2: 2,
|
|
1168
|
+
P3: 3,
|
|
1169
|
+
};
|
|
1003
1170
|
candidates.sort((a, b) => {
|
|
1004
1171
|
const pA = getFieldValue(a, "Priority");
|
|
1005
1172
|
const pB = getFieldValue(b, "Priority");
|
|
@@ -1016,10 +1183,34 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1016
1183
|
}
|
|
1017
1184
|
const best = candidates[0];
|
|
1018
1185
|
const content = best.content;
|
|
1186
|
+
const issueNumber = content.number;
|
|
1187
|
+
// Detect group context for the picked issue (best-effort)
|
|
1188
|
+
let group = null;
|
|
1189
|
+
try {
|
|
1190
|
+
const groupResult = await detectGroup(client, owner, repo, issueNumber);
|
|
1191
|
+
group = {
|
|
1192
|
+
isGroup: groupResult.isGroup,
|
|
1193
|
+
primary: {
|
|
1194
|
+
number: groupResult.groupPrimary.number,
|
|
1195
|
+
title: groupResult.groupPrimary.title,
|
|
1196
|
+
},
|
|
1197
|
+
members: groupResult.groupTickets.map((t) => ({
|
|
1198
|
+
number: t.number,
|
|
1199
|
+
title: t.title,
|
|
1200
|
+
state: t.state,
|
|
1201
|
+
order: t.order,
|
|
1202
|
+
})),
|
|
1203
|
+
totalTickets: groupResult.totalTickets,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
// Group detection is best-effort
|
|
1208
|
+
group = null;
|
|
1209
|
+
}
|
|
1019
1210
|
return toolSuccess({
|
|
1020
1211
|
found: true,
|
|
1021
1212
|
issue: {
|
|
1022
|
-
number:
|
|
1213
|
+
number: issueNumber,
|
|
1023
1214
|
title: content.title,
|
|
1024
1215
|
description: content.body || "",
|
|
1025
1216
|
workflowState: getFieldValue(best, "Workflow State"),
|
|
@@ -1028,6 +1219,7 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1028
1219
|
isLocked: false,
|
|
1029
1220
|
blockedBy: [],
|
|
1030
1221
|
},
|
|
1222
|
+
group,
|
|
1031
1223
|
alternatives: candidates.length - 1,
|
|
1032
1224
|
});
|
|
1033
1225
|
}
|
|
@@ -1038,7 +1230,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1038
1230
|
});
|
|
1039
1231
|
}
|
|
1040
1232
|
function getFieldValue(item, fieldName) {
|
|
1041
|
-
const fieldValue = item.fieldValues.nodes.find((fv) => fv.field?.name === fieldName &&
|
|
1233
|
+
const fieldValue = item.fieldValues.nodes.find((fv) => fv.field?.name === fieldName &&
|
|
1234
|
+
fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
|
|
1042
1235
|
return fieldValue?.name;
|
|
1043
1236
|
}
|
|
1044
1237
|
// ---------------------------------------------------------------------------
|
|
@@ -10,19 +10,59 @@ import { toolSuccess, toolError } from "../types.js";
|
|
|
10
10
|
import { resolveProjectOwner } from "../types.js";
|
|
11
11
|
const WORKFLOW_STATE_OPTIONS = [
|
|
12
12
|
{ name: "Backlog", color: "GRAY", description: "Awaiting triage" },
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
|
|
13
|
+
{
|
|
14
|
+
name: "Research Needed",
|
|
15
|
+
color: "PURPLE",
|
|
16
|
+
description: "Needs investigation before planning",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "Research in Progress",
|
|
20
|
+
color: "PURPLE",
|
|
21
|
+
description: "Investigation underway (locked)",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "Ready for Plan",
|
|
25
|
+
color: "BLUE",
|
|
26
|
+
description: "Research complete, ready for planning",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Plan in Progress",
|
|
30
|
+
color: "BLUE",
|
|
31
|
+
description: "Plan being written (locked)",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "Plan in Review",
|
|
35
|
+
color: "BLUE",
|
|
36
|
+
description: "Plan awaiting approval",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "In Progress",
|
|
40
|
+
color: "ORANGE",
|
|
41
|
+
description: "Implementation underway",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "In Review",
|
|
45
|
+
color: "YELLOW",
|
|
46
|
+
description: "PR created, awaiting code review",
|
|
47
|
+
},
|
|
20
48
|
{ name: "Done", color: "GREEN", description: "Completed and merged" },
|
|
21
|
-
{
|
|
22
|
-
|
|
49
|
+
{
|
|
50
|
+
name: "Human Needed",
|
|
51
|
+
color: "RED",
|
|
52
|
+
description: "Escalated - requires human intervention",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "Canceled",
|
|
56
|
+
color: "GRAY",
|
|
57
|
+
description: "Ticket canceled or superseded",
|
|
58
|
+
},
|
|
23
59
|
];
|
|
24
60
|
const PRIORITY_OPTIONS = [
|
|
25
|
-
{
|
|
61
|
+
{
|
|
62
|
+
name: "P0",
|
|
63
|
+
color: "RED",
|
|
64
|
+
description: "Critical - Drop everything, fix now",
|
|
65
|
+
},
|
|
26
66
|
{ name: "P1", color: "ORANGE", description: "High - Must do this sprint" },
|
|
27
67
|
{ name: "P2", color: "YELLOW", description: "Medium - Should do soon" },
|
|
28
68
|
{ name: "P3", color: "GRAY", description: "Low - Nice to have" },
|
|
@@ -138,8 +178,14 @@ export function registerProjectTools(server, client, fieldCache) {
|
|
|
138
178
|
// ralph_hero__get_project
|
|
139
179
|
// -------------------------------------------------------------------------
|
|
140
180
|
server.tool("ralph_hero__get_project", "Get a GitHub Project V2 with all fields and their options", {
|
|
141
|
-
owner: z
|
|
142
|
-
|
|
181
|
+
owner: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("GitHub owner (user or org). Defaults to GITHUB_OWNER env var"),
|
|
185
|
+
number: z
|
|
186
|
+
.number()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("Project number. Defaults to RALPH_GH_PROJECT_NUMBER env var"),
|
|
143
189
|
}, async (args) => {
|
|
144
190
|
try {
|
|
145
191
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
@@ -185,12 +231,31 @@ export function registerProjectTools(server, client, fieldCache) {
|
|
|
185
231
|
// ralph_hero__list_project_items
|
|
186
232
|
// -------------------------------------------------------------------------
|
|
187
233
|
server.tool("ralph_hero__list_project_items", "List items in a GitHub Project V2, optionally filtered by Workflow State, Estimate, or Priority", {
|
|
188
|
-
owner: z
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
234
|
+
owner: z
|
|
235
|
+
.string()
|
|
236
|
+
.optional()
|
|
237
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
238
|
+
number: z
|
|
239
|
+
.number()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe("Project number. Defaults to RALPH_GH_PROJECT_NUMBER env var"),
|
|
242
|
+
workflowState: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("Filter by Workflow State name"),
|
|
246
|
+
estimate: z
|
|
247
|
+
.string()
|
|
248
|
+
.optional()
|
|
249
|
+
.describe("Filter by Estimate name (XS, S, M, L, XL)"),
|
|
250
|
+
priority: z
|
|
251
|
+
.string()
|
|
252
|
+
.optional()
|
|
253
|
+
.describe("Filter by Priority name (P0, P1, P2, P3)"),
|
|
254
|
+
limit: z
|
|
255
|
+
.number()
|
|
256
|
+
.optional()
|
|
257
|
+
.default(50)
|
|
258
|
+
.describe("Max items to return (default 50)"),
|
|
194
259
|
}, async (args) => {
|
|
195
260
|
try {
|
|
196
261
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
@@ -303,7 +368,8 @@ export function registerProjectTools(server, client, fieldCache) {
|
|
|
303
368
|
});
|
|
304
369
|
}
|
|
305
370
|
function getFieldValue(item, fieldName) {
|
|
306
|
-
const fieldValue = item.fieldValues.nodes.find((fv) => fv.field?.name === fieldName &&
|
|
371
|
+
const fieldValue = item.fieldValues.nodes.find((fv) => fv.field?.name === fieldName &&
|
|
372
|
+
fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
|
|
307
373
|
return fieldValue?.name;
|
|
308
374
|
}
|
|
309
375
|
async function fetchProject(client, owner, number) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { detectGroup } from "../lib/group-detection.js";
|
|
12
|
-
import { isValidState, isEarlierState, VALID_STATES } from "../lib/workflow-states.js";
|
|
12
|
+
import { isValidState, isEarlierState, VALID_STATES, } from "../lib/workflow-states.js";
|
|
13
13
|
import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Helper: Resolve issue number to node ID (with caching)
|
|
@@ -51,11 +51,23 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
51
51
|
// ralph_hero__add_sub_issue
|
|
52
52
|
// -------------------------------------------------------------------------
|
|
53
53
|
server.tool("ralph_hero__add_sub_issue", "Create a parent/child (sub-issue) relationship between two GitHub issues. The parent issue becomes the container for the child issue.", {
|
|
54
|
-
owner: z
|
|
55
|
-
|
|
54
|
+
owner: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
58
|
+
repo: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
56
62
|
parentNumber: z.number().describe("Parent issue number"),
|
|
57
|
-
childNumber: z
|
|
58
|
-
|
|
63
|
+
childNumber: z
|
|
64
|
+
.number()
|
|
65
|
+
.describe("Child issue number (will become sub-issue of parent)"),
|
|
66
|
+
replaceParent: z
|
|
67
|
+
.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.default(false)
|
|
70
|
+
.describe("If true, move child even if it already has a parent"),
|
|
59
71
|
}, async (args) => {
|
|
60
72
|
try {
|
|
61
73
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -93,8 +105,14 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
93
105
|
// ralph_hero__list_sub_issues
|
|
94
106
|
// -------------------------------------------------------------------------
|
|
95
107
|
server.tool("ralph_hero__list_sub_issues", "List all sub-issues (children) of a parent GitHub issue, with completion summary", {
|
|
96
|
-
owner: z
|
|
97
|
-
|
|
108
|
+
owner: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
112
|
+
repo: z
|
|
113
|
+
.string()
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
98
116
|
number: z.number().describe("Parent issue number"),
|
|
99
117
|
}, async (args) => {
|
|
100
118
|
try {
|
|
@@ -133,7 +151,8 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
133
151
|
total: issue.subIssues.nodes.length,
|
|
134
152
|
completed: issue.subIssues.nodes.filter((si) => si.state === "CLOSED").length,
|
|
135
153
|
percentCompleted: issue.subIssues.nodes.length > 0
|
|
136
|
-
? Math.round((issue.subIssues.nodes.filter((si) => si.state === "CLOSED")
|
|
154
|
+
? Math.round((issue.subIssues.nodes.filter((si) => si.state === "CLOSED")
|
|
155
|
+
.length /
|
|
137
156
|
issue.subIssues.nodes.length) *
|
|
138
157
|
100)
|
|
139
158
|
: 0,
|
|
@@ -150,10 +169,20 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
150
169
|
// ralph_hero__add_dependency
|
|
151
170
|
// -------------------------------------------------------------------------
|
|
152
171
|
server.tool("ralph_hero__add_dependency", "Create a blocking dependency between two GitHub issues. The 'blockingNumber' issue blocks the 'blockedNumber' issue.", {
|
|
153
|
-
owner: z
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
owner: z
|
|
173
|
+
.string()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
176
|
+
repo: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
180
|
+
blockedNumber: z
|
|
181
|
+
.number()
|
|
182
|
+
.describe("Issue number that IS blocked (cannot proceed until blocker is done)"),
|
|
183
|
+
blockingNumber: z
|
|
184
|
+
.number()
|
|
185
|
+
.describe("Issue number that IS the blocker (must be completed first)"),
|
|
157
186
|
}, async (args) => {
|
|
158
187
|
try {
|
|
159
188
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -190,8 +219,14 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
190
219
|
// ralph_hero__remove_dependency
|
|
191
220
|
// -------------------------------------------------------------------------
|
|
192
221
|
server.tool("ralph_hero__remove_dependency", "Remove a blocking dependency between two GitHub issues", {
|
|
193
|
-
owner: z
|
|
194
|
-
|
|
222
|
+
owner: z
|
|
223
|
+
.string()
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
226
|
+
repo: z
|
|
227
|
+
.string()
|
|
228
|
+
.optional()
|
|
229
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
195
230
|
blockedNumber: z.number().describe("Issue number that was blocked"),
|
|
196
231
|
blockingNumber: z.number().describe("Issue number that was the blocker"),
|
|
197
232
|
}, async (args) => {
|
|
@@ -230,8 +265,14 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
230
265
|
// ralph_hero__list_dependencies
|
|
231
266
|
// -------------------------------------------------------------------------
|
|
232
267
|
server.tool("ralph_hero__list_dependencies", "List all dependencies (blocking and blocked-by) for a GitHub issue", {
|
|
233
|
-
owner: z
|
|
234
|
-
|
|
268
|
+
owner: z
|
|
269
|
+
.string()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
272
|
+
repo: z
|
|
273
|
+
.string()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
235
276
|
number: z.number().describe("Issue number"),
|
|
236
277
|
}, async (args) => {
|
|
237
278
|
try {
|
|
@@ -290,9 +331,17 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
290
331
|
// ralph_hero__detect_group
|
|
291
332
|
// -------------------------------------------------------------------------
|
|
292
333
|
server.tool("ralph_hero__detect_group", "Detect the group of related issues by traversing sub-issues and dependencies transitively from a seed issue. Returns all group members in topological order (blockers first). Used by Ralph workflow to discover atomic implementation groups.", {
|
|
293
|
-
owner: z
|
|
294
|
-
|
|
295
|
-
|
|
334
|
+
owner: z
|
|
335
|
+
.string()
|
|
336
|
+
.optional()
|
|
337
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
338
|
+
repo: z
|
|
339
|
+
.string()
|
|
340
|
+
.optional()
|
|
341
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
342
|
+
number: z
|
|
343
|
+
.number()
|
|
344
|
+
.describe("Seed issue number to start group detection from"),
|
|
296
345
|
}, async (args) => {
|
|
297
346
|
try {
|
|
298
347
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -308,10 +357,18 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
308
357
|
// ralph_hero__advance_children
|
|
309
358
|
// -------------------------------------------------------------------------
|
|
310
359
|
server.tool("ralph_hero__advance_children", "Advance all child/sub-issues of a parent to match the parent's new state. Only advances children that are in earlier workflow states. Returns what changed, what was skipped, and any errors.", {
|
|
311
|
-
owner: z
|
|
312
|
-
|
|
360
|
+
owner: z
|
|
361
|
+
.string()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
364
|
+
repo: z
|
|
365
|
+
.string()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
313
368
|
number: z.number().describe("Parent issue number"),
|
|
314
|
-
targetState: z
|
|
369
|
+
targetState: z
|
|
370
|
+
.string()
|
|
371
|
+
.describe("State to advance children to (e.g., 'Research Needed', 'Ready for Plan')"),
|
|
315
372
|
}, async (args) => {
|
|
316
373
|
try {
|
|
317
374
|
// Validate target state
|
|
@@ -506,7 +563,8 @@ async function getCurrentFieldValueForRelationships(client, fieldCache, owner, r
|
|
|
506
563
|
}
|
|
507
564
|
}
|
|
508
565
|
}`, { itemId: projectItemId });
|
|
509
|
-
const fieldValue = result.node?.fieldValues?.nodes?.find((fv) => fv.field?.name === "Workflow State" &&
|
|
566
|
+
const fieldValue = result.node?.fieldValues?.nodes?.find((fv) => fv.field?.name === "Workflow State" &&
|
|
567
|
+
fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
|
|
510
568
|
return fieldValue?.name;
|
|
511
569
|
}
|
|
512
570
|
async function updateProjectItemFieldForRelationships(client, fieldCache, projectItemId, fieldName, optionName) {
|
package/dist/tools/view-tools.js
CHANGED
|
@@ -17,8 +17,14 @@ export function registerViewTools(server, client, fieldCache) {
|
|
|
17
17
|
// ralph_hero__list_views
|
|
18
18
|
// -------------------------------------------------------------------------
|
|
19
19
|
server.tool("ralph_hero__list_views", "List all views for a GitHub Project V2", {
|
|
20
|
-
owner: z
|
|
21
|
-
|
|
20
|
+
owner: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
24
|
+
number: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Project number. Defaults to RALPH_GH_PROJECT_NUMBER env var"),
|
|
22
28
|
}, async (args) => {
|
|
23
29
|
try {
|
|
24
30
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
@@ -52,14 +58,35 @@ export function registerViewTools(server, client, fieldCache) {
|
|
|
52
58
|
// ralph_hero__update_field_options
|
|
53
59
|
// -------------------------------------------------------------------------
|
|
54
60
|
server.tool("ralph_hero__update_field_options", "Update a single-select field's options (names, colors, descriptions). Overwrites ALL existing options — include unchanged options to preserve them.", {
|
|
55
|
-
owner: z
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
owner: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
65
|
+
number: z
|
|
66
|
+
.number()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Project number. Defaults to RALPH_GH_PROJECT_NUMBER env var"),
|
|
69
|
+
fieldName: z
|
|
70
|
+
.string()
|
|
71
|
+
.describe("Name of the single-select field to update (e.g., 'Workflow State', 'Priority', 'Estimate')"),
|
|
72
|
+
options: z
|
|
73
|
+
.array(z.object({
|
|
59
74
|
name: z.string().describe("Option name"),
|
|
60
|
-
color: z
|
|
75
|
+
color: z
|
|
76
|
+
.enum([
|
|
77
|
+
"GRAY",
|
|
78
|
+
"BLUE",
|
|
79
|
+
"GREEN",
|
|
80
|
+
"YELLOW",
|
|
81
|
+
"ORANGE",
|
|
82
|
+
"RED",
|
|
83
|
+
"PINK",
|
|
84
|
+
"PURPLE",
|
|
85
|
+
])
|
|
86
|
+
.describe("Display color"),
|
|
61
87
|
description: z.string().describe("Description text"),
|
|
62
|
-
}))
|
|
88
|
+
}))
|
|
89
|
+
.describe("Complete list of options (replaces all existing options)"),
|
|
63
90
|
}, async (args) => {
|
|
64
91
|
try {
|
|
65
92
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
@@ -144,7 +171,11 @@ async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
|
144
171
|
const result = await client.projectQuery(QUERY.replace("OWNER_TYPE", ownerType), { owner, number: projectNumber }, { cache: true });
|
|
145
172
|
const project = result[ownerType]?.projectV2;
|
|
146
173
|
if (project) {
|
|
147
|
-
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
174
|
+
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
175
|
+
id: f.id,
|
|
176
|
+
name: f.name,
|
|
177
|
+
options: f.options,
|
|
178
|
+
})));
|
|
148
179
|
return;
|
|
149
180
|
}
|
|
150
181
|
}
|