ralph-hero-mcp-server 2.4.99 → 2.4.102

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.
@@ -55,17 +55,17 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary, options =
55
55
  required: false,
56
56
  met: true,
57
57
  blocking: [],
58
- });
58
+ }, options.streamCount);
59
59
  }
60
60
  // Step 1: Check for oversized issues needing split (skip already-split issues)
61
61
  const oversized = issues.filter((i) => i.estimate !== null && OVERSIZED_ESTIMATES.has(i.estimate) && i.subIssueCount === 0);
62
62
  if (oversized.length > 0) {
63
- return buildResult("SPLIT", `${oversized.length} issue(s) need splitting (estimate: ${oversized.map((i) => `#${i.number}=${i.estimate}`).join(", ")})`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
63
+ return buildResult("SPLIT", `${oversized.length} issue(s) need splitting (estimate: ${oversized.map((i) => `#${i.number}=${i.estimate}`).join(", ")})`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
64
64
  }
65
65
  // Step 2: Check for issues without workflow state
66
66
  const noState = issues.filter((i) => !i.workflowState || i.workflowState === "unknown");
67
67
  if (noState.length > 0) {
68
- return buildResult("TRIAGE", `${noState.length} issue(s) have no workflow state; triage first`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
68
+ return buildResult("TRIAGE", `${noState.length} issue(s) have no workflow state; triage first`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
69
69
  }
70
70
  // Categorize issues by state
71
71
  const needsResearch = issues.filter((i) => i.workflowState === "Research Needed");
@@ -95,47 +95,47 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary, options =
95
95
  })),
96
96
  ],
97
97
  };
98
- return buildResult("RESEARCH", `${needsResearch.length} need research, ${inResearch.length} in progress`, issues, isGroup, groupPrimary, convergence);
98
+ return buildResult("RESEARCH", `${needsResearch.length} need research, ${inResearch.length} in progress`, issues, isGroup, groupPrimary, convergence, options.streamCount);
99
99
  }
100
100
  // Step 4: All issues in Ready for Plan -> PLAN (convergence met)
101
101
  if (readyForPlan.length === issues.length) {
102
- return buildResult("PLAN", "All issues ready for planning", issues, isGroup, groupPrimary, { required: isGroup, met: true, blocking: [] });
102
+ return buildResult("PLAN", "All issues ready for planning", issues, isGroup, groupPrimary, { required: isGroup, met: true, blocking: [] }, options.streamCount);
103
103
  }
104
104
  // Step 5: Some Ready for Plan but not all (mixed with earlier states) -> still need earlier work
105
105
  // This is handled by the checks above (research, backlog) and below (plan in progress/review)
106
106
  // Step 6: Any issues in Plan in Progress -> REVIEW (plans still being written)
107
107
  // If some are in Plan in Progress and some in Plan in Review, we're still in REVIEW phase
108
108
  if (planInProgress.length > 0) {
109
- return buildResult("REVIEW", `${planInProgress.length} plan(s) in progress, ${planInReview.length} in review`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
109
+ return buildResult("REVIEW", `${planInProgress.length} plan(s) in progress, ${planInReview.length} in review`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
110
110
  }
111
111
  // Step 7: All issues in Plan in Review -> HUMAN_GATE (all plans awaiting approval)
112
112
  if (planInReview.length === issues.length) {
113
- return buildResult("HUMAN_GATE", "All plans awaiting human approval", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
113
+ return buildResult("HUMAN_GATE", "All plans awaiting human approval", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
114
114
  }
115
115
  // Some in Plan in Review (but not all, and none in Plan in Progress) -> REVIEW
116
116
  if (planInReview.length > 0) {
117
- return buildResult("REVIEW", `${planInReview.length} plan(s) in review`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
117
+ return buildResult("REVIEW", `${planInReview.length} plan(s) in review`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
118
118
  }
119
119
  // Step 8: Any issues in In Progress -> IMPLEMENT
120
120
  if (inProgress.length > 0) {
121
- return buildResult("IMPLEMENT", `${inProgress.length} issue(s) in progress`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
121
+ return buildResult("IMPLEMENT", `${inProgress.length} issue(s) in progress`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
122
122
  }
123
123
  // Step 9: All issues terminal (In Review or Done or Canceled)
124
124
  const terminal = inReview.length + done.length + canceled.length;
125
125
  if (terminal === issues.length) {
126
126
  // In auto mode, "In Review" issues are actionable (integrator can merge)
127
127
  if (options.autoMode && inReview.length > 0 && done.length + canceled.length < issues.length) {
128
- return buildResult("INTEGRATE", `${inReview.length} issue(s) awaiting integration`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
128
+ return buildResult("INTEGRATE", `${inReview.length} issue(s) awaiting integration`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
129
129
  }
130
- return buildResult("TERMINAL", "All issues in review or done", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
130
+ return buildResult("TERMINAL", "All issues in review or done", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
131
131
  }
132
132
  // Step 10: Any issues need human intervention -> TERMINAL
133
133
  if (humanNeeded.length > 0) {
134
- return buildResult("TERMINAL", `${humanNeeded.length} issue(s) need human intervention`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
134
+ return buildResult("TERMINAL", `${humanNeeded.length} issue(s) need human intervention`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
135
135
  }
136
136
  // Step 11: Backlog issues -> TRIAGE
137
137
  if (backlog.length > 0) {
138
- return buildResult("TRIAGE", `${backlog.length} issue(s) in Backlog`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
138
+ return buildResult("TRIAGE", `${backlog.length} issue(s) in Backlog`, issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
139
139
  }
140
140
  // Step 12: Mixed states - default to earliest incomplete phase
141
141
  // Check Ready for Plan mixed with later states
@@ -143,10 +143,10 @@ export function detectPipelinePosition(issues, isGroup, groupPrimary, options =
143
143
  const blocking = issues
144
144
  .filter((i) => i.workflowState !== "Ready for Plan")
145
145
  .map((i) => ({ number: i.number, state: i.workflowState }));
146
- return buildResult("PLAN", "Some issues ready for planning, mixed states", issues, isGroup, groupPrimary, { required: isGroup, met: false, blocking });
146
+ return buildResult("PLAN", "Some issues ready for planning, mixed states", issues, isGroup, groupPrimary, { required: isGroup, met: false, blocking }, options.streamCount);
147
147
  }
148
148
  // Fallback
149
- return buildResult("TRIAGE", "Mixed states, defaulting to triage", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] });
149
+ return buildResult("TRIAGE", "Mixed states, defaulting to triage", issues, isGroup, groupPrimary, { required: false, met: true, blocking: [] }, options.streamCount);
150
150
  }
151
151
  // ---------------------------------------------------------------------------
152
152
  // Stream-level detection
@@ -165,14 +165,17 @@ export function detectStreamPipelinePositions(streams, issueStates, options = {}
165
165
  return {
166
166
  streamId: stream.id,
167
167
  issues: filteredIssues,
168
- position: detectPipelinePosition(filteredIssues, isGroup, groupPrimary, options),
168
+ position: detectPipelinePosition(filteredIssues, isGroup, groupPrimary, {
169
+ ...options,
170
+ streamCount: streams.length,
171
+ }),
169
172
  };
170
173
  });
171
174
  }
172
175
  // ---------------------------------------------------------------------------
173
176
  // Helpers
174
177
  // ---------------------------------------------------------------------------
175
- function computeSuggestedRoster(phase, issues) {
178
+ function computeSuggestedRoster(phase, issues, streamCount) {
176
179
  // TERMINAL: no workers needed
177
180
  if (phase === 'TERMINAL') {
178
181
  return { analyst: 0, builder: 0, integrator: 0 };
@@ -189,13 +192,12 @@ function computeSuggestedRoster(phase, issues) {
189
192
  : needsResearch.length <= 5 ? 2
190
193
  : 3;
191
194
  }
192
- // Builder scaling: default 1; 2 if 5+ issues with M/L estimates
193
- const largeSized = issues.filter(i => i.estimate != null && ['M', 'L', 'XL'].includes(i.estimate));
194
- const builder = largeSized.length >= 5 ? 2 : 1;
195
+ // Builder scaling: 1 per independent stream, capped at 3
196
+ const builder = Math.min(streamCount || 1, 3);
195
197
  const integrator = issues.length >= 5 ? 2 : 1;
196
198
  return { analyst, builder, integrator };
197
199
  }
198
- function buildResult(phase, reason, issues, isGroup, groupPrimary, convergence) {
200
+ function buildResult(phase, reason, issues, isGroup, groupPrimary, convergence, streamCount) {
199
201
  // Derive recommendation from convergence state
200
202
  let recommendation;
201
203
  if (convergence.met) {
@@ -207,7 +209,7 @@ function buildResult(phase, reason, issues, isGroup, groupPrimary, convergence)
207
209
  else {
208
210
  recommendation = "wait";
209
211
  }
210
- const suggestedRoster = computeSuggestedRoster(phase, issues);
212
+ const suggestedRoster = computeSuggestedRoster(phase, issues, streamCount);
211
213
  return {
212
214
  phase,
213
215
  reason,
@@ -361,11 +361,20 @@ export function registerDashboardTools(server, client, fieldCache) {
361
361
  subIssueCount: s.subIssueCount ?? 0,
362
362
  }));
363
363
  const positions = detectStreamPipelinePositions(streamResult.streams, states, { autoMode: client.config.autoMode });
364
+ // Aggregate roster: max analyst/integrator across streams, stream-count-based builder
365
+ const suggestedRoster = positions.length > 0
366
+ ? {
367
+ analyst: Math.max(...positions.map(p => p.position.suggestedRoster.analyst)),
368
+ builder: Math.min(streamResult.totalStreams, 3),
369
+ integrator: Math.max(...positions.map(p => p.position.suggestedRoster.integrator)),
370
+ }
371
+ : { analyst: 0, builder: 0, integrator: 0 };
364
372
  return toolSuccess({
365
373
  streams: positions,
366
374
  totalStreams: streamResult.totalStreams,
367
375
  totalIssues: streamResult.totalIssues,
368
376
  rationale: streamResult.rationale,
377
+ suggestedRoster,
369
378
  });
370
379
  }
371
380
  catch (error) {
@@ -854,23 +854,47 @@ export function registerIssueTools(server, client, fieldCache) {
854
854
  .map((name) => allLabels.find((l) => l.name === name)?.id)
855
855
  .filter((id) => id !== undefined);
856
856
  }
857
- await client.mutate(`mutation($issueId: ID!, $title: String, $body: String, $labelIds: [ID!], $assigneeIds: [ID!]) {
858
- updateIssue(input: {
859
- id: $issueId,
860
- title: $title,
861
- body: $body,
862
- labelIds: $labelIds,
863
- assigneeIds: $assigneeIds
864
- }) {
857
+ // Resolve assignee IDs if provided
858
+ let assigneeIds;
859
+ if (args.assignees) {
860
+ assigneeIds = [];
861
+ for (const username of args.assignees) {
862
+ const userResult = await client.query(`query($login: String!) { user(login: $login) { id } }`, { login: username }, { cache: true, cacheTtlMs: 5 * 60 * 1000 });
863
+ if (userResult.user) {
864
+ assigneeIds.push(userResult.user.id);
865
+ }
866
+ }
867
+ }
868
+ // Build mutation dynamically to avoid sending null for unprovided fields
869
+ // (GitHub treats null as "clear" not "leave unchanged")
870
+ const varDefs = ["$issueId: ID!"];
871
+ const inputFields = ["id: $issueId"];
872
+ const variables = { issueId };
873
+ if (args.title !== undefined) {
874
+ varDefs.push("$title: String");
875
+ inputFields.push("title: $title");
876
+ variables.title = args.title;
877
+ }
878
+ if (args.body !== undefined) {
879
+ varDefs.push("$body: String");
880
+ inputFields.push("body: $body");
881
+ variables.body = args.body;
882
+ }
883
+ if (labelIds !== undefined) {
884
+ varDefs.push("$labelIds: [ID!]");
885
+ inputFields.push("labelIds: $labelIds");
886
+ variables.labelIds = labelIds;
887
+ }
888
+ if (assigneeIds !== undefined) {
889
+ varDefs.push("$assigneeIds: [ID!]");
890
+ inputFields.push("assigneeIds: $assigneeIds");
891
+ variables.assigneeIds = assigneeIds;
892
+ }
893
+ await client.mutate(`mutation(${varDefs.join(", ")}) {
894
+ updateIssue(input: { ${inputFields.join(", ")} }) {
865
895
  issue { number title url }
866
896
  }
867
- }`, {
868
- issueId,
869
- title: args.title ?? null,
870
- body: args.body ?? null,
871
- labelIds: labelIds ?? null,
872
- assigneeIds: null, // Would need username -> ID resolution
873
- });
897
+ }`, variables);
874
898
  if (args.title !== undefined)
875
899
  changes.title = args.title;
876
900
  if (args.body !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.4.99",
3
+ "version": "2.4.102",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",