ralph-hero-mcp-server 2.4.105 → 2.4.109

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.
@@ -123,6 +123,7 @@ function buildSnapshot(state, items, now) {
123
123
  ageHours: Math.max(0, (now - new Date(item.updatedAt).getTime()) / (1000 * 60 * 60)),
124
124
  isLocked: LOCK_STATES.includes(state),
125
125
  blockedBy: item.blockedBy,
126
+ subIssueCount: item.subIssueCount,
126
127
  })),
127
128
  };
128
129
  }
@@ -202,9 +203,10 @@ export function detectHealthIssues(phases, config = DEFAULT_HEALTH_CONFIG) {
202
203
  issues: [issue.number],
203
204
  });
204
205
  }
205
- // Oversized in pipeline: M/L/XL estimate past Backlog
206
+ // Oversized in pipeline: M/L/XL estimate past Backlog (skip already-split parents)
206
207
  if (issue.estimate &&
207
208
  OVERSIZED_ESTIMATES.has(issue.estimate) &&
209
+ issue.subIssueCount === 0 &&
208
210
  phase.state !== "Backlog" &&
209
211
  !TERMINAL_STATES.includes(phase.state) &&
210
212
  phase.state !== "Human Needed") {
@@ -6,7 +6,8 @@
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
+ import { WORKFLOW_STATE_TO_STATUS, stateIndex } from "./workflow-states.js";
10
+ import { buildBatchResolveQuery, buildBatchFieldValueQuery, } from "../tools/batch-tools.js";
10
11
  // ---------------------------------------------------------------------------
11
12
  // Helper: Fetch project data for field cache population
12
13
  // ---------------------------------------------------------------------------
@@ -338,4 +339,108 @@ export async function syncStatusField(client, fieldCache, projectItemId, workflo
338
339
  // Best-effort sync - don't fail the primary operation
339
340
  }
340
341
  }
342
+ // ---------------------------------------------------------------------------
343
+ // Helper: Extract Workflow State from batch field value response
344
+ // ---------------------------------------------------------------------------
345
+ export function extractWorkflowState(item) {
346
+ const node = item?.fieldValues?.nodes?.find((fv) => fv.field?.name === "Workflow State" &&
347
+ fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
348
+ return node?.name ?? null;
349
+ }
350
+ // ---------------------------------------------------------------------------
351
+ // Helper: Auto-advance parent when all siblings reach a gate state
352
+ // ---------------------------------------------------------------------------
353
+ /**
354
+ * Auto-advance the parent issue if all siblings have reached the same gate state.
355
+ * Uses batch queries for constant-time API cost (4-6 calls max regardless of sibling count).
356
+ * Best-effort: returns null on any failure without throwing.
357
+ */
358
+ export async function autoAdvanceParent(client, fieldCache, owner, repo, issueNumber, gateState, projectNumber) {
359
+ try {
360
+ // Step A: Fetch parent number (1 query)
361
+ const parentResult = await client.query(`query($owner: String!, $repo: String!, $issueNum: Int!) {
362
+ repository(owner: $owner, name: $repo) {
363
+ issue(number: $issueNum) { parent { number } }
364
+ }
365
+ }`, { owner, repo, issueNum: issueNumber });
366
+ const parentNumber = parentResult.repository?.issue?.parent?.number;
367
+ if (!parentNumber)
368
+ return null;
369
+ // Step B: Fetch siblings (1 query)
370
+ const siblingResult = await client.query(`query($owner: String!, $repo: String!, $parentNum: Int!) {
371
+ repository(owner: $owner, name: $repo) {
372
+ issue(number: $parentNum) {
373
+ subIssues(first: 50) { nodes { number } }
374
+ }
375
+ }
376
+ }`, { owner, repo, parentNum: parentNumber });
377
+ const siblings = siblingResult.repository?.issue?.subIssues?.nodes || [];
378
+ if (siblings.length === 0)
379
+ return { advanced: false, parentNumber };
380
+ // Step C: Batch resolve project item IDs (1 query)
381
+ const allNumbers = [...siblings.map((s) => s.number), parentNumber];
382
+ const { queryString: resolveQueryString, variables: resolveVars } = buildBatchResolveQuery(owner, repo, allNumbers);
383
+ const resolveResult = await client.query(resolveQueryString, resolveVars);
384
+ // Parse resolved IDs and cache them
385
+ const projectId = fieldCache.getProjectId(projectNumber);
386
+ if (!projectId)
387
+ return null;
388
+ const projectItemIds = [];
389
+ for (let i = 0; i < allNumbers.length; i++) {
390
+ const entry = resolveResult[`i${i}`];
391
+ const issue = entry?.issue;
392
+ if (!issue) {
393
+ projectItemIds.push(null);
394
+ continue;
395
+ }
396
+ // Cache issue node ID
397
+ const issueCacheKey = `issue-node-id:${owner}/${repo}#${allNumbers[i]}`;
398
+ client.getCache().set(issueCacheKey, issue.id, 30 * 60 * 1000);
399
+ // Find matching project item
400
+ const projectItem = issue.projectItems?.nodes?.find((item) => item.project.id === projectId);
401
+ if (projectItem) {
402
+ const itemCacheKey = `project-item-id:${owner}/${repo}#${allNumbers[i]}`;
403
+ client.getCache().set(itemCacheKey, projectItem.id, 30 * 60 * 1000);
404
+ projectItemIds.push(projectItem.id);
405
+ }
406
+ else {
407
+ projectItemIds.push(null);
408
+ }
409
+ }
410
+ // All siblings + parent must have project item IDs
411
+ if (projectItemIds.some((id) => id === null)) {
412
+ return { advanced: false, parentNumber };
413
+ }
414
+ // Step D: Batch read field values (1 query)
415
+ const itemEntries = projectItemIds.map((id, i) => ({
416
+ alias: `fv${i}`,
417
+ itemId: id,
418
+ }));
419
+ const { queryString: fvQueryString, variables: fvVars } = buildBatchFieldValueQuery(itemEntries);
420
+ const fvResult = await client.query(fvQueryString, fvVars);
421
+ // Step E: Gate check (in-memory, zero cost)
422
+ const siblingCount = siblings.length;
423
+ const siblingStates = [];
424
+ for (let i = 0; i < siblingCount; i++) {
425
+ siblingStates.push(extractWorkflowState(fvResult[`fv${i}`]));
426
+ }
427
+ const allAtGate = siblingStates.every((state) => state === gateState);
428
+ if (!allAtGate)
429
+ return { advanced: false, parentNumber };
430
+ const parentIdx = allNumbers.length - 1;
431
+ const parentState = extractWorkflowState(fvResult[`fv${parentIdx}`]);
432
+ if (stateIndex(parentState || "") >= stateIndex(gateState)) {
433
+ return { advanced: false, parentNumber };
434
+ }
435
+ // Step F: Advance parent (1-2 mutations)
436
+ const parentItemId = projectItemIds[parentIdx];
437
+ await updateProjectItemField(client, fieldCache, parentItemId, "Workflow State", gateState, projectNumber);
438
+ await syncStatusField(client, fieldCache, parentItemId, gateState, projectNumber);
439
+ return { advanced: true, parentNumber, toState: gateState };
440
+ }
441
+ catch {
442
+ // Best-effort: don't fail the primary save_issue operation
443
+ return null;
444
+ }
445
+ }
341
446
  //# sourceMappingURL=helpers.js.map
@@ -44,6 +44,7 @@ export const HUMAN_STATES = [
44
44
  */
45
45
  export const PARENT_GATE_STATES = [
46
46
  "Ready for Plan",
47
+ "Plan in Review",
47
48
  "In Review",
48
49
  "Done",
49
50
  ];
@@ -91,6 +91,7 @@ export function toDashboardItems(raw, projectNumber, projectTitle) {
91
91
  priority: getFieldValue(r, "Priority"),
92
92
  estimate: getFieldValue(r, "Estimate"),
93
93
  assignees: r.content.assignees?.nodes?.map((a) => a.login) ?? [],
94
+ subIssueCount: r.content.subIssues?.totalCount ?? 0,
94
95
  blockedBy: [], // blockedBy requires separate queries; omit for now
95
96
  ...(projectNumber !== undefined ? { projectNumber } : {}),
96
97
  ...(projectTitle !== undefined ? { projectTitle } : {}),
@@ -121,6 +122,7 @@ export const DASHBOARD_ITEMS_QUERY = `query($projectId: ID!, $cursor: String, $f
121
122
  closedAt
122
123
  assignees(first: 5) { nodes { login } }
123
124
  repository { nameWithOwner name }
125
+ subIssues { totalCount }
124
126
  }
125
127
  ... on PullRequest {
126
128
  __typename
@@ -8,13 +8,13 @@ import { z } from "zod";
8
8
  import { paginateConnection } from "../lib/pagination.js";
9
9
  import { detectGroup } from "../lib/group-detection.js";
10
10
  import { detectPipelinePosition, OVERSIZED_ESTIMATES, } from "../lib/pipeline-detection.js";
11
- import { isValidState, VALID_STATES, LOCK_STATES, TERMINAL_STATES, WORKFLOW_STATE_TO_STATUS, } from "../lib/workflow-states.js";
11
+ import { isValidState, isParentGateState, VALID_STATES, LOCK_STATES, TERMINAL_STATES, WORKFLOW_STATE_TO_STATUS, } from "../lib/workflow-states.js";
12
12
  import { buildBatchMutationQuery } from "./batch-tools.js";
13
13
  import { resolveState } from "../lib/state-resolution.js";
14
14
  import { parseDateMath } from "../lib/date-math.js";
15
15
  import { expandProfile } from "../lib/filter-profiles.js";
16
16
  import { toolSuccess, toolError } from "../types.js";
17
- import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, syncStatusField, } from "../lib/helpers.js";
17
+ import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, syncStatusField, autoAdvanceParent, } from "../lib/helpers.js";
18
18
  // ---------------------------------------------------------------------------
19
19
  // Register issue tools
20
20
  // ---------------------------------------------------------------------------
@@ -997,6 +997,21 @@ export function registerIssueTools(server, client, fieldCache) {
997
997
  }
998
998
  }`, { projectId, itemId: projectItemId, fieldId });
999
999
  }
1000
+ // 4f. Auto-advance parent if we just moved to a gate state
1001
+ if (resolvedWorkflowState && isParentGateState(resolvedWorkflowState)) {
1002
+ try {
1003
+ const advanceResult = await autoAdvanceParent(client, fieldCache, owner, repo, args.number, resolvedWorkflowState, projectNumber);
1004
+ if (advanceResult?.advanced) {
1005
+ changes.parentAdvanced = {
1006
+ number: advanceResult.parentNumber,
1007
+ toState: advanceResult.toState,
1008
+ };
1009
+ }
1010
+ }
1011
+ catch {
1012
+ // Best-effort: don't fail the primary save_issue operation
1013
+ }
1014
+ }
1000
1015
  }
1001
1016
  return toolSuccess({
1002
1017
  number: args.number,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.4.105",
3
+ "version": "2.4.109",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",