ralph-hero-mcp-server 2.4.89 → 2.4.91

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.
@@ -14,7 +14,7 @@ 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, getCurrentFieldValue, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, syncStatusField, } from "../lib/helpers.js";
17
+ import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, } from "../lib/helpers.js";
18
18
  // ---------------------------------------------------------------------------
19
19
  // Register issue tools
20
20
  // ---------------------------------------------------------------------------
@@ -338,6 +338,11 @@ export function registerIssueTools(server, client, fieldCache) {
338
338
  .optional()
339
339
  .default(true)
340
340
  .describe("Include group detection results (default: true). Set to false to skip group detection and save API calls when group context is not needed."),
341
+ includePipeline: z
342
+ .boolean()
343
+ .optional()
344
+ .default(false)
345
+ .describe("Include pipeline position: phase, convergence, member states, remaining phases. Auto-enables includeGroup."),
341
346
  }, async (args) => {
342
347
  try {
343
348
  const { owner, repo } = resolveConfig(client, args);
@@ -434,7 +439,9 @@ export function registerIssueTools(server, client, fieldCache) {
434
439
  }
435
440
  // Optionally detect group context
436
441
  let group = null;
437
- if (args.includeGroup !== false) {
442
+ // Force includeGroup when includePipeline is requested
443
+ const shouldIncludeGroup = args.includePipeline || args.includeGroup !== false;
444
+ if (shouldIncludeGroup) {
438
445
  try {
439
446
  const { owner: cfgOwner, repo: cfgRepo } = resolveConfig(client, args);
440
447
  const groupResult = await detectGroup(client, cfgOwner, cfgRepo, args.number);
@@ -458,6 +465,88 @@ export function registerIssueTools(server, client, fieldCache) {
458
465
  group = null;
459
466
  }
460
467
  }
468
+ // Optionally detect pipeline position
469
+ let pipeline = null;
470
+ if (args.includePipeline) {
471
+ try {
472
+ // Need resolveFullConfig for project field lookups
473
+ const { owner: cfgOwner, repo: cfgRepo } = resolveConfig(client, args);
474
+ const { projectNumber: pn, projectOwner: po } = resolveFullConfig(client, args);
475
+ await ensureFieldCache(client, fieldCache, po, pn);
476
+ // Force group if not already detected
477
+ if (!group) {
478
+ const groupResult = await detectGroup(client, cfgOwner, cfgRepo, args.number);
479
+ group = {
480
+ isGroup: groupResult.isGroup,
481
+ primary: {
482
+ number: groupResult.groupPrimary.number,
483
+ title: groupResult.groupPrimary.title,
484
+ },
485
+ members: groupResult.groupTickets.map((t) => ({
486
+ number: t.number,
487
+ title: t.title,
488
+ state: t.state,
489
+ order: t.order,
490
+ })),
491
+ totalTickets: groupResult.totalTickets,
492
+ };
493
+ }
494
+ // Build IssueState[] from group members
495
+ const issueStates = await Promise.all((group.members || []).map(async (member) => {
496
+ if (member.number === args.number) {
497
+ // Use already-fetched values for the seed issue
498
+ return {
499
+ number: member.number,
500
+ title: member.title,
501
+ workflowState: workflowState || "unknown",
502
+ estimate: estimate || null,
503
+ subIssueCount: 0,
504
+ };
505
+ }
506
+ // Fetch field values for non-seed members
507
+ const state = await getIssueFieldValues(client, fieldCache, cfgOwner, cfgRepo, member.number);
508
+ return {
509
+ number: member.number,
510
+ title: member.title,
511
+ workflowState: state.workflowState || "unknown",
512
+ estimate: state.estimate || null,
513
+ subIssueCount: 0,
514
+ };
515
+ }));
516
+ // Fetch sub-issue counts for M/L/XL estimates
517
+ const oversized = issueStates.filter((s) => s.estimate && OVERSIZED_ESTIMATES.has(s.estimate));
518
+ if (oversized.length > 0) {
519
+ await Promise.all(oversized.map(async (s) => {
520
+ try {
521
+ const subResult = await client.query(`query($owner: String!, $repo: String!, $issueNum: Int!) {
522
+ repository(owner: $owner, name: $repo) {
523
+ issue(number: $issueNum) { subIssuesSummary { total } }
524
+ }
525
+ }`, { owner: cfgOwner, repo: cfgRepo, issueNum: s.number });
526
+ if (subResult.repository?.issue?.subIssuesSummary) {
527
+ s.subIssueCount = subResult.repository.issue.subIssuesSummary.total;
528
+ }
529
+ }
530
+ catch {
531
+ // Best-effort, leave at 0
532
+ }
533
+ }));
534
+ }
535
+ // Run pipeline detection
536
+ const pipelineResult = detectPipelinePosition(issueStates, group.isGroup, group.primary?.number ?? null);
537
+ pipeline = {
538
+ phase: pipelineResult.phase,
539
+ reason: pipelineResult.reason,
540
+ remainingPhases: pipelineResult.remainingPhases,
541
+ convergence: pipelineResult.convergence,
542
+ memberStates: pipelineResult.issues,
543
+ suggestedRoster: pipelineResult.suggestedRoster,
544
+ };
545
+ }
546
+ catch {
547
+ pipeline = null; // Best-effort, same as includeGroup
548
+ }
549
+ }
461
550
  return toolSuccess({
462
551
  number: issue.number,
463
552
  id: issue.id,
@@ -504,6 +593,7 @@ export function registerIssueTools(server, client, fieldCache) {
504
593
  createdAt: c.createdAt,
505
594
  })),
506
595
  group,
596
+ ...(pipeline !== null ? { pipeline } : {}),
507
597
  });
508
598
  }
509
599
  catch (error) {
@@ -876,198 +966,6 @@ export function registerIssueTools(server, client, fieldCache) {
876
966
  }
877
967
  });
878
968
  // -------------------------------------------------------------------------
879
- // ralph_hero__update_issue
880
- // -------------------------------------------------------------------------
881
- server.tool("ralph_hero__update_issue", "Update a GitHub issue's basic properties (title, body, labels, assignees). Returns: number, title, url. Use update_workflow_state for state changes, update_estimate for estimates, update_priority for priorities.", {
882
- owner: z
883
- .string()
884
- .optional()
885
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
886
- repo: z
887
- .string()
888
- .optional()
889
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
890
- number: z.coerce.number().describe("Issue number"),
891
- title: z.string().optional().describe("New issue title"),
892
- body: z.string().optional().describe("New issue body (Markdown)"),
893
- labels: z
894
- .array(z.string())
895
- .optional()
896
- .describe("Label names (replaces existing labels)"),
897
- assignees: z
898
- .array(z.string())
899
- .optional()
900
- .describe("GitHub usernames to assign (replaces existing)"),
901
- }, async (args) => {
902
- try {
903
- const { owner, repo } = resolveConfig(client, args);
904
- const issueId = await resolveIssueNodeId(client, owner, repo, args.number);
905
- // Resolve label IDs if provided
906
- let labelIds;
907
- if (args.labels) {
908
- const labelResult = await client.query(`query($owner: String!, $repo: String!) {
909
- repository(owner: $owner, name: $repo) {
910
- labels(first: 100) {
911
- nodes { id name }
912
- }
913
- }
914
- }`, { owner, repo }, { cache: true, cacheTtlMs: 5 * 60 * 1000 });
915
- const allLabels = labelResult.repository.labels.nodes;
916
- labelIds = args.labels
917
- .map((name) => allLabels.find((l) => l.name === name)?.id)
918
- .filter((id) => id !== undefined);
919
- }
920
- const result = await client.mutate(`mutation($issueId: ID!, $title: String, $body: String, $labelIds: [ID!], $assigneeIds: [ID!]) {
921
- updateIssue(input: {
922
- id: $issueId,
923
- title: $title,
924
- body: $body,
925
- labelIds: $labelIds,
926
- assigneeIds: $assigneeIds
927
- }) {
928
- issue {
929
- number
930
- title
931
- url
932
- }
933
- }
934
- }`, {
935
- issueId,
936
- title: args.title || null,
937
- body: args.body || null,
938
- labelIds: labelIds || null,
939
- assigneeIds: null, // Would need username -> ID resolution
940
- });
941
- return toolSuccess({
942
- number: result.updateIssue.issue.number,
943
- title: result.updateIssue.issue.title,
944
- url: result.updateIssue.issue.url,
945
- });
946
- }
947
- catch (error) {
948
- const message = error instanceof Error ? error.message : String(error);
949
- return toolError(`Failed to update issue: ${message}`);
950
- }
951
- });
952
- // -------------------------------------------------------------------------
953
- // ralph_hero__update_workflow_state
954
- // -------------------------------------------------------------------------
955
- server.tool("ralph_hero__update_workflow_state", "Change an issue's Workflow State using semantic intents or direct state names. Returns: number, previousState, newState, command. Semantic intents: __LOCK__ (lock for processing), __COMPLETE__ (mark done), __ESCALATE__ (needs human), __CLOSE__, __CANCEL__. Recovery: if state transition fails, verify the issue is in the project and the state name is valid.", {
956
- owner: z
957
- .string()
958
- .optional()
959
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
960
- repo: z
961
- .string()
962
- .optional()
963
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
964
- projectNumber: z.coerce.number().optional()
965
- .describe("Project number override (defaults to configured project)"),
966
- number: z.coerce.number().describe("Issue number"),
967
- state: z
968
- .string()
969
- .describe("Target state: semantic intent (__LOCK__, __COMPLETE__, __ESCALATE__, __CLOSE__, __CANCEL__) " +
970
- "or direct state name (e.g., 'Research Needed', 'In Progress')"),
971
- command: z
972
- .string()
973
- .describe("Ralph command making this transition (e.g., 'ralph_research', 'ralph_plan'). " +
974
- "Required for validation and semantic intent resolution."),
975
- }, async (args) => {
976
- try {
977
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
978
- // Resolve semantic intent or validate direct state
979
- const { resolvedState, wasIntent, originalState } = resolveState(args.state, args.command);
980
- // Ensure field cache is populated
981
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
982
- // Get current state for the response
983
- const previousState = await getCurrentFieldValue(client, fieldCache, owner, repo, args.number, "Workflow State", projectNumber);
984
- // Resolve project item ID
985
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
986
- // Update the field with the resolved state
987
- await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", resolvedState, projectNumber);
988
- // Sync default Status field (best-effort, one-way)
989
- await syncStatusField(client, fieldCache, projectItemId, resolvedState, projectNumber);
990
- const result = {
991
- number: args.number,
992
- previousState: previousState || "(unknown)",
993
- newState: resolvedState,
994
- command: args.command,
995
- };
996
- if (wasIntent) {
997
- result.resolvedFrom = originalState;
998
- }
999
- return toolSuccess(result);
1000
- }
1001
- catch (error) {
1002
- const message = error instanceof Error ? error.message : String(error);
1003
- return toolError(`Failed to update workflow state: ${message}`);
1004
- }
1005
- });
1006
- // -------------------------------------------------------------------------
1007
- // ralph_hero__update_estimate
1008
- // -------------------------------------------------------------------------
1009
- server.tool("ralph_hero__update_estimate", "Change an issue's Estimate in the project. Returns: number, estimate. Valid values: XS, S, M, L, XL. Recovery: if the issue is not in the project, add it first via create_issue.", {
1010
- owner: z
1011
- .string()
1012
- .optional()
1013
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
1014
- repo: z
1015
- .string()
1016
- .optional()
1017
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
1018
- projectNumber: z.coerce.number().optional()
1019
- .describe("Project number override (defaults to configured project)"),
1020
- number: z.coerce.number().describe("Issue number"),
1021
- estimate: z.string().describe("Estimate value (XS, S, M, L, XL)"),
1022
- }, async (args) => {
1023
- try {
1024
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
1025
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1026
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
1027
- await updateProjectItemField(client, fieldCache, projectItemId, "Estimate", args.estimate, projectNumber);
1028
- return toolSuccess({
1029
- number: args.number,
1030
- estimate: args.estimate,
1031
- });
1032
- }
1033
- catch (error) {
1034
- const message = error instanceof Error ? error.message : String(error);
1035
- return toolError(`Failed to update estimate: ${message}`);
1036
- }
1037
- });
1038
- // -------------------------------------------------------------------------
1039
- // ralph_hero__update_priority
1040
- // -------------------------------------------------------------------------
1041
- server.tool("ralph_hero__update_priority", "Change an issue's Priority in the project. Returns: number, priority. Valid values: P0, P1, P2, P3. Recovery: if the issue is not in the project, add it first via create_issue.", {
1042
- owner: z
1043
- .string()
1044
- .optional()
1045
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
1046
- repo: z
1047
- .string()
1048
- .optional()
1049
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
1050
- projectNumber: z.coerce.number().optional()
1051
- .describe("Project number override (defaults to configured project)"),
1052
- number: z.coerce.number().describe("Issue number"),
1053
- priority: z.string().describe("Priority value (P0, P1, P2, P3)"),
1054
- }, async (args) => {
1055
- try {
1056
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
1057
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1058
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
1059
- await updateProjectItemField(client, fieldCache, projectItemId, "Priority", args.priority, projectNumber);
1060
- return toolSuccess({
1061
- number: args.number,
1062
- priority: args.priority,
1063
- });
1064
- }
1065
- catch (error) {
1066
- const message = error instanceof Error ? error.message : String(error);
1067
- return toolError(`Failed to update priority: ${message}`);
1068
- }
1069
- });
1070
- // -------------------------------------------------------------------------
1071
969
  // ralph_hero__create_comment
1072
970
  // -------------------------------------------------------------------------
1073
971
  server.tool("ralph_hero__create_comment", "Add a comment to a GitHub issue. Returns: commentId, issueNumber. Recovery: if issue not found, verify the issue number exists in the repository.", {
@@ -1106,170 +1004,6 @@ export function registerIssueTools(server, client, fieldCache) {
1106
1004
  }
1107
1005
  });
1108
1006
  // -------------------------------------------------------------------------
1109
- // ralph_hero__detect_pipeline_position
1110
- // -------------------------------------------------------------------------
1111
- server.tool("ralph_hero__detect_pipeline_position", "Determine which workflow phase to execute next for an issue or its group. Returns: phase (SPLIT/TRIAGE/RESEARCH/PLAN/REVIEW/IMPLEMENT/COMPLETE/HUMAN_GATE/TERMINAL), convergence status with recommendation (proceed/wait/escalate), all group member states, and remaining phases. Call this INSTEAD of separate detect_group + check_convergence calls. Recovery: if issue not found, verify the issue number and that it has been added to the project.", {
1112
- owner: z
1113
- .string()
1114
- .optional()
1115
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
1116
- repo: z
1117
- .string()
1118
- .optional()
1119
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
1120
- projectNumber: z.coerce.number().optional()
1121
- .describe("Project number override (defaults to configured project)"),
1122
- number: z.coerce.number().describe("Issue number (seed for group detection)"),
1123
- }, async (args) => {
1124
- try {
1125
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
1126
- // Ensure field cache is populated
1127
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1128
- // Detect group from seed issue
1129
- const group = await detectGroup(client, owner, repo, args.number);
1130
- // Fetch workflow state and estimate for each group member
1131
- const issueStates = await Promise.all(group.groupTickets.map(async (ticket) => {
1132
- const state = await getIssueFieldValues(client, fieldCache, owner, repo, ticket.number);
1133
- return {
1134
- number: ticket.number,
1135
- title: ticket.title,
1136
- workflowState: state.workflowState || "unknown",
1137
- estimate: state.estimate || null,
1138
- subIssueCount: 0,
1139
- };
1140
- }));
1141
- // Fetch sub-issue counts for oversized issues (targeted query, not all issues)
1142
- const oversizedNumbers = issueStates
1143
- .filter((i) => i.estimate !== null && OVERSIZED_ESTIMATES.has(i.estimate))
1144
- .map((i) => i.number);
1145
- if (oversizedNumbers.length > 0) {
1146
- await Promise.all(oversizedNumbers.map(async (num) => {
1147
- const subResult = await client.query(`query($owner: String!, $repo: String!, $issueNum: Int!) {
1148
- repository(owner: $owner, name: $repo) {
1149
- issue(number: $issueNum) {
1150
- subIssuesSummary { total }
1151
- }
1152
- }
1153
- }`, { owner, repo, issueNum: num });
1154
- const issueState = issueStates.find((i) => i.number === num);
1155
- if (issueState && subResult.repository?.issue?.subIssuesSummary) {
1156
- issueState.subIssueCount = subResult.repository.issue.subIssuesSummary.total;
1157
- }
1158
- }));
1159
- }
1160
- // Detect pipeline position
1161
- const position = detectPipelinePosition(issueStates, group.isGroup, group.groupPrimary.number);
1162
- return toolSuccess(position);
1163
- }
1164
- catch (error) {
1165
- const message = error instanceof Error ? error.message : String(error);
1166
- // Check if it's a "not found" error
1167
- if (message.includes("not found")) {
1168
- return toolError(`Issue #${args.number} not found in project. ` +
1169
- `Recovery: verify the issue number is correct and the issue has been added to the project ` +
1170
- `via ralph_hero__create_issue or ralph_hero__get_issue.`);
1171
- }
1172
- return toolError(`Failed to detect pipeline position: ${message}`);
1173
- }
1174
- });
1175
- // -------------------------------------------------------------------------
1176
- // ralph_hero__check_convergence
1177
- // -------------------------------------------------------------------------
1178
- server.tool("ralph_hero__check_convergence", "Check if all issues in a group have reached the required state for the next phase. Returns: converged, targetState, total, ready, blocking (with distanceToTarget), recommendation (proceed/wait/escalate). Note: detect_pipeline_position already includes convergence data; use this only when checking convergence against a specific target state not covered by pipeline detection.", {
1179
- owner: z
1180
- .string()
1181
- .optional()
1182
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
1183
- repo: z
1184
- .string()
1185
- .optional()
1186
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
1187
- projectNumber: z.coerce.number().optional()
1188
- .describe("Project number override (defaults to configured project)"),
1189
- number: z.coerce.number().describe("Issue number (any issue in the group)"),
1190
- targetState: z
1191
- .string()
1192
- .describe("The state all issues must be in (e.g., 'Ready for Plan')"),
1193
- }, async (args) => {
1194
- try {
1195
- // Validate target state
1196
- if (!isValidState(args.targetState)) {
1197
- return toolError(`Unknown target state '${args.targetState}'. ` +
1198
- `Valid states: ${VALID_STATES.join(", ")}. ` +
1199
- `Recovery: retry with a valid state name.`);
1200
- }
1201
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
1202
- // Ensure field cache is populated
1203
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1204
- // Detect group from seed issue
1205
- const group = await detectGroup(client, owner, repo, args.number);
1206
- // Single issue trivially converges
1207
- if (!group.isGroup) {
1208
- const state = await getIssueFieldValues(client, fieldCache, owner, repo, args.number);
1209
- const atTarget = state.workflowState === args.targetState;
1210
- return toolSuccess({
1211
- converged: atTarget,
1212
- targetState: args.targetState,
1213
- total: 1,
1214
- ready: atTarget ? 1 : 0,
1215
- blocking: atTarget
1216
- ? []
1217
- : [
1218
- {
1219
- number: args.number,
1220
- title: group.groupPrimary.title,
1221
- currentState: state.workflowState || "unknown",
1222
- distanceToTarget: computeDistance(state.workflowState || "unknown", args.targetState),
1223
- },
1224
- ],
1225
- recommendation: atTarget ? "proceed" : "wait",
1226
- });
1227
- }
1228
- // Check each group member
1229
- const blocking = [];
1230
- let readyCount = 0;
1231
- for (const ticket of group.groupTickets) {
1232
- const state = await getIssueFieldValues(client, fieldCache, owner, repo, ticket.number);
1233
- const currentState = state.workflowState || "unknown";
1234
- if (currentState === args.targetState) {
1235
- readyCount++;
1236
- }
1237
- else {
1238
- blocking.push({
1239
- number: ticket.number,
1240
- title: ticket.title,
1241
- currentState,
1242
- distanceToTarget: computeDistance(currentState, args.targetState),
1243
- });
1244
- }
1245
- }
1246
- const converged = blocking.length === 0;
1247
- const hasHumanNeeded = blocking.some((b) => b.currentState === "Human Needed");
1248
- let recommendation;
1249
- if (converged) {
1250
- recommendation = "proceed";
1251
- }
1252
- else if (hasHumanNeeded) {
1253
- recommendation = "escalate";
1254
- }
1255
- else {
1256
- recommendation = "wait";
1257
- }
1258
- return toolSuccess({
1259
- converged,
1260
- targetState: args.targetState,
1261
- total: group.totalTickets,
1262
- ready: readyCount,
1263
- blocking,
1264
- recommendation,
1265
- });
1266
- }
1267
- catch (error) {
1268
- const message = error instanceof Error ? error.message : String(error);
1269
- return toolError(`Failed to check convergence: ${message}`);
1270
- }
1271
- });
1272
- // -------------------------------------------------------------------------
1273
1007
  // ralph_hero__pick_actionable_issue
1274
1008
  // -------------------------------------------------------------------------
1275
1009
  server.tool("ralph_hero__pick_actionable_issue", "Find the highest-priority issue matching a workflow state that is not blocked or locked. Returns: found, issue (with number, title, workflowState, estimate, priority, group context), alternatives count. Used by dispatch loop to find work for idle teammates. Recovery: if no issues found, try a different workflowState or increase maxEstimate.", {
@@ -1523,15 +1257,4 @@ async function getIssueFieldValues(client, fieldCache, owner, repo, issueNumber)
1523
1257
  }
1524
1258
  return { workflowState, estimate, priority };
1525
1259
  }
1526
- // ---------------------------------------------------------------------------
1527
- // Helper: Compute "distance" between two states in the pipeline
1528
- // ---------------------------------------------------------------------------
1529
- import { stateIndex } from "../lib/workflow-states.js";
1530
- function computeDistance(currentState, targetState) {
1531
- const currentIdx = stateIndex(currentState);
1532
- const targetIdx = stateIndex(targetState);
1533
- if (currentIdx === -1 || targetIdx === -1)
1534
- return -1;
1535
- return targetIdx - currentIdx;
1536
- }
1537
1260
  //# sourceMappingURL=issue-tools.js.map
@@ -20,67 +20,6 @@ export const PROTECTED_FIELDS = ["Workflow State", "Priority", "Estimate", "Stat
20
20
  // Register project management tools
21
21
  // ---------------------------------------------------------------------------
22
22
  export function registerProjectManagementTools(server, client, fieldCache) {
23
- // -------------------------------------------------------------------------
24
- // ralph_hero__archive_item
25
- // -------------------------------------------------------------------------
26
- server.tool("ralph_hero__archive_item", "Archive or unarchive a project item. Archived items are hidden from default views but not deleted. Accepts either number (for issues) or projectItemId (for draft items). Returns: number, archived, projectItemId.", {
27
- owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
28
- repo: z.string().optional().describe("Repository name. Defaults to env var"),
29
- projectNumber: z.coerce.number().optional()
30
- .describe("Project number override (defaults to configured project)"),
31
- number: z.coerce.number().optional().describe("Issue number (provide this or projectItemId)"),
32
- projectItemId: z.string().optional()
33
- .describe("Project item node ID (PVTI_...) — use instead of number for draft items"),
34
- unarchive: z.boolean().optional().default(false)
35
- .describe("If true, unarchive instead of archive (default: false)"),
36
- }, async (args) => {
37
- try {
38
- if (!args.number && !args.projectItemId) {
39
- return toolError("Either number or projectItemId must be provided");
40
- }
41
- if (args.number && args.projectItemId) {
42
- return toolError("Provide either number or projectItemId, not both");
43
- }
44
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
45
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
46
- const projectId = fieldCache.getProjectId(projectNumber);
47
- if (!projectId) {
48
- return toolError("Could not resolve project ID");
49
- }
50
- const itemId = args.projectItemId
51
- ? args.projectItemId
52
- : await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
53
- if (args.unarchive) {
54
- await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
55
- unarchiveProjectV2Item(input: {
56
- projectId: $projectId,
57
- itemId: $itemId
58
- }) {
59
- item { id }
60
- }
61
- }`, { projectId, itemId });
62
- }
63
- else {
64
- await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
65
- archiveProjectV2Item(input: {
66
- projectId: $projectId,
67
- itemId: $itemId
68
- }) {
69
- item { id }
70
- }
71
- }`, { projectId, itemId });
72
- }
73
- return toolSuccess({
74
- number: args.number ?? null,
75
- archived: !args.unarchive,
76
- projectItemId: itemId,
77
- });
78
- }
79
- catch (error) {
80
- const message = error instanceof Error ? error.message : String(error);
81
- return toolError(`Failed to ${args.unarchive ? "unarchive" : "archive"} item: ${message}`);
82
- }
83
- });
84
23
  // -------------------------------------------------------------------------
85
24
  // ralph_hero__remove_from_project
86
25
  // -------------------------------------------------------------------------
@@ -242,51 +181,6 @@ export function registerProjectManagementTools(server, client, fieldCache) {
242
181
  }
243
182
  });
244
183
  // -------------------------------------------------------------------------
245
- // ralph_hero__clear_field
246
- // -------------------------------------------------------------------------
247
- 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.", {
248
- owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
249
- repo: z.string().optional().describe("Repository name. Defaults to env var"),
250
- projectNumber: z.coerce.number().optional()
251
- .describe("Project number override (defaults to configured project)"),
252
- number: z.coerce.number().describe("Issue number"),
253
- field: z.string().describe("Field name to clear (e.g., 'Estimate', 'Priority', 'Workflow State')"),
254
- }, async (args) => {
255
- try {
256
- const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
257
- await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
258
- const projectId = fieldCache.getProjectId(projectNumber);
259
- if (!projectId) {
260
- return toolError("Could not resolve project ID");
261
- }
262
- const fieldId = fieldCache.getFieldId(args.field, projectNumber);
263
- if (!fieldId) {
264
- const validFields = fieldCache.getFieldNames(projectNumber);
265
- return toolError(`Field "${args.field}" not found in project. ` +
266
- `Valid fields: ${validFields.join(", ")}`);
267
- }
268
- const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
269
- await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
270
- clearProjectV2ItemFieldValue(input: {
271
- projectId: $projectId,
272
- itemId: $itemId,
273
- fieldId: $fieldId
274
- }) {
275
- projectV2Item { id }
276
- }
277
- }`, { projectId, itemId: projectItemId, fieldId });
278
- return toolSuccess({
279
- number: args.number,
280
- field: args.field,
281
- cleared: true,
282
- });
283
- }
284
- catch (error) {
285
- const message = error instanceof Error ? error.message : String(error);
286
- return toolError(`Failed to clear field: ${message}`);
287
- }
288
- });
289
- // -------------------------------------------------------------------------
290
184
  // ralph_hero__create_draft_issue
291
185
  // -------------------------------------------------------------------------
292
186
  server.tool("ralph_hero__create_draft_issue", "Create a draft issue in the project (no repo required). Optionally set workflow state, priority, and estimate after creation. Returns: projectItemId, title, fieldsSet.", {
@@ -1021,33 +915,92 @@ export function registerProjectManagementTools(server, client, fieldCache) {
1021
915
  }
1022
916
  });
1023
917
  // -------------------------------------------------------------------------
1024
- // ralph_hero__bulk_archive
918
+ // ralph_hero__archive_items
1025
919
  // -------------------------------------------------------------------------
1026
- server.tool("ralph_hero__bulk_archive", "Archive multiple project items matching workflow state filter. Uses aliased GraphQL mutations for efficiency (chunked at 50). Archived items are hidden from views but not deleted. Returns: archivedCount, items, errors.", {
920
+ server.tool("ralph_hero__archive_items", "Archive or unarchive project items. Single-item mode: provide number or projectItemId (supports unarchive). Bulk mode: provide workflowStates filter to archive multiple items matching those states. Uses aliased GraphQL mutations for efficiency (chunked at 50). Archived items are hidden from views but not deleted.", {
1027
921
  owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
1028
922
  repo: z.string().optional().describe("Repository name. Defaults to env var"),
1029
923
  projectNumber: z.coerce.number().optional()
1030
924
  .describe("Project number override (defaults to configured project)"),
925
+ number: z.coerce.number().optional()
926
+ .describe("Archive a single issue by number. Mutually exclusive with workflowStates filter."),
927
+ projectItemId: z.string().optional()
928
+ .describe("Archive by project item ID (for draft issues). Mutually exclusive with number and workflowStates."),
929
+ unarchive: z.boolean().optional().default(false)
930
+ .describe("Unarchive instead of archive. Only works with number or projectItemId (single-item mode)."),
1031
931
  workflowStates: z
1032
932
  .array(z.string())
1033
- .min(1)
1034
- .describe('Workflow states to archive (e.g., ["Done", "Canceled"])'),
933
+ .optional()
934
+ .describe('Workflow states to archive (e.g., ["Done", "Canceled"]). Required unless number or projectItemId is provided.'),
1035
935
  maxItems: z
1036
936
  .number()
1037
937
  .optional()
1038
938
  .default(50)
1039
- .describe("Max items to archive per invocation (default 50, cap 200)"),
939
+ .describe("Max items to archive per invocation (default 50, cap 200). Bulk mode only."),
1040
940
  dryRun: z
1041
941
  .boolean()
1042
942
  .optional()
1043
943
  .default(false)
1044
- .describe("If true, return matching items without archiving them (default: false)"),
944
+ .describe("If true, return matching items without archiving them (default: false). Bulk mode only."),
1045
945
  updatedBefore: z
1046
946
  .string()
1047
947
  .optional()
1048
- .describe("ISO 8601 date (UTC). Only archive items with updatedAt before this date. Composable with workflowStates (AND logic)."),
948
+ .describe("ISO 8601 date (UTC). Only archive items with updatedAt before this date. Composable with workflowStates (AND logic). Bulk mode only."),
1049
949
  }, async (args) => {
1050
950
  try {
951
+ // Determine mode
952
+ const isSingleItem = args.number !== undefined || args.projectItemId !== undefined;
953
+ const isBulk = args.workflowStates && args.workflowStates.length > 0;
954
+ if (!isSingleItem && !isBulk) {
955
+ return toolError("Provide either 'number'/'projectItemId' (single item) or 'workflowStates' (bulk filter).");
956
+ }
957
+ if (isSingleItem && isBulk) {
958
+ return toolError("Cannot combine number/projectItemId with workflowStates. Use one mode.");
959
+ }
960
+ if (args.unarchive && isBulk) {
961
+ return toolError("Unarchive is only supported for single items (number or projectItemId).");
962
+ }
963
+ // Single-item mode
964
+ if (isSingleItem) {
965
+ if (args.number && args.projectItemId) {
966
+ return toolError("Provide either number or projectItemId, not both");
967
+ }
968
+ const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
969
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
970
+ const projectId = fieldCache.getProjectId(projectNumber);
971
+ if (!projectId) {
972
+ return toolError("Could not resolve project ID");
973
+ }
974
+ const itemId = args.projectItemId
975
+ ? args.projectItemId
976
+ : await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
977
+ if (args.unarchive) {
978
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
979
+ unarchiveProjectV2Item(input: {
980
+ projectId: $projectId,
981
+ itemId: $itemId
982
+ }) {
983
+ item { id }
984
+ }
985
+ }`, { projectId, itemId });
986
+ }
987
+ else {
988
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
989
+ archiveProjectV2Item(input: {
990
+ projectId: $projectId,
991
+ itemId: $itemId
992
+ }) {
993
+ item { id }
994
+ }
995
+ }`, { projectId, itemId });
996
+ }
997
+ return toolSuccess({
998
+ number: args.number ?? null,
999
+ archived: !args.unarchive,
1000
+ projectItemId: itemId,
1001
+ });
1002
+ }
1003
+ // Bulk mode (workflowStates filter)
1051
1004
  const { projectNumber, projectOwner } = resolveFullConfig(client, args);
1052
1005
  await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
1053
1006
  const projectId = fieldCache.getProjectId(projectNumber);
@@ -6,8 +6,6 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  import { paginateConnection } from "../lib/pagination.js";
9
- import { parseDateMath } from "../lib/date-math.js";
10
- import { expandProfile } from "../lib/filter-profiles.js";
11
9
  import { toolSuccess, toolError } from "../types.js";
12
10
  import { resolveProjectOwner } from "../types.js";
13
11
  import { queryProjectRepositories } from "../lib/helpers.js";
@@ -486,257 +484,6 @@ export function registerProjectTools(server, client, fieldCache) {
486
484
  }
487
485
  });
488
486
  // -------------------------------------------------------------------------
489
- // ralph_hero__list_project_items
490
- // -------------------------------------------------------------------------
491
- server.tool("ralph_hero__list_project_items", "List items in a GitHub Project V2, optionally filtered by Workflow State, Estimate, or Priority", {
492
- owner: z
493
- .string()
494
- .optional()
495
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
496
- number: z
497
- .number()
498
- .optional()
499
- .describe("Project number. Defaults to RALPH_GH_PROJECT_NUMBER env var"),
500
- profile: z
501
- .string()
502
- .optional()
503
- .describe("Named filter profile (e.g., 'analyst-triage', 'builder-active'). " +
504
- "Profile filters are defaults; explicit params override them."),
505
- workflowState: z
506
- .string()
507
- .optional()
508
- .describe("Filter by Workflow State name"),
509
- estimate: z
510
- .string()
511
- .optional()
512
- .describe("Filter by Estimate name (XS, S, M, L, XL)"),
513
- priority: z
514
- .string()
515
- .optional()
516
- .describe("Filter by Priority name (P0, P1, P2, P3)"),
517
- has: z
518
- .array(z.enum(["workflowState", "estimate", "priority", "labels", "assignees"]))
519
- .optional()
520
- .describe("Include only items where these fields are non-empty. " +
521
- "Valid fields: workflowState, estimate, priority, labels, assignees"),
522
- no: z
523
- .array(z.enum(["workflowState", "estimate", "priority", "labels", "assignees"]))
524
- .optional()
525
- .describe("Include only items where these fields are empty/absent. " +
526
- "Valid fields: workflowState, estimate, priority, labels, assignees"),
527
- excludeWorkflowStates: z
528
- .array(z.string())
529
- .optional()
530
- .describe("Exclude items matching any of these Workflow State names " +
531
- '(e.g., ["Done", "Canceled"])'),
532
- excludeEstimates: z
533
- .array(z.string())
534
- .optional()
535
- .describe("Exclude items matching any of these Estimate values " +
536
- '(e.g., ["M", "L", "XL"])'),
537
- excludePriorities: z
538
- .array(z.string())
539
- .optional()
540
- .describe("Exclude items matching any of these Priority values " +
541
- '(e.g., ["P3"])'),
542
- itemType: z
543
- .enum(["ISSUE", "PULL_REQUEST", "DRAFT_ISSUE"])
544
- .optional()
545
- .describe("Filter by item type (ISSUE, PULL_REQUEST, DRAFT_ISSUE). Omit to include all types."),
546
- updatedSince: z
547
- .string()
548
- .optional()
549
- .describe("Include items updated on or after this date. Supports date-math (@today-7d, @now-24h) or ISO dates."),
550
- updatedBefore: z
551
- .string()
552
- .optional()
553
- .describe("Include items updated before this date. Supports date-math (@today-7d, @now-24h) or ISO dates."),
554
- limit: z
555
- .number()
556
- .optional()
557
- .default(50)
558
- .describe("Max items to return (default 50)"),
559
- }, async (args) => {
560
- try {
561
- // Expand profile into filter defaults (explicit args override)
562
- if (args.profile) {
563
- const profileFilters = expandProfile(args.profile);
564
- for (const [key, value] of Object.entries(profileFilters)) {
565
- if (args[key] === undefined) {
566
- args[key] = value;
567
- }
568
- }
569
- }
570
- const owner = args.owner || resolveProjectOwner(client.config);
571
- const projectNumber = args.number || client.config.projectNumber;
572
- if (!owner) {
573
- return toolError("owner is required");
574
- }
575
- if (!projectNumber) {
576
- return toolError("number is required");
577
- }
578
- // Ensure field cache is populated
579
- await ensureFieldCache(client, fieldCache, owner, projectNumber);
580
- const projectId = fieldCache.getProjectId(projectNumber);
581
- if (!projectId) {
582
- return toolError("Could not resolve project ID");
583
- }
584
- // When filters are active, fetch more items to ensure adequate results after filtering
585
- const hasFilters = args.updatedSince || args.updatedBefore || args.itemType ||
586
- (args.has && args.has.length > 0) ||
587
- (args.no && args.no.length > 0) ||
588
- (args.excludeWorkflowStates && args.excludeWorkflowStates.length > 0) ||
589
- (args.excludeEstimates && args.excludeEstimates.length > 0) ||
590
- (args.excludePriorities && args.excludePriorities.length > 0);
591
- const maxItems = hasFilters ? 500 : (args.limit || 50);
592
- // Fetch all project items with field values
593
- const itemsResult = await paginateConnection((q, v) => client.projectQuery(q, v), `query($projectId: ID!, $cursor: String, $first: Int!) {
594
- node(id: $projectId) {
595
- ... on ProjectV2 {
596
- items(first: $first, after: $cursor) {
597
- totalCount
598
- pageInfo { hasNextPage endCursor }
599
- nodes {
600
- id
601
- type
602
- content {
603
- ... on Issue {
604
- number
605
- title
606
- state
607
- url
608
- updatedAt
609
- labels(first: 10) { nodes { name } }
610
- assignees(first: 5) { nodes { login } }
611
- repository { nameWithOwner name owner { login } }
612
- }
613
- ... on PullRequest {
614
- number
615
- title
616
- state
617
- url
618
- repository { nameWithOwner name owner { login } }
619
- }
620
- ... on DraftIssue {
621
- id
622
- title
623
- body
624
- }
625
- }
626
- fieldValues(first: 20) {
627
- nodes {
628
- ... on ProjectV2ItemFieldSingleSelectValue {
629
- __typename
630
- name
631
- optionId
632
- field { ... on ProjectV2FieldCommon { name } }
633
- }
634
- ... on ProjectV2ItemFieldTextValue {
635
- __typename
636
- text
637
- field { ... on ProjectV2FieldCommon { name } }
638
- }
639
- ... on ProjectV2ItemFieldNumberValue {
640
- __typename
641
- number
642
- field { ... on ProjectV2FieldCommon { name } }
643
- }
644
- }
645
- }
646
- }
647
- }
648
- }
649
- }
650
- }`, { projectId, first: Math.min(maxItems, 100) }, "node.items", { maxItems });
651
- // Filter items by field values
652
- let items = itemsResult.nodes;
653
- // Filter by item type (broadest filter first to reduce working set)
654
- if (args.itemType) {
655
- items = items.filter((item) => item.type === args.itemType);
656
- }
657
- if (args.workflowState) {
658
- items = items.filter((item) => getFieldValue(item, "Workflow State") === args.workflowState);
659
- }
660
- if (args.estimate) {
661
- items = items.filter((item) => getFieldValue(item, "Estimate") === args.estimate);
662
- }
663
- if (args.priority) {
664
- items = items.filter((item) => getFieldValue(item, "Priority") === args.priority);
665
- }
666
- // Filter by field presence (has)
667
- if (args.has && args.has.length > 0) {
668
- items = items.filter((item) => args.has.every((field) => hasField(item, field)));
669
- }
670
- // Filter by field absence (no)
671
- if (args.no && args.no.length > 0) {
672
- items = items.filter((item) => args.no.every((field) => !hasField(item, field)));
673
- }
674
- // Filter by excluded workflow states
675
- if (args.excludeWorkflowStates && args.excludeWorkflowStates.length > 0) {
676
- items = items.filter((item) => !args.excludeWorkflowStates.includes(getFieldValue(item, "Workflow State") ?? ""));
677
- }
678
- // Filter by excluded estimates
679
- if (args.excludeEstimates && args.excludeEstimates.length > 0) {
680
- items = items.filter((item) => !args.excludeEstimates.includes(getFieldValue(item, "Estimate") ?? ""));
681
- }
682
- // Filter by excluded priorities
683
- if (args.excludePriorities && args.excludePriorities.length > 0) {
684
- items = items.filter((item) => !args.excludePriorities.includes(getFieldValue(item, "Priority") ?? ""));
685
- }
686
- // Filter by updatedSince
687
- if (args.updatedSince) {
688
- const since = parseDateMath(args.updatedSince).getTime();
689
- items = items.filter((item) => {
690
- const content = item.content;
691
- const updatedAt = content?.updatedAt;
692
- return updatedAt ? new Date(updatedAt).getTime() >= since : false;
693
- });
694
- }
695
- // Filter by updatedBefore
696
- if (args.updatedBefore) {
697
- const before = parseDateMath(args.updatedBefore).getTime();
698
- items = items.filter((item) => {
699
- const content = item.content;
700
- const updatedAt = content?.updatedAt;
701
- return updatedAt ? new Date(updatedAt).getTime() < before : false;
702
- });
703
- }
704
- // Apply limit after filtering
705
- items = items.slice(0, args.limit || 50);
706
- // Format response
707
- const formattedItems = items.map((item) => {
708
- const content = item.content;
709
- return {
710
- itemId: item.id,
711
- type: item.type,
712
- draftIssueId: item.type === "DRAFT_ISSUE" ? content?.id ?? null : null,
713
- number: content?.number,
714
- title: content?.title,
715
- state: content?.state,
716
- url: content?.url,
717
- updatedAt: content?.updatedAt ?? null,
718
- owner: content?.repository?.owner?.login ?? null,
719
- repo: content?.repository?.name ?? null,
720
- nameWithOwner: content?.repository?.nameWithOwner ?? null,
721
- workflowState: getFieldValue(item, "Workflow State"),
722
- estimate: getFieldValue(item, "Estimate"),
723
- priority: getFieldValue(item, "Priority"),
724
- labels: content?.labels?.nodes?.map((l) => l.name),
725
- assignees: content?.assignees?.nodes?.map((a) => a.login),
726
- };
727
- });
728
- return toolSuccess({
729
- totalCount: itemsResult.totalCount,
730
- filteredCount: formattedItems.length,
731
- items: formattedItems,
732
- });
733
- }
734
- catch (error) {
735
- const message = error instanceof Error ? error.message : String(error);
736
- return toolError(`Failed to list project items: ${message}`);
737
- }
738
- });
739
- // -------------------------------------------------------------------------
740
487
  // ralph_hero__list_project_repos
741
488
  // -------------------------------------------------------------------------
742
489
  server.tool("ralph_hero__list_project_repos", "List all repositories linked to a GitHub Project V2. Returns owner, name, and nameWithOwner for each linked repo.", {
@@ -774,31 +521,6 @@ export function registerProjectTools(server, client, fieldCache) {
774
521
  }
775
522
  });
776
523
  }
777
- function getFieldValue(item, fieldName) {
778
- const fieldValue = item.fieldValues.nodes.find((fv) => fv.field?.name === fieldName &&
779
- fv.__typename === "ProjectV2ItemFieldSingleSelectValue");
780
- return fieldValue?.name;
781
- }
782
- function hasField(item, field) {
783
- switch (field) {
784
- case "workflowState":
785
- return getFieldValue(item, "Workflow State") !== undefined;
786
- case "estimate":
787
- return getFieldValue(item, "Estimate") !== undefined;
788
- case "priority":
789
- return getFieldValue(item, "Priority") !== undefined;
790
- case "labels": {
791
- const content = item.content;
792
- const labels = content?.labels?.nodes || [];
793
- return labels.length > 0;
794
- }
795
- case "assignees": {
796
- const content = item.content;
797
- const assignees = content?.assignees?.nodes || [];
798
- return assignees.length > 0;
799
- }
800
- }
801
- }
802
524
  async function fetchProject(client, owner, number) {
803
525
  // Try user query first
804
526
  try {
@@ -8,8 +8,6 @@
8
8
  * GitHub node IDs internally via cached lookups.
9
9
  */
10
10
  import { z } from "zod";
11
- import { detectGroup } from "../lib/group-detection.js";
12
- import { detectWorkStreams, } from "../lib/work-stream-detection.js";
13
11
  import { isValidState, isEarlierState, VALID_STATES, PARENT_GATE_STATES, isParentGateState, stateIndex, } from "../lib/workflow-states.js";
14
12
  import { toolSuccess, toolError } from "../types.js";
15
13
  import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, syncStatusField, } from "../lib/helpers.js";
@@ -349,70 +347,6 @@ export function registerRelationshipTools(server, client, fieldCache) {
349
347
  }
350
348
  });
351
349
  // -------------------------------------------------------------------------
352
- // ralph_hero__detect_group
353
- // -------------------------------------------------------------------------
354
- server.tool("ralph_hero__detect_group", "Detect the group of related issues by traversing sub-issues and dependencies transitively from a seed issue. Returns all group members in topological order (blockers first). Used by Ralph workflow to discover atomic implementation groups.", {
355
- owner: z
356
- .string()
357
- .optional()
358
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
359
- repo: z
360
- .string()
361
- .optional()
362
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
363
- number: z
364
- .number()
365
- .describe("Seed issue number to start group detection from"),
366
- }, async (args) => {
367
- try {
368
- const { owner, repo } = resolveConfig(client, args);
369
- const result = await detectGroup(client, owner, repo, args.number);
370
- return toolSuccess(result);
371
- }
372
- catch (error) {
373
- const message = error instanceof Error ? error.message : String(error);
374
- return toolError(`Failed to detect group: ${message}`);
375
- }
376
- });
377
- // -------------------------------------------------------------------------
378
- // ralph_hero__detect_work_streams
379
- // -------------------------------------------------------------------------
380
- server.tool("ralph_hero__detect_work_streams", "Cluster GitHub issues into independent work streams based on shared file ownership and blockedBy relationships. Uses union-find to group issues that share Will Modify files or are co-dependent. Returns WorkStreamResult with streams, sharedFiles, and a human-readable rationale.", {
381
- owner: z
382
- .string()
383
- .optional()
384
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
385
- repo: z
386
- .string()
387
- .optional()
388
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
389
- issues: z
390
- .array(z.object({
391
- number: z.number().describe("Issue number"),
392
- files: z
393
- .array(z.string())
394
- .describe("Will Modify file paths from research doc"),
395
- blockedBy: z
396
- .array(z.number())
397
- .describe("GitHub blockedBy issue numbers"),
398
- }))
399
- .describe("List of issues with their file ownership and dependencies"),
400
- }, async (args) => {
401
- try {
402
- const issueOwnership = args.issues.map((i) => ({
403
- number: i.number,
404
- files: i.files,
405
- blockedBy: i.blockedBy,
406
- }));
407
- const result = detectWorkStreams(issueOwnership);
408
- return toolSuccess(result);
409
- }
410
- catch (error) {
411
- const message = error instanceof Error ? error.message : String(error);
412
- return toolError(`Failed to detect work streams: ${message}`);
413
- }
414
- });
415
- // -------------------------------------------------------------------------
416
350
  // ralph_hero__advance_children
417
351
  // -------------------------------------------------------------------------
418
352
  server.tool("ralph_hero__advance_children", "Advance issues to a target workflow state. Provide either 'number' (parent issue, advances sub-issues) or 'issues' (explicit list of issue numbers). Only advances issues in earlier workflow states. Returns what changed, what was skipped, and any errors.", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.4.89",
3
+ "version": "2.4.91",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",