ralph-hero-mcp-server 2.5.75 → 2.5.79

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.
@@ -34,6 +34,7 @@ export const DEFAULT_RANK_CONFIG = {
34
34
  lockStaleHours: 24,
35
35
  treeRecentDoneDays: 7,
36
36
  prStaleHours: 24,
37
+ audience: "human",
37
38
  };
38
39
  // ---------------------------------------------------------------------------
39
40
  // Internal scoring constants
@@ -60,6 +61,18 @@ const STALE_BOOST = -50;
60
61
  const LOCK_STALE_BOOST = -100;
61
62
  const TREE_CONTINUE_BOOST = -75;
62
63
  const PR_REVIEW_REQUIRED_BOOST = -200;
64
+ /**
65
+ * Per-estimate penalty applied when audience === "agent". Larger items
66
+ * cost more (positive score), pushing them down the ranking so agent
67
+ * loops dispatch on XS/S items first.
68
+ */
69
+ const ESTIMATE_PENALTY = {
70
+ XS: 0,
71
+ S: 0,
72
+ M: 20,
73
+ L: 40,
74
+ XL: 60,
75
+ };
63
76
  const HOUR_MS = 60 * 60 * 1000;
64
77
  const DAY_MS = 24 * HOUR_MS;
65
78
  // ---------------------------------------------------------------------------
@@ -100,6 +113,20 @@ function hasOpenBlockers(item) {
100
113
  function isLockState(state) {
101
114
  return state !== null && LOCK_STATES.includes(state);
102
115
  }
116
+ /**
117
+ * Returns a positive score adjustment when audience === "agent" so larger
118
+ * estimates get pushed down the ranking. Returns 0 for human audience
119
+ * (existing behavior). Items with unknown estimate get a mid-tier penalty
120
+ * (30) so unestimated work doesn't accidentally outrank XS/S items.
121
+ */
122
+ function audiencePenalty(item, audience) {
123
+ if (audience !== "agent")
124
+ return 0;
125
+ const est = item.estimate;
126
+ if (est === null || est === undefined)
127
+ return 30; // unknown: mid penalty
128
+ return ESTIMATE_PENALTY[est] ?? 30;
129
+ }
103
130
  // ---------------------------------------------------------------------------
104
131
  // Detection helpers
105
132
  // ---------------------------------------------------------------------------
@@ -176,6 +203,9 @@ export function detectTreeContinue(item, allItems, config) {
176
203
  export function scoreIssue(item, allItems, config) {
177
204
  const tags = [];
178
205
  let score = priorityScore(item.priority) + phaseScore(item.workflowState);
206
+ // Audience-aware estimate penalty (no-op for "human"; pushes XL items
207
+ // down for "agent" so autonomous loops favor XS/S work).
208
+ score += audiencePenalty(item, config.audience);
179
209
  const lockStale = detectLockStale(item, config);
180
210
  const treeContinue = detectTreeContinue(item, allItems, config);
181
211
  // Stale boost (non-lock states only)
@@ -295,7 +325,20 @@ export function buildReason(kind, issue, pr, tags, config, linkedIssueNumber = n
295
325
  const hours = Math.round(ageHours(issue.updatedAt, config.now));
296
326
  const days = Math.max(1, Math.floor(hours / 24));
297
327
  const dayLabel = days === 1 ? "day" : "days";
298
- return `${phase} for ${days} ${dayLabel} — likely the most unblocking thing`;
328
+ const priority = issue.priority;
329
+ if (priority === "P0") {
330
+ return `P0 stalled in ${phase} for ${days} ${dayLabel} — top of the queue`;
331
+ }
332
+ if (priority === "P1") {
333
+ return `P1 stalled in ${phase} for ${days} ${dayLabel} — likely the most unblocking thing`;
334
+ }
335
+ if (priority === "P2") {
336
+ return `Sitting in ${phase} for ${days} ${dayLabel} — small unblock if you have a moment`;
337
+ }
338
+ if (priority === "P3") {
339
+ return `Low-priority item in ${phase} for ${days} ${dayLabel}`;
340
+ }
341
+ return `Unprioritized in ${phase} for ${days} ${dayLabel}`;
299
342
  }
300
343
  if (issue.priority === "P0") {
301
344
  return `P0 in ${phase} — top of the queue`;
@@ -423,6 +466,7 @@ export function rankDirections(items, openPRs, config) {
423
466
  const reason = buildReason(c.kind, c.item, null, c.tags, config, null);
424
467
  return {
425
468
  rank,
469
+ recommended: false,
426
470
  kind: c.kind,
427
471
  issue: toDirectionIssue(c.item),
428
472
  pr: null,
@@ -436,6 +480,7 @@ export function rankDirections(items, openPRs, config) {
436
480
  const reason = buildReason("pr", null, p.pr, p.tags, config, p.linkedIssueNumber);
437
481
  return {
438
482
  rank,
483
+ recommended: false,
439
484
  kind: "pr",
440
485
  issue: null,
441
486
  pr: toDirectionPR(p.pr),
@@ -444,6 +489,12 @@ export function rankDirections(items, openPRs, config) {
444
489
  score: p.score,
445
490
  };
446
491
  });
492
+ // Mark the top-ranked entry as recommended. Both modes use this
493
+ // flag for selection: interactive picker pre-selects it; headless
494
+ // orchestrators dispatch on it.
495
+ if (directions.length > 0) {
496
+ directions[0].recommended = true;
497
+ }
447
498
  return directions;
448
499
  }
449
500
  //# sourceMappingURL=directions.js.map
@@ -1,11 +1,14 @@
1
1
  /**
2
- * MCP tool wrapping the pure ranker at `lib/directions.ts`.
2
+ * MCP tools wrapping the pure ranker at `lib/directions.ts`.
3
3
  *
4
- * Exposes `ralph_hero__hello_directions`: a single-tool, fixed-shape,
5
- * deterministic surface that returns up to N ranked "directions" for the
6
- * `hello` skill's session briefing. The skill is responsible for fetching
7
- * open PRs (via `gh pr list`) and passing them in as a parameter — the
8
- * MCP server does not itself open an Octokit-style PR API surface.
4
+ * Exposes two tools that share a single implementation:
5
+ * - `ralph_hero__next_actions` (current name; accepts `audience` param)
6
+ * - `ralph_hero__hello_directions` (DEPRECATED alias; fixed at audience="human")
7
+ *
8
+ * Both return a fixed-shape JSON payload with up to N ranked "directions"
9
+ * for the `hello` skill's session briefing. The skill is responsible for
10
+ * fetching open PRs (via `gh pr list`) and passing them in as a parameter
11
+ * — the MCP server does not itself open an Octokit-style PR API surface.
9
12
  *
10
13
  * Behaviour:
11
14
  * 1. Resolve owner + project numbers from args or client config.
@@ -32,68 +35,27 @@ import { toolSuccess, toolError, resolveProjectOwner, resolveProjectNumbers, } f
32
35
  // ---------------------------------------------------------------------------
33
36
  const HOUR_MS = 60 * 60 * 1000;
34
37
  // ---------------------------------------------------------------------------
35
- // Register
38
+ // Shared schema fragments
36
39
  // ---------------------------------------------------------------------------
37
- export function registerDirectionsTools(server, client, fieldCache) {
38
- server.tool("ralph_hero__hello_directions", "Compute up to N deterministic 'directions' for the hello skill's session briefing. Single tool call returns a fixed-shape JSON payload with ranked issue/PR action items. Open PRs must be passed in as a parameter (the MCP server does not fetch them itself).", {
39
- owner: z
40
- .string()
41
- .optional()
42
- .describe("GitHub owner. Defaults to RALPH_GH_OWNER env var."),
43
- projectNumbers: z
44
- .array(z.coerce.number())
45
- .optional()
46
- .describe("Project numbers to include. Defaults to RALPH_GH_PROJECT_NUMBERS or single configured project."),
47
- limit: z
48
- .number()
49
- .int()
50
- .nonnegative()
51
- .optional()
52
- .default(3)
53
- .describe("Max directions to return (default: 3)."),
54
- stuckThresholdHours: z
55
- .number()
56
- .nonnegative()
57
- .optional()
58
- .default(48)
59
- .describe("Hours before a non-lock issue is considered stale (default: 48)."),
60
- lockStaleHours: z
61
- .number()
62
- .nonnegative()
63
- .optional()
64
- .default(24)
65
- .describe("Hours before a lock-state issue is considered stalled (default: 24)."),
66
- treeRecentDoneDays: z
67
- .number()
68
- .nonnegative()
69
- .optional()
70
- .default(7)
71
- .describe("Days within which a sibling Done event still pulls a tree forward (default: 7)."),
72
- prStaleHours: z
73
- .number()
74
- .nonnegative()
75
- .optional()
76
- .default(24)
77
- .describe("Hours before an open PR is considered stale (default: 24)."),
78
- openPRs: z
79
- .array(z.object({
80
- number: z.number(),
81
- title: z.string(),
82
- url: z.string(),
83
- isDraft: z.boolean(),
84
- reviewDecision: z
85
- .string()
86
- .nullable()
87
- .describe("REVIEW_REQUIRED | APPROVED | CHANGES_REQUESTED | null"),
88
- headRefName: z.string(),
89
- createdAt: z
90
- .string()
91
- .describe("ISO timestamp from gh pr list"),
92
- }))
93
- .optional()
94
- .default([])
95
- .describe("Open PRs gathered by the caller (e.g. via `gh pr list`). Drafts and APPROVED PRs are filtered internally."),
96
- }, async (args) => {
40
+ const openPRSchema = z.object({
41
+ number: z.number(),
42
+ title: z.string(),
43
+ url: z.string(),
44
+ isDraft: z.boolean(),
45
+ reviewDecision: z
46
+ .string()
47
+ .nullable()
48
+ .describe("REVIEW_REQUIRED | APPROVED | CHANGES_REQUESTED | null"),
49
+ headRefName: z.string(),
50
+ createdAt: z.string().describe("ISO timestamp from gh pr list"),
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // Shared implementation — extracted so both `hello_directions` (deprecated)
54
+ // and `next_actions` (current) can route through the same code path. Keep
55
+ // file-private (no export) — alias lives in this same file.
56
+ // ---------------------------------------------------------------------------
57
+ function makeRunDirections(client, fieldCache) {
58
+ return async function runDirections(args) {
97
59
  try {
98
60
  const owner = args.owner || resolveProjectOwner(client.config);
99
61
  if (!owner) {
@@ -124,6 +86,7 @@ export function registerDirectionsTools(server, client, fieldCache) {
124
86
  lockStaleHours: args.lockStaleHours ?? DEFAULT_RANK_CONFIG.lockStaleHours,
125
87
  treeRecentDoneDays: args.treeRecentDoneDays ?? DEFAULT_RANK_CONFIG.treeRecentDoneDays,
126
88
  prStaleHours: args.prStaleHours ?? DEFAULT_RANK_CONFIG.prStaleHours,
89
+ audience: args.audience,
127
90
  now,
128
91
  };
129
92
  // Compute PR ageHours at the boundary so the lib never reads the wall clock.
@@ -154,6 +117,113 @@ export function registerDirectionsTools(server, client, fieldCache) {
154
117
  const message = error instanceof Error ? error.message : String(error);
155
118
  return toolError(`Failed to compute hello directions: ${message}`);
156
119
  }
120
+ };
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Register
124
+ // ---------------------------------------------------------------------------
125
+ export function registerDirectionsTools(server, client, fieldCache) {
126
+ const runDirections = makeRunDirections(client, fieldCache);
127
+ server.tool("ralph_hero__hello_directions", "[DEPRECATED — use ralph_hero__next_actions instead. Removed in 2.7.0.] Compute up to N deterministic 'directions' for the hello skill's session briefing.", {
128
+ owner: z
129
+ .string()
130
+ .optional()
131
+ .describe("GitHub owner. Defaults to RALPH_GH_OWNER env var."),
132
+ projectNumbers: z
133
+ .array(z.coerce.number())
134
+ .optional()
135
+ .describe("Project numbers to include. Defaults to RALPH_GH_PROJECT_NUMBERS or single configured project."),
136
+ limit: z
137
+ .number()
138
+ .int()
139
+ .nonnegative()
140
+ .optional()
141
+ .default(3)
142
+ .describe("Max directions to return (default: 3)."),
143
+ stuckThresholdHours: z
144
+ .number()
145
+ .nonnegative()
146
+ .optional()
147
+ .default(48)
148
+ .describe("Hours before a non-lock issue is considered stale (default: 48)."),
149
+ lockStaleHours: z
150
+ .number()
151
+ .nonnegative()
152
+ .optional()
153
+ .default(24)
154
+ .describe("Hours before a lock-state issue is considered stalled (default: 24)."),
155
+ treeRecentDoneDays: z
156
+ .number()
157
+ .nonnegative()
158
+ .optional()
159
+ .default(7)
160
+ .describe("Days within which a sibling Done event still pulls a tree forward (default: 7)."),
161
+ prStaleHours: z
162
+ .number()
163
+ .nonnegative()
164
+ .optional()
165
+ .default(24)
166
+ .describe("Hours before an open PR is considered stale (default: 24)."),
167
+ openPRs: z
168
+ .array(openPRSchema)
169
+ .optional()
170
+ .default([])
171
+ .describe("Open PRs gathered by the caller (e.g. via `gh pr list`). Drafts and APPROVED PRs are filtered internally."),
172
+ }, async (args) => {
173
+ return await runDirections({ ...args, audience: "human" });
174
+ });
175
+ server.tool("ralph_hero__next_actions", "Compute up to N deterministic 'directions' (next actions) with one flagged `recommended: true`. Used by the /hello skill picker (interactive) and by headless orchestrators (auto-select recommended). Open PRs must be passed in as a parameter.", {
176
+ owner: z
177
+ .string()
178
+ .optional()
179
+ .describe("GitHub owner. Defaults to RALPH_GH_OWNER env var."),
180
+ projectNumbers: z
181
+ .array(z.coerce.number())
182
+ .optional()
183
+ .describe("Project numbers to include. Defaults to RALPH_GH_PROJECT_NUMBERS or single configured project."),
184
+ limit: z
185
+ .number()
186
+ .int()
187
+ .nonnegative()
188
+ .optional()
189
+ .default(3)
190
+ .describe("Max directions to return (default: 3)."),
191
+ audience: z
192
+ .enum(["human", "agent"])
193
+ .optional()
194
+ .default("human")
195
+ .describe("Tilts scoring per consumer; agent penalizes large estimates to honor autonomous-loop XS/S preference (default: human)."),
196
+ stuckThresholdHours: z
197
+ .number()
198
+ .nonnegative()
199
+ .optional()
200
+ .default(48)
201
+ .describe("Hours before a non-lock issue is considered stale (default: 48)."),
202
+ lockStaleHours: z
203
+ .number()
204
+ .nonnegative()
205
+ .optional()
206
+ .default(24)
207
+ .describe("Hours before a lock-state issue is considered stalled (default: 24)."),
208
+ treeRecentDoneDays: z
209
+ .number()
210
+ .nonnegative()
211
+ .optional()
212
+ .default(7)
213
+ .describe("Days within which a sibling Done event still pulls a tree forward (default: 7)."),
214
+ prStaleHours: z
215
+ .number()
216
+ .nonnegative()
217
+ .optional()
218
+ .default(24)
219
+ .describe("Hours before an open PR is considered stale (default: 24)."),
220
+ openPRs: z
221
+ .array(openPRSchema)
222
+ .optional()
223
+ .default([])
224
+ .describe("Open PRs gathered by the caller (e.g. via `gh pr list`). Drafts and APPROVED PRs are filtered internally."),
225
+ }, async (args) => {
226
+ return await runDirections({ ...args, audience: args.audience ?? "human" });
157
227
  });
158
228
  }
159
229
  //# sourceMappingURL=directions-tools.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.75",
3
+ "version": "2.5.79",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",