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,
|
|
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
|
-
|
|
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
|
-
//
|
|
918
|
+
// ralph_hero__archive_items
|
|
1025
919
|
// -------------------------------------------------------------------------
|
|
1026
|
-
server.tool("
|
|
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
|
-
.
|
|
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.", {
|