ralph-hero-mcp-server 2.5.85 → 2.5.87
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/lib/directions.js +117 -9
- package/dist/tools/directions-tools.js +2 -2
- package/package.json +1 -1
package/dist/lib/directions.js
CHANGED
|
@@ -182,6 +182,52 @@ export function detectTreeContinue(item, allItems, config) {
|
|
|
182
182
|
sib.workflowState !== "Canceled");
|
|
183
183
|
return openSiblings.length > 0;
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Compute a structured note describing which `detectTreeContinue` branch
|
|
187
|
+
* fired and the supporting evidence. Returns `null` if the candidate is
|
|
188
|
+
* not actually a tree-continue match (caller should not have called this).
|
|
189
|
+
*
|
|
190
|
+
* Two shapes (matching `detectTreeContinue`'s rule order):
|
|
191
|
+
* - rule (a): "sibling #NNN closed N day(s) ago"
|
|
192
|
+
* - rule (b): "candidate moved N day(s) ago; M open sibling(s)"
|
|
193
|
+
*/
|
|
194
|
+
function buildParentChainNote(item, allItems, config) {
|
|
195
|
+
const parent = item.parentNumber ?? null;
|
|
196
|
+
if (parent === null || parent === undefined)
|
|
197
|
+
return null;
|
|
198
|
+
const siblings = allItems.filter((other) => other.number !== item.number && other.parentNumber === parent);
|
|
199
|
+
// Rule (a): find the most-recently-closed sibling within the window
|
|
200
|
+
// (deterministic: pick the one with the smallest sibling number among
|
|
201
|
+
// those tied on closedAt, matching the natural source order).
|
|
202
|
+
let closestSibling = null;
|
|
203
|
+
for (const sib of siblings) {
|
|
204
|
+
if (!sib.closedAt)
|
|
205
|
+
continue;
|
|
206
|
+
const days = ageDays(sib.closedAt, config.now);
|
|
207
|
+
if (days > config.treeRecentDoneDays)
|
|
208
|
+
continue;
|
|
209
|
+
const dayInt = Math.max(1, Math.floor(days));
|
|
210
|
+
if (closestSibling === null ||
|
|
211
|
+
dayInt < closestSibling.days ||
|
|
212
|
+
(dayInt === closestSibling.days && sib.number < closestSibling.number)) {
|
|
213
|
+
closestSibling = { number: sib.number, days: dayInt };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (closestSibling !== null) {
|
|
217
|
+
const dayLabel = closestSibling.days === 1 ? "day" : "days";
|
|
218
|
+
return `sibling #${closestSibling.number} closed ${closestSibling.days} ${dayLabel} ago`;
|
|
219
|
+
}
|
|
220
|
+
// Rule (b): candidate-moved branch
|
|
221
|
+
const candidateDays = Math.max(1, Math.floor(ageDays(item.updatedAt, config.now)));
|
|
222
|
+
const openSiblings = siblings.filter((sib) => sib.closedAt === null &&
|
|
223
|
+
sib.workflowState !== "Done" &&
|
|
224
|
+
sib.workflowState !== "Canceled");
|
|
225
|
+
if (openSiblings.length === 0)
|
|
226
|
+
return null;
|
|
227
|
+
const dayLabel = candidateDays === 1 ? "day" : "days";
|
|
228
|
+
const sibLabel = openSiblings.length === 1 ? "sibling" : "siblings";
|
|
229
|
+
return `candidate moved ${candidateDays} ${dayLabel} ago; ${openSiblings.length} open ${sibLabel}`;
|
|
230
|
+
}
|
|
185
231
|
// ---------------------------------------------------------------------------
|
|
186
232
|
// scoreIssue
|
|
187
233
|
// ---------------------------------------------------------------------------
|
|
@@ -194,8 +240,10 @@ export function detectTreeContinue(item, allItems, config) {
|
|
|
194
240
|
* else -> kind: "issue"
|
|
195
241
|
*
|
|
196
242
|
* `tags[]` carries descriptive signals (e.g. "stale", "high-priority",
|
|
197
|
-
* "blocked")
|
|
198
|
-
*
|
|
243
|
+
* "blocked"). `signals` carries the structured per-direction explanation
|
|
244
|
+
* (staleDays, parentChainNote, estimateWeight, etc.) that skills use to
|
|
245
|
+
* synthesize prose without rendering the legacy `reason` template
|
|
246
|
+
* verbatim.
|
|
199
247
|
*
|
|
200
248
|
* PR ranking is handled separately by `rankDirections` — this function
|
|
201
249
|
* never returns kind "pr".
|
|
@@ -205,7 +253,8 @@ export function scoreIssue(item, allItems, config) {
|
|
|
205
253
|
let score = priorityScore(item.priority) + phaseScore(item.workflowState);
|
|
206
254
|
// Audience-aware estimate penalty (no-op for "human"; pushes XL items
|
|
207
255
|
// down for "agent" so autonomous loops favor XS/S work).
|
|
208
|
-
|
|
256
|
+
const estPenalty = audiencePenalty(item, config.audience);
|
|
257
|
+
score += estPenalty;
|
|
209
258
|
const lockStale = detectLockStale(item, config);
|
|
210
259
|
const treeContinue = detectTreeContinue(item, allItems, config);
|
|
211
260
|
// Stale boost (non-lock states only)
|
|
@@ -241,7 +290,33 @@ export function scoreIssue(item, allItems, config) {
|
|
|
241
290
|
else {
|
|
242
291
|
kind = "issue";
|
|
243
292
|
}
|
|
244
|
-
|
|
293
|
+
// Compute structured signals for skills to synthesize prose. tiedAtScore
|
|
294
|
+
// is added later by rankDirections after the post-sort pass.
|
|
295
|
+
const signals = { tags: [...tags] };
|
|
296
|
+
if (kind === "lock-stale") {
|
|
297
|
+
const days = Math.max(1, Math.floor(ageHours(item.updatedAt, config.now) / 24));
|
|
298
|
+
signals.staleDays = days;
|
|
299
|
+
signals.staleThresholdDays = config.lockStaleHours / 24;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// For non-lock items, surface the threshold informationally and
|
|
303
|
+
// populate staleDays only when the stale tag fired.
|
|
304
|
+
signals.staleThresholdDays = config.stuckThresholdHours / 24;
|
|
305
|
+
if (isStale) {
|
|
306
|
+
const days = Math.max(1, Math.floor(ageHours(item.updatedAt, config.now) / 24));
|
|
307
|
+
signals.staleDays = days;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (estPenalty > 0) {
|
|
311
|
+
signals.estimateWeight = estPenalty;
|
|
312
|
+
}
|
|
313
|
+
if (kind === "tree-continue") {
|
|
314
|
+
const note = buildParentChainNote(item, allItems, config);
|
|
315
|
+
if (note !== null) {
|
|
316
|
+
signals.parentChainNote = note;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { score, kind, tags, signals };
|
|
245
320
|
}
|
|
246
321
|
function parseIssueNumberFromHeadRef(headRefName) {
|
|
247
322
|
// Match "GH-42" or "GH-0042" anywhere in the ref.
|
|
@@ -279,12 +354,21 @@ function scorePR(pr, config) {
|
|
|
279
354
|
if (score === 0)
|
|
280
355
|
return null;
|
|
281
356
|
const linkedIssueNumber = parseIssueNumberFromHeadRef(pr.headRefName);
|
|
357
|
+
const signals = {
|
|
358
|
+
tags: [...tags],
|
|
359
|
+
prAgeDays: Math.max(1, Math.floor(pr.ageHours / 24)),
|
|
360
|
+
prReviewDecision: pr.reviewDecision,
|
|
361
|
+
};
|
|
362
|
+
if (linkedIssueNumber !== null) {
|
|
363
|
+
signals.linkedIssueNumber = linkedIssueNumber;
|
|
364
|
+
}
|
|
282
365
|
return {
|
|
283
366
|
pr,
|
|
284
367
|
score,
|
|
285
368
|
reason: "", // filled in by buildReason at finalization time
|
|
286
369
|
tags,
|
|
287
370
|
linkedIssueNumber,
|
|
371
|
+
signals,
|
|
288
372
|
};
|
|
289
373
|
}
|
|
290
374
|
// ---------------------------------------------------------------------------
|
|
@@ -295,8 +379,12 @@ function scorePR(pr, config) {
|
|
|
295
379
|
* per kind so the output reads as natural English rather than
|
|
296
380
|
* template-y. No trailing period — the consumer wraps the sentence into
|
|
297
381
|
* a paragraph at presentation time.
|
|
382
|
+
*
|
|
383
|
+
* @deprecated Reason strings are derived from signals for back-compat.
|
|
384
|
+
* Skills should synthesize prose from signals directly. Removed in 2.7.0.
|
|
298
385
|
*/
|
|
299
|
-
export function buildReason(kind, issue, pr,
|
|
386
|
+
export function buildReason(kind, issue, pr, signals, config, linkedIssueNumber = null) {
|
|
387
|
+
const tags = signals.tags;
|
|
300
388
|
if (kind === "pr" && pr) {
|
|
301
389
|
const days = Math.max(1, Math.floor(pr.ageHours / 24));
|
|
302
390
|
const dayLabel = days === 1 ? "day" : "days";
|
|
@@ -390,8 +478,8 @@ export function rankDirections(items, openPRs, config) {
|
|
|
390
478
|
const passesPhaseFilter = isCandidatePhase(item.workflowState) || isLockStale;
|
|
391
479
|
if (!passesPhaseFilter)
|
|
392
480
|
continue;
|
|
393
|
-
const { score, kind, tags } = scoreIssue(item, items, config);
|
|
394
|
-
scored.push({ item, score, kind, tags });
|
|
481
|
+
const { score, kind, tags, signals } = scoreIssue(item, items, config);
|
|
482
|
+
scored.push({ item, score, kind, tags, signals });
|
|
395
483
|
}
|
|
396
484
|
// 2. Drop blocked items unless that would empty the candidate set.
|
|
397
485
|
const unblocked = scored.filter((s) => !hasOpenBlockers(s.item));
|
|
@@ -459,17 +547,32 @@ export function rankDirections(items, openPRs, config) {
|
|
|
459
547
|
}
|
|
460
548
|
// 6. Slice to limit and assign rank.
|
|
461
549
|
const sliced = merged.slice(0, Math.max(0, config.limit));
|
|
550
|
+
// 6a. Compute tied-at-top-score count from the sliced (final) list so the
|
|
551
|
+
// tie reflects what the user actually sees. Stamped onto each entry's
|
|
552
|
+
// signals only when the count is > 1; omitted otherwise.
|
|
553
|
+
const tiedCount = sliced.length === 0
|
|
554
|
+
? 0
|
|
555
|
+
: sliced.filter((entry) => {
|
|
556
|
+
const s = entry.kind === "issueRow" ? entry.payload.score : entry.payload.score;
|
|
557
|
+
const top = sliced[0].kind === "issueRow" ? sliced[0].payload.score : sliced[0].payload.score;
|
|
558
|
+
return s === top;
|
|
559
|
+
}).length;
|
|
462
560
|
const directions = sliced.map((entry, idx) => {
|
|
463
561
|
const rank = idx + 1;
|
|
464
562
|
if (entry.kind === "issueRow") {
|
|
465
563
|
const c = entry.payload;
|
|
466
|
-
const
|
|
564
|
+
const signals = { ...c.signals };
|
|
565
|
+
if (tiedCount > 1 && c.score === sliced[0].payload.score) {
|
|
566
|
+
signals.tiedAtScore = tiedCount;
|
|
567
|
+
}
|
|
568
|
+
const reason = buildReason(c.kind, c.item, null, signals, config, null);
|
|
467
569
|
return {
|
|
468
570
|
rank,
|
|
469
571
|
recommended: false,
|
|
470
572
|
kind: c.kind,
|
|
471
573
|
issue: toDirectionIssue(c.item),
|
|
472
574
|
pr: null,
|
|
575
|
+
signals,
|
|
473
576
|
reason,
|
|
474
577
|
tags: c.tags,
|
|
475
578
|
score: c.score,
|
|
@@ -477,13 +580,18 @@ export function rankDirections(items, openPRs, config) {
|
|
|
477
580
|
}
|
|
478
581
|
// PR row
|
|
479
582
|
const p = entry.payload;
|
|
480
|
-
const
|
|
583
|
+
const signals = { ...p.signals };
|
|
584
|
+
if (tiedCount > 1 && p.score === sliced[0].payload.score) {
|
|
585
|
+
signals.tiedAtScore = tiedCount;
|
|
586
|
+
}
|
|
587
|
+
const reason = buildReason("pr", null, p.pr, signals, config, p.linkedIssueNumber);
|
|
481
588
|
return {
|
|
482
589
|
rank,
|
|
483
590
|
recommended: false,
|
|
484
591
|
kind: "pr",
|
|
485
592
|
issue: null,
|
|
486
593
|
pr: toDirectionPR(p.pr),
|
|
594
|
+
signals,
|
|
487
595
|
reason,
|
|
488
596
|
tags: p.tags,
|
|
489
597
|
score: p.score,
|
|
@@ -126,7 +126,7 @@ export function makeRunDirections(client, fieldCache) {
|
|
|
126
126
|
// ---------------------------------------------------------------------------
|
|
127
127
|
export function registerDirectionsTools(server, client, fieldCache) {
|
|
128
128
|
const runDirections = makeRunDirections(client, fieldCache);
|
|
129
|
-
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.", {
|
|
129
|
+
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.", {
|
|
130
130
|
owner: z
|
|
131
131
|
.string()
|
|
132
132
|
.optional()
|
|
@@ -174,7 +174,7 @@ export function registerDirectionsTools(server, client, fieldCache) {
|
|
|
174
174
|
}, async (args) => {
|
|
175
175
|
return await runDirections({ ...args, audience: "human" });
|
|
176
176
|
});
|
|
177
|
-
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.", {
|
|
177
|
+
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.", {
|
|
178
178
|
owner: z
|
|
179
179
|
.string()
|
|
180
180
|
.optional()
|