ralph-hero-mcp-server 2.5.74 → 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.
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { registerProjectTools } from "./tools/project-tools.js";
20
20
  import { registerIssueTools } from "./tools/issue-tools.js";
21
21
  import { registerRelationshipTools } from "./tools/relationship-tools.js";
22
22
  import { registerDashboardTools } from "./tools/dashboard-tools.js";
23
+ import { registerDirectionsTools } from "./tools/directions-tools.js";
23
24
  import { registerBatchTools } from "./tools/batch-tools.js";
24
25
  import { registerProjectManagementTools } from "./tools/project-management-tools.js";
25
26
  import { registerHygieneTools } from "./tools/hygiene-tools.js";
@@ -386,6 +387,8 @@ async function main() {
386
387
  registerRelationshipTools(server, client, fieldCache);
387
388
  // Dashboard and pipeline visualization tools
388
389
  registerDashboardTools(server, client, fieldCache);
390
+ // Hello directions tool — deterministic ranked action items
391
+ registerDirectionsTools(server, client, fieldCache);
389
392
  // Phase 5: Batch operations
390
393
  registerBatchTools(server, client, fieldCache);
391
394
  // Project management tools (archive, remove, add, link repo, clear field)
@@ -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
@@ -0,0 +1,229 @@
1
+ /**
2
+ * MCP tools wrapping the pure ranker at `lib/directions.ts`.
3
+ *
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.
12
+ *
13
+ * Behaviour:
14
+ * 1. Resolve owner + project numbers from args or client config.
15
+ * 2. For each project, ensure the field cache is populated, then
16
+ * paginate `DASHBOARD_ITEMS_QUERY` to gather up to 500 items.
17
+ * 3. Convert raw items to `DashboardItem[]` via `toDashboardItems`.
18
+ * 4. Build a `RankConfig` from the args + defaults (with injected `now`).
19
+ * 5. Compute each PR's `ageHours` at the boundary and call
20
+ * `rankDirections(allItems, enrichedPRs, config)`.
21
+ * 6. Return `{ directions, fetchedAt, totalCandidates }`.
22
+ *
23
+ * Determinism: `fetchedAt` is the only time-varying field. Two consecutive
24
+ * calls on the same board state produce byte-identical `directions[]`
25
+ * (after stripping `fetchedAt`).
26
+ */
27
+ import { z } from "zod";
28
+ import { ensureFieldCache } from "../lib/helpers.js";
29
+ import { paginateConnection } from "../lib/pagination.js";
30
+ import { DASHBOARD_ITEMS_QUERY, toDashboardItems, } from "./dashboard-tools.js";
31
+ import { rankDirections, DEFAULT_RANK_CONFIG, } from "../lib/directions.js";
32
+ import { toolSuccess, toolError, resolveProjectOwner, resolveProjectNumbers, } from "../types.js";
33
+ // ---------------------------------------------------------------------------
34
+ // Constants
35
+ // ---------------------------------------------------------------------------
36
+ const HOUR_MS = 60 * 60 * 1000;
37
+ // ---------------------------------------------------------------------------
38
+ // Shared schema fragments
39
+ // ---------------------------------------------------------------------------
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) {
59
+ try {
60
+ const owner = args.owner || resolveProjectOwner(client.config);
61
+ if (!owner) {
62
+ return toolError("owner is required");
63
+ }
64
+ const projectNumbers = args.projectNumbers ?? resolveProjectNumbers(client.config);
65
+ if (projectNumbers.length === 0) {
66
+ return toolError("No project numbers configured. Set RALPH_GH_PROJECT_NUMBER or RALPH_GH_PROJECT_NUMBERS.");
67
+ }
68
+ // Inject `now` at the boundary so the lib remains deterministic.
69
+ const now = new Date();
70
+ const allItems = [];
71
+ for (const pn of projectNumbers) {
72
+ await ensureFieldCache(client, fieldCache, owner, pn);
73
+ const projectId = fieldCache.getProjectId(pn);
74
+ if (!projectId) {
75
+ // Defensive: ensureFieldCache succeeded but no projectId.
76
+ // Skip this project rather than blowing up the whole call.
77
+ continue;
78
+ }
79
+ const result = await paginateConnection((q, v) => client.projectQuery(q, v), DASHBOARD_ITEMS_QUERY, { projectId, first: 100 }, "node.items", { maxItems: 500 });
80
+ allItems.push(...toDashboardItems(result.nodes, pn));
81
+ }
82
+ // Build the RankConfig from args + defaults + injected `now`.
83
+ const config = {
84
+ limit: args.limit ?? DEFAULT_RANK_CONFIG.limit,
85
+ stuckThresholdHours: args.stuckThresholdHours ?? DEFAULT_RANK_CONFIG.stuckThresholdHours,
86
+ lockStaleHours: args.lockStaleHours ?? DEFAULT_RANK_CONFIG.lockStaleHours,
87
+ treeRecentDoneDays: args.treeRecentDoneDays ?? DEFAULT_RANK_CONFIG.treeRecentDoneDays,
88
+ prStaleHours: args.prStaleHours ?? DEFAULT_RANK_CONFIG.prStaleHours,
89
+ audience: args.audience,
90
+ now,
91
+ };
92
+ // Compute PR ageHours at the boundary so the lib never reads the wall clock.
93
+ const enrichedPRs = (args.openPRs ?? []).map((pr) => {
94
+ const t = new Date(pr.createdAt).getTime();
95
+ const ageHours = Number.isNaN(t)
96
+ ? 0
97
+ : Math.max(0, (now.getTime() - t) / HOUR_MS);
98
+ return {
99
+ number: pr.number,
100
+ title: pr.title,
101
+ url: pr.url,
102
+ isDraft: pr.isDraft,
103
+ reviewDecision: pr.reviewDecision,
104
+ headRefName: pr.headRefName,
105
+ createdAt: pr.createdAt,
106
+ ageHours,
107
+ };
108
+ });
109
+ const directions = rankDirections(allItems, enrichedPRs, config);
110
+ return toolSuccess({
111
+ directions,
112
+ fetchedAt: now.toISOString(),
113
+ totalCandidates: allItems.length,
114
+ });
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ return toolError(`Failed to compute hello directions: ${message}`);
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" });
227
+ });
228
+ }
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.74",
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",