ralph-hero-mcp-server 2.5.140 → 2.5.142

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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Issue body + comment body builders for `ralph_hero__collate_debug` Phase 3b.
3
+ *
4
+ * Each `SignatureGroup` returned by `groupSpansBySignature` becomes either:
5
+ * - a fresh GitHub issue (when no existing `debug-auto` issue carries the
6
+ * same hash within the dedup window), or
7
+ * - a comment on an existing issue (occurrence-update).
8
+ *
9
+ * The body MUST include a machine-parseable hash marker on its own line —
10
+ * `**Hash**: \`<8-char-hash>\`` — because dedup in Phase 3b matches on this
11
+ * exact line via GitHub's code-search index.
12
+ *
13
+ * Token-shaped values are scrubbed from every emitted field. The regex set
14
+ * mirrors `redactTokenAttributes` in `telemetry.ts`: GitHub tokens
15
+ * (`^gh[ps]_`), basic-auth headers, and any attribute key ending in
16
+ * `_TOKEN` are replaced with `[REDACTED]`.
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Token redaction (kept local to avoid cross-module coupling at runtime; the
20
+ // shape mirrors telemetry.ts:redactTokenAttributes for consistency)
21
+ // ---------------------------------------------------------------------------
22
+ const GH_TOKEN_VALUE_RE = /\bgh[psour]_[A-Za-z0-9_]{16,}\b/g;
23
+ const TOKEN_KEY_RE = /(_TOKEN|authorization)$/i;
24
+ const BASIC_AUTH_RE = /\bBasic\s+[A-Za-z0-9+/=]{8,}\b/g;
25
+ /**
26
+ * Scrub token-shaped substrings from a free-form string. Used for error
27
+ * messages and serialised metadata before they land in an issue body.
28
+ */
29
+ export function scrubTokensFromString(input) {
30
+ if (!input)
31
+ return input;
32
+ return input.replace(GH_TOKEN_VALUE_RE, "[REDACTED]").replace(BASIC_AUTH_RE, "Basic [REDACTED]");
33
+ }
34
+ /**
35
+ * Scrub token-shaped values from a plain attribute bag. Keys matching
36
+ * `_TOKEN` or `authorization` (case-insensitive) are replaced with
37
+ * `[REDACTED]`; values matching the GitHub token regex are scrubbed too.
38
+ *
39
+ * The result is a shallow copy — callers can serialise it without mutating
40
+ * the input. Nested objects are stringified before scrubbing to keep the
41
+ * function flat and predictable.
42
+ */
43
+ export function scrubTokensFromAttrs(attrs) {
44
+ const out = {};
45
+ for (const [key, value] of Object.entries(attrs)) {
46
+ if (TOKEN_KEY_RE.test(key)) {
47
+ out[key] = "[REDACTED]";
48
+ continue;
49
+ }
50
+ if (typeof value === "string") {
51
+ out[key] = scrubTokensFromString(value);
52
+ }
53
+ else if (value !== null && typeof value === "object") {
54
+ out[key] = scrubTokensFromString(JSON.stringify(value));
55
+ }
56
+ else {
57
+ out[key] = value;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Helpers
64
+ // ---------------------------------------------------------------------------
65
+ const TITLE_MAX = 100;
66
+ const NORMALIZED_MAX = 60;
67
+ function truncate(s, max) {
68
+ if (s.length <= max)
69
+ return s;
70
+ return s.slice(0, max - 1) + "…"; // ellipsis
71
+ }
72
+ /**
73
+ * Pull a short, human-readable error blurb out of a span. Falls back through
74
+ * statusMessage -> exception.message -> metadata.error -> the span name.
75
+ */
76
+ function extractMessage(span) {
77
+ if (!span)
78
+ return "";
79
+ if (span.message)
80
+ return span.message;
81
+ const meta = span.metadata ?? {};
82
+ const exception = meta.exception;
83
+ if (exception &&
84
+ typeof exception === "object" &&
85
+ "message" in exception &&
86
+ typeof exception.message === "string") {
87
+ return exception.message;
88
+ }
89
+ if (typeof meta.error === "string")
90
+ return meta.error;
91
+ if (typeof meta.message === "string")
92
+ return meta.message;
93
+ return "";
94
+ }
95
+ /**
96
+ * Take the third segment of a `${spanName}:${errorType}:${normalized}`
97
+ * signature string. Used to populate the title when no sample span message
98
+ * is available.
99
+ */
100
+ function normalizedFromSignature(signature) {
101
+ const parts = signature.split(":");
102
+ if (parts.length < 3)
103
+ return signature;
104
+ return parts.slice(2).join(":");
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // buildIssueBody
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Build the title + body for a freshly-filed `debug-auto` issue.
111
+ *
112
+ * The body layout is deliberately stable so Phase 3b's dedup regex
113
+ * (`/^\*\*Hash\*\*: `([0-9a-f]{8})`/m`) keeps matching across versions.
114
+ */
115
+ export function buildIssueBody(group, env) {
116
+ const sample = group.sampleSpans[0];
117
+ const rawMessage = extractMessage(sample) || normalizedFromSignature(group.signature);
118
+ const message = scrubTokensFromString(rawMessage);
119
+ const spanName = sample?.name ?? "ralph_hero.error";
120
+ const title = truncate(`[Debug] ${spanName}: ${truncate(message, NORMALIZED_MAX)}`, TITLE_MAX);
121
+ const occurrenceRows = [
122
+ `| Count | First seen | Last seen |`,
123
+ `|---|---|---|`,
124
+ `| ${group.count} | ${group.firstSeen} | ${group.lastSeen} |`,
125
+ ].join("\n");
126
+ const sampleAttrs = sample?.metadata
127
+ ? scrubTokensFromAttrs(sample.metadata)
128
+ : {};
129
+ const errorDetails = Object.keys(sampleAttrs).length
130
+ ? "```json\n" + JSON.stringify(sampleAttrs, null, 2) + "\n```"
131
+ : "_(no attributes captured on sample span)_";
132
+ const reproduction = sample
133
+ ? "```json\n" +
134
+ JSON.stringify({
135
+ spanName: sample.name,
136
+ traceId: sample.traceId,
137
+ startTime: sample.startTime,
138
+ errorType: sample.errorType,
139
+ message: scrubTokensFromString(extractMessage(sample)),
140
+ }, null, 2) +
141
+ "\n```"
142
+ : "_(no sample span available)_";
143
+ const body = [
144
+ `**Hash**: \`${group.hash}\``,
145
+ ``,
146
+ `**Signature**: \`${scrubTokensFromString(group.signature)}\``,
147
+ ``,
148
+ `## First seen`,
149
+ ``,
150
+ `- mcp-server version: \`${env.mcpVersion}\``,
151
+ `- node: \`${env.nodeVersion}\``,
152
+ `- os: \`${env.os}\``,
153
+ ``,
154
+ `## Error details`,
155
+ ``,
156
+ errorDetails,
157
+ ``,
158
+ `## Reproduction (sample span)`,
159
+ ``,
160
+ reproduction,
161
+ ``,
162
+ `## Occurrences`,
163
+ ``,
164
+ occurrenceRows,
165
+ ``,
166
+ `## Langfuse trace`,
167
+ ``,
168
+ `[Open latest example trace](${group.exampleTraceUrl})`,
169
+ ``,
170
+ `---`,
171
+ ``,
172
+ `_Filed automatically by \`ralph_hero__collate_debug\` — Phase 3b (GH-1100). ` +
173
+ `Re-running collation over the same window will append occurrence ` +
174
+ `comments here instead of creating a duplicate issue._`,
175
+ ].join("\n");
176
+ return { title, body };
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // buildCommentBody
180
+ // ---------------------------------------------------------------------------
181
+ /**
182
+ * Build the occurrence-update comment body posted when an existing
183
+ * `debug-auto` issue is matched by hash. `newCount` is the count returned by
184
+ * the current `groupSpansBySignature` run — i.e., occurrences in the *new*
185
+ * window, not the cumulative total (we don't have read access to historical
186
+ * comment counts without extra queries).
187
+ */
188
+ export function buildCommentBody(group, newCount, latestTraceUrl) {
189
+ return [
190
+ `## Recurring occurrence`,
191
+ ``,
192
+ `Detected **${newCount}** new occurrence${newCount === 1 ? "" : "s"} of this signature in the latest collation window.`,
193
+ ``,
194
+ `- Hash: \`${group.hash}\``,
195
+ `- First seen (this window): ${group.firstSeen}`,
196
+ `- Last seen (this window): ${group.lastSeen}`,
197
+ `- [Latest example trace](${scrubTokensFromString(latestTraceUrl)})`,
198
+ ``,
199
+ `_Posted automatically by \`ralph_hero__collate_debug\` (Phase 3b)._`,
200
+ ].join("\n");
201
+ }
202
+ //# sourceMappingURL=debug-issue-shape.js.map
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Provides:
5
5
  * - `ralph_hero__collate_debug` (v2 — queries Langfuse for error spans,
6
- * groups by normalized signature, returns the grouped report; GitHub
7
- * issue creation lands in Phase 3b / GH-1100)
6
+ * groups by normalized signature, dedupes against open `debug-auto`
7
+ * issues, and either creates new issues or appends occurrence comments
8
+ * when `dryRun=false`)
8
9
  * - `ralph_hero__debug_stats` (v1 — aggregates JSONL logs; preserved for
9
10
  * backward compat, not extended)
10
11
  *
@@ -12,14 +13,18 @@
12
13
  * `debug_stats`; the new Langfuse path is fully separate.
13
14
  */
14
15
  import { readdir, readFile } from "node:fs/promises";
15
- import { join } from "node:path";
16
- import { homedir } from "node:os";
16
+ import { readFileSync } from "node:fs";
17
+ import { join, resolve, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { homedir, platform, release } from "node:os";
17
20
  import { createHash } from "node:crypto";
18
21
  import { z } from "zod";
19
22
  import { toolSuccess, toolError } from "../types.js";
20
23
  import { zBoolish } from "../lib/zod-helpers.js";
21
24
  import { createLangfuseClient, } from "../lib/langfuse-client.js";
22
25
  import { groupSpansBySignature, observationToSpan, } from "../lib/error-signature.js";
26
+ import { buildIssueBody, buildCommentBody, } from "../lib/debug-issue-shape.js";
27
+ import { resolveConfig } from "../lib/helpers.js";
23
28
  // ---------------------------------------------------------------------------
24
29
  // JSONL Parsing
25
30
  // ---------------------------------------------------------------------------
@@ -181,6 +186,208 @@ export function aggregateStats(events, groupBy) {
181
186
  groups,
182
187
  };
183
188
  }
189
+ /**
190
+ * Look for an open `debug-auto` issue whose body carries the given 8-char
191
+ * hash on a `**Hash**: \`<hash>\`` line. Only issues updated within the last
192
+ * `withinDays` are considered (default 7), matching the spec's "dedup window".
193
+ *
194
+ * Returns the first matching issue `{ number, id }` or `null` if no match.
195
+ * Search rate-limit errors are swallowed (caller will create a duplicate;
196
+ * the next run will collapse it via comment).
197
+ */
198
+ export async function findExistingDebugIssue(client, owner, repo, hash, withinDays = 7) {
199
+ const sinceIso = new Date(Date.now() - withinDays * 24 * 60 * 60 * 1000)
200
+ .toISOString()
201
+ .slice(0, 10); // YYYY-MM-DD
202
+ // The hash marker `**Hash**: ` is too punctuation-heavy for GitHub's text
203
+ // search index — search on the bare 8-char hex; the marker line is verified
204
+ // by inspecting the issue body below.
205
+ const q = `repo:${owner}/${repo} is:issue is:open label:debug-auto ${hash} in:body updated:>=${sinceIso}`;
206
+ try {
207
+ const data = await client.query(`query DebugIssueSearch($q: String!) {
208
+ search(query: $q, type: ISSUE, first: 10) {
209
+ nodes {
210
+ ... on Issue {
211
+ number
212
+ id
213
+ body
214
+ }
215
+ }
216
+ }
217
+ }`, { q });
218
+ const marker = new RegExp(`^\\*\\*Hash\\*\\*: \`${hash}\``, "m");
219
+ for (const node of data.search.nodes ?? []) {
220
+ if (typeof node.number === "number" &&
221
+ typeof node.id === "string" &&
222
+ typeof node.body === "string" &&
223
+ marker.test(node.body)) {
224
+ return { number: node.number, id: node.id };
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+ catch (error) {
230
+ console.error(`[debug-tools] findExistingDebugIssue search failed (treating as no-match): ${error instanceof Error ? error.message : String(error)}`);
231
+ return null;
232
+ }
233
+ }
234
+ /**
235
+ * Resolve the repository's GraphQL node ID, with SessionCache memoization.
236
+ */
237
+ async function resolveRepoNodeId(client, owner, repo) {
238
+ const cacheKey = `repo-node-id:${owner}/${repo}`;
239
+ const cached = client.getCache().get(cacheKey);
240
+ if (cached)
241
+ return cached;
242
+ const result = await client.query(`query($owner: String!, $repo: String!) {
243
+ repository(owner: $owner, name: $repo) { id }
244
+ }`, { owner, repo });
245
+ const id = result.repository?.id;
246
+ if (!id) {
247
+ throw new Error(`Repository ${owner}/${repo} not found`);
248
+ }
249
+ client.getCache().set(cacheKey, id, 60 * 60 * 1000); // 1 hour
250
+ return id;
251
+ }
252
+ /**
253
+ * Resolve repo-scoped label IDs for `debug-auto` and `ralph-self-report`.
254
+ * Labels that don't exist in the repo are skipped silently — issue creation
255
+ * still proceeds, the label just won't be applied.
256
+ */
257
+ async function resolveLabelIds(client, owner, repo, labelNames) {
258
+ const cacheKey = `repo-labels:${owner}/${repo}`;
259
+ let labels = client.getCache().get(cacheKey);
260
+ if (!labels) {
261
+ const result = await client.query(`query($owner: String!, $repo: String!) {
262
+ repository(owner: $owner, name: $repo) {
263
+ labels(first: 100) { nodes { id name } }
264
+ }
265
+ }`, { owner, repo });
266
+ labels = result.repository?.labels.nodes ?? [];
267
+ client.getCache().set(cacheKey, labels, 5 * 60 * 1000);
268
+ }
269
+ return labelNames
270
+ .map((n) => labels.find((l) => l.name === n)?.id)
271
+ .filter((id) => typeof id === "string");
272
+ }
273
+ /**
274
+ * Create a fresh `debug-auto` issue for a new signature. Returns the new
275
+ * issue number + node ID. Project board placement (Backlog state) is
276
+ * delegated to the existing route-issues.yml workflow — we set labels and
277
+ * the body marker; the workflow handles board routing.
278
+ */
279
+ async function createDebugIssue(client, owner, repo, title, body) {
280
+ const repoId = await resolveRepoNodeId(client, owner, repo);
281
+ const labelIds = await resolveLabelIds(client, owner, repo, [
282
+ "debug-auto",
283
+ "ralph-self-report",
284
+ ]);
285
+ const result = await client.mutate(`mutation($repoId: ID!, $title: String!, $body: String!, $labelIds: [ID!]) {
286
+ createIssue(input: {
287
+ repositoryId: $repoId,
288
+ title: $title,
289
+ body: $body,
290
+ labelIds: $labelIds
291
+ }) {
292
+ issue { id number url }
293
+ }
294
+ }`, {
295
+ repoId,
296
+ title,
297
+ body,
298
+ labelIds: labelIds.length ? labelIds : null,
299
+ });
300
+ const issue = result.createIssue.issue;
301
+ client
302
+ .getCache()
303
+ .set(`issue-node-id:${owner}/${repo}#${issue.number}`, issue.id, 30 * 60 * 1000);
304
+ return issue;
305
+ }
306
+ /**
307
+ * Append an occurrence-update comment to an existing `debug-auto` issue.
308
+ */
309
+ async function commentOnDebugIssue(client, issueNodeId, body) {
310
+ const result = await client.mutate(`mutation($subjectId: ID!, $body: String!) {
311
+ addComment(input: { subjectId: $subjectId, body: $body }) {
312
+ commentEdge { node { id } }
313
+ }
314
+ }`, { subjectId: issueNodeId, body });
315
+ return result.addComment.commentEdge.node.id;
316
+ }
317
+ /**
318
+ * Read the MCP server semver from package.json next to this module. Falls
319
+ * back to `"unknown"` if the file is missing or unreadable. Mirrors the
320
+ * approach used in `telemetry.ts:resolveServiceVersion` but kept local so
321
+ * the debug surface has zero cross-dependency on telemetry init order.
322
+ */
323
+ function readMcpServerVersion() {
324
+ try {
325
+ const here = dirname(fileURLToPath(import.meta.url));
326
+ const pkgPath = resolve(here, "..", "..", "package.json");
327
+ const raw = readFileSync(pkgPath, "utf8");
328
+ const pkg = JSON.parse(raw);
329
+ return pkg.version ?? "unknown";
330
+ }
331
+ catch {
332
+ return "unknown";
333
+ }
334
+ }
335
+ /**
336
+ * Default `IssueShapeEnv` builder — captures the MCP server version, the
337
+ * Node version, and a short OS descriptor at call time. Exposed so tests
338
+ * can override (e.g., deterministic version stamps).
339
+ */
340
+ function defaultEnv(mcpVersion) {
341
+ return {
342
+ mcpVersion: mcpVersion === "unknown" ? readMcpServerVersion() : mcpVersion,
343
+ nodeVersion: process.version,
344
+ os: `${platform()} ${release()}`,
345
+ };
346
+ }
347
+ /**
348
+ * Iterate groups, dedupe each against existing issues, and either create a
349
+ * new issue or post a comment. Returns the counts the tool surfaces back to
350
+ * the caller. Per-group failures are recorded and surfaced but do NOT abort
351
+ * the loop — partial success is preferable to losing the whole run.
352
+ */
353
+ export async function fileOrCommentForGroups(client, owner, repo, groups, env) {
354
+ const results = [];
355
+ let issuesCreated = 0;
356
+ let issuesUpdated = 0;
357
+ for (const group of groups) {
358
+ try {
359
+ const existing = await findExistingDebugIssue(client, owner, repo, group.hash);
360
+ if (existing) {
361
+ const commentBody = buildCommentBody(group, group.count, group.exampleTraceUrl);
362
+ await commentOnDebugIssue(client, existing.id, commentBody);
363
+ issuesUpdated += 1;
364
+ results.push({
365
+ hash: group.hash,
366
+ action: "commented",
367
+ issueNumber: existing.number,
368
+ });
369
+ }
370
+ else {
371
+ const { title, body } = buildIssueBody(group, env);
372
+ const created = await createDebugIssue(client, owner, repo, title, body);
373
+ issuesCreated += 1;
374
+ results.push({
375
+ hash: group.hash,
376
+ action: "created",
377
+ issueNumber: created.number,
378
+ });
379
+ }
380
+ }
381
+ catch (error) {
382
+ results.push({
383
+ hash: group.hash,
384
+ action: "error",
385
+ error: error instanceof Error ? error.message : String(error),
386
+ });
387
+ }
388
+ }
389
+ return { issuesCreated, issuesUpdated, results };
390
+ }
184
391
  let langfuseClientFactory = () => createLangfuseClient();
185
392
  /**
186
393
  * Override the Langfuse client factory. Returns a disposer that restores the
@@ -193,23 +400,20 @@ export function setLangfuseClientFactory(factory) {
193
400
  langfuseClientFactory = prev;
194
401
  };
195
402
  }
196
- export function registerDebugTools(server, client) {
403
+ export function registerDebugTools(server, client, mcpVersion = "unknown") {
197
404
  const logDir = join(homedir(), ".ralph-hero", "logs");
198
- // `client` is referenced by `debug_stats` (legacy) and reserved for Phase 3b
199
- // (GH-1100), which will use it for GitHub dedup + issue creation.
200
- void client;
201
405
  // -------------------------------------------------------------------------
202
- // ralph_hero__collate_debug (v2 — Langfuse path)
406
+ // ralph_hero__collate_debug (v2 — Langfuse + GitHub dedup)
203
407
  // -------------------------------------------------------------------------
204
- server.tool("ralph_hero__collate_debug", "Query Langfuse for error spans in a time window, normalize messages, and group by signature. Phase 3a returns the grouped report only (dryRun forced true); Phase 3b (GH-1100) adds GitHub issue dedup + create/comment. Returns: { since, errorGroups, totalOccurrences, dryRun, groups[] }.", {
408
+ server.tool("ralph_hero__collate_debug", "Query Langfuse for error spans in a time window, normalize messages, group by signature, then either return the grouped report (dryRun=true) or dedupe against open `debug-auto` issues and create / comment (dryRun=false, default). Returns: { since, errorGroups, totalOccurrences, dryRun, issuesCreated?, issuesUpdated?, groups[] }.", {
205
409
  since: z
206
410
  .string()
207
411
  .optional()
208
412
  .describe("ISO date string. Only spans whose startTime >= this value are considered (default: 24h ago)."),
209
413
  dryRun: zBoolish()
210
414
  .optional()
211
- .default(true)
212
- .describe("Phase 3a only honors dryRun=true; passing false returns a stub error until Phase 3b lands."),
415
+ .default(false)
416
+ .describe("If true, return the grouped report without touching GitHub. Default false creates / comments on `debug-auto` issues per signature."),
213
417
  minOccurrences: z
214
418
  .number()
215
419
  .int()
@@ -220,13 +424,10 @@ export function registerDebugTools(server, client) {
220
424
  projectNumber: z
221
425
  .number()
222
426
  .optional()
223
- .describe("Project number override (reserved for Phase 3b)."),
427
+ .describe("Project number override. Currently informational issues land in the configured project via the existing route-issues workflow."),
224
428
  }, async (args) => {
225
429
  try {
226
- const dryRun = args.dryRun ?? true;
227
- if (!dryRun) {
228
- return toolError("dryRun=false requires GH-1100 (Phase 3b) — not yet implemented");
229
- }
430
+ const dryRun = args.dryRun ?? false;
230
431
  const minOccurrences = args.minOccurrences ?? 3;
231
432
  const sinceDate = args.since
232
433
  ? new Date(args.since)
@@ -254,20 +455,46 @@ export function registerDebugTools(server, client) {
254
455
  langfuseHost: langfuse.host,
255
456
  });
256
457
  const totalOccurrences = groups.reduce((sum, g) => sum + g.count, 0);
458
+ const summaryGroups = groups.map((g) => ({
459
+ signature: g.signature,
460
+ hash: g.hash,
461
+ count: g.count,
462
+ firstSeen: g.firstSeen,
463
+ lastSeen: g.lastSeen,
464
+ exampleTraceUrl: g.exampleTraceUrl,
465
+ sampleSpans: g.sampleSpans.slice(0, 3),
466
+ }));
467
+ if (dryRun) {
468
+ return toolSuccess({
469
+ since: fromStartTime,
470
+ errorGroups: groups.length,
471
+ totalOccurrences,
472
+ dryRun: true,
473
+ groups: summaryGroups,
474
+ });
475
+ }
476
+ // dryRun=false — file or comment per signature.
477
+ let owner;
478
+ let repo;
479
+ try {
480
+ const resolved = resolveConfig(client, {});
481
+ owner = resolved.owner;
482
+ repo = resolved.repo;
483
+ }
484
+ catch (error) {
485
+ return toolError(`Cannot resolve owner/repo for issue creation: ${error instanceof Error ? error.message : String(error)}`);
486
+ }
487
+ const env = defaultEnv(mcpVersion);
488
+ const fileResult = await fileOrCommentForGroups(client, owner, repo, groups, env);
257
489
  return toolSuccess({
258
490
  since: fromStartTime,
259
491
  errorGroups: groups.length,
260
492
  totalOccurrences,
261
- dryRun: true,
262
- groups: groups.map((g) => ({
263
- signature: g.signature,
264
- hash: g.hash,
265
- count: g.count,
266
- firstSeen: g.firstSeen,
267
- lastSeen: g.lastSeen,
268
- exampleTraceUrl: g.exampleTraceUrl,
269
- sampleSpans: g.sampleSpans.slice(0, 3),
270
- })),
493
+ dryRun: false,
494
+ issuesCreated: fileResult.issuesCreated,
495
+ issuesUpdated: fileResult.issuesUpdated,
496
+ results: fileResult.results,
497
+ groups: summaryGroups,
271
498
  });
272
499
  }
273
500
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.140",
3
+ "version": "2.5.142",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",