ralph-hero-mcp-server 2.5.118 → 2.5.120

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
@@ -16,6 +16,7 @@ import { FieldOptionCache } from "./lib/cache.js";
16
16
  import { createDebugLogger, wrapServerToolWithLogging } from "./lib/debug-logger.js";
17
17
  import { toolSuccess, resolveProjectOwner } from "./types.js";
18
18
  import { resolveRepoFromProject } from "./lib/helpers.js";
19
+ import { detectOrphanRepoIssues } from "./lib/health.js";
19
20
  import { registerProjectTools } from "./tools/project-tools.js";
20
21
  import { registerIssueTools } from "./tools/issue-tools.js";
21
22
  import { registerRelationshipTools } from "./tools/relationship-tools.js";
@@ -188,7 +189,7 @@ function initGitHubClient(debugLogger) {
188
189
  */
189
190
  function registerCoreTools(server, client) {
190
191
  // Health check tool - comprehensive validation of auth, repo, project, and fields
191
- server.tool("ralph_hero__health_check", "Validate GitHub API connectivity, token permissions, repo access, project access, and required fields", {}, async () => {
192
+ server.tool("ralph_hero__health_check", "Validate GitHub API connectivity, token permissions, repo access, project access, and required fields. Also surfaces repo-scope mismatch via the `orphanRepoIssues` field when OPEN issues exist in the repo but are NOT on the configured project board (such issues are invisible to discovery tools like next_actions, list_issues, pipeline_dashboard, project_hygiene). The field is omitted entirely when the board contains every OPEN repo issue. Shape when present: `{ count, repoOpen, boardItems, sample: number[], note }` — `sample` is up to 10 orphan issue numbers (ascending) for diagnostic display.", {}, async () => {
192
193
  const checks = {};
193
194
  // 1. Auth check (repo token)
194
195
  try {
@@ -297,6 +298,26 @@ function registerCoreTools(server, client) {
297
298
  detail: "RALPH_GH_PROJECT_NUMBER not set",
298
299
  };
299
300
  }
301
+ // 5. Orphan repo issues check — only runs when both repo and project
302
+ // access succeeded above. Failures here are non-fatal: orphan detection
303
+ // is informational and shouldn't downgrade the overall health status,
304
+ // because the underlying access checks already capture the real failure
305
+ // mode (broken token, missing project, etc.).
306
+ let orphanRepoIssues = null;
307
+ if (checks.repoAccess?.status === "ok" &&
308
+ checks.projectAccess?.status === "ok" &&
309
+ client.config.owner &&
310
+ client.config.repo &&
311
+ projOwner &&
312
+ projNum) {
313
+ try {
314
+ orphanRepoIssues = await detectOrphanRepoIssues(client, client.config.owner, client.config.repo, projOwner, projNum);
315
+ }
316
+ catch (e) {
317
+ // Non-fatal — log via stderr but don't fail health_check.
318
+ console.error(`[ralph-hero] Orphan repo issues check failed: ${e instanceof Error ? e.message : String(e)}`);
319
+ }
320
+ }
300
321
  // Token source detection — re-derive which env vars resolved
301
322
  const repoTokenSource = resolveEnv("RALPH_GH_REPO_TOKEN")
302
323
  ? "RALPH_GH_REPO_TOKEN"
@@ -327,6 +348,9 @@ function registerCoreTools(server, client) {
327
348
  ? `Repo operations use ${repoTokenSource}, project operations use ${projectTokenSource}`
328
349
  : `Both repo and project operations use ${repoTokenSource}`,
329
350
  },
351
+ // Omit the field entirely when there are no orphans, per the field's
352
+ // contract (`null` from `detectOrphanRepoIssues` means "clean board").
353
+ ...(orphanRepoIssues ? { orphanRepoIssues } : {}),
330
354
  });
331
355
  });
332
356
  }
@@ -5,12 +5,13 @@
5
5
  * I/O (GraphQL fetching) lives in tools/dashboard-tools.ts.
6
6
  */
7
7
  import { STATE_ORDER, LOCK_STATES, TERMINAL_STATES, HUMAN_STATES, } from "./workflow-states.js";
8
+ import { ARCHIVE_AGE_DAYS, CRITICAL_STUCK_HOURS, RECENT_WINDOW_DAYS, STUCK_THRESHOLD_HOURS, } from "./thresholds.js";
8
9
  export const DEFAULT_HEALTH_CONFIG = {
9
- stuckThresholdHours: 48,
10
- criticalStuckHours: 96,
10
+ stuckThresholdHours: STUCK_THRESHOLD_HOURS,
11
+ criticalStuckHours: CRITICAL_STUCK_HOURS,
11
12
  wipLimits: {},
12
- doneWindowDays: 7,
13
- archiveThresholdDays: 14,
13
+ doneWindowDays: RECENT_WINDOW_DAYS,
14
+ archiveAgeDays: ARCHIVE_AGE_DAYS,
14
15
  };
15
16
  // ---------------------------------------------------------------------------
16
17
  // Priority helpers
@@ -282,13 +283,13 @@ const DAY_MS = 24 * 60 * 60 * 1000;
282
283
  /**
283
284
  * Compute archive eligibility stats from project items.
284
285
  *
285
- * - "Eligible for archive": Done/Canceled items stale beyond archiveThresholdDays
286
+ * - "Eligible for archive": Done/Canceled items stale beyond archiveAgeDays
286
287
  * - "Recently completed": Done/Canceled items within doneWindowDays
287
288
  * - Staleness computed from closedAt (preferred) or updatedAt (fallback)
288
289
  * - Zero additional API calls — works on already-fetched items.
289
290
  */
290
- export function computeArchiveStats(items, now, archiveThresholdDays, doneWindowDays) {
291
- const thresholdMs = archiveThresholdDays * DAY_MS;
291
+ export function computeArchiveStats(items, now, archiveAgeDays, doneWindowDays) {
292
+ const thresholdMs = archiveAgeDays * DAY_MS;
292
293
  const recentMs = doneWindowDays * DAY_MS;
293
294
  const eligible = [];
294
295
  let recentlyCompleted = 0;
@@ -317,7 +318,7 @@ export function computeArchiveStats(items, now, archiveThresholdDays, doneWindow
317
318
  eligibleForArchive: eligible.length,
318
319
  eligibleItems: eligible,
319
320
  recentlyCompleted,
320
- archiveThresholdDays,
321
+ archiveAgeDays,
321
322
  };
322
323
  }
323
324
  // ---------------------------------------------------------------------------
@@ -437,7 +438,7 @@ export function buildIterationSection(items) {
437
438
  export function buildDashboard(items, config = DEFAULT_HEALTH_CONFIG, now = Date.now(), streams) {
438
439
  const phases = aggregateByPhase(items, now, config);
439
440
  const warnings = detectHealthIssues(phases, config);
440
- const archive = computeArchiveStats(items, now, config.archiveThresholdDays, config.doneWindowDays);
441
+ const archive = computeArchiveStats(items, now, config.archiveAgeDays, config.doneWindowDays);
441
442
  // Per-project breakdown (only when items span multiple projects)
442
443
  const projectGroups = new Map();
443
444
  for (const item of items) {
@@ -580,7 +581,7 @@ export function formatMarkdown(data, issuesPerPhase = 10) {
580
581
  lines.push("");
581
582
  lines.push("## Archive Eligibility");
582
583
  lines.push("");
583
- lines.push(`**Eligible for archive**: ${data.archive.eligibleForArchive} items (stale > ${data.archive.archiveThresholdDays} days in Done/Canceled)`);
584
+ lines.push(`**Eligible for archive**: ${data.archive.eligibleForArchive} items (stale > ${data.archive.archiveAgeDays} days in Done/Canceled)`);
584
585
  lines.push(`**Recently completed**: ${data.archive.recentlyCompleted} items`);
585
586
  if (data.archive.eligibleItems.length > 0) {
586
587
  lines.push("");
@@ -755,7 +756,7 @@ export function formatAscii(data) {
755
756
  }
756
757
  // Archive summary
757
758
  if (data.archive) {
758
- lines.push(`Archive: ${data.archive.eligibleForArchive} eligible (threshold: ${data.archive.archiveThresholdDays}d), ${data.archive.recentlyCompleted} recent`);
759
+ lines.push(`Archive: ${data.archive.eligibleForArchive} eligible (threshold: ${data.archive.archiveAgeDays}d), ${data.archive.recentlyCompleted} recent`);
759
760
  }
760
761
  // Per-project breakdown (only for multi-project)
761
762
  if (data.projectBreakdowns &&
@@ -28,12 +28,13 @@
28
28
  * PRs are scored separately and only merged into the final ranking.
29
29
  */
30
30
  import { LOCK_STATES, STATE_ORDER } from "./workflow-states.js";
31
+ import { AGENT_BACKLOG_FALLBACK_PENALTY, LOCK_STALE_HOURS, PR_STALE_HOURS, RECENT_WINDOW_DAYS, STUCK_THRESHOLD_HOURS, } from "./thresholds.js";
31
32
  export const DEFAULT_RANK_CONFIG = {
32
33
  limit: 3,
33
- stuckThresholdHours: 48,
34
- lockStaleHours: 24,
35
- treeRecentDoneDays: 7,
36
- prStaleHours: 24,
34
+ stuckThresholdHours: STUCK_THRESHOLD_HOURS,
35
+ lockStaleHours: LOCK_STALE_HOURS,
36
+ treeRecentDoneDays: RECENT_WINDOW_DAYS,
37
+ prStaleHours: PR_STALE_HOURS,
37
38
  audience: "human",
38
39
  };
39
40
  // ---------------------------------------------------------------------------
@@ -68,13 +69,6 @@ const PR_REVIEW_REQUIRED_BOOST = -200;
68
69
  * waiting for the human's attention.
69
70
  */
70
71
  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;
78
72
  /**
79
73
  * Per-estimate penalty applied when audience === "agent". Larger items
80
74
  * cost more (positive score), pushing them down the ranking so agent
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Health-check helpers — pure functions that probe the GitHub API for
3
+ * configuration drift between the project board and its underlying repo.
4
+ *
5
+ * `detectOrphanRepoIssues` compares the set of OPEN issues in the configured
6
+ * repo against the set of `type=ISSUE` items on the configured project
7
+ * board. Issues present in the repo but not on the board are "orphans" —
8
+ * they exist but are structurally invisible to the discovery tools
9
+ * (`next_actions`, `list_issues`, `pipeline_dashboard`, `project_hygiene`),
10
+ * which all read from the project board.
11
+ *
12
+ * Surfaced via `health_check` so users discover the mismatch without having
13
+ * to grep `gh issue list` against the board contents by hand.
14
+ */
15
+ /**
16
+ * Maximum number of orphan issue numbers to include in the response sample.
17
+ * The sample is for diagnostic display only; the full set is summarized via `count`.
18
+ */
19
+ export const ORPHAN_SAMPLE_LIMIT = 10;
20
+ /**
21
+ * Page size used when paginating repo issues and project items.
22
+ *
23
+ * GitHub GraphQL caps `first:` at 100. Most projects fit comfortably in a
24
+ * handful of pages — pagination is implemented for correctness, not because
25
+ * every call hits it.
26
+ */
27
+ const PAGE_SIZE = 100;
28
+ /**
29
+ * Hard cap on pages walked when enumerating repo issues or project items.
30
+ * Defensive: prevents a runaway loop on a misconfigured giant repo.
31
+ */
32
+ const MAX_PAGES = 20;
33
+ /**
34
+ * Standard explanatory note attached to the orphan-repo-issues warning.
35
+ * Lifted to a constant so tests can assert against a single canonical string.
36
+ */
37
+ export const ORPHAN_REPO_ISSUES_NOTE = "Issues exist in the repo that are not on the project board. They are " +
38
+ "invisible to discovery tools (next_actions, list_issues, pipeline_dashboard, " +
39
+ "project_hygiene). To make them visible, add them to the project or use " +
40
+ "'gh issue list' directly.";
41
+ /**
42
+ * Fetch all OPEN issue numbers in the repo via paginated GraphQL.
43
+ *
44
+ * Returns `{ totalCount, numbers }`. `totalCount` is taken from the first
45
+ * page (it is constant across pages). `numbers` is the union of all
46
+ * `node.number` values walked.
47
+ */
48
+ async function fetchRepoOpenIssueNumbers(client, owner, repo) {
49
+ const numbers = new Set();
50
+ let totalCount = 0;
51
+ let cursor = null;
52
+ let pages = 0;
53
+ while (pages < MAX_PAGES) {
54
+ const response = await client.query(`query($owner: String!, $repo: String!, $cursor: String, $first: Int!) {
55
+ repository(owner: $owner, name: $repo) {
56
+ issues(states: OPEN, first: $first, after: $cursor) {
57
+ totalCount
58
+ pageInfo { hasNextPage endCursor }
59
+ nodes { number }
60
+ }
61
+ }
62
+ }`, { owner, repo, cursor, first: PAGE_SIZE });
63
+ const page = response.repository?.issues;
64
+ if (!page)
65
+ break;
66
+ if (pages === 0)
67
+ totalCount = page.totalCount;
68
+ for (const node of page.nodes)
69
+ numbers.add(node.number);
70
+ if (!page.pageInfo.hasNextPage)
71
+ break;
72
+ cursor = page.pageInfo.endCursor;
73
+ pages += 1;
74
+ }
75
+ return { totalCount, numbers };
76
+ }
77
+ /**
78
+ * Fetch all `type=ISSUE` item numbers on the project board via paginated GraphQL.
79
+ *
80
+ * The query tries `user(login)` first, then falls back to `organization(login)`
81
+ * to handle both account types. PRs and DraftIssues are skipped — only true
82
+ * `Issue`-typed content with a `number` field is collected.
83
+ */
84
+ async function fetchBoardIssueNumbers(client, projectOwner, projectNumber) {
85
+ const numbers = new Set();
86
+ let totalCount = 0;
87
+ let cursor = null;
88
+ let pages = 0;
89
+ // The project items query is identical across user/org owner — only the
90
+ // root selector changes. Build a closure that runs the same shape and
91
+ // tries both owner types on the first page.
92
+ const runQuery = async (cur) => {
93
+ for (const ownerType of ["user", "organization"]) {
94
+ try {
95
+ const res = await client.projectQuery(`query($owner: String!, $number: Int!, $cursor: String, $first: Int!) {
96
+ ${ownerType}(login: $owner) {
97
+ projectV2(number: $number) {
98
+ items(first: $first, after: $cursor) {
99
+ totalCount
100
+ pageInfo { hasNextPage endCursor }
101
+ nodes {
102
+ type
103
+ content {
104
+ __typename
105
+ ... on Issue { number }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }`, { owner: projectOwner, number: projectNumber, cursor: cur, first: PAGE_SIZE });
112
+ const proj = res[ownerType]?.projectV2;
113
+ if (proj)
114
+ return proj;
115
+ }
116
+ catch {
117
+ // Try next owner type
118
+ }
119
+ }
120
+ return null;
121
+ };
122
+ while (pages < MAX_PAGES) {
123
+ const proj = await runQuery(cursor);
124
+ if (!proj || !proj.items)
125
+ break;
126
+ if (pages === 0)
127
+ totalCount = proj.items.totalCount;
128
+ for (const node of proj.items.nodes) {
129
+ // Only count Issue-typed content. Type field is "ISSUE" but we double-check
130
+ // via __typename because draft-issue items can have type=DRAFT_ISSUE and
131
+ // should never count as repo-issue overlap.
132
+ if (node.type === "ISSUE" &&
133
+ node.content?.__typename === "Issue" &&
134
+ typeof node.content.number === "number") {
135
+ numbers.add(node.content.number);
136
+ }
137
+ }
138
+ if (!proj.items.pageInfo.hasNextPage)
139
+ break;
140
+ cursor = proj.items.pageInfo.endCursor;
141
+ pages += 1;
142
+ }
143
+ return { totalCount, numbers };
144
+ }
145
+ /**
146
+ * Detect repo issues that are absent from the project board.
147
+ *
148
+ * Returns the orphan summary when at least one orphan exists, or `null`
149
+ * when the board contains every OPEN repo issue.
150
+ *
151
+ * Returns `null` (not an empty result) so the `health_check` caller can
152
+ * omit the field entirely on a clean board — keeping the response tight
153
+ * for the common no-orphans case.
154
+ */
155
+ export async function detectOrphanRepoIssues(client, owner, repo, projectOwner, projectNumber) {
156
+ const [repoSide, boardSide] = await Promise.all([
157
+ fetchRepoOpenIssueNumbers(client, owner, repo),
158
+ fetchBoardIssueNumbers(client, projectOwner, projectNumber),
159
+ ]);
160
+ // Orphans = OPEN issues in the repo whose number is NOT in the board set.
161
+ // We compare against the actual numbers walked rather than just totalCount,
162
+ // because a board may contain issues from OTHER repos (multi-repo project)
163
+ // — those would inflate `boardItems` but contribute zero to the overlap.
164
+ const orphanNumbers = [];
165
+ for (const n of repoSide.numbers) {
166
+ if (!boardSide.numbers.has(n))
167
+ orphanNumbers.push(n);
168
+ }
169
+ orphanNumbers.sort((a, b) => a - b);
170
+ if (orphanNumbers.length === 0)
171
+ return null;
172
+ return {
173
+ count: orphanNumbers.length,
174
+ repoOpen: repoSide.totalCount,
175
+ boardItems: boardSide.totalCount,
176
+ sample: orphanNumbers.slice(0, ORPHAN_SAMPLE_LIMIT),
177
+ note: ORPHAN_REPO_ISSUES_NOTE,
178
+ };
179
+ }
180
+ //# sourceMappingURL=health.js.map
@@ -6,12 +6,13 @@
6
6
  */
7
7
  import { TERMINAL_STATES } from "./workflow-states.js";
8
8
  import { groupDashboardItemsByRepo } from "./dashboard.js";
9
+ import { ARCHIVE_AGE_DAYS, ORPHAN_AGE_DAYS, RECENT_WINDOW_DAYS, SIMILARITY_THRESHOLD, } from "./thresholds.js";
9
10
  export const DEFAULT_HYGIENE_CONFIG = {
10
- archiveDays: 14,
11
- staleDays: 7,
12
- orphanDays: 14,
11
+ archiveAgeDays: ARCHIVE_AGE_DAYS,
12
+ staleDays: RECENT_WINDOW_DAYS,
13
+ orphanDays: ORPHAN_AGE_DAYS,
13
14
  wipLimits: {},
14
- similarityThreshold: 0.8,
15
+ similarityThreshold: SIMILARITY_THRESHOLD,
15
16
  };
16
17
  // ---------------------------------------------------------------------------
17
18
  // Helpers
@@ -32,9 +33,9 @@ function toHygieneItem(item, now) {
32
33
  // Section functions
33
34
  // ---------------------------------------------------------------------------
34
35
  /**
35
- * Items in terminal states (Done/Canceled) older than archiveDays.
36
+ * Items in terminal states (Done/Canceled) older than archiveAgeDays.
36
37
  */
37
- export function findArchiveCandidates(items, now, archiveDays) {
38
+ export function findArchiveCandidates(items, now, archiveAgeDays) {
38
39
  return items
39
40
  .filter((item) => {
40
41
  if (item.subIssueCount > 0)
@@ -43,7 +44,7 @@ export function findArchiveCandidates(items, now, archiveDays) {
43
44
  if (!ws || !TERMINAL_STATES.includes(ws))
44
45
  return false;
45
46
  const ts = item.closedAt ?? item.updatedAt;
46
- return ageDays(ts, now) > archiveDays;
47
+ return ageDays(ts, now) > archiveAgeDays;
47
48
  })
48
49
  .map((item) => toHygieneItem(item, now));
49
50
  }
@@ -199,7 +200,7 @@ export function findDuplicateCandidates(items, now, threshold) {
199
200
  * Does NOT recursively emit `repoBreakdowns` — per-repo breakdowns are flat.
200
201
  */
201
202
  function buildRepoBreakdown(repoName, items, config, now) {
202
- const archiveCandidates = findArchiveCandidates(items, now, config.archiveDays);
203
+ const archiveCandidates = findArchiveCandidates(items, now, config.archiveAgeDays);
203
204
  const staleItems = findStaleItems(items, now, config.staleDays);
204
205
  const orphanedItems = findOrphanedItems(items, now, config.orphanDays);
205
206
  const fieldGaps = findFieldGaps(items, now);
@@ -235,7 +236,7 @@ function buildRepoBreakdown(repoName, items, config, now) {
235
236
  * Build a complete hygiene report from project items.
236
237
  */
237
238
  export function buildHygieneReport(items, config = DEFAULT_HYGIENE_CONFIG, now = Date.now()) {
238
- const archiveCandidates = findArchiveCandidates(items, now, config.archiveDays);
239
+ const archiveCandidates = findArchiveCandidates(items, now, config.archiveAgeDays);
239
240
  const staleItems = findStaleItems(items, now, config.staleDays);
240
241
  const orphanedItems = findOrphanedItems(items, now, config.orphanDays);
241
242
  const fieldGaps = findFieldGaps(items, now);
@@ -4,10 +4,11 @@
4
4
  * All functions are side-effect-free: dashboard data in, metrics out.
5
5
  * Designed to complement lib/dashboard.ts without modifying it.
6
6
  */
7
+ import { AT_RISK_THRESHOLD, OFF_TRACK_THRESHOLD, RECENT_WINDOW_DAYS, } from "./thresholds.js";
7
8
  export const DEFAULT_METRICS_CONFIG = {
8
- velocityWindowDays: 7,
9
- atRiskThreshold: 2,
10
- offTrackThreshold: 6,
9
+ velocityWindowDays: RECENT_WINDOW_DAYS,
10
+ atRiskThreshold: AT_RISK_THRESHOLD,
11
+ offTrackThreshold: OFF_TRACK_THRESHOLD,
11
12
  severityWeights: { critical: 3, warning: 1, info: 0 },
12
13
  };
13
14
  // ---------------------------------------------------------------------------
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared threshold defaults across discovery tools.
3
+ *
4
+ * Single source of truth for every "magic number" that gates the
5
+ * discovery surface (next_actions, pipeline_dashboard, project_hygiene,
6
+ * metrics_trends). Per-module CONFIG objects (DEFAULT_RANK_CONFIG,
7
+ * DEFAULT_HEALTH_CONFIG, DEFAULT_HYGIENE_CONFIG, DEFAULT_METRICS_CONFIG)
8
+ * pull from this module so changing a value in one place propagates
9
+ * everywhere it's referenced.
10
+ *
11
+ * Where the same value appears under multiple names (RECENT_WINDOW_DAYS),
12
+ * the names describe distinct concepts but the value is shared so changing
13
+ * one changes all related places.
14
+ */
15
+ // Lock-state staleness — short, hours-based because lock collisions are urgent.
16
+ export const LOCK_STALE_HOURS = 24;
17
+ // PR staleness — short, hours-based because review timeliness matters.
18
+ export const PR_STALE_HOURS = 24;
19
+ // Non-lock stuck threshold — longer, hours-based for warnings.
20
+ export const STUCK_THRESHOLD_HOURS = 48;
21
+ export const CRITICAL_STUCK_HOURS = STUCK_THRESHOLD_HOURS * 2;
22
+ // Recent activity window — single shared value for "recent enough to be relevant."
23
+ // Used by:
24
+ // - hygiene.staleDays → "non-terminal item hasn't moved in N days"
25
+ // - dashboard.doneWindowDays → "show recent completions"
26
+ // - directions.treeRecentDoneDays → "sibling done within window"
27
+ // - metrics.velocityWindowDays → "completion window for velocity calc"
28
+ export const RECENT_WINDOW_DAYS = 7;
29
+ // Archive age — unified replacement for the old archiveThresholdDays
30
+ // (dashboard) + archiveDays (hygiene) duplication. Both previously
31
+ // defaulted to 14 and described the same concept (Done/Canceled item age
32
+ // before archive eligibility).
33
+ export const ARCHIVE_AGE_DAYS = 14;
34
+ // Backlog assignment-gap — separate concept from archive age.
35
+ export const ORPHAN_AGE_DAYS = 14;
36
+ // Risk score classifications.
37
+ export const AT_RISK_THRESHOLD = 2;
38
+ export const OFF_TRACK_THRESHOLD = 6;
39
+ // Duplicate detection similarity (0-1).
40
+ export const SIMILARITY_THRESHOLD = 0.8;
41
+ // Phase 1 fallback penalty — keeps Backlog items below actionable items
42
+ // when the audience="agent" fallback fires.
43
+ export const AGENT_BACKLOG_FALLBACK_PENALTY = 100;
44
+ //# sourceMappingURL=thresholds.js.map
@@ -43,7 +43,7 @@ export function registerDashboardTools(server, client, fieldCache) {
43
43
  .number()
44
44
  .optional()
45
45
  .default(48)
46
- .describe("Hours before flagging stuck issues (default: 48)"),
46
+ .describe("Hours before flagging stuck issues (default: 48, unit: hours). Shared with next_actions.stuckThresholdHours — both pull from STUCK_THRESHOLD_HOURS in src/lib/thresholds.ts."),
47
47
  wipLimits: z
48
48
  .record(z.coerce.number())
49
49
  .optional()
@@ -52,7 +52,7 @@ export function registerDashboardTools(server, client, fieldCache) {
52
52
  .number()
53
53
  .optional()
54
54
  .default(7)
55
- .describe("Only show Done issues from last N days (default: 7)"),
55
+ .describe("Only show Done issues from last N days (default: 7, unit: days). Shares the RECENT_WINDOW_DAYS value with hygiene.staleDays, next_actions.treeRecentDoneDays, and metrics.velocityWindowDays."),
56
56
  issuesPerPhase: z
57
57
  .number()
58
58
  .optional()
@@ -67,22 +67,22 @@ export function registerDashboardTools(server, client, fieldCache) {
67
67
  .number()
68
68
  .optional()
69
69
  .default(7)
70
- .describe("Days to look back for velocity calculation (default: 7)"),
70
+ .describe("Days to look back for velocity calculation (default: 7, unit: days). Shares the RECENT_WINDOW_DAYS value with hygiene.staleDays, dashboard.doneWindowDays, and next_actions.treeRecentDoneDays."),
71
71
  atRiskThreshold: z
72
72
  .number()
73
73
  .optional()
74
74
  .default(2)
75
- .describe("Risk score threshold for AT_RISK status (default: 2)"),
75
+ .describe("Risk score threshold for AT_RISK status (default: 2, unit: count). Pulls from AT_RISK_THRESHOLD in src/lib/thresholds.ts."),
76
76
  offTrackThreshold: z
77
77
  .number()
78
78
  .optional()
79
79
  .default(6)
80
- .describe("Risk score threshold for OFF_TRACK status (default: 6)"),
81
- archiveThresholdDays: z
80
+ .describe("Risk score threshold for OFF_TRACK status (default: 6, unit: count). Pulls from OFF_TRACK_THRESHOLD in src/lib/thresholds.ts."),
81
+ archiveAgeDays: z
82
82
  .number()
83
83
  .optional()
84
84
  .default(14)
85
- .describe("Days in Done/Canceled before eligible for archive (default: 14)"),
85
+ .describe("Days in Done/Canceled before eligible for archive (default: 14, unit: days). Same concept as project_hygiene.archiveAgeDays — both renamed from the legacy archiveThresholdDays/archiveDays pair so the value is shared across discovery tools."),
86
86
  streams: z
87
87
  .array(z.object({
88
88
  id: z.string(),
@@ -134,7 +134,7 @@ export function registerDashboardTools(server, client, fieldCache) {
134
134
  criticalStuckHours: (args.stuckThresholdHours ?? 48) * 2,
135
135
  wipLimits: args.wipLimits ?? {},
136
136
  doneWindowDays: args.doneWindowDays ?? 7,
137
- archiveThresholdDays: args.archiveThresholdDays ?? 14,
137
+ archiveAgeDays: args.archiveAgeDays ?? 14,
138
138
  };
139
139
  // Build dashboard from merged items
140
140
  const dashboard = buildDashboard(allItems, healthConfig, undefined, args.streams);
@@ -334,25 +334,25 @@ export function registerDirectionsTools(server, client, fieldCache) {
334
334
  .nonnegative()
335
335
  .optional()
336
336
  .default(48)
337
- .describe("Hours before a non-lock issue is considered stale (default: 48)."),
337
+ .describe("Hours before a non-lock issue is considered stale (default: 48, unit: hours). Pulls from STUCK_THRESHOLD_HOURS in src/lib/thresholds.ts."),
338
338
  lockStaleHours: z
339
339
  .number()
340
340
  .nonnegative()
341
341
  .optional()
342
342
  .default(24)
343
- .describe("Hours before a lock-state issue is considered stalled (default: 24)."),
343
+ .describe("Hours before a lock-state issue is considered stalled (default: 24, unit: hours). Pulls from LOCK_STALE_HOURS in src/lib/thresholds.ts."),
344
344
  treeRecentDoneDays: z
345
345
  .number()
346
346
  .nonnegative()
347
347
  .optional()
348
348
  .default(7)
349
- .describe("Days within which a sibling Done event still pulls a tree forward (default: 7)."),
349
+ .describe("Days within which a sibling Done event still pulls a tree forward (default: 7, unit: days). Pulls from RECENT_WINDOW_DAYS in src/lib/thresholds.ts."),
350
350
  prStaleHours: z
351
351
  .number()
352
352
  .nonnegative()
353
353
  .optional()
354
354
  .default(24)
355
- .describe("Hours before an open PR is considered stale (default: 24)."),
355
+ .describe("Hours before an open PR is considered stale (default: 24, unit: hours). Pulls from PR_STALE_HOURS in src/lib/thresholds.ts."),
356
356
  }, async (args) => {
357
357
  return await runDirections({ ...args, audience: "human" });
358
358
  });
@@ -382,25 +382,25 @@ export function registerDirectionsTools(server, client, fieldCache) {
382
382
  .nonnegative()
383
383
  .optional()
384
384
  .default(48)
385
- .describe("Hours before a non-lock issue is considered stale (default: 48)."),
385
+ .describe("Hours before a non-lock issue is considered stale (default: 48, unit: hours). Shared with pipeline_dashboard.stuckThresholdHours — both pull from STUCK_THRESHOLD_HOURS in src/lib/thresholds.ts."),
386
386
  lockStaleHours: z
387
387
  .number()
388
388
  .nonnegative()
389
389
  .optional()
390
390
  .default(24)
391
- .describe("Hours before a lock-state issue is considered stalled (default: 24)."),
391
+ .describe("Hours before a lock-state issue is considered stalled (default: 24, unit: hours). Pulls from LOCK_STALE_HOURS in src/lib/thresholds.ts."),
392
392
  treeRecentDoneDays: z
393
393
  .number()
394
394
  .nonnegative()
395
395
  .optional()
396
396
  .default(7)
397
- .describe("Days within which a sibling Done event still pulls a tree forward (default: 7)."),
397
+ .describe("Days within which a sibling Done event still pulls a tree forward (default: 7, unit: days). Shares the RECENT_WINDOW_DAYS value with hygiene.staleDays, dashboard.doneWindowDays, and metrics.velocityWindowDays."),
398
398
  prStaleHours: z
399
399
  .number()
400
400
  .nonnegative()
401
401
  .optional()
402
402
  .default(24)
403
- .describe("Hours before an open PR is considered stale (default: 24)."),
403
+ .describe("Hours before an open PR is considered stale (default: 24, unit: hours). Pulls from PR_STALE_HOURS in src/lib/thresholds.ts."),
404
404
  }, async (args) => {
405
405
  return await runDirections({ ...args, audience: args.audience ?? "human" });
406
406
  });
@@ -23,21 +23,21 @@ export function registerHygieneTools(server, client, fieldCache) {
23
23
  .array(z.coerce.number())
24
24
  .optional()
25
25
  .describe("Project numbers to include. Defaults to RALPH_GH_PROJECT_NUMBERS or single configured project."),
26
- archiveDays: z
26
+ archiveAgeDays: z
27
27
  .number()
28
28
  .optional()
29
29
  .default(14)
30
- .describe("Days before Done/Canceled items become archive candidates (default: 14)"),
30
+ .describe("Days before Done/Canceled items become archive candidates (default: 14, unit: days). Same concept as pipeline_dashboard.archiveAgeDays — both renamed from the legacy archiveDays/archiveThresholdDays pair so the value is shared across discovery tools."),
31
31
  staleDays: z
32
32
  .number()
33
33
  .optional()
34
34
  .default(7)
35
- .describe("Days before non-terminal items are flagged as stale (default: 7)"),
35
+ .describe("Days before non-terminal items are flagged as stale (default: 7, unit: days). Shares the RECENT_WINDOW_DAYS value with dashboard.doneWindowDays, next_actions.treeRecentDoneDays, and metrics.velocityWindowDays."),
36
36
  orphanDays: z
37
37
  .number()
38
38
  .optional()
39
39
  .default(14)
40
- .describe("Days before unassigned Backlog items are flagged as orphaned (default: 14)"),
40
+ .describe("Days before unassigned Backlog items are flagged as orphaned (default: 14, unit: days). Pulls from ORPHAN_AGE_DAYS in src/lib/thresholds.ts (separate concept from archiveAgeDays)."),
41
41
  wipLimits: z
42
42
  .record(z.coerce.number())
43
43
  .optional()
@@ -46,7 +46,7 @@ export function registerHygieneTools(server, client, fieldCache) {
46
46
  .number()
47
47
  .optional()
48
48
  .default(0.8)
49
- .describe("Similarity threshold for duplicate detection (0.5-1.0, default: 0.8)"),
49
+ .describe("Similarity threshold for duplicate detection (0.5-1.0, default: 0.8, unit: ratio). Pulls from SIMILARITY_THRESHOLD in src/lib/thresholds.ts."),
50
50
  format: z
51
51
  .enum(["json", "markdown"])
52
52
  .optional()
@@ -89,7 +89,7 @@ export function registerHygieneTools(server, client, fieldCache) {
89
89
  // Build hygiene config
90
90
  const hygieneConfig = {
91
91
  ...DEFAULT_HYGIENE_CONFIG,
92
- archiveDays: args.archiveDays ?? 14,
92
+ archiveAgeDays: args.archiveAgeDays ?? 14,
93
93
  staleDays: args.staleDays ?? 7,
94
94
  orphanDays: args.orphanDays ?? 14,
95
95
  wipLimits: args.wipLimits ?? {},
@@ -29,7 +29,7 @@ export function registerTrendsTools(server, client, fieldCache) {
29
29
  .int()
30
30
  .positive()
31
31
  .default(7)
32
- .describe("Velocity / highlights window in days (default: 7)."),
32
+ .describe("Velocity / highlights window in days (default: 7, unit: days). Shares the RECENT_WINDOW_DAYS value with hygiene.staleDays, dashboard.doneWindowDays, next_actions.treeRecentDoneDays, and metrics.velocityWindowDays."),
33
33
  }, async (args) => {
34
34
  try {
35
35
  const owner = resolveProjectOwner(client.config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.118",
3
+ "version": "2.5.120",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",