ralph-hero-mcp-server 2.4.43 → 2.4.48

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.
@@ -219,6 +219,52 @@ export function detectHealthIssues(phases, config = DEFAULT_HEALTH_CONFIG) {
219
219
  return warnings;
220
220
  }
221
221
  // ---------------------------------------------------------------------------
222
+ // detectCrossProjectHealth
223
+ // ---------------------------------------------------------------------------
224
+ /**
225
+ * Detect health issues that span multiple projects.
226
+ *
227
+ * Currently detects:
228
+ * - unbalanced_workload: one project has >3x active items vs another
229
+ *
230
+ * "Active" = states in STATE_ORDER that are NOT terminal and NOT "Backlog".
231
+ */
232
+ export function detectCrossProjectHealth(breakdowns) {
233
+ const warnings = [];
234
+ const projectNumbers = Object.keys(breakdowns).map(Number);
235
+ // Count active items per project
236
+ const activeCounts = [];
237
+ for (const pn of projectNumbers) {
238
+ const { phases } = breakdowns[pn];
239
+ let active = 0;
240
+ for (const phase of phases) {
241
+ if (phase.state !== "Backlog" &&
242
+ !TERMINAL_STATES.includes(phase.state) &&
243
+ STATE_ORDER.includes(phase.state)) {
244
+ active += phase.count;
245
+ }
246
+ }
247
+ if (active > 0) {
248
+ activeCounts.push({ projectNumber: pn, count: active });
249
+ }
250
+ }
251
+ // Need at least 2 projects with active items to compare
252
+ if (activeCounts.length < 2) {
253
+ return warnings;
254
+ }
255
+ const maxEntry = activeCounts.reduce((a, b) => a.count > b.count ? a : b);
256
+ const minEntry = activeCounts.reduce((a, b) => a.count < b.count ? a : b);
257
+ if (maxEntry.count > 3 * minEntry.count) {
258
+ warnings.push({
259
+ type: "unbalanced_workload",
260
+ severity: "warning",
261
+ message: `Unbalanced workload: project ${maxEntry.projectNumber} has ${maxEntry.count} active items vs project ${minEntry.projectNumber} with ${minEntry.count}`,
262
+ issues: [],
263
+ });
264
+ }
265
+ return warnings;
266
+ }
267
+ // ---------------------------------------------------------------------------
222
268
  // computeArchiveStats
223
269
  // ---------------------------------------------------------------------------
224
270
  const ARCHIVE_TERMINAL_STATES = new Set(["Done", "Canceled"]);
@@ -275,6 +321,45 @@ export function buildDashboard(items, config = DEFAULT_HEALTH_CONFIG, now = Date
275
321
  const phases = aggregateByPhase(items, now, config);
276
322
  const warnings = detectHealthIssues(phases, config);
277
323
  const archive = computeArchiveStats(items, now, config.archiveThresholdDays, config.doneWindowDays);
324
+ // Per-project breakdown (only when items span multiple projects)
325
+ const projectGroups = new Map();
326
+ for (const item of items) {
327
+ if (item.projectNumber !== undefined) {
328
+ const group = projectGroups.get(item.projectNumber);
329
+ if (group) {
330
+ group.push(item);
331
+ }
332
+ else {
333
+ projectGroups.set(item.projectNumber, [item]);
334
+ }
335
+ }
336
+ }
337
+ let projectBreakdowns;
338
+ if (projectGroups.size >= 2) {
339
+ projectBreakdowns = {};
340
+ for (const [pn, projectItems] of projectGroups) {
341
+ const pPhases = aggregateByPhase(projectItems, now, config);
342
+ const pWarnings = detectHealthIssues(pPhases, config);
343
+ projectBreakdowns[pn] = {
344
+ projectTitle: projectItems[0].projectTitle ?? `Project ${pn}`,
345
+ phases: pPhases,
346
+ health: { ok: pWarnings.length === 0, warnings: pWarnings },
347
+ };
348
+ }
349
+ // Cross-project health detection
350
+ const crossWarnings = detectCrossProjectHealth(projectBreakdowns);
351
+ if (crossWarnings.length > 0) {
352
+ warnings.push(...crossWarnings);
353
+ // Re-sort by severity
354
+ const severityRank = {
355
+ critical: 0,
356
+ warning: 1,
357
+ info: 2,
358
+ };
359
+ warnings.sort((a, b) => (severityRank[a.severity] ?? 99) -
360
+ (severityRank[b.severity] ?? 99));
361
+ }
362
+ }
278
363
  return {
279
364
  generatedAt: new Date(now).toISOString(),
280
365
  totalIssues: items.length,
@@ -284,6 +369,7 @@ export function buildDashboard(items, config = DEFAULT_HEALTH_CONFIG, now = Date
284
369
  warnings,
285
370
  },
286
371
  archive,
372
+ ...(projectBreakdowns ? { projectBreakdowns } : {}),
287
373
  };
288
374
  }
289
375
  // ---------------------------------------------------------------------------
@@ -352,6 +438,45 @@ export function formatMarkdown(data, issuesPerPhase = 10) {
352
438
  }
353
439
  }
354
440
  }
441
+ // Per-project breakdown (only for multi-project)
442
+ if (data.projectBreakdowns &&
443
+ Object.keys(data.projectBreakdowns).length > 1) {
444
+ lines.push("");
445
+ lines.push("## Per-Project Breakdown");
446
+ const sortedProjects = Object.entries(data.projectBreakdowns)
447
+ .map(([pn, bd]) => ({ projectNumber: Number(pn), ...bd }))
448
+ .sort((a, b) => a.projectNumber - b.projectNumber);
449
+ for (const project of sortedProjects) {
450
+ lines.push("");
451
+ lines.push(`### ${project.projectTitle}`);
452
+ lines.push("");
453
+ const nonZeroPhases = project.phases.filter((p) => p.count > 0);
454
+ if (nonZeroPhases.length > 0) {
455
+ lines.push("| Phase | Count |");
456
+ lines.push("|-------|------:|");
457
+ for (const phase of nonZeroPhases) {
458
+ lines.push(`| ${phase.state} | ${phase.count} |`);
459
+ }
460
+ }
461
+ else {
462
+ lines.push("_No active items_");
463
+ }
464
+ lines.push("");
465
+ if (project.health.ok) {
466
+ lines.push("**Health**: All clear");
467
+ }
468
+ else {
469
+ for (const w of project.health.warnings) {
470
+ const icon = w.severity === "critical"
471
+ ? "[CRITICAL]"
472
+ : w.severity === "warning"
473
+ ? "[WARNING]"
474
+ : "[INFO]";
475
+ lines.push(`- ${icon} ${w.message}`);
476
+ }
477
+ }
478
+ }
479
+ }
355
480
  return lines.join("\n");
356
481
  }
357
482
  /**
@@ -393,6 +518,37 @@ export function formatAscii(data) {
393
518
  if (data.archive) {
394
519
  lines.push(`Archive: ${data.archive.eligibleForArchive} eligible (threshold: ${data.archive.archiveThresholdDays}d), ${data.archive.recentlyCompleted} recent`);
395
520
  }
521
+ // Per-project breakdown (only for multi-project)
522
+ if (data.projectBreakdowns &&
523
+ Object.keys(data.projectBreakdowns).length > 1) {
524
+ lines.push("");
525
+ lines.push("--- Per-Project ---");
526
+ const sortedProjects = Object.entries(data.projectBreakdowns)
527
+ .map(([pn, bd]) => ({ projectNumber: Number(pn), ...bd }))
528
+ .sort((a, b) => a.projectNumber - b.projectNumber);
529
+ for (const project of sortedProjects) {
530
+ lines.push("");
531
+ lines.push(project.projectTitle);
532
+ const nonZeroPhases = project.phases.filter((p) => p.count > 0);
533
+ if (nonZeroPhases.length > 0) {
534
+ const projMax = Math.max(1, ...nonZeroPhases.map((p) => p.count));
535
+ for (const phase of nonZeroPhases) {
536
+ const label = phase.state.padStart(20);
537
+ const barLen = Math.round((phase.count / projMax) * 20);
538
+ const bar = barLen > 0 ? "\u2588".repeat(barLen) : "\u2591";
539
+ lines.push(`${label} ${bar} ${phase.count}`);
540
+ }
541
+ }
542
+ if (project.health.ok) {
543
+ lines.push(" Health: OK");
544
+ }
545
+ else {
546
+ for (const w of project.health.warnings) {
547
+ lines.push(` ${w.severity.toUpperCase()}: ${w.message}`);
548
+ }
549
+ }
550
+ }
551
+ }
396
552
  return lines.join("\n");
397
553
  }
398
554
  //# sourceMappingURL=dashboard.js.map
@@ -86,8 +86,8 @@ export async function resolveIssueNodeId(client, owner, repo, number) {
86
86
  // ---------------------------------------------------------------------------
87
87
  // Helper: Resolve issue's project item ID (for field updates)
88
88
  // ---------------------------------------------------------------------------
89
- export async function resolveProjectItemId(client, fieldCache, owner, repo, issueNumber) {
90
- const projectId = fieldCache.getProjectId();
89
+ export async function resolveProjectItemId(client, fieldCache, owner, repo, issueNumber, projectNumber) {
90
+ const projectId = fieldCache.getProjectId(projectNumber);
91
91
  if (!projectId) {
92
92
  throw new Error("Field cache not populated - cannot resolve project item ID");
93
93
  }
@@ -121,18 +121,18 @@ export async function resolveProjectItemId(client, fieldCache, owner, repo, issu
121
121
  // ---------------------------------------------------------------------------
122
122
  // Helper: Update a single-select field value on a project item
123
123
  // ---------------------------------------------------------------------------
124
- export async function updateProjectItemField(client, fieldCache, projectItemId, fieldName, optionName) {
125
- const projectId = fieldCache.getProjectId();
124
+ export async function updateProjectItemField(client, fieldCache, projectItemId, fieldName, optionName, projectNumber) {
125
+ const projectId = fieldCache.getProjectId(projectNumber);
126
126
  if (!projectId) {
127
127
  throw new Error("Field cache not populated");
128
128
  }
129
- const fieldId = fieldCache.getFieldId(fieldName);
129
+ const fieldId = fieldCache.getFieldId(fieldName, projectNumber);
130
130
  if (!fieldId) {
131
131
  throw new Error(`Field "${fieldName}" not found in project`);
132
132
  }
133
- const optionId = fieldCache.resolveOptionId(fieldName, optionName);
133
+ const optionId = fieldCache.resolveOptionId(fieldName, optionName, projectNumber);
134
134
  if (!optionId) {
135
- const validOptions = fieldCache.getOptionNames(fieldName);
135
+ const validOptions = fieldCache.getOptionNames(fieldName, projectNumber);
136
136
  throw new Error(`Option "${optionName}" not found for field "${fieldName}". ` +
137
137
  `Valid options: ${validOptions.join(", ")}`);
138
138
  }
@@ -150,8 +150,8 @@ export async function updateProjectItemField(client, fieldCache, projectItemId,
150
150
  // ---------------------------------------------------------------------------
151
151
  // Helper: Get current field value for an issue's project item
152
152
  // ---------------------------------------------------------------------------
153
- export async function getCurrentFieldValue(client, fieldCache, owner, repo, issueNumber, fieldName) {
154
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, issueNumber);
153
+ export async function getCurrentFieldValue(client, fieldCache, owner, repo, issueNumber, fieldName, projectNumber) {
154
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, issueNumber, projectNumber);
155
155
  const result = await client.query(`query($itemId: ID!) {
156
156
  node(id: $itemId) {
157
157
  ... on ProjectV2Item {
@@ -286,18 +286,18 @@ export function resolveFullConfig(client, args) {
286
286
  * Sync the default Status field to match a Workflow State change.
287
287
  * Best-effort: logs warning on failure but does not throw.
288
288
  */
289
- export async function syncStatusField(client, fieldCache, projectItemId, workflowState) {
289
+ export async function syncStatusField(client, fieldCache, projectItemId, workflowState, projectNumber) {
290
290
  const targetStatus = WORKFLOW_STATE_TO_STATUS[workflowState];
291
291
  if (!targetStatus)
292
292
  return;
293
- const statusFieldId = fieldCache.getFieldId("Status");
293
+ const statusFieldId = fieldCache.getFieldId("Status", projectNumber);
294
294
  if (!statusFieldId)
295
295
  return;
296
- const statusOptionId = fieldCache.resolveOptionId("Status", targetStatus);
296
+ const statusOptionId = fieldCache.resolveOptionId("Status", targetStatus, projectNumber);
297
297
  if (!statusOptionId)
298
298
  return;
299
299
  try {
300
- await updateProjectItemField(client, fieldCache, projectItemId, "Status", targetStatus);
300
+ await updateProjectItemField(client, fieldCache, projectItemId, "Status", targetStatus, projectNumber);
301
301
  }
302
302
  catch {
303
303
  // Best-effort sync - don't fail the primary operation
@@ -75,7 +75,7 @@ export async function loadRoutingConfig(configPath) {
75
75
  *
76
76
  * Skips disabled rules (enabled === false).
77
77
  */
78
- export function validateRulesLive(config, fieldCache) {
78
+ export function validateRulesLive(config, fieldCache, projectNumber) {
79
79
  const errors = [];
80
80
  for (let i = 0; i < config.rules.length; i++) {
81
81
  const rule = config.rules[i];
@@ -84,9 +84,9 @@ export function validateRulesLive(config, fieldCache) {
84
84
  continue;
85
85
  // Validate workflowState references
86
86
  if (rule.action.workflowState) {
87
- const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState);
87
+ const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState, projectNumber);
88
88
  if (optionId === undefined) {
89
- const valid = fieldCache.getOptionNames("Workflow State");
89
+ const valid = fieldCache.getOptionNames("Workflow State", projectNumber);
90
90
  errors.push({
91
91
  phase: "live_validation",
92
92
  path: ["rules", String(i), "action", "workflowState"],
@@ -181,16 +181,16 @@ export function registerBatchTools(server, client, fieldCache) {
181
181
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
182
182
  // Ensure field cache is populated
183
183
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
184
- const projectId = fieldCache.getProjectId();
184
+ const projectId = fieldCache.getProjectId(projectNumber);
185
185
  if (!projectId) {
186
186
  return toolError("Could not resolve project ID");
187
187
  }
188
188
  // Validate option names up front (before any API calls)
189
189
  for (const op of args.operations) {
190
190
  const projectFieldName = FIELD_NAME_MAP[op.field];
191
- const optionId = fieldCache.resolveOptionId(projectFieldName, op.value);
191
+ const optionId = fieldCache.resolveOptionId(projectFieldName, op.value, projectNumber);
192
192
  if (!optionId) {
193
- const validOptions = fieldCache.getOptionNames(projectFieldName);
193
+ const validOptions = fieldCache.getOptionNames(projectFieldName, projectNumber);
194
194
  return toolError(`Invalid value "${op.value}" for field "${op.field}". ` +
195
195
  `Valid options: ${validOptions.join(", ")}`);
196
196
  }
@@ -280,8 +280,8 @@ export function registerBatchTools(server, client, fieldCache) {
280
280
  for (let opIdx = 0; opIdx < args.operations.length; opIdx++) {
281
281
  const op = args.operations[opIdx];
282
282
  const projectFieldName = FIELD_NAME_MAP[op.field];
283
- const fieldId = fieldCache.getFieldId(projectFieldName);
284
- const optionId = fieldCache.resolveOptionId(projectFieldName, op.value);
283
+ const fieldId = fieldCache.getFieldId(projectFieldName, projectNumber);
284
+ const optionId = fieldCache.resolveOptionId(projectFieldName, op.value, projectNumber);
285
285
  updates.push({
286
286
  alias: `u${num}_${opIdx}`,
287
287
  itemId: issue.projectItemId,
@@ -295,9 +295,9 @@ export function registerBatchTools(server, client, fieldCache) {
295
295
  if (op.field === "workflow_state") {
296
296
  const targetStatus = WORKFLOW_STATE_TO_STATUS[op.value];
297
297
  if (targetStatus) {
298
- const statusFieldId = fieldCache.getFieldId("Status");
298
+ const statusFieldId = fieldCache.getFieldId("Status", projectNumber);
299
299
  const statusOptionId = statusFieldId
300
- ? fieldCache.resolveOptionId("Status", targetStatus)
300
+ ? fieldCache.resolveOptionId("Status", targetStatus, projectNumber)
301
301
  : undefined;
302
302
  if (statusFieldId && statusOptionId) {
303
303
  updates.push({
@@ -61,7 +61,7 @@ export function registerHygieneTools(server, client, fieldCache) {
61
61
  }
62
62
  // Ensure field cache
63
63
  await ensureFieldCache(client, fieldCache, owner, projectNumber);
64
- const projectId = fieldCache.getProjectId();
64
+ const projectId = fieldCache.getProjectId(projectNumber);
65
65
  if (!projectId) {
66
66
  return toolError("Could not resolve project ID");
67
67
  }
@@ -122,7 +122,7 @@ export function registerIssueTools(server, client, fieldCache) {
122
122
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
123
123
  // Ensure field cache is populated
124
124
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
125
- const projectId = fieldCache.getProjectId();
125
+ const projectId = fieldCache.getProjectId(projectNumber);
126
126
  if (!projectId) {
127
127
  return toolError("Could not resolve project ID");
128
128
  }
@@ -575,7 +575,7 @@ export function registerIssueTools(server, client, fieldCache) {
575
575
  .getCache()
576
576
  .set(`issue-node-id:${owner}/${repo}#${issue.number}`, issue.id, 30 * 60 * 1000);
577
577
  // Step 4: Add to project
578
- const projectId = fieldCache.getProjectId();
578
+ const projectId = fieldCache.getProjectId(projectNumber);
579
579
  if (!projectId) {
580
580
  return toolError("Could not resolve project ID for adding issue to project");
581
581
  }
@@ -594,13 +594,13 @@ export function registerIssueTools(server, client, fieldCache) {
594
594
  .set(`project-item-id:${owner}/${repo}#${issue.number}`, projectItemId, 30 * 60 * 1000);
595
595
  // Step 5: Set field values
596
596
  if (args.workflowState) {
597
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.workflowState);
597
+ await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.workflowState, projectNumber);
598
598
  }
599
599
  if (args.estimate) {
600
- await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate);
600
+ await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate, projectNumber);
601
601
  }
602
602
  if (args.priority) {
603
- await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority);
603
+ await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority, projectNumber);
604
604
  }
605
605
  return toolSuccess({
606
606
  number: issue.number,
@@ -725,13 +725,13 @@ export function registerIssueTools(server, client, fieldCache) {
725
725
  // Ensure field cache is populated
726
726
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
727
727
  // Get current state for the response
728
- const previousState = await getCurrentFieldValue(client, fieldCache, owner, repo, args.number, "Workflow State");
728
+ const previousState = await getCurrentFieldValue(client, fieldCache, owner, repo, args.number, "Workflow State", projectNumber);
729
729
  // Resolve project item ID
730
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
730
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
731
731
  // Update the field with the resolved state
732
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", resolvedState);
732
+ await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", resolvedState, projectNumber);
733
733
  // Sync default Status field (best-effort, one-way)
734
- await syncStatusField(client, fieldCache, projectItemId, resolvedState);
734
+ await syncStatusField(client, fieldCache, projectItemId, resolvedState, projectNumber);
735
735
  const result = {
736
736
  number: args.number,
737
737
  previousState: previousState || "(unknown)",
@@ -768,8 +768,8 @@ export function registerIssueTools(server, client, fieldCache) {
768
768
  try {
769
769
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
770
770
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
771
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
772
- await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate);
771
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
772
+ await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate, projectNumber);
773
773
  return toolSuccess({
774
774
  number: args.number,
775
775
  estimate: args.estimate,
@@ -800,8 +800,8 @@ export function registerIssueTools(server, client, fieldCache) {
800
800
  try {
801
801
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
802
802
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
803
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
804
- await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority);
803
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
804
+ await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority, projectNumber);
805
805
  return toolSuccess({
806
806
  number: args.number,
807
807
  priority: args.priority,
@@ -1057,7 +1057,7 @@ export function registerIssueTools(server, client, fieldCache) {
1057
1057
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
1058
1058
  // Ensure field cache is populated
1059
1059
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1060
- const projectId = fieldCache.getProjectId();
1060
+ const projectId = fieldCache.getProjectId(projectNumber);
1061
1061
  if (!projectId) {
1062
1062
  return toolError("Could not resolve project ID");
1063
1063
  }
@@ -35,11 +35,11 @@ export function registerProjectManagementTools(server, client, fieldCache) {
35
35
  try {
36
36
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
37
37
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
38
- const projectId = fieldCache.getProjectId();
38
+ const projectId = fieldCache.getProjectId(projectNumber);
39
39
  if (!projectId) {
40
40
  return toolError("Could not resolve project ID");
41
41
  }
42
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
42
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
43
43
  if (args.unarchive) {
44
44
  await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
45
45
  unarchiveProjectV2Item(input: {
@@ -84,11 +84,11 @@ export function registerProjectManagementTools(server, client, fieldCache) {
84
84
  try {
85
85
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
86
86
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
87
- const projectId = fieldCache.getProjectId();
87
+ const projectId = fieldCache.getProjectId(projectNumber);
88
88
  if (!projectId) {
89
89
  return toolError("Could not resolve project ID");
90
90
  }
91
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
91
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
92
92
  await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
93
93
  deleteProjectV2Item(input: {
94
94
  projectId: $projectId,
@@ -122,7 +122,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
122
122
  try {
123
123
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
124
124
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
125
- const projectId = fieldCache.getProjectId();
125
+ const projectId = fieldCache.getProjectId(projectNumber);
126
126
  if (!projectId) {
127
127
  return toolError("Could not resolve project ID");
128
128
  }
@@ -164,7 +164,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
164
164
  try {
165
165
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
166
166
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
167
- const projectId = fieldCache.getProjectId();
167
+ const projectId = fieldCache.getProjectId(projectNumber);
168
168
  if (!projectId) {
169
169
  return toolError("Could not resolve project ID");
170
170
  }
@@ -232,17 +232,17 @@ export function registerProjectManagementTools(server, client, fieldCache) {
232
232
  try {
233
233
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
234
234
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
235
- const projectId = fieldCache.getProjectId();
235
+ const projectId = fieldCache.getProjectId(projectNumber);
236
236
  if (!projectId) {
237
237
  return toolError("Could not resolve project ID");
238
238
  }
239
- const fieldId = fieldCache.getFieldId(args.field);
239
+ const fieldId = fieldCache.getFieldId(args.field, projectNumber);
240
240
  if (!fieldId) {
241
- const validFields = fieldCache.getFieldNames();
241
+ const validFields = fieldCache.getFieldNames(projectNumber);
242
242
  return toolError(`Field "${args.field}" not found in project. ` +
243
243
  `Valid fields: ${validFields.join(", ")}`);
244
244
  }
245
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
245
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
246
246
  await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
247
247
  clearProjectV2ItemFieldValue(input: {
248
248
  projectId: $projectId,
@@ -280,7 +280,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
280
280
  try {
281
281
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
282
282
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
283
- const projectId = fieldCache.getProjectId();
283
+ const projectId = fieldCache.getProjectId(projectNumber);
284
284
  if (!projectId) {
285
285
  return toolError("Could not resolve project ID");
286
286
  }
@@ -296,15 +296,15 @@ export function registerProjectManagementTools(server, client, fieldCache) {
296
296
  const projectItemId = result.addProjectV2DraftIssue.projectItem.id;
297
297
  const fieldsSet = [];
298
298
  if (args.workflowState) {
299
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.workflowState);
299
+ await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.workflowState, projectNumber);
300
300
  fieldsSet.push("Workflow State");
301
301
  }
302
302
  if (args.priority) {
303
- await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority);
303
+ await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority, projectNumber);
304
304
  fieldsSet.push("Priority");
305
305
  }
306
306
  if (args.estimate) {
307
- await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate);
307
+ await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate, projectNumber);
308
308
  fieldsSet.push("Estimate");
309
309
  }
310
310
  return toolSuccess({
@@ -375,14 +375,14 @@ export function registerProjectManagementTools(server, client, fieldCache) {
375
375
  try {
376
376
  const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
377
377
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
378
- const projectId = fieldCache.getProjectId();
378
+ const projectId = fieldCache.getProjectId(projectNumber);
379
379
  if (!projectId) {
380
380
  return toolError("Could not resolve project ID");
381
381
  }
382
- const itemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
382
+ const itemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
383
383
  let afterId;
384
384
  if (args.afterNumber !== undefined) {
385
- afterId = await resolveProjectItemId(client, fieldCache, owner, repo, args.afterNumber);
385
+ afterId = await resolveProjectItemId(client, fieldCache, owner, repo, args.afterNumber, projectNumber);
386
386
  }
387
387
  await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!, $afterId: ID) {
388
388
  updateProjectV2ItemPosition(input: {
@@ -457,7 +457,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
457
457
  }
458
458
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
459
459
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
460
- const projectId = fieldCache.getProjectId();
460
+ const projectId = fieldCache.getProjectId(projectNumber);
461
461
  if (!projectId) {
462
462
  return toolError("Could not resolve project ID");
463
463
  }
@@ -499,13 +499,13 @@ export function registerProjectManagementTools(server, client, fieldCache) {
499
499
  }
500
500
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
501
501
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
502
- const projectId = fieldCache.getProjectId();
502
+ const projectId = fieldCache.getProjectId(projectNumber);
503
503
  if (!projectId) {
504
504
  return toolError("Could not resolve project ID");
505
505
  }
506
- const fieldId = fieldCache.getFieldId(args.field);
506
+ const fieldId = fieldCache.getFieldId(args.field, projectNumber);
507
507
  if (!fieldId) {
508
- const validFields = fieldCache.getFieldNames();
508
+ const validFields = fieldCache.getFieldNames(projectNumber);
509
509
  return toolError(`Field "${args.field}" not found in project. ` +
510
510
  `Valid fields: ${validFields.join(", ")}`);
511
511
  }
@@ -562,7 +562,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
562
562
  }
563
563
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
564
564
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
565
- const projectId = fieldCache.getProjectId();
565
+ const projectId = fieldCache.getProjectId(projectNumber);
566
566
  if (!projectId) {
567
567
  return toolError("Could not resolve project ID");
568
568
  }
@@ -639,7 +639,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
639
639
  try {
640
640
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
641
641
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
642
- const projectId = fieldCache.getProjectId();
642
+ const projectId = fieldCache.getProjectId(projectNumber);
643
643
  if (!projectId) {
644
644
  return toolError("Could not resolve project ID");
645
645
  }
@@ -813,7 +813,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
813
813
  try {
814
814
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
815
815
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
816
- const projectId = fieldCache.getProjectId();
816
+ const projectId = fieldCache.getProjectId(projectNumber);
817
817
  if (!projectId) {
818
818
  return toolError("Could not resolve project ID");
819
819
  }
@@ -949,7 +949,7 @@ export function registerProjectManagementTools(server, client, fieldCache) {
949
949
  try {
950
950
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
951
951
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
952
- const projectId = fieldCache.getProjectId();
952
+ const projectId = fieldCache.getProjectId(projectNumber);
953
953
  if (!projectId) {
954
954
  return toolError("Could not resolve project ID");
955
955
  }
@@ -577,7 +577,7 @@ export function registerProjectTools(server, client, fieldCache) {
577
577
  }
578
578
  // Ensure field cache is populated
579
579
  await ensureFieldCache(client, fieldCache, owner, projectNumber);
580
- const projectId = fieldCache.getProjectId();
580
+ const projectId = fieldCache.getProjectId(projectNumber);
581
581
  if (!projectId) {
582
582
  return toolError("Could not resolve project ID");
583
583
  }
@@ -452,7 +452,7 @@ export function registerRelationshipTools(server, client, fieldCache) {
452
452
  for (const issueNum of issueNumbers) {
453
453
  try {
454
454
  // Get current workflow state
455
- const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, issueNum, "Workflow State");
455
+ const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, issueNum, "Workflow State", projectNumber);
456
456
  if (!currentState) {
457
457
  skipped.push({
458
458
  number: issueNum,
@@ -473,10 +473,10 @@ export function registerRelationshipTools(server, client, fieldCache) {
473
473
  continue;
474
474
  }
475
475
  // Advance the issue
476
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, issueNum);
477
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.targetState);
476
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, issueNum, projectNumber);
477
+ await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.targetState, projectNumber);
478
478
  // Sync default Status field (best-effort, one-way)
479
- await syncStatusField(client, fieldCache, projectItemId, args.targetState);
479
+ await syncStatusField(client, fieldCache, projectItemId, args.targetState, projectNumber);
480
480
  advanced.push({
481
481
  number: issueNum,
482
482
  fromState: currentState,
@@ -573,7 +573,7 @@ export function registerRelationshipTools(server, client, fieldCache) {
573
573
  const childStates = [];
574
574
  let minStateIdx = Infinity;
575
575
  for (const sibling of siblings) {
576
- const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, sibling.number, "Workflow State");
576
+ const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, sibling.number, "Workflow State", projectNumber);
577
577
  const state = currentState || "unknown";
578
578
  childStates.push({
579
579
  number: sibling.number,
@@ -614,7 +614,7 @@ export function registerRelationshipTools(server, client, fieldCache) {
614
614
  });
615
615
  }
616
616
  // Get parent's current workflow state
617
- const parentState = await getCurrentFieldValue(client, fieldCache, owner, repo, parentNumber, "Workflow State");
617
+ const parentState = await getCurrentFieldValue(client, fieldCache, owner, repo, parentNumber, "Workflow State", projectNumber);
618
618
  // Check if parent is already at or past the target state
619
619
  const parentIdx = stateIndex(parentState || "");
620
620
  if (parentIdx >= minStateIdx) {
@@ -631,9 +631,9 @@ export function registerRelationshipTools(server, client, fieldCache) {
631
631
  });
632
632
  }
633
633
  // Advance the parent
634
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, parentNumber);
635
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", minState);
636
- await syncStatusField(client, fieldCache, projectItemId, minState);
634
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, parentNumber, projectNumber);
635
+ await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", minState, projectNumber);
636
+ await syncStatusField(client, fieldCache, projectItemId, minState, projectNumber);
637
637
  return toolSuccess({
638
638
  advanced: true,
639
639
  parent: {
@@ -99,9 +99,9 @@ export function registerRoutingTools(server, client, fieldCache) {
99
99
  for (let i = 0; i < config.rules.length; i++) {
100
100
  const rule = config.rules[i];
101
101
  if (rule.action.workflowState) {
102
- const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState);
102
+ const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState, projectNumber);
103
103
  if (optionId === undefined) {
104
- const valid = fieldCache.getOptionNames("Workflow State");
104
+ const valid = fieldCache.getOptionNames("Workflow State", projectNumber);
105
105
  errors.push({
106
106
  ruleIndex: i,
107
107
  field: "action.workflowState",
@@ -99,9 +99,9 @@ export function registerViewTools(server, client, fieldCache) {
99
99
  }
100
100
  // Ensure field cache is populated
101
101
  await ensureFieldCache(client, fieldCache, owner, projectNumber);
102
- const fieldId = fieldCache.getFieldId(args.fieldName);
102
+ const fieldId = fieldCache.getFieldId(args.fieldName, projectNumber);
103
103
  if (!fieldId) {
104
- return toolError(`Field "${args.fieldName}" not found. Available fields: ${fieldCache.getFieldNames().join(", ")}`);
104
+ return toolError(`Field "${args.fieldName}" not found. Available fields: ${fieldCache.getFieldNames(projectNumber).join(", ")}`);
105
105
  }
106
106
  const result = await client.projectMutate(`mutation($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
107
107
  updateProjectV2Field(input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.4.43",
3
+ "version": "2.4.48",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",