ralph-hero-mcp-server 2.5.80 → 2.5.85
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.
- package/dist/tools/directions-tools.js +5 -3
- package/dist/tools/issue-tools.js +71 -106
- package/package.json +1 -1
|
@@ -51,10 +51,12 @@ const openPRSchema = z.object({
|
|
|
51
51
|
});
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
// Shared implementation — extracted so both `hello_directions` (deprecated)
|
|
54
|
-
// and `next_actions` (current) can route through the same code path.
|
|
55
|
-
//
|
|
54
|
+
// and `next_actions` (current) can route through the same code path. Also
|
|
55
|
+
// exported so the deprecated `pick_actionable_issue` wrapper in
|
|
56
|
+
// `issue-tools.ts` can delegate without duplicating the data-fetch +
|
|
57
|
+
// scoring pipeline.
|
|
56
58
|
// ---------------------------------------------------------------------------
|
|
57
|
-
function makeRunDirections(client, fieldCache) {
|
|
59
|
+
export function makeRunDirections(client, fieldCache) {
|
|
58
60
|
return async function runDirections(args) {
|
|
59
61
|
try {
|
|
60
62
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
@@ -10,6 +10,7 @@ import { detectGroup } from "../lib/group-detection.js";
|
|
|
10
10
|
import { detectPipelinePosition, OVERSIZED_ESTIMATES, } from "../lib/pipeline-detection.js";
|
|
11
11
|
import { isValidState, isParentGateState, VALID_STATES, LOCK_STATES, TERMINAL_STATES, WORKFLOW_STATE_TO_STATUS, } from "../lib/workflow-states.js";
|
|
12
12
|
import { buildBatchMutationQuery } from "./batch-tools.js";
|
|
13
|
+
import { makeRunDirections } from "./directions-tools.js";
|
|
13
14
|
import { resolveState } from "../lib/state-resolution.js";
|
|
14
15
|
import { parseDateMath } from "../lib/date-math.js";
|
|
15
16
|
import { expandProfile } from "../lib/filter-profiles.js";
|
|
@@ -1185,9 +1186,20 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1185
1186
|
}
|
|
1186
1187
|
});
|
|
1187
1188
|
// -------------------------------------------------------------------------
|
|
1188
|
-
// ralph_hero__pick_actionable_issue
|
|
1189
|
+
// ralph_hero__pick_actionable_issue [DEPRECATED]
|
|
1190
|
+
//
|
|
1191
|
+
// Thin wrapper that delegates to the shared `runDirections` helper from
|
|
1192
|
+
// `directions-tools.ts` (audience="agent"). Preserves the legacy
|
|
1193
|
+
// `{ found, issue, group, alternatives }` shape so existing callers
|
|
1194
|
+
// (justfile recipes, hero/team allowlists) keep working until removal in
|
|
1195
|
+
// 2.7.0. Same backwards-compat pattern as `hello_directions` from Phase 2.
|
|
1196
|
+
//
|
|
1197
|
+
// Migration: callers should switch to
|
|
1198
|
+
// ralph_hero__next_actions(limit=1, audience="agent")
|
|
1199
|
+
// and consume the rank-1 (recommended) entry directly.
|
|
1189
1200
|
// -------------------------------------------------------------------------
|
|
1190
|
-
|
|
1201
|
+
const runDirections = makeRunDirections(client, fieldCache);
|
|
1202
|
+
server.tool("ralph_hero__pick_actionable_issue", "[DEPRECATED — use ralph_hero__next_actions(limit=1, audience='agent') instead. Removed in 2.7.0.] 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.", {
|
|
1191
1203
|
owner: z
|
|
1192
1204
|
.string()
|
|
1193
1205
|
.optional()
|
|
@@ -1200,7 +1212,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1200
1212
|
.describe("Project number override (defaults to configured project)"),
|
|
1201
1213
|
workflowState: z
|
|
1202
1214
|
.string()
|
|
1203
|
-
.
|
|
1215
|
+
.optional()
|
|
1216
|
+
.describe("Target workflow state (e.g., 'Research Needed', 'Ready for Plan'). Optional — when omitted, the rank-1 (recommended) direction across all actionable phases is returned (same as next_actions(limit=1, audience='agent'))."),
|
|
1204
1217
|
maxEstimate: z
|
|
1205
1218
|
.string()
|
|
1206
1219
|
.optional()
|
|
@@ -1208,8 +1221,9 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1208
1221
|
.describe("Maximum estimate to include (XS, S, M, L, XL). Default: S"),
|
|
1209
1222
|
}, async (args) => {
|
|
1210
1223
|
try {
|
|
1211
|
-
// Validate workflow state
|
|
1212
|
-
|
|
1224
|
+
// Validate workflow state (only when provided — wrapper allows
|
|
1225
|
+
// omission so it can mirror next_actions(limit=1, audience='agent')).
|
|
1226
|
+
if (args.workflowState !== undefined && !isValidState(args.workflowState)) {
|
|
1213
1227
|
return toolError(`Unknown workflow state '${args.workflowState}'. ` +
|
|
1214
1228
|
`Valid states: ${VALID_STATES.join(", ")}. ` +
|
|
1215
1229
|
`Recovery: retry with a valid state name. ` +
|
|
@@ -1224,111 +1238,59 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1224
1238
|
`Valid estimates: ${validEstimates.join(", ")}. ` +
|
|
1225
1239
|
`Recovery: retry with a valid estimate or omit for default (S).`);
|
|
1226
1240
|
}
|
|
1227
|
-
const { owner, repo
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1241
|
+
const { owner, repo } = resolveFullConfig(client, args);
|
|
1242
|
+
// Delegate to the shared ranker with audience="agent". Use a
|
|
1243
|
+
// generous limit so we have enough entries to filter by
|
|
1244
|
+
// workflowState + maxEstimate while still finding the highest
|
|
1245
|
+
// priority candidate. The ranker uses priority + audience-aware
|
|
1246
|
+
// estimate penalty as the dominant ordering signal, which matches
|
|
1247
|
+
// the legacy P0 -> P3 sort.
|
|
1248
|
+
const directionsResult = await runDirections({
|
|
1249
|
+
owner,
|
|
1250
|
+
projectNumbers: args.projectNumber !== undefined ? [args.projectNumber] : undefined,
|
|
1251
|
+
limit: 50,
|
|
1252
|
+
openPRs: [],
|
|
1253
|
+
audience: "agent",
|
|
1254
|
+
});
|
|
1255
|
+
if (directionsResult.isError) {
|
|
1256
|
+
// Surface the underlying error verbatim so callers see the same
|
|
1257
|
+
// recovery hints they would from next_actions.
|
|
1258
|
+
return directionsResult;
|
|
1233
1259
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
type
|
|
1244
|
-
content {
|
|
1245
|
-
... on Issue {
|
|
1246
|
-
number
|
|
1247
|
-
title
|
|
1248
|
-
body
|
|
1249
|
-
state
|
|
1250
|
-
url
|
|
1251
|
-
labels(first: 10) { nodes { name } }
|
|
1252
|
-
trackedIssues(first: 10) {
|
|
1253
|
-
nodes { number state }
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
fieldValues(first: 20) {
|
|
1258
|
-
nodes {
|
|
1259
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1260
|
-
__typename
|
|
1261
|
-
name
|
|
1262
|
-
optionId
|
|
1263
|
-
field { ... on ProjectV2FieldCommon { name } }
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1260
|
+
const payload = JSON.parse(directionsResult.content[0].text);
|
|
1261
|
+
// Restrict to plain "issue" directions — exclude PRs (kind="pr"),
|
|
1262
|
+
// lock-stale items (kind="lock-stale"), and tree-continue picks
|
|
1263
|
+
// (kind="tree-continue") since legacy semantics returned a single
|
|
1264
|
+
// ready-to-claim issue.
|
|
1265
|
+
let issueDirections = payload.directions.filter((d) => d.kind === "issue" && d.issue !== null);
|
|
1266
|
+
// Apply legacy filters: workflowState (if provided) and maxEstimate.
|
|
1267
|
+
if (args.workflowState !== undefined) {
|
|
1268
|
+
issueDirections = issueDirections.filter((d) => d.issue.workflowState === args.workflowState);
|
|
1270
1269
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1275
|
-
return false;
|
|
1276
|
-
const content = item.content;
|
|
1277
|
-
if (content.state !== "OPEN")
|
|
1278
|
-
return false;
|
|
1279
|
-
// Check workflow state
|
|
1280
|
-
const ws = getFieldValue(item, "Workflow State");
|
|
1281
|
-
if (ws !== args.workflowState)
|
|
1282
|
-
return false;
|
|
1283
|
-
// Check estimate
|
|
1284
|
-
const est = getFieldValue(item, "Estimate");
|
|
1285
|
-
if (est) {
|
|
1286
|
-
const estIdx = validEstimates.indexOf(est);
|
|
1287
|
-
const maxIdx = validEstimates.indexOf(maxEstimate);
|
|
1288
|
-
if (estIdx > maxIdx)
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
return true;
|
|
1292
|
-
});
|
|
1293
|
-
// Filter out locked issues (in a lock state - shouldn't happen if matching target state, but safety check)
|
|
1294
|
-
candidates = candidates.filter((item) => {
|
|
1295
|
-
const ws = getFieldValue(item, "Workflow State");
|
|
1296
|
-
return !ws || !LOCK_STATES.includes(ws);
|
|
1297
|
-
});
|
|
1298
|
-
// Filter out issues with unresolved blockers
|
|
1299
|
-
candidates = candidates.filter((item) => {
|
|
1300
|
-
const content = item.content;
|
|
1301
|
-
const blockedBy = content.trackedIssues;
|
|
1302
|
-
if (!blockedBy?.nodes || blockedBy.nodes.length === 0)
|
|
1270
|
+
const maxIdx = validEstimates.indexOf(maxEstimate);
|
|
1271
|
+
issueDirections = issueDirections.filter((d) => {
|
|
1272
|
+
const est = d.issue.estimate;
|
|
1273
|
+
if (!est)
|
|
1303
1274
|
return true;
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const priorityOrder = {
|
|
1309
|
-
P0: 0,
|
|
1310
|
-
P1: 1,
|
|
1311
|
-
P2: 2,
|
|
1312
|
-
P3: 3,
|
|
1313
|
-
};
|
|
1314
|
-
candidates.sort((a, b) => {
|
|
1315
|
-
const pA = getFieldValue(a, "Priority");
|
|
1316
|
-
const pB = getFieldValue(b, "Priority");
|
|
1317
|
-
const orderA = pA ? (priorityOrder[pA] ?? 99) : 99;
|
|
1318
|
-
const orderB = pB ? (priorityOrder[pB] ?? 99) : 99;
|
|
1319
|
-
return orderA - orderB;
|
|
1275
|
+
const estIdx = validEstimates.indexOf(est);
|
|
1276
|
+
if (estIdx < 0)
|
|
1277
|
+
return true;
|
|
1278
|
+
return estIdx <= maxIdx;
|
|
1320
1279
|
});
|
|
1321
|
-
|
|
1280
|
+
// Drop blocked items (legacy behavior). The ranker already strips
|
|
1281
|
+
// these unless that would empty the candidate set; the "blocked"
|
|
1282
|
+
// tag still rides along when it kept them as a fallback.
|
|
1283
|
+
issueDirections = issueDirections.filter((d) => !d.tags.includes("blocked"));
|
|
1284
|
+
if (issueDirections.length === 0) {
|
|
1322
1285
|
return toolSuccess({
|
|
1323
1286
|
found: false,
|
|
1324
1287
|
issue: null,
|
|
1325
1288
|
alternatives: 0,
|
|
1326
1289
|
});
|
|
1327
1290
|
}
|
|
1328
|
-
const best =
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
// Detect group context for the picked issue (best-effort)
|
|
1291
|
+
const best = issueDirections[0].issue;
|
|
1292
|
+
const issueNumber = best.number;
|
|
1293
|
+
// Detect group context for the picked issue (best-effort).
|
|
1332
1294
|
let group = null;
|
|
1333
1295
|
try {
|
|
1334
1296
|
const groupResult = await detectGroup(client, owner, repo, issueNumber);
|
|
@@ -1355,16 +1317,19 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
1355
1317
|
found: true,
|
|
1356
1318
|
issue: {
|
|
1357
1319
|
number: issueNumber,
|
|
1358
|
-
title:
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1320
|
+
title: best.title,
|
|
1321
|
+
// Body is not fetched by the ranker's data pipeline — kept
|
|
1322
|
+
// empty for backward compat. Callers needing the body should
|
|
1323
|
+
// chain a `get_issue` call (or migrate to `next_actions`).
|
|
1324
|
+
description: "",
|
|
1325
|
+
workflowState: best.workflowState,
|
|
1326
|
+
estimate: best.estimate || null,
|
|
1327
|
+
priority: best.priority || null,
|
|
1363
1328
|
isLocked: false,
|
|
1364
1329
|
blockedBy: [],
|
|
1365
1330
|
},
|
|
1366
1331
|
group,
|
|
1367
|
-
alternatives:
|
|
1332
|
+
alternatives: issueDirections.length - 1,
|
|
1368
1333
|
});
|
|
1369
1334
|
}
|
|
1370
1335
|
catch (error) {
|