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 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);
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "1.3.2",
3
+ "version": "2.4.4",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",