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,
|
|
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:
|
|
193
|
-
const
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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)
|