ralph-hero-mcp-server 1.3.2 → 2.4.4
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/index.js +3 -0
- package/dist/lib/helpers.js +25 -0
- package/dist/lib/workflow-states.js +37 -0
- package/dist/tools/batch-tools.js +22 -1
- package/dist/tools/issue-tools.js +3 -1
- package/dist/tools/project-management-tools.js +248 -0
- package/dist/tools/relationship-tools.js +158 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerIssueTools } from "./tools/issue-tools.js";
|
|
|
17
17
|
import { registerRelationshipTools } from "./tools/relationship-tools.js";
|
|
18
18
|
import { registerDashboardTools } from "./tools/dashboard-tools.js";
|
|
19
19
|
import { registerBatchTools } from "./tools/batch-tools.js";
|
|
20
|
+
import { registerProjectManagementTools } from "./tools/project-management-tools.js";
|
|
20
21
|
/**
|
|
21
22
|
* Initialize the GitHub client from environment variables.
|
|
22
23
|
*/
|
|
@@ -242,6 +243,8 @@ async function main() {
|
|
|
242
243
|
registerDashboardTools(server, client, fieldCache);
|
|
243
244
|
// Phase 5: Batch operations
|
|
244
245
|
registerBatchTools(server, client, fieldCache);
|
|
246
|
+
// Project management tools (archive, remove, add, link repo, clear field)
|
|
247
|
+
registerProjectManagementTools(server, client, fieldCache);
|
|
245
248
|
// Connect via stdio transport
|
|
246
249
|
const transport = new StdioServerTransport();
|
|
247
250
|
await server.connect(transport);
|
package/dist/lib/helpers.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* implementations to their original versions.
|
|
7
7
|
*/
|
|
8
8
|
import { resolveProjectOwner } from "../types.js";
|
|
9
|
+
import { WORKFLOW_STATE_TO_STATUS } from "./workflow-states.js";
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Helper: Fetch project data for field cache population
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -197,4 +198,28 @@ export function resolveFullConfig(client, args) {
|
|
|
197
198
|
}
|
|
198
199
|
return { owner, repo, projectNumber, projectOwner };
|
|
199
200
|
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Helper: Sync default Status field after Workflow State change
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
/**
|
|
205
|
+
* Sync the default Status field to match a Workflow State change.
|
|
206
|
+
* Best-effort: logs warning on failure but does not throw.
|
|
207
|
+
*/
|
|
208
|
+
export async function syncStatusField(client, fieldCache, projectItemId, workflowState) {
|
|
209
|
+
const targetStatus = WORKFLOW_STATE_TO_STATUS[workflowState];
|
|
210
|
+
if (!targetStatus)
|
|
211
|
+
return;
|
|
212
|
+
const statusFieldId = fieldCache.getFieldId("Status");
|
|
213
|
+
if (!statusFieldId)
|
|
214
|
+
return;
|
|
215
|
+
const statusOptionId = fieldCache.resolveOptionId("Status", targetStatus);
|
|
216
|
+
if (!statusOptionId)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
await updateProjectItemField(client, fieldCache, projectItemId, "Status", targetStatus);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Best-effort sync - don't fail the primary operation
|
|
223
|
+
}
|
|
224
|
+
}
|
|
200
225
|
//# sourceMappingURL=helpers.js.map
|
|
@@ -38,6 +38,21 @@ export const HUMAN_STATES = [
|
|
|
38
38
|
"Human Needed",
|
|
39
39
|
"Plan in Review",
|
|
40
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* Gate states that trigger parent advancement when ALL children reach them.
|
|
43
|
+
* Intermediate "in progress" states should NOT advance the parent.
|
|
44
|
+
*/
|
|
45
|
+
export const PARENT_GATE_STATES = [
|
|
46
|
+
"Ready for Plan",
|
|
47
|
+
"In Review",
|
|
48
|
+
"Done",
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Check if a state is a parent advancement gate.
|
|
52
|
+
*/
|
|
53
|
+
export function isParentGateState(state) {
|
|
54
|
+
return PARENT_GATE_STATES.includes(state);
|
|
55
|
+
}
|
|
41
56
|
/**
|
|
42
57
|
* Valid workflow states for the project (all known states).
|
|
43
58
|
*/
|
|
@@ -79,4 +94,26 @@ export function isEarlierState(a, b) {
|
|
|
79
94
|
export function isValidState(state) {
|
|
80
95
|
return VALID_STATES.includes(state);
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Maps Ralph Workflow States to GitHub's default Status field values.
|
|
99
|
+
* Used for one-way sync: Workflow State changes -> Status field updates.
|
|
100
|
+
*
|
|
101
|
+
* Rationale:
|
|
102
|
+
* - Todo = work not yet actively started (queued states)
|
|
103
|
+
* - In Progress = work actively being processed (lock states + review)
|
|
104
|
+
* - Done = terminal/escalated states (no automated progression)
|
|
105
|
+
*/
|
|
106
|
+
export const WORKFLOW_STATE_TO_STATUS = {
|
|
107
|
+
"Backlog": "Todo",
|
|
108
|
+
"Research Needed": "Todo",
|
|
109
|
+
"Ready for Plan": "Todo",
|
|
110
|
+
"Plan in Review": "Todo",
|
|
111
|
+
"Research in Progress": "In Progress",
|
|
112
|
+
"Plan in Progress": "In Progress",
|
|
113
|
+
"In Progress": "In Progress",
|
|
114
|
+
"In Review": "In Progress",
|
|
115
|
+
"Done": "Done",
|
|
116
|
+
"Canceled": "Done",
|
|
117
|
+
"Human Needed": "Done",
|
|
118
|
+
};
|
|
82
119
|
//# sourceMappingURL=workflow-states.js.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* instead of one per issue).
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { isEarlierState } from "../lib/workflow-states.js";
|
|
9
|
+
import { isEarlierState, WORKFLOW_STATE_TO_STATUS } from "../lib/workflow-states.js";
|
|
10
10
|
import { toolSuccess, toolError } from "../types.js";
|
|
11
11
|
import { ensureFieldCache, resolveFullConfig, } from "../lib/helpers.js";
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -267,6 +267,27 @@ export function registerBatchTools(server, client, fieldCache) {
|
|
|
267
267
|
field: op.field,
|
|
268
268
|
value: op.value,
|
|
269
269
|
});
|
|
270
|
+
// For workflow_state operations, also sync the default Status field
|
|
271
|
+
if (op.field === "workflow_state") {
|
|
272
|
+
const targetStatus = WORKFLOW_STATE_TO_STATUS[op.value];
|
|
273
|
+
if (targetStatus) {
|
|
274
|
+
const statusFieldId = fieldCache.getFieldId("Status");
|
|
275
|
+
const statusOptionId = statusFieldId
|
|
276
|
+
? fieldCache.resolveOptionId("Status", targetStatus)
|
|
277
|
+
: undefined;
|
|
278
|
+
if (statusFieldId && statusOptionId) {
|
|
279
|
+
updates.push({
|
|
280
|
+
alias: `s${num}_${opIdx}`,
|
|
281
|
+
itemId: issue.projectItemId,
|
|
282
|
+
fieldId: statusFieldId,
|
|
283
|
+
optionId: statusOptionId,
|
|
284
|
+
issueNumber: num,
|
|
285
|
+
field: "status_sync",
|
|
286
|
+
value: targetStatus,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
270
291
|
}
|
|
271
292
|
}
|
|
272
293
|
// Chunk mutations if needed
|
|
@@ -11,7 +11,7 @@ import { detectPipelinePosition, } 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 { toolSuccess, toolError } from "../types.js";
|
|
14
|
-
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, } from "../lib/helpers.js";
|
|
14
|
+
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, syncStatusField, } from "../lib/helpers.js";
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Register issue tools
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -607,6 +607,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
607
607
|
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
|
|
608
608
|
// Update the field with the resolved state
|
|
609
609
|
await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", resolvedState);
|
|
610
|
+
// Sync default Status field (best-effort, one-way)
|
|
611
|
+
await syncStatusField(client, fieldCache, projectItemId, resolvedState);
|
|
610
612
|
const result = {
|
|
611
613
|
number: args.number,
|
|
612
614
|
previousState: previousState || "(unknown)",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools for GitHub Projects V2 management operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for archiving/unarchiving items, removing items from projects,
|
|
5
|
+
* adding existing issues to projects, linking repositories, and clearing field values.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { toolSuccess, toolError } from "../types.js";
|
|
9
|
+
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, resolveFullConfig, } from "../lib/helpers.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Register project management tools
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function registerProjectManagementTools(server, client, fieldCache) {
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// ralph_hero__archive_item
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
server.tool("ralph_hero__archive_item", "Archive or unarchive a project item. Archived items are hidden from default views but not deleted. Returns: number, archived, projectItemId.", {
|
|
18
|
+
owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
|
|
19
|
+
repo: z.string().optional().describe("Repository name. Defaults to env var"),
|
|
20
|
+
number: z.number().describe("Issue number"),
|
|
21
|
+
unarchive: z.boolean().optional().default(false)
|
|
22
|
+
.describe("If true, unarchive instead of archive (default: false)"),
|
|
23
|
+
}, async (args) => {
|
|
24
|
+
try {
|
|
25
|
+
const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
26
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
27
|
+
const projectId = fieldCache.getProjectId();
|
|
28
|
+
if (!projectId) {
|
|
29
|
+
return toolError("Could not resolve project ID");
|
|
30
|
+
}
|
|
31
|
+
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
|
|
32
|
+
if (args.unarchive) {
|
|
33
|
+
await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
|
|
34
|
+
unarchiveProjectV2Item(input: {
|
|
35
|
+
projectId: $projectId,
|
|
36
|
+
itemId: $itemId
|
|
37
|
+
}) {
|
|
38
|
+
item { id }
|
|
39
|
+
}
|
|
40
|
+
}`, { projectId, itemId: projectItemId });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
|
|
44
|
+
archiveProjectV2Item(input: {
|
|
45
|
+
projectId: $projectId,
|
|
46
|
+
itemId: $itemId
|
|
47
|
+
}) {
|
|
48
|
+
item { id }
|
|
49
|
+
}
|
|
50
|
+
}`, { projectId, itemId: projectItemId });
|
|
51
|
+
}
|
|
52
|
+
return toolSuccess({
|
|
53
|
+
number: args.number,
|
|
54
|
+
archived: !args.unarchive,
|
|
55
|
+
projectItemId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return toolError(`Failed to ${args.unarchive ? "unarchive" : "archive"} item: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// -------------------------------------------------------------------------
|
|
64
|
+
// ralph_hero__remove_from_project
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
server.tool("ralph_hero__remove_from_project", "Remove an issue from the project. This deletes the project item (not the issue itself). Returns: number, removed.", {
|
|
67
|
+
owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
|
|
68
|
+
repo: z.string().optional().describe("Repository name. Defaults to env var"),
|
|
69
|
+
number: z.number().describe("Issue number"),
|
|
70
|
+
}, async (args) => {
|
|
71
|
+
try {
|
|
72
|
+
const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
73
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
74
|
+
const projectId = fieldCache.getProjectId();
|
|
75
|
+
if (!projectId) {
|
|
76
|
+
return toolError("Could not resolve project ID");
|
|
77
|
+
}
|
|
78
|
+
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
|
|
79
|
+
await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
|
|
80
|
+
deleteProjectV2Item(input: {
|
|
81
|
+
projectId: $projectId,
|
|
82
|
+
itemId: $itemId
|
|
83
|
+
}) {
|
|
84
|
+
deletedItemId
|
|
85
|
+
}
|
|
86
|
+
}`, { projectId, itemId: projectItemId });
|
|
87
|
+
// Invalidate cached project item ID since it no longer exists
|
|
88
|
+
client.getCache().invalidate(`project-item-id:${owner}/${repo}#${args.number}`);
|
|
89
|
+
return toolSuccess({
|
|
90
|
+
number: args.number,
|
|
91
|
+
removed: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
return toolError(`Failed to remove from project: ${message}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
// ralph_hero__add_to_project
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
server.tool("ralph_hero__add_to_project", "Add an existing issue to the project. The issue must already exist in the repository. Returns: number, projectItemId, added.", {
|
|
103
|
+
owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
|
|
104
|
+
repo: z.string().optional().describe("Repository name. Defaults to env var"),
|
|
105
|
+
number: z.number().describe("Issue number"),
|
|
106
|
+
}, async (args) => {
|
|
107
|
+
try {
|
|
108
|
+
const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
109
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
110
|
+
const projectId = fieldCache.getProjectId();
|
|
111
|
+
if (!projectId) {
|
|
112
|
+
return toolError("Could not resolve project ID");
|
|
113
|
+
}
|
|
114
|
+
const issueNodeId = await resolveIssueNodeId(client, owner, repo, args.number);
|
|
115
|
+
const result = await client.projectMutate(`mutation($projectId: ID!, $contentId: ID!) {
|
|
116
|
+
addProjectV2ItemById(input: {
|
|
117
|
+
projectId: $projectId,
|
|
118
|
+
contentId: $contentId
|
|
119
|
+
}) {
|
|
120
|
+
item { id }
|
|
121
|
+
}
|
|
122
|
+
}`, { projectId, contentId: issueNodeId });
|
|
123
|
+
const projectItemId = result.addProjectV2ItemById.item.id;
|
|
124
|
+
// Cache the new project item ID
|
|
125
|
+
client.getCache().set(`project-item-id:${owner}/${repo}#${args.number}`, projectItemId, 30 * 60 * 1000);
|
|
126
|
+
return toolSuccess({
|
|
127
|
+
number: args.number,
|
|
128
|
+
projectItemId,
|
|
129
|
+
added: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
return toolError(`Failed to add to project: ${message}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// -------------------------------------------------------------------------
|
|
138
|
+
// ralph_hero__link_repository
|
|
139
|
+
// -------------------------------------------------------------------------
|
|
140
|
+
server.tool("ralph_hero__link_repository", "Link or unlink a repository to/from the project. Linked repositories enable auto-add workflows and issue filtering. Returns: repository, linked.", {
|
|
141
|
+
owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
|
|
142
|
+
repoToLink: z.string()
|
|
143
|
+
.describe("Repository to link, as 'owner/name' or just 'name' (uses default owner)"),
|
|
144
|
+
unlink: z.boolean().optional().default(false)
|
|
145
|
+
.describe("If true, unlink instead of link (default: false)"),
|
|
146
|
+
}, async (args) => {
|
|
147
|
+
try {
|
|
148
|
+
const { projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
149
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
150
|
+
const projectId = fieldCache.getProjectId();
|
|
151
|
+
if (!projectId) {
|
|
152
|
+
return toolError("Could not resolve project ID");
|
|
153
|
+
}
|
|
154
|
+
// Parse repoToLink: "owner/name" or just "name" (using default owner)
|
|
155
|
+
let repoOwner;
|
|
156
|
+
let repoName;
|
|
157
|
+
if (args.repoToLink.includes("/")) {
|
|
158
|
+
const parts = args.repoToLink.split("/");
|
|
159
|
+
repoOwner = parts[0];
|
|
160
|
+
repoName = parts[1];
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
repoOwner = client.config.owner || projectOwner;
|
|
164
|
+
repoName = args.repoToLink;
|
|
165
|
+
}
|
|
166
|
+
// Resolve repository node ID
|
|
167
|
+
const repoResult = await client.query(`query($repoOwner: String!, $repoName: String!) {
|
|
168
|
+
repository(owner: $repoOwner, name: $repoName) { id }
|
|
169
|
+
}`, { repoOwner, repoName }, { cache: true, cacheTtlMs: 60 * 60 * 1000 });
|
|
170
|
+
const repoId = repoResult.repository?.id;
|
|
171
|
+
if (!repoId) {
|
|
172
|
+
return toolError(`Repository ${repoOwner}/${repoName} not found`);
|
|
173
|
+
}
|
|
174
|
+
if (args.unlink) {
|
|
175
|
+
await client.projectMutate(`mutation($projectId: ID!, $repositoryId: ID!) {
|
|
176
|
+
unlinkProjectV2FromRepository(input: {
|
|
177
|
+
projectId: $projectId,
|
|
178
|
+
repositoryId: $repositoryId
|
|
179
|
+
}) {
|
|
180
|
+
repository { id }
|
|
181
|
+
}
|
|
182
|
+
}`, { projectId, repositoryId: repoId });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
await client.projectMutate(`mutation($projectId: ID!, $repositoryId: ID!) {
|
|
186
|
+
linkProjectV2ToRepository(input: {
|
|
187
|
+
projectId: $projectId,
|
|
188
|
+
repositoryId: $repositoryId
|
|
189
|
+
}) {
|
|
190
|
+
repository { id }
|
|
191
|
+
}
|
|
192
|
+
}`, { projectId, repositoryId: repoId });
|
|
193
|
+
}
|
|
194
|
+
return toolSuccess({
|
|
195
|
+
repository: `${repoOwner}/${repoName}`,
|
|
196
|
+
linked: !args.unlink,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
return toolError(`Failed to ${args.unlink ? "unlink" : "link"} repository: ${message}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// -------------------------------------------------------------------------
|
|
205
|
+
// ralph_hero__clear_field
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
server.tool("ralph_hero__clear_field", "Clear a field value on a project item. Works for any single-select field (Workflow State, Estimate, Priority, Status, etc.). Returns: number, field, cleared.", {
|
|
208
|
+
owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
|
|
209
|
+
repo: z.string().optional().describe("Repository name. Defaults to env var"),
|
|
210
|
+
number: z.number().describe("Issue number"),
|
|
211
|
+
field: z.string().describe("Field name to clear (e.g., 'Estimate', 'Priority', 'Workflow State')"),
|
|
212
|
+
}, async (args) => {
|
|
213
|
+
try {
|
|
214
|
+
const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
215
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
216
|
+
const projectId = fieldCache.getProjectId();
|
|
217
|
+
if (!projectId) {
|
|
218
|
+
return toolError("Could not resolve project ID");
|
|
219
|
+
}
|
|
220
|
+
const fieldId = fieldCache.getFieldId(args.field);
|
|
221
|
+
if (!fieldId) {
|
|
222
|
+
const validFields = fieldCache.getFieldNames();
|
|
223
|
+
return toolError(`Field "${args.field}" not found in project. ` +
|
|
224
|
+
`Valid fields: ${validFields.join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
|
|
227
|
+
await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
|
|
228
|
+
clearProjectV2ItemFieldValue(input: {
|
|
229
|
+
projectId: $projectId,
|
|
230
|
+
itemId: $itemId,
|
|
231
|
+
fieldId: $fieldId
|
|
232
|
+
}) {
|
|
233
|
+
projectV2Item { id }
|
|
234
|
+
}
|
|
235
|
+
}`, { projectId, itemId: projectItemId, fieldId });
|
|
236
|
+
return toolSuccess({
|
|
237
|
+
number: args.number,
|
|
238
|
+
field: args.field,
|
|
239
|
+
cleared: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
244
|
+
return toolError(`Failed to clear field: ${message}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=project-management-tools.js.map
|
|
@@ -9,9 +9,9 @@
|
|
|
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, PARENT_GATE_STATES, isParentGateState, stateIndex, } from "../lib/workflow-states.js";
|
|
13
13
|
import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
|
|
14
|
-
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, } from "../lib/helpers.js";
|
|
14
|
+
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, syncStatusField, } from "../lib/helpers.js";
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Register relationship tools
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -411,6 +411,8 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
411
411
|
// Advance the child
|
|
412
412
|
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, child.number);
|
|
413
413
|
await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.targetState);
|
|
414
|
+
// Sync default Status field (best-effort, one-way)
|
|
415
|
+
await syncStatusField(client, fieldCache, projectItemId, args.targetState);
|
|
414
416
|
advanced.push({
|
|
415
417
|
number: child.number,
|
|
416
418
|
fromState: currentState,
|
|
@@ -436,5 +438,159 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
436
438
|
return toolError(`Failed to advance children: ${message}`);
|
|
437
439
|
}
|
|
438
440
|
});
|
|
441
|
+
// -------------------------------------------------------------------------
|
|
442
|
+
// ralph_hero__advance_parent
|
|
443
|
+
// -------------------------------------------------------------------------
|
|
444
|
+
server.tool("ralph_hero__advance_parent", "Check if all siblings of a child issue have reached a gate state, and if so, advance the parent issue to match. Gate states: Ready for Plan, In Review, Done. Only applies to parent/child (sub-issue) relationships. Returns what changed or why the parent was not advanced.", {
|
|
445
|
+
owner: z
|
|
446
|
+
.string()
|
|
447
|
+
.optional()
|
|
448
|
+
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
449
|
+
repo: z
|
|
450
|
+
.string()
|
|
451
|
+
.optional()
|
|
452
|
+
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
453
|
+
number: z
|
|
454
|
+
.number()
|
|
455
|
+
.describe("Child issue number (any child in the group)"),
|
|
456
|
+
}, async (args) => {
|
|
457
|
+
try {
|
|
458
|
+
const { owner, repo } = resolveConfig(client, args);
|
|
459
|
+
const projectNumber = client.config.projectNumber;
|
|
460
|
+
if (!projectNumber) {
|
|
461
|
+
return toolError("projectNumber is required (set RALPH_GH_PROJECT_NUMBER env var)");
|
|
462
|
+
}
|
|
463
|
+
const projectOwner = resolveProjectOwner(client.config);
|
|
464
|
+
if (!projectOwner) {
|
|
465
|
+
return toolError("projectOwner is required (set RALPH_GH_PROJECT_OWNER or RALPH_GH_OWNER env var)");
|
|
466
|
+
}
|
|
467
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
468
|
+
// Fetch child issue to find its parent
|
|
469
|
+
const childResult = await client.query(`query($owner: String!, $repo: String!, $issueNumber: Int!) {
|
|
470
|
+
repository(owner: $owner, name: $repo) {
|
|
471
|
+
issue(number: $issueNumber) {
|
|
472
|
+
number
|
|
473
|
+
title
|
|
474
|
+
parent { number title state }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}`, { owner, repo, issueNumber: args.number });
|
|
478
|
+
const childIssue = childResult.repository?.issue;
|
|
479
|
+
if (!childIssue) {
|
|
480
|
+
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
481
|
+
}
|
|
482
|
+
if (!childIssue.parent) {
|
|
483
|
+
return toolSuccess({
|
|
484
|
+
advanced: false,
|
|
485
|
+
reason: "Issue has no parent",
|
|
486
|
+
child: { number: childIssue.number, title: childIssue.title },
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const parentNumber = childIssue.parent.number;
|
|
490
|
+
// Fetch all siblings (sub-issues of the parent)
|
|
491
|
+
const siblingResult = await client.query(`query($owner: String!, $repo: String!, $parentNum: Int!) {
|
|
492
|
+
repository(owner: $owner, name: $repo) {
|
|
493
|
+
issue(number: $parentNum) {
|
|
494
|
+
number
|
|
495
|
+
title
|
|
496
|
+
subIssues(first: 50) {
|
|
497
|
+
nodes { id number title state }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}`, { owner, repo, parentNum: parentNumber });
|
|
502
|
+
const parentIssue = siblingResult.repository?.issue;
|
|
503
|
+
if (!parentIssue) {
|
|
504
|
+
return toolError(`Parent issue #${parentNumber} not found in ${owner}/${repo}`);
|
|
505
|
+
}
|
|
506
|
+
const siblings = parentIssue.subIssues.nodes;
|
|
507
|
+
if (siblings.length === 0) {
|
|
508
|
+
return toolSuccess({
|
|
509
|
+
advanced: false,
|
|
510
|
+
reason: "Parent has no sub-issues",
|
|
511
|
+
parent: { number: parentNumber, title: parentIssue.title },
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// Get workflow state for each sibling and find the minimum
|
|
515
|
+
const childStates = [];
|
|
516
|
+
let minStateIdx = Infinity;
|
|
517
|
+
for (const sibling of siblings) {
|
|
518
|
+
const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, sibling.number, "Workflow State");
|
|
519
|
+
const state = currentState || "unknown";
|
|
520
|
+
childStates.push({
|
|
521
|
+
number: sibling.number,
|
|
522
|
+
title: sibling.title,
|
|
523
|
+
workflowState: state,
|
|
524
|
+
});
|
|
525
|
+
const idx = stateIndex(state);
|
|
526
|
+
// States not in STATE_ORDER (Human Needed, Canceled, unknown) block advancement
|
|
527
|
+
if (idx === -1) {
|
|
528
|
+
return toolSuccess({
|
|
529
|
+
advanced: false,
|
|
530
|
+
reason: `Child #${sibling.number} is in state "${state}" which is outside the pipeline -- blocks parent advancement`,
|
|
531
|
+
parent: { number: parentNumber, title: parentIssue.title },
|
|
532
|
+
childStates,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (idx < minStateIdx) {
|
|
536
|
+
minStateIdx = idx;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Find the minimum state name
|
|
540
|
+
const minState = siblings.length > 0
|
|
541
|
+
? childStates.reduce((min, cs) => {
|
|
542
|
+
const idx = stateIndex(cs.workflowState);
|
|
543
|
+
const minIdx = stateIndex(min.workflowState);
|
|
544
|
+
return idx < minIdx ? cs : min;
|
|
545
|
+
}).workflowState
|
|
546
|
+
: "unknown";
|
|
547
|
+
// Check if the minimum state is a gate state
|
|
548
|
+
if (!isParentGateState(minState)) {
|
|
549
|
+
return toolSuccess({
|
|
550
|
+
advanced: false,
|
|
551
|
+
reason: "Not all children at a gate state",
|
|
552
|
+
minimumChildState: minState,
|
|
553
|
+
gateStates: [...PARENT_GATE_STATES],
|
|
554
|
+
parent: { number: parentNumber, title: parentIssue.title },
|
|
555
|
+
childStates,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// Get parent's current workflow state
|
|
559
|
+
const parentState = await getCurrentFieldValue(client, fieldCache, owner, repo, parentNumber, "Workflow State");
|
|
560
|
+
// Check if parent is already at or past the target state
|
|
561
|
+
const parentIdx = stateIndex(parentState || "");
|
|
562
|
+
if (parentIdx >= minStateIdx) {
|
|
563
|
+
return toolSuccess({
|
|
564
|
+
advanced: false,
|
|
565
|
+
reason: "Parent already at or past target state",
|
|
566
|
+
parent: {
|
|
567
|
+
number: parentNumber,
|
|
568
|
+
title: parentIssue.title,
|
|
569
|
+
currentState: parentState,
|
|
570
|
+
},
|
|
571
|
+
targetState: minState,
|
|
572
|
+
childStates,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
// Advance the parent
|
|
576
|
+
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, parentNumber);
|
|
577
|
+
await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", minState);
|
|
578
|
+
await syncStatusField(client, fieldCache, projectItemId, minState);
|
|
579
|
+
return toolSuccess({
|
|
580
|
+
advanced: true,
|
|
581
|
+
parent: {
|
|
582
|
+
number: parentNumber,
|
|
583
|
+
title: parentIssue.title,
|
|
584
|
+
fromState: parentState || "unknown",
|
|
585
|
+
toState: minState,
|
|
586
|
+
},
|
|
587
|
+
childStates,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
592
|
+
return toolError(`Failed to advance parent: ${message}`);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
439
595
|
}
|
|
440
596
|
//# sourceMappingURL=relationship-tools.js.map
|