ralph-hero-mcp-server 2.5.115 → 2.5.117

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.
@@ -68,6 +68,13 @@ const PR_REVIEW_REQUIRED_BOOST = -200;
68
68
  * waiting for the human's attention.
69
69
  */
70
70
  const HUMAN_NEEDED_UNBLOCK_BOOST = -150;
71
+ /**
72
+ * Penalty applied to Backlog / null-state items that surface only via the
73
+ * `audience === "agent"` fallback. Large positive value ensures Backlog
74
+ * fallback items always rank below any actionable-phase item — Backlog
75
+ * surfaces only when the actionable pool is empty.
76
+ */
77
+ const AGENT_BACKLOG_FALLBACK_PENALTY = 100;
71
78
  /**
72
79
  * Per-estimate penalty applied when audience === "agent". Larger items
73
80
  * cost more (positive score), pushing them down the ranking so agent
@@ -528,6 +535,28 @@ export function rankDirections(items, openPRs, config) {
528
535
  const { score, kind, tags, signals } = scoreIssue(item, items, config);
529
536
  scored.push({ item, score, kind, tags, signals });
530
537
  }
538
+ // 1b. Phase fallback for autonomous audience: when no items passed the
539
+ // standard phase filter, widen the candidate set to include items in
540
+ // `Backlog` and items with a null `workflowState`. This restores
541
+ // autopilot's ability to clear a Backlog-heavy board. Fallback items
542
+ // get +AGENT_BACKLOG_FALLBACK_PENALTY so they always rank below any
543
+ // actionable-phase item — they surface only when the actionable pool
544
+ // is empty. Mirrors the blocker fallback in step 2.
545
+ if (config.audience === "agent" && scored.length === 0) {
546
+ for (const item of items) {
547
+ if (item.workflowState !== "Backlog" && item.workflowState !== null) {
548
+ continue;
549
+ }
550
+ const { score, kind, tags, signals } = scoreIssue(item, items, config);
551
+ scored.push({
552
+ item,
553
+ score: score + AGENT_BACKLOG_FALLBACK_PENALTY,
554
+ kind,
555
+ tags,
556
+ signals,
557
+ });
558
+ }
559
+ }
531
560
  // 2. Drop blocked items unless that would empty the candidate set.
532
561
  const unblocked = scored.filter((s) => !hasOpenBlockers(s.item));
533
562
  let candidates;
@@ -6,19 +6,21 @@
6
6
  * - `ralph_hero__hello_directions` (DEPRECATED alias; fixed at audience="human")
7
7
  *
8
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
+ * for the `hello` skill's session briefing. Open PRs are fetched internally
10
+ * via the configured GitHub token's `repo` scope; callers no longer pass an
11
+ * `openPRs` argument.
12
12
  *
13
13
  * Behaviour:
14
14
  * 1. Resolve owner + project numbers from args or client config.
15
15
  * 2. For each project, ensure the field cache is populated, then
16
16
  * paginate `DASHBOARD_ITEMS_QUERY` to gather up to 500 items.
17
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
18
+ * 4. Derive the unique `repo:owner/name` set from items and run the
19
+ * internal `fetchOpenPRs` helper to gather open PRs.
20
+ * 5. Build a `RankConfig` from the args + defaults (with injected `now`).
21
+ * 6. Compute each PR's `ageHours` at the boundary and call
20
22
  * `rankDirections(allItems, enrichedPRs, config)`.
21
- * 6. Return `{ directions, fetchedAt, totalCandidates }`.
23
+ * 7. Return `{ directions, fetchedAt, totalCandidates }`.
22
24
  *
23
25
  * Determinism: `fetchedAt` is the only time-varying field. Two consecutive
24
26
  * calls on the same board state produce byte-identical `directions[]`
@@ -149,21 +151,76 @@ async function buildUnblockSignalMap(client, items, now) {
149
151
  }
150
152
  return map;
151
153
  }
152
- // ---------------------------------------------------------------------------
153
- // Shared schema fragments
154
- // ---------------------------------------------------------------------------
155
- const openPRSchema = z.object({
156
- number: z.number(),
157
- title: z.string(),
158
- url: z.string(),
159
- isDraft: z.boolean(),
160
- reviewDecision: z
161
- .string()
162
- .nullable()
163
- .describe("REVIEW_REQUIRED | APPROVED | CHANGES_REQUESTED | null"),
164
- headRefName: z.string(),
165
- createdAt: z.string().describe("ISO timestamp from gh pr list"),
166
- });
154
+ /**
155
+ * Derive the unique `owner/repo` set from the project items so the PR search
156
+ * mirrors the project's repo scope. Items lacking a `repository` field (e.g.
157
+ * draft items) are skipped. Returns deterministic order (sorted) so the
158
+ * downstream search query is stable.
159
+ */
160
+ function uniqueRepos(items) {
161
+ const seen = new Set();
162
+ for (const item of items) {
163
+ if (item.repository)
164
+ seen.add(item.repository);
165
+ }
166
+ return Array.from(seen).sort();
167
+ }
168
+ /**
169
+ * Fetch open PRs via GraphQL `search(type: ISSUE)` filtered to
170
+ * `is:pr is:open repo:<nameWithOwner>`. One query per repo — the project's
171
+ * repo set is typically tiny (1–3 repos) and most boards are single-repo so
172
+ * the call shape matches the previous `gh pr list` cost.
173
+ *
174
+ * Returns the raw shape consumed by `runDirections`'s age-enrichment pass.
175
+ * Errors are swallowed and logged via `console.error` so a token without
176
+ * `repo` scope cannot block direction computation — PR-kind directions
177
+ * simply don't surface.
178
+ */
179
+ export async function fetchOpenPRs(client, repos) {
180
+ if (repos.length === 0)
181
+ return [];
182
+ const out = [];
183
+ for (const nameWithOwner of repos) {
184
+ const q = `is:pr is:open repo:${nameWithOwner}`;
185
+ try {
186
+ const data = await client.query(`query OpenPRs($q: String!) {
187
+ search(query: $q, type: ISSUE, first: 100) {
188
+ nodes {
189
+ ... on PullRequest {
190
+ number
191
+ title
192
+ url
193
+ isDraft
194
+ reviewDecision
195
+ headRefName
196
+ createdAt
197
+ }
198
+ }
199
+ }
200
+ }`, { q });
201
+ for (const pr of data.search.nodes) {
202
+ // Defensive: search responses can include empty fragments when the
203
+ // node is not a PullRequest. Skip rows missing required fields.
204
+ if (typeof pr.number !== "number" || !pr.url)
205
+ continue;
206
+ out.push({
207
+ number: pr.number,
208
+ title: pr.title,
209
+ url: pr.url,
210
+ isDraft: pr.isDraft,
211
+ reviewDecision: pr.reviewDecision,
212
+ headRefName: pr.headRefName,
213
+ createdAt: pr.createdAt,
214
+ });
215
+ }
216
+ }
217
+ catch (err) {
218
+ const msg = err instanceof Error ? err.message : String(err);
219
+ console.error(`[ralph-hero] fetchOpenPRs failed for repo ${nameWithOwner}: ${msg}`);
220
+ }
221
+ }
222
+ return out;
223
+ }
167
224
  // ---------------------------------------------------------------------------
168
225
  // Shared implementation — extracted so both `hello_directions` (deprecated)
169
226
  // and `next_actions` (current) can route through the same code path. Also
@@ -211,8 +268,14 @@ export function makeRunDirections(client, fieldCache) {
211
268
  now,
212
269
  unblockSignals,
213
270
  };
271
+ // Fetch open PRs internally for the unique repo set covered by the
272
+ // project items. Replaces the caller-supplied `openPRs` parameter
273
+ // (removed in 2.6.0). One search query per repo; failures yield an
274
+ // empty list so the rest of the ranking still runs.
275
+ const repos = uniqueRepos(allItems);
276
+ const rawOpenPRs = await fetchOpenPRs(client, repos);
214
277
  // Compute PR ageHours at the boundary so the lib never reads the wall clock.
215
- const enrichedPRs = (args.openPRs ?? []).map((pr) => {
278
+ const enrichedPRs = rawOpenPRs.map((pr) => {
216
279
  const t = new Date(pr.createdAt).getTime();
217
280
  const ageHours = Number.isNaN(t)
218
281
  ? 0
@@ -246,7 +309,7 @@ export function makeRunDirections(client, fieldCache) {
246
309
  // ---------------------------------------------------------------------------
247
310
  export function registerDirectionsTools(server, client, fieldCache) {
248
311
  const runDirections = makeRunDirections(client, fieldCache);
249
- 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. Each direction includes a structured signals object (staleDays, staleThresholdDays, tiedAtScore, estimateWeight, parentChainNote) for skills to synthesize prose. The legacy 'reason' string is @deprecated and removed in 2.7.0.", {
312
+ 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. Open PRs are fetched internally via the configured GitHub token's `repo` scope (one `is:pr is:open repo:owner/name` GraphQL search per unique repo represented in the project items). Each direction includes a structured signals object (staleDays, staleThresholdDays, tiedAtScore, estimateWeight, parentChainNote) for skills to synthesize prose. The legacy 'reason' string is @deprecated and removed in 2.7.0.", {
250
313
  owner: z
251
314
  .string()
252
315
  .optional()
@@ -286,15 +349,10 @@ export function registerDirectionsTools(server, client, fieldCache) {
286
349
  .optional()
287
350
  .default(24)
288
351
  .describe("Hours before an open PR is considered stale (default: 24)."),
289
- openPRs: z
290
- .array(openPRSchema)
291
- .optional()
292
- .default([])
293
- .describe("Open PRs gathered by the caller (e.g. via `gh pr list`). Drafts and APPROVED PRs are filtered internally."),
294
352
  }, async (args) => {
295
353
  return await runDirections({ ...args, audience: "human" });
296
354
  });
297
- 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. Each direction includes a structured signals object (staleDays, staleThresholdDays, tiedAtScore, estimateWeight, parentChainNote) for skills to synthesize prose. The legacy 'reason' string is @deprecated and removed in 2.7.0.", {
355
+ 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 are fetched internally via the configured GitHub token's `repo` scope (one `is:pr is:open repo:owner/name` GraphQL search per unique repo represented in the project items) — callers no longer pass an `openPRs` argument. Each direction includes a structured signals object (staleDays, staleThresholdDays, tiedAtScore, estimateWeight, parentChainNote) for skills to synthesize prose. The legacy 'reason' string is @deprecated and removed in 2.7.0. When `audience='agent'` and no items are in actionable phases (Plan in Review, In Review, Ready for Plan, Research Needed) or otherwise surfacing (lock-stale, unblock-requested), the picker falls back to Backlog and null-state items so autopilot can drive triage. Fallback items receive a fixed score penalty so they never outrank actionable items when those exist; the fallback never fires for `audience='human'`.", {
298
356
  owner: z
299
357
  .string()
300
358
  .optional()
@@ -339,11 +397,6 @@ export function registerDirectionsTools(server, client, fieldCache) {
339
397
  .optional()
340
398
  .default(24)
341
399
  .describe("Hours before an open PR is considered stale (default: 24)."),
342
- openPRs: z
343
- .array(openPRSchema)
344
- .optional()
345
- .default([])
346
- .describe("Open PRs gathered by the caller (e.g. via `gh pr list`). Drafts and APPROVED PRs are filtered internally."),
347
400
  }, async (args) => {
348
401
  return await runDirections({ ...args, audience: args.audience ?? "human" });
349
402
  });
@@ -1248,7 +1248,6 @@ export function registerIssueTools(server, client, fieldCache) {
1248
1248
  owner,
1249
1249
  projectNumbers: args.projectNumber !== undefined ? [args.projectNumber] : undefined,
1250
1250
  limit: 50,
1251
- openPRs: [],
1252
1251
  audience: "agent",
1253
1252
  });
1254
1253
  if (directionsResult.isError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.115",
3
+ "version": "2.5.117",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",