@oss-autopilot/core 0.44.2 → 0.44.15

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.
Files changed (43) hide show
  1. package/dist/cli-registry.js +61 -0
  2. package/dist/cli.bundle.cjs +101 -127
  3. package/dist/cli.bundle.cjs.map +4 -4
  4. package/dist/commands/daily.d.ts +6 -1
  5. package/dist/commands/daily.js +29 -64
  6. package/dist/commands/dashboard-data.d.ts +22 -1
  7. package/dist/commands/dashboard-data.js +85 -62
  8. package/dist/commands/dashboard-lifecycle.js +39 -2
  9. package/dist/commands/dashboard-scripts.d.ts +1 -1
  10. package/dist/commands/dashboard-scripts.js +2 -1
  11. package/dist/commands/dashboard-server.d.ts +2 -1
  12. package/dist/commands/dashboard-server.js +120 -81
  13. package/dist/commands/dashboard-templates.js +15 -69
  14. package/dist/commands/override.d.ts +21 -0
  15. package/dist/commands/override.js +35 -0
  16. package/dist/core/checklist-analysis.js +3 -1
  17. package/dist/core/daily-logic.d.ts +13 -10
  18. package/dist/core/daily-logic.js +79 -166
  19. package/dist/core/display-utils.d.ts +4 -0
  20. package/dist/core/display-utils.js +53 -54
  21. package/dist/core/errors.d.ts +8 -0
  22. package/dist/core/errors.js +26 -0
  23. package/dist/core/github-stats.d.ts +3 -3
  24. package/dist/core/github-stats.js +15 -7
  25. package/dist/core/index.d.ts +2 -2
  26. package/dist/core/index.js +2 -2
  27. package/dist/core/issue-conversation.js +2 -2
  28. package/dist/core/issue-discovery.d.ts +0 -5
  29. package/dist/core/issue-discovery.js +4 -11
  30. package/dist/core/issue-vetting.d.ts +0 -2
  31. package/dist/core/issue-vetting.js +31 -45
  32. package/dist/core/pr-monitor.d.ts +26 -3
  33. package/dist/core/pr-monitor.js +106 -93
  34. package/dist/core/state.d.ts +22 -1
  35. package/dist/core/state.js +50 -1
  36. package/dist/core/test-utils.js +6 -16
  37. package/dist/core/types.d.ts +51 -38
  38. package/dist/core/types.js +8 -0
  39. package/dist/core/utils.d.ts +2 -0
  40. package/dist/core/utils.js +5 -1
  41. package/dist/formatters/json.d.ts +1 -13
  42. package/dist/formatters/json.js +1 -13
  43. package/package.json +2 -2
@@ -6,9 +6,14 @@
6
6
  * Domain logic lives in src/core/daily-logic.ts; this file is a thin
7
7
  * orchestration layer that wires up the phases and handles I/O.
8
8
  */
9
- import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
9
+ import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup, type AgentState, type StarFilter } from '../core/index.js';
10
10
  import { type DailyOutput, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
11
11
  export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
12
+ /**
13
+ * Build a star filter from state for use in fetchUserPRCounts.
14
+ * Returns undefined if no star data is available (first run).
15
+ */
16
+ export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
12
17
  /**
13
18
  * Internal result of the daily check, using full (non-deduplicated) types.
14
19
  * Consumed by printDigest() (text mode) and converted to DailyOutput (JSON mode)
@@ -7,25 +7,32 @@
7
7
  * orchestration layer that wires up the phases and handles I/O.
8
8
  */
9
9
  import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
10
- import { errorMessage, getHttpStatusCode } from '../core/errors.js';
10
+ import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
11
11
  import { warn } from '../core/logger.js';
12
12
  import { emptyPRCountsResult } from '../core/github-stats.js';
13
+ import { updateMonthlyAnalytics } from './dashboard-data.js';
13
14
  import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
14
15
  const MODULE = 'daily';
15
- /** Return true for errors that should propagate (not degrade gracefully). */
16
- function isRateLimitOrAuthError(err) {
17
- const status = getHttpStatusCode(err);
18
- if (status === 401 || status === 429)
19
- return true;
20
- if (status === 403) {
21
- const msg = errorMessage(err).toLowerCase();
22
- return msg.includes('rate limit') || msg.includes('abuse detection');
23
- }
24
- return false;
25
- }
26
16
  // Re-export domain functions so existing consumers (tests, dashboard, startup)
27
17
  // can continue importing from './daily.js' without changes.
28
18
  export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
19
+ /**
20
+ * Build a star filter from state for use in fetchUserPRCounts.
21
+ * Returns undefined if no star data is available (first run).
22
+ */
23
+ export function buildStarFilter(state) {
24
+ const minStars = state.config.minStars ?? 50;
25
+ const knownStarCounts = new Map();
26
+ for (const [repo, score] of Object.entries(state.repoScores)) {
27
+ if (score.stargazersCount !== undefined) {
28
+ knownStarCounts.set(repo, score.stargazersCount);
29
+ }
30
+ }
31
+ // Only filter if we have some star data to work with
32
+ if (knownStarCounts.size === 0)
33
+ return undefined;
34
+ return { minStars, knownStarCounts };
35
+ }
29
36
  // ---------------------------------------------------------------------------
30
37
  // Phase functions
31
38
  // ---------------------------------------------------------------------------
@@ -41,17 +48,21 @@ async function fetchPRData(prMonitor, token) {
41
48
  if (failures.length > 0) {
42
49
  warn(MODULE, `${failures.length} PR fetch(es) failed`);
43
50
  }
51
+ // Build star filter from cached repoScores so low-star repos are excluded
52
+ // from merged/closed histograms (#576). Repos with no cached star data pass through.
53
+ const state = getStateManager().getState();
54
+ const starFilter = buildStarFilter(state);
44
55
  // Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
45
56
  // All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
46
57
  const issueMonitor = new IssueConversationMonitor(token);
47
58
  const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
48
- prMonitor.fetchUserMergedPRCounts().catch((err) => {
59
+ prMonitor.fetchUserMergedPRCounts(starFilter).catch((err) => {
49
60
  if (isRateLimitOrAuthError(err))
50
61
  throw err;
51
62
  warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
52
63
  return emptyPRCountsResult();
53
64
  }),
54
- prMonitor.fetchUserClosedPRCounts().catch((err) => {
65
+ prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
55
66
  if (isRateLimitOrAuthError(err))
56
67
  throw err;
57
68
  warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
@@ -218,53 +229,6 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
218
229
  warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
219
230
  }
220
231
  }
221
- /**
222
- * Phase 3: Persist monthly chart analytics to state.
223
- * Stores merged, closed, and combined opened counts per month.
224
- * Each metric is isolated so partial failures don't produce inconsistent state.
225
- */
226
- function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
227
- const stateManager = getStateManager();
228
- // Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state).
229
- // Guard: skip overwriting when the data is empty to avoid wiping existing chart data on transient API failures.
230
- // An empty object means the fetch failed and fell back to emptyPRCountsResult(), so we preserve previous state.
231
- try {
232
- if (Object.keys(monthlyCounts).length > 0) {
233
- stateManager.setMonthlyMergedCounts(monthlyCounts);
234
- }
235
- }
236
- catch (error) {
237
- warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
238
- }
239
- try {
240
- if (Object.keys(monthlyClosedCounts).length > 0) {
241
- stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
242
- }
243
- }
244
- catch (error) {
245
- warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
246
- }
247
- try {
248
- // Build combined monthly opened counts from merged + closed + currently-open PRs
249
- const combinedOpenedCounts = { ...openedFromMerged };
250
- for (const [month, count] of Object.entries(openedFromClosed)) {
251
- combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
252
- }
253
- // Add currently-open PR creation dates
254
- for (const pr of prs) {
255
- if (pr.createdAt) {
256
- const month = pr.createdAt.slice(0, 7);
257
- combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
258
- }
259
- }
260
- if (Object.keys(combinedOpenedCounts).length > 0) {
261
- stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
262
- }
263
- }
264
- catch (error) {
265
- warn(MODULE, `Failed to compute/store monthly opened counts: ${errorMessage(error)}`);
266
- }
267
- }
268
232
  /**
269
233
  * Phase 4: Expire snoozes and partition PRs into active vs shelved buckets.
270
234
  * Auto-unshelves PRs where maintainers have engaged, generates the digest,
@@ -300,8 +264,9 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
300
264
  shelvedPRs.push(toShelvedPRRef(pr));
301
265
  }
302
266
  }
303
- else if (pr.status === 'dormant') {
304
- // Dormant PRs are auto-shelved (not persisted — they return when activity resumes)
267
+ else if (pr.stalenessTier === 'dormant' && !CRITICAL_STATUSES.has(pr.status)) {
268
+ // Dormant PRs are auto-shelved unless they need addressing
269
+ // (e.g. maintainer commented on a stale PR — it should resurface)
305
270
  shelvedPRs.push(toShelvedPRRef(pr));
306
271
  }
307
272
  else {
@@ -459,7 +424,7 @@ async function executeDailyCheckInternal(token) {
459
424
  // Phase 2: Update repo scores (signals, star counts, trust sync)
460
425
  await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
461
426
  // Phase 3: Persist monthly analytics
462
- updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
427
+ updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
463
428
  // Phase 4: Expire snoozes, partition PRs, generate and save digest
464
429
  const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
465
430
  // Phase 5: Build structured output (capacity, dismiss filter, action menu)
@@ -3,7 +3,7 @@
3
3
  * Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
4
4
  * Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
5
5
  */
6
- import type { DailyDigest, AgentState, CommentedIssue } from '../core/types.js';
6
+ import { type DailyDigest, type AgentState, type CommentedIssue } from '../core/types.js';
7
7
  export interface DashboardStats {
8
8
  activePRs: number;
9
9
  shelvedPRs: number;
@@ -12,10 +12,31 @@ export interface DashboardStats {
12
12
  mergeRate: string;
13
13
  }
14
14
  export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
15
+ /**
16
+ * Merge fresh API counts into existing stored counts.
17
+ * Months present in the fresh data are updated; months only in the existing data are preserved.
18
+ * This prevents historical data loss when the API returns incomplete results
19
+ * (e.g. due to pagination limits or transient failures).
20
+ */
21
+ export declare function mergeMonthlyCounts(existing: Record<string, number>, fresh: Record<string, number>): Record<string, number>;
22
+ /**
23
+ * Persist monthly chart analytics (merged, closed, opened) to state.
24
+ * Each metric is isolated so partial failures don't produce inconsistent state.
25
+ * Fresh API results are merged into existing data so historical months are preserved.
26
+ * Skips updating when fresh data is empty to avoid wiping chart data on transient API failures.
27
+ */
28
+ export declare function updateMonthlyAnalytics(prs: Array<{
29
+ createdAt?: string;
30
+ }>, monthlyCounts: Record<string, number>, monthlyClosedCounts: Record<string, number>, openedFromMerged: Record<string, number>, openedFromClosed: Record<string, number>): void;
15
31
  export interface DashboardFetchResult {
16
32
  digest: DailyDigest;
17
33
  commentedIssues: CommentedIssue[];
18
34
  }
35
+ /**
36
+ * Fetch fresh dashboard data from GitHub.
37
+ * Returns the digest and commented issues, updating state as a side effect.
38
+ * Throws if the fetch fails entirely (caller should fall back to cached data).
39
+ */
19
40
  export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
20
41
  /**
21
42
  * Compute PRs grouped by repository from a digest and state.
@@ -4,9 +4,12 @@
4
4
  * Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
5
5
  */
6
6
  import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
7
- import { errorMessage, getHttpStatusCode } from '../core/errors.js';
7
+ import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
8
+ import { warn } from '../core/logger.js';
8
9
  import { emptyPRCountsResult } from '../core/github-stats.js';
9
- import { toShelvedPRRef } from './daily.js';
10
+ import { toShelvedPRRef, buildStarFilter } from './daily.js';
11
+ const MODULE = 'dashboard-data';
12
+ import { isBelowMinStars, } from '../core/types.js';
10
13
  export function buildDashboardStats(digest, state) {
11
14
  const summary = digest.summary || {
12
15
  totalActivePRs: 0,
@@ -22,58 +25,109 @@ export function buildDashboardStats(digest, state) {
22
25
  mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
23
26
  };
24
27
  }
28
+ /**
29
+ * Merge fresh API counts into existing stored counts.
30
+ * Months present in the fresh data are updated; months only in the existing data are preserved.
31
+ * This prevents historical data loss when the API returns incomplete results
32
+ * (e.g. due to pagination limits or transient failures).
33
+ */
34
+ export function mergeMonthlyCounts(existing, fresh) {
35
+ const merged = { ...existing };
36
+ for (const [month, count] of Object.entries(fresh)) {
37
+ merged[month] = count;
38
+ }
39
+ return merged;
40
+ }
41
+ /**
42
+ * Persist monthly chart analytics (merged, closed, opened) to state.
43
+ * Each metric is isolated so partial failures don't produce inconsistent state.
44
+ * Fresh API results are merged into existing data so historical months are preserved.
45
+ * Skips updating when fresh data is empty to avoid wiping chart data on transient API failures.
46
+ */
47
+ export function updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
48
+ const stateManager = getStateManager();
49
+ const state = stateManager.getState();
50
+ try {
51
+ if (Object.keys(monthlyCounts).length > 0) {
52
+ stateManager.setMonthlyMergedCounts(mergeMonthlyCounts(state.monthlyMergedCounts || {}, monthlyCounts));
53
+ }
54
+ }
55
+ catch (error) {
56
+ warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
57
+ }
58
+ try {
59
+ if (Object.keys(monthlyClosedCounts).length > 0) {
60
+ stateManager.setMonthlyClosedCounts(mergeMonthlyCounts(state.monthlyClosedCounts || {}, monthlyClosedCounts));
61
+ }
62
+ }
63
+ catch (error) {
64
+ warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
65
+ }
66
+ try {
67
+ const combinedOpenedCounts = { ...openedFromMerged };
68
+ for (const [month, count] of Object.entries(openedFromClosed)) {
69
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
70
+ }
71
+ for (const pr of prs) {
72
+ if (pr.createdAt) {
73
+ const month = pr.createdAt.slice(0, 7);
74
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
75
+ }
76
+ }
77
+ if (Object.keys(combinedOpenedCounts).length > 0) {
78
+ stateManager.setMonthlyOpenedCounts(mergeMonthlyCounts(state.monthlyOpenedCounts || {}, combinedOpenedCounts));
79
+ }
80
+ }
81
+ catch (error) {
82
+ warn(MODULE, `Failed to store monthly opened counts: ${errorMessage(error)}`);
83
+ }
84
+ }
25
85
  /**
26
86
  * Fetch fresh dashboard data from GitHub.
27
87
  * Returns the digest and commented issues, updating state as a side effect.
28
88
  * Throws if the fetch fails entirely (caller should fall back to cached data).
29
89
  */
30
- function isRateLimitOrAuthError(err) {
31
- const status = getHttpStatusCode(err);
32
- if (status === 401 || status === 429)
33
- return true;
34
- if (status === 403) {
35
- const msg = errorMessage(err).toLowerCase();
36
- return msg.includes('rate limit') || msg.includes('abuse detection');
37
- }
38
- return false;
39
- }
40
90
  export async function fetchDashboardData(token) {
41
91
  const stateManager = getStateManager();
42
92
  const prMonitor = new PRMonitor(token);
43
93
  const issueMonitor = new IssueConversationMonitor(token);
94
+ // Build star filter from cached repoScores (#576)
95
+ const starFilter = buildStarFilter(stateManager.getState());
44
96
  const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
45
97
  prMonitor.fetchUserOpenPRs(),
46
98
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
47
99
  if (isRateLimitOrAuthError(err))
48
100
  throw err;
49
- console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
101
+ warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
50
102
  return [];
51
103
  }),
52
104
  prMonitor.fetchRecentlyMergedPRs().catch((err) => {
53
105
  if (isRateLimitOrAuthError(err))
54
106
  throw err;
55
- console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
107
+ warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
56
108
  return [];
57
109
  }),
58
- prMonitor.fetchUserMergedPRCounts().catch((err) => {
110
+ prMonitor.fetchUserMergedPRCounts(starFilter).catch((err) => {
59
111
  if (isRateLimitOrAuthError(err))
60
112
  throw err;
61
- console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
113
+ warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
62
114
  return emptyPRCountsResult();
63
115
  }),
64
- prMonitor.fetchUserClosedPRCounts().catch((err) => {
116
+ prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
65
117
  if (isRateLimitOrAuthError(err))
66
118
  throw err;
67
- console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
119
+ warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
68
120
  return emptyPRCountsResult();
69
121
  }),
70
122
  issueMonitor.fetchCommentedIssues().catch((error) => {
123
+ if (isRateLimitOrAuthError(error))
124
+ throw error;
71
125
  const msg = errorMessage(error);
72
126
  if (msg.includes('No GitHub username configured')) {
73
- console.error(`[DASHBOARD] Issue conversation tracking requires setup: ${msg}`);
127
+ warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
74
128
  }
75
129
  else {
76
- console.error(`[DASHBOARD] Issue conversation fetch failed: ${msg}`);
130
+ warn(MODULE, `Issue conversation fetch failed: ${msg}`);
77
131
  }
78
132
  return {
79
133
  issues: [],
@@ -83,54 +137,20 @@ export async function fetchDashboardData(token) {
83
137
  ]);
84
138
  const commentedIssues = fetchedIssues.issues;
85
139
  if (fetchedIssues.failures.length > 0) {
86
- console.error(`[DASHBOARD] ${fetchedIssues.failures.length} issue conversation check(s) failed`);
140
+ warn(MODULE, `${fetchedIssues.failures.length} issue conversation check(s) failed`);
87
141
  }
88
142
  if (failures.length > 0) {
89
- console.error(`Warning: ${failures.length} PR fetch(es) failed`);
143
+ warn(MODULE, `${failures.length} PR fetch(es) failed`);
90
144
  }
91
145
  // Store monthly chart data (opened/merged/closed) so charts have data
92
146
  const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
93
147
  const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
94
- // Guard: skip overwriting when data is empty to avoid wiping chart data on transient API failures.
95
- try {
96
- if (Object.keys(monthlyCounts).length > 0) {
97
- stateManager.setMonthlyMergedCounts(monthlyCounts);
98
- }
99
- }
100
- catch (error) {
101
- console.error('[DASHBOARD] Failed to store monthly merged counts:', errorMessage(error));
102
- }
103
- try {
104
- if (Object.keys(monthlyClosedCounts).length > 0) {
105
- stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
106
- }
107
- }
108
- catch (error) {
109
- console.error('[DASHBOARD] Failed to store monthly closed counts:', errorMessage(error));
110
- }
111
- try {
112
- const combinedOpenedCounts = { ...openedFromMerged };
113
- for (const [month, count] of Object.entries(openedFromClosed)) {
114
- combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
115
- }
116
- for (const pr of prs) {
117
- if (pr.createdAt) {
118
- const month = pr.createdAt.slice(0, 7);
119
- combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
120
- }
121
- }
122
- if (Object.keys(combinedOpenedCounts).length > 0) {
123
- stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
124
- }
125
- }
126
- catch (error) {
127
- console.error('[DASHBOARD] Failed to store monthly opened counts:', errorMessage(error));
128
- }
148
+ updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
129
149
  const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
130
150
  // Apply shelve partitioning for display (auto-unshelve only runs in daily check)
131
- // Dormant PRs are treated as shelved for display purposes
151
+ // Dormant PRs are treated as shelved unless they need addressing
132
152
  const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
133
- const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || pr.status === 'dormant');
153
+ const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
134
154
  digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
135
155
  digest.autoUnshelvedPRs = [];
136
156
  digest.summary.totalActivePRs = prs.length - freshShelved.length;
@@ -139,9 +159,9 @@ export async function fetchDashboardData(token) {
139
159
  stateManager.save();
140
160
  }
141
161
  catch (error) {
142
- console.error('Warning: Failed to save dashboard digest to state:', errorMessage(error));
162
+ warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
143
163
  }
144
- console.error(`Refreshed: ${prs.length} PRs fetched`);
164
+ warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
145
165
  return { digest, commentedIssues };
146
166
  }
147
167
  /**
@@ -156,8 +176,11 @@ export function computePRsByRepo(digest, state) {
156
176
  prsByRepo[pr.repo] = { active: 0, merged: 0, closed: 0 };
157
177
  prsByRepo[pr.repo].active++;
158
178
  }
159
- // Add merged/closed counts from repo scores (historical data)
179
+ // Add merged/closed counts from repo scores (historical data), filtering by minStars (#576)
180
+ const minStars = state.config.minStars ?? 50;
160
181
  for (const [repo, score] of Object.entries(state.repoScores || {})) {
182
+ if (isBelowMinStars(score.stargazersCount, minStars))
183
+ continue;
161
184
  if (!prsByRepo[repo])
162
185
  prsByRepo[repo] = { active: 0, merged: 0, closed: 0 };
163
186
  prsByRepo[repo].merged = score.mergedPRCount;
@@ -4,8 +4,9 @@
4
4
  * and detecting whether a server is already running.
5
5
  */
6
6
  import { spawn } from 'child_process';
7
- import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo } from './dashboard-server.js';
7
+ import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo, removeDashboardServerInfo, } from './dashboard-server.js';
8
8
  import { resolveAssetsDir } from './dashboard.js';
9
+ import { getCLIVersion } from '../core/index.js';
9
10
  const DEFAULT_PORT = 3000;
10
11
  const POLL_INTERVAL_MS = 200;
11
12
  const MAX_POLL_ATTEMPTS = 25; // 5 seconds total
@@ -30,7 +31,43 @@ export async function launchDashboardServer(options) {
30
31
  // 2. Check if a server is already running
31
32
  const existing = await findRunningDashboardServer();
32
33
  if (existing) {
33
- return { url: existing.url, port: existing.port, alreadyRunning: true };
34
+ // If the running server is from a different CLI version, kill it and relaunch
35
+ // so the dashboard uses the current version's code (#548)
36
+ const info = readDashboardServerInfo();
37
+ const currentVersion = getCLIVersion();
38
+ if (!info) {
39
+ // PID file disappeared between health check and now (race condition).
40
+ // Fall through to launch a new server.
41
+ }
42
+ else if (info.version && currentVersion !== '0.0.0' && info.version !== currentVersion) {
43
+ console.error(`[STARTUP] Dashboard server version mismatch (running: ${info.version}, current: ${currentVersion}). Restarting...`);
44
+ let killed = false;
45
+ try {
46
+ process.kill(info.pid, 'SIGTERM');
47
+ killed = true;
48
+ }
49
+ catch (err) {
50
+ const code = err.code;
51
+ if (code === 'ESRCH') {
52
+ killed = true; // Already exited
53
+ }
54
+ else {
55
+ console.error(`[STARTUP] Could not kill outdated dashboard (PID ${info.pid}): ${err.message}`);
56
+ }
57
+ }
58
+ if (killed) {
59
+ removeDashboardServerInfo();
60
+ }
61
+ else {
62
+ // Could not kill old server (e.g. EPERM); return it rather than
63
+ // attempting a doomed spawn on the same port.
64
+ return { url: existing.url, port: existing.port, alreadyRunning: true };
65
+ }
66
+ // Fall through to launch a new server
67
+ }
68
+ else {
69
+ return { url: existing.url, port: existing.port, alreadyRunning: true };
70
+ }
34
71
  }
35
72
  // 3. Launch as detached child process
36
73
  const port = options?.port ?? DEFAULT_PORT;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
3
3
  */
4
- import type { DailyDigest, AgentState } from '../core/types.js';
4
+ import { type DailyDigest, type AgentState } from '../core/types.js';
5
5
  import type { DashboardStats } from './dashboard-data.js';
6
6
  /** Generate the Chart.js JavaScript for the dashboard. */
7
7
  export declare function generateDashboardScripts(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>): string;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
3
3
  */
4
+ import { isBelowMinStars } from '../core/types.js';
4
5
  /** Static client-side JS: theme toggle + filter/search logic. */
5
6
  const THEME_AND_FILTER_SCRIPT = `
6
7
  // === Theme Toggle ===
@@ -144,7 +145,7 @@ export function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, mo
144
145
  // Fail-open: repos without cached star data are shown (not excluded).
145
146
  // Unlike issue-discovery (fail-closed), the dashboard shows the user's own
146
147
  // contribution history — hiding repos just because a star fetch failed would be confusing.
147
- if (score?.stargazersCount !== undefined && score.stargazersCount < starThreshold)
148
+ if (isBelowMinStars(score?.stargazersCount, starThreshold))
148
149
  return true;
149
150
  return false;
150
151
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dashboard HTTP server.
3
3
  * Serves the Preact SPA from packages/dashboard/dist/ and provides API endpoints
4
- * for live data fetching and state mutations (shelve, snooze, etc.).
4
+ * for live data fetching and state mutations (shelve, unshelve, override, etc.).
5
5
  *
6
6
  * Uses Node's built-in http module — no Express/Fastify.
7
7
  */
@@ -15,6 +15,7 @@ export interface DashboardServerInfo {
15
15
  pid: number;
16
16
  port: number;
17
17
  startedAt: string;
18
+ version?: string;
18
19
  }
19
20
  export declare function getDashboardPidPath(): string;
20
21
  export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;