@oss-autopilot/core 0.41.0

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/cli.bundle.cjs +17657 -0
  4. package/dist/cli.d.ts +12 -0
  5. package/dist/cli.js +325 -0
  6. package/dist/commands/check-integration.d.ts +10 -0
  7. package/dist/commands/check-integration.js +192 -0
  8. package/dist/commands/comments.d.ts +24 -0
  9. package/dist/commands/comments.js +311 -0
  10. package/dist/commands/config.d.ts +11 -0
  11. package/dist/commands/config.js +82 -0
  12. package/dist/commands/daily.d.ts +29 -0
  13. package/dist/commands/daily.js +433 -0
  14. package/dist/commands/dashboard-data.d.ts +45 -0
  15. package/dist/commands/dashboard-data.js +132 -0
  16. package/dist/commands/dashboard-templates.d.ts +23 -0
  17. package/dist/commands/dashboard-templates.js +1627 -0
  18. package/dist/commands/dashboard.d.ts +18 -0
  19. package/dist/commands/dashboard.js +134 -0
  20. package/dist/commands/dismiss.d.ts +13 -0
  21. package/dist/commands/dismiss.js +49 -0
  22. package/dist/commands/init.d.ts +10 -0
  23. package/dist/commands/init.js +27 -0
  24. package/dist/commands/local-repos.d.ts +14 -0
  25. package/dist/commands/local-repos.js +155 -0
  26. package/dist/commands/parse-list.d.ts +13 -0
  27. package/dist/commands/parse-list.js +139 -0
  28. package/dist/commands/read.d.ts +12 -0
  29. package/dist/commands/read.js +33 -0
  30. package/dist/commands/search.d.ts +10 -0
  31. package/dist/commands/search.js +74 -0
  32. package/dist/commands/setup.d.ts +15 -0
  33. package/dist/commands/setup.js +276 -0
  34. package/dist/commands/shelve.d.ts +13 -0
  35. package/dist/commands/shelve.js +49 -0
  36. package/dist/commands/snooze.d.ts +18 -0
  37. package/dist/commands/snooze.js +83 -0
  38. package/dist/commands/startup.d.ts +33 -0
  39. package/dist/commands/startup.js +197 -0
  40. package/dist/commands/status.d.ts +10 -0
  41. package/dist/commands/status.js +43 -0
  42. package/dist/commands/track.d.ts +16 -0
  43. package/dist/commands/track.js +59 -0
  44. package/dist/commands/validation.d.ts +43 -0
  45. package/dist/commands/validation.js +112 -0
  46. package/dist/commands/vet.d.ts +10 -0
  47. package/dist/commands/vet.js +36 -0
  48. package/dist/core/checklist-analysis.d.ts +17 -0
  49. package/dist/core/checklist-analysis.js +39 -0
  50. package/dist/core/ci-analysis.d.ts +78 -0
  51. package/dist/core/ci-analysis.js +163 -0
  52. package/dist/core/comment-utils.d.ts +15 -0
  53. package/dist/core/comment-utils.js +52 -0
  54. package/dist/core/concurrency.d.ts +5 -0
  55. package/dist/core/concurrency.js +15 -0
  56. package/dist/core/daily-logic.d.ts +77 -0
  57. package/dist/core/daily-logic.js +512 -0
  58. package/dist/core/display-utils.d.ts +10 -0
  59. package/dist/core/display-utils.js +100 -0
  60. package/dist/core/errors.d.ts +24 -0
  61. package/dist/core/errors.js +34 -0
  62. package/dist/core/github-stats.d.ts +73 -0
  63. package/dist/core/github-stats.js +272 -0
  64. package/dist/core/github.d.ts +19 -0
  65. package/dist/core/github.js +60 -0
  66. package/dist/core/http-cache.d.ts +97 -0
  67. package/dist/core/http-cache.js +269 -0
  68. package/dist/core/index.d.ts +15 -0
  69. package/dist/core/index.js +15 -0
  70. package/dist/core/issue-conversation.d.ts +29 -0
  71. package/dist/core/issue-conversation.js +231 -0
  72. package/dist/core/issue-discovery.d.ts +85 -0
  73. package/dist/core/issue-discovery.js +589 -0
  74. package/dist/core/issue-filtering.d.ts +51 -0
  75. package/dist/core/issue-filtering.js +103 -0
  76. package/dist/core/issue-scoring.d.ts +40 -0
  77. package/dist/core/issue-scoring.js +92 -0
  78. package/dist/core/issue-vetting.d.ts +49 -0
  79. package/dist/core/issue-vetting.js +536 -0
  80. package/dist/core/logger.d.ts +21 -0
  81. package/dist/core/logger.js +49 -0
  82. package/dist/core/maintainer-analysis.d.ts +10 -0
  83. package/dist/core/maintainer-analysis.js +59 -0
  84. package/dist/core/pagination.d.ts +11 -0
  85. package/dist/core/pagination.js +20 -0
  86. package/dist/core/pr-monitor.d.ts +109 -0
  87. package/dist/core/pr-monitor.js +594 -0
  88. package/dist/core/review-analysis.d.ts +72 -0
  89. package/dist/core/review-analysis.js +163 -0
  90. package/dist/core/state.d.ts +371 -0
  91. package/dist/core/state.js +1089 -0
  92. package/dist/core/types.d.ts +507 -0
  93. package/dist/core/types.js +34 -0
  94. package/dist/core/utils.d.ts +249 -0
  95. package/dist/core/utils.js +422 -0
  96. package/dist/formatters/json.d.ts +269 -0
  97. package/dist/formatters/json.js +88 -0
  98. package/package.json +67 -0
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Daily check command
3
+ * Fetches all open PRs fresh from GitHub (v2: no PR-level state tracking),
4
+ * generates a digest, and updates repo scores and analytics in local state.
5
+ *
6
+ * Domain logic lives in src/core/daily-logic.ts; this file is a thin
7
+ * orchestration layer that wires up the phases and handles I/O.
8
+ */
9
+ import { getStateManager, PRMonitor, IssueConversationMonitor, getGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, } from '../core/index.js';
10
+ import { outputJson, outputJsonError, deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
11
+ // Re-export domain functions so existing consumers (tests, dashboard, startup)
12
+ // can continue importing from './daily.js' without changes.
13
+ export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
14
+ export async function runDaily(options) {
15
+ // Token is guaranteed by the preAction hook in cli.ts for non-LOCAL_ONLY_COMMANDS.
16
+ const token = getGitHubToken();
17
+ try {
18
+ await runDailyInner(token, options);
19
+ }
20
+ catch (error) {
21
+ const msg = error instanceof Error ? error.message : String(error);
22
+ if (options.json) {
23
+ outputJsonError(`Daily check failed: ${msg}`);
24
+ }
25
+ else {
26
+ console.error(`[FATAL] Daily check failed: ${msg}`);
27
+ if (error instanceof Error && error.stack) {
28
+ console.error(error.stack);
29
+ }
30
+ }
31
+ process.exit(1);
32
+ }
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Phase functions
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Phase 1: Fetch all PR data from GitHub.
39
+ * Retrieves open PRs, merged/closed counts, recently closed/merged PRs, and
40
+ * issue conversation data — all in parallel where possible.
41
+ */
42
+ async function fetchPRData(prMonitor, token) {
43
+ // Fetch all open PRs fresh from GitHub
44
+ const { prs, failures } = await prMonitor.fetchUserOpenPRs();
45
+ // Log any failures (but continue with successful checks)
46
+ if (failures.length > 0) {
47
+ console.error(`Warning: ${failures.length} PR fetch(es) failed`);
48
+ }
49
+ // Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
50
+ // Recently closed/merged are non-critical (cosmetic sections), so isolate their failure
51
+ const issueMonitor = new IssueConversationMonitor(token);
52
+ const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
53
+ prMonitor.fetchUserMergedPRCounts(),
54
+ prMonitor.fetchUserClosedPRCounts(),
55
+ prMonitor.fetchRecentlyClosedPRs().catch((err) => {
56
+ console.error(`Warning: Failed to fetch recently closed PRs: ${err instanceof Error ? err.message : err}`);
57
+ return [];
58
+ }),
59
+ prMonitor.fetchRecentlyMergedPRs().catch((err) => {
60
+ console.error(`Warning: Failed to fetch recently merged PRs: ${err instanceof Error ? err.message : err}`);
61
+ return [];
62
+ }),
63
+ issueMonitor.fetchCommentedIssues().catch((error) => {
64
+ const msg = error instanceof Error ? error.message : String(error);
65
+ if (msg.includes('No GitHub username configured')) {
66
+ console.error(`[DAILY] Issue conversation tracking requires setup: ${msg}`);
67
+ }
68
+ else {
69
+ console.error(`[DAILY] Issue conversation fetch failed: ${msg}`);
70
+ }
71
+ return {
72
+ issues: [],
73
+ failures: [{ issueUrl: 'N/A', error: `Issue conversation fetch failed: ${msg}` }],
74
+ };
75
+ }),
76
+ ]);
77
+ const commentedIssues = issueConversationResult.issues;
78
+ if (issueConversationResult.failures.length > 0) {
79
+ console.error(`[DAILY] ${issueConversationResult.failures.length} issue conversation check(s) failed`);
80
+ }
81
+ const { repos: mergedCounts, monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
82
+ const { repos: closedCounts, monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed, } = closedResult;
83
+ return {
84
+ prs,
85
+ failures,
86
+ mergedCounts,
87
+ closedCounts,
88
+ monthlyCounts,
89
+ monthlyClosedCounts,
90
+ openedFromMerged,
91
+ openedFromClosed,
92
+ recentlyClosedPRs,
93
+ recentlyMergedPRs,
94
+ commentedIssues,
95
+ };
96
+ }
97
+ /**
98
+ * Phase 2: Update repo scores in local state.
99
+ * Applies stale repo reset, updates merged/closed counts, computes and stores
100
+ * repo signals from open PR data, refreshes star counts, and syncs trusted projects.
101
+ */
102
+ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
103
+ const stateManager = getStateManager();
104
+ // Reset stale repos first (so excluded/removed repos get zeroed).
105
+ // Guard: if the API returned zero results but we have existing repos with merged PRs,
106
+ // skip the reset to avoid wiping scores due to transient API failures.
107
+ const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
108
+ if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
109
+ console.error(`[DAILY] Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
110
+ }
111
+ else {
112
+ for (const score of Object.values(stateManager.getState().repoScores)) {
113
+ if (!mergedCounts.has(score.repo)) {
114
+ stateManager.updateRepoScore(score.repo, { mergedPRCount: 0 });
115
+ }
116
+ }
117
+ }
118
+ // Update merged/closed counts with per-repo error isolation (matches signal/trust loops below)
119
+ let mergedCountFailures = 0;
120
+ for (const [repo, { count, lastMergedAt }] of mergedCounts) {
121
+ try {
122
+ stateManager.updateRepoScore(repo, { mergedPRCount: count, lastMergedAt: lastMergedAt || undefined });
123
+ }
124
+ catch (error) {
125
+ mergedCountFailures++;
126
+ console.error(`[DAILY] Failed to update merged count for ${repo}:`, error instanceof Error ? error.message : error);
127
+ }
128
+ }
129
+ if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
130
+ console.error(`[DAILY_ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
131
+ }
132
+ // Populate closedWithoutMergeCount in repo scores.
133
+ // Diagnostic: warn if API returned empty but we have known closed PRs (possible transient API failure).
134
+ // Unlike merged counts above, there is no stale-reset loop for closed counts, so no skip is needed.
135
+ const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
136
+ if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
137
+ console.error(`[DAILY] Warning: API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
138
+ }
139
+ let closedCountFailures = 0;
140
+ for (const [repo, count] of closedCounts) {
141
+ try {
142
+ stateManager.updateRepoScore(repo, { closedWithoutMergeCount: count });
143
+ }
144
+ catch (error) {
145
+ closedCountFailures++;
146
+ console.error(`[DAILY] Failed to update closed count for ${repo}:`, error instanceof Error ? error.message : error);
147
+ }
148
+ }
149
+ if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
150
+ console.error(`[DAILY_ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
151
+ }
152
+ // Update repo signals from observed open PR data (responsiveness, active maintainers).
153
+ // Only repos with current open PRs get signal updates — repos with no open PRs
154
+ // preserve their existing signals to avoid degrading scores when PRs are merged.
155
+ // Per-repo try-catch: signal/trust syncing is secondary to the daily digest —
156
+ // a single corrupted repo score should not prevent updates to other repos.
157
+ const repoSignals = computeRepoSignals(prs);
158
+ let signalUpdateFailures = 0;
159
+ for (const [repo, signals] of repoSignals) {
160
+ try {
161
+ stateManager.updateRepoScore(repo, { signals });
162
+ }
163
+ catch (error) {
164
+ signalUpdateFailures++;
165
+ console.error(`[DAILY] Failed to update signals for ${repo}:`, error instanceof Error ? error.message : error);
166
+ }
167
+ }
168
+ if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
169
+ console.error(`[DAILY_ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
170
+ }
171
+ // Fetch star counts for all scored repos (used by dashboard minStars filter, #216)
172
+ const allRepos = Object.keys(stateManager.getState().repoScores);
173
+ let starCounts;
174
+ try {
175
+ starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
176
+ }
177
+ catch (error) {
178
+ console.error('[DAILY] Failed to fetch repo star counts:', error instanceof Error ? error.message : error);
179
+ console.error('[DAILY] Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
180
+ starCounts = new Map();
181
+ }
182
+ let starUpdateFailures = 0;
183
+ for (const [repo, stars] of starCounts) {
184
+ try {
185
+ stateManager.updateRepoScore(repo, { stargazersCount: stars });
186
+ }
187
+ catch (error) {
188
+ starUpdateFailures++;
189
+ console.error(`[DAILY] Failed to update star count for ${repo}:`, error instanceof Error ? error.message : error);
190
+ }
191
+ }
192
+ if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
193
+ console.error(`[DAILY_ALL_STAR_COUNT_UPDATES_FAILED] All ${starCounts.size} star count update(s) failed.`);
194
+ }
195
+ // Auto-sync trustedProjects from repos with merged PRs
196
+ let trustSyncFailures = 0;
197
+ for (const [repo] of mergedCounts) {
198
+ try {
199
+ stateManager.addTrustedProject(repo);
200
+ }
201
+ catch (error) {
202
+ trustSyncFailures++;
203
+ console.error(`[DAILY] Failed to sync trusted project ${repo}:`, error instanceof Error ? error.message : error);
204
+ }
205
+ }
206
+ if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
207
+ console.error(`[DAILY_ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
208
+ }
209
+ }
210
+ /**
211
+ * Phase 3: Persist monthly chart analytics to state.
212
+ * Stores merged, closed, and combined opened counts per month.
213
+ * Each metric is isolated so partial failures don't produce inconsistent state.
214
+ */
215
+ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
216
+ const stateManager = getStateManager();
217
+ // Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state)
218
+ try {
219
+ stateManager.setMonthlyMergedCounts(monthlyCounts);
220
+ }
221
+ catch (error) {
222
+ console.error('[DAILY] Failed to store monthly merged counts:', error instanceof Error ? error.message : error);
223
+ }
224
+ try {
225
+ stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
226
+ }
227
+ catch (error) {
228
+ console.error('[DAILY] Failed to store monthly closed counts:', error instanceof Error ? error.message : error);
229
+ }
230
+ try {
231
+ // Build combined monthly opened counts from merged + closed + currently-open PRs
232
+ const combinedOpenedCounts = { ...openedFromMerged };
233
+ for (const [month, count] of Object.entries(openedFromClosed)) {
234
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
235
+ }
236
+ // Add currently-open PR creation dates
237
+ for (const pr of prs) {
238
+ if (pr.createdAt) {
239
+ const month = pr.createdAt.slice(0, 7);
240
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
241
+ }
242
+ }
243
+ stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
244
+ }
245
+ catch (error) {
246
+ console.error('[DAILY] Failed to compute/store monthly opened counts:', error instanceof Error ? error.message : error);
247
+ }
248
+ }
249
+ /**
250
+ * Phase 4: Expire snoozes and partition PRs into active vs shelved buckets.
251
+ * Auto-unshelves PRs where maintainers have engaged, generates the digest,
252
+ * and persists state.
253
+ */
254
+ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
255
+ const stateManager = getStateManager();
256
+ // Expire any snoozes that have passed their expiresAt timestamp.
257
+ // Non-critical: corrupted snooze entries should not abort the daily check.
258
+ try {
259
+ const expiredSnoozes = stateManager.expireSnoozes();
260
+ if (expiredSnoozes.length > 0) {
261
+ console.error(`[DAILY] ${expiredSnoozes.length} snoozed PR(s) expired and will resurface:`);
262
+ for (const url of expiredSnoozes) {
263
+ console.error(` - ${url}`);
264
+ }
265
+ stateManager.save();
266
+ }
267
+ }
268
+ catch (error) {
269
+ console.error('[DAILY] Failed to expire/persist snoozes:', error instanceof Error ? error.message : error);
270
+ }
271
+ // Partition PRs into active vs shelved, auto-unshelving when maintainers engage
272
+ const shelvedPRs = [];
273
+ const autoUnshelvedPRs = [];
274
+ const activePRs = [];
275
+ for (const pr of prs) {
276
+ if (stateManager.isPRShelved(pr.url)) {
277
+ if (CRITICAL_STATUSES.has(pr.status)) {
278
+ stateManager.unshelvePR(pr.url);
279
+ autoUnshelvedPRs.push(toShelvedPRRef(pr));
280
+ activePRs.push(pr);
281
+ }
282
+ else {
283
+ shelvedPRs.push(toShelvedPRRef(pr));
284
+ }
285
+ }
286
+ else if (pr.status === 'dormant') {
287
+ // Dormant PRs are auto-shelved (not persisted — they return when activity resumes)
288
+ shelvedPRs.push(toShelvedPRRef(pr));
289
+ }
290
+ else {
291
+ activePRs.push(pr);
292
+ }
293
+ }
294
+ // Generate digest from fresh data.
295
+ // Note: digest.openPRs contains ALL fetched PRs (including shelved).
296
+ // We override summary fields below to reflect active-only counts.
297
+ const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
298
+ // Attach shelve info to digest
299
+ digest.shelvedPRs = shelvedPRs;
300
+ digest.autoUnshelvedPRs = autoUnshelvedPRs;
301
+ digest.summary.totalActivePRs = activePRs.length;
302
+ // Store digest in state so dashboard can render it
303
+ stateManager.setLastDigest(digest);
304
+ // Save state (updates lastRunAt, lastDigest, and any auto-unshelve changes)
305
+ stateManager.save();
306
+ return { activePRs, shelvedPRs, autoUnshelvedPRs, digest };
307
+ }
308
+ /**
309
+ * Phase 5: Build the structured output for the daily command.
310
+ * Assesses capacity, filters dismissed issues, computes actionable items,
311
+ * and assembles the action menu.
312
+ */
313
+ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures) {
314
+ const stateManager = getStateManager();
315
+ // Assess capacity from active PRs only (shelved PRs excluded)
316
+ const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
317
+ // Filter dismissed issues: suppress if dismissed after last response, resurface + auto-undismiss if new activity
318
+ let hasAutoUndismissed = false;
319
+ const filteredCommentedIssues = commentedIssues.filter((issue) => {
320
+ const dismissedAt = stateManager.getIssueDismissedAt(issue.url);
321
+ if (!dismissedAt)
322
+ return true; // Not dismissed — include
323
+ if (issue.status === 'new_response') {
324
+ const responseTime = new Date(issue.lastResponseAt).getTime();
325
+ const dismissTime = new Date(dismissedAt).getTime();
326
+ if (isNaN(responseTime) || isNaN(dismissTime)) {
327
+ // Invalid timestamp — fail open (include issue to be safe)
328
+ console.error(`[DAILY] Invalid timestamp in dismiss check for ${issue.url}, including issue`);
329
+ stateManager.undismissIssue(issue.url);
330
+ hasAutoUndismissed = true;
331
+ return true;
332
+ }
333
+ if (responseTime > dismissTime) {
334
+ // New activity after dismiss — auto-undismiss and resurface
335
+ stateManager.undismissIssue(issue.url);
336
+ hasAutoUndismissed = true;
337
+ return true;
338
+ }
339
+ }
340
+ // Still dismissed (last response is at or before dismiss timestamp)
341
+ return false;
342
+ });
343
+ if (hasAutoUndismissed) {
344
+ stateManager.save();
345
+ }
346
+ const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
347
+ const summary = formatSummary(digest, capacity, issueResponses);
348
+ const snoozedUrls = new Set(Object.keys(stateManager.getState().config.snoozedPRs ?? {}).filter((url) => stateManager.isSnoozed(url)));
349
+ const actionableIssues = collectActionableIssues(activePRs, snoozedUrls);
350
+ digest.summary.totalNeedingAttention = actionableIssues.length;
351
+ const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
352
+ const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
353
+ const repoGroups = groupPRsByRepo(activePRs);
354
+ return {
355
+ digest,
356
+ updates: [],
357
+ capacity,
358
+ summary,
359
+ briefSummary,
360
+ actionableIssues,
361
+ actionMenu,
362
+ commentedIssues: filteredCommentedIssues,
363
+ repoGroups,
364
+ failures,
365
+ };
366
+ }
367
+ // ---------------------------------------------------------------------------
368
+ // Public API
369
+ // ---------------------------------------------------------------------------
370
+ /**
371
+ * Convert a full DailyCheckResult to the compact DailyOutput for JSON serialization (#287).
372
+ * Deduplicates PR objects: category arrays become PR URL references,
373
+ * full objects live only in digest.openPRs. Reduces JSON payload size ~60-70%.
374
+ */
375
+ function toDailyOutput(result) {
376
+ return {
377
+ digest: deduplicateDigest(result.digest),
378
+ updates: result.updates,
379
+ capacity: result.capacity,
380
+ summary: result.summary,
381
+ briefSummary: result.briefSummary,
382
+ actionableIssues: compactActionableIssues(result.actionableIssues),
383
+ actionMenu: result.actionMenu,
384
+ commentedIssues: result.commentedIssues,
385
+ repoGroups: compactRepoGroups(result.repoGroups),
386
+ failures: result.failures,
387
+ };
388
+ }
389
+ /**
390
+ * Core daily check logic, extracted for reuse by the startup command.
391
+ * Fetches all open PRs, updates state, and returns structured output.
392
+ *
393
+ * Returns a deduplicated DailyOutput where category arrays contain PR URLs
394
+ * instead of full objects (#287). Full PR objects are in digest.openPRs only.
395
+ *
396
+ * Orchestrates five named phases:
397
+ * 1. fetchPRData — fetch open PRs, merged/closed counts, issues
398
+ * 2. updateRepoScores — update signals, star counts, trust in state
399
+ * 3. updateAnalytics — store monthly chart data
400
+ * 4. partitionPRs — expire snoozes, shelve/unshelve, generate digest
401
+ * 5. generateDigestOutput — capacity, dismiss filter, action menu assembly
402
+ */
403
+ export async function executeDailyCheck(token) {
404
+ const result = await executeDailyCheckInternal(token);
405
+ return toDailyOutput(result);
406
+ }
407
+ /**
408
+ * Internal daily check returning full (non-deduplicated) result.
409
+ * Used by runDailyInner for text-mode output where full PR objects are needed.
410
+ */
411
+ async function executeDailyCheckInternal(token) {
412
+ const prMonitor = new PRMonitor(token);
413
+ // Phase 1: Fetch all PR data from GitHub
414
+ const { prs, failures, mergedCounts, closedCounts, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed, recentlyClosedPRs, recentlyMergedPRs, commentedIssues, } = await fetchPRData(prMonitor, token);
415
+ // Phase 2: Update repo scores (signals, star counts, trust sync)
416
+ await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
417
+ // Phase 3: Persist monthly analytics
418
+ updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
419
+ // Phase 4: Expire snoozes, partition PRs, generate and save digest
420
+ const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
421
+ // Phase 5: Build structured output (capacity, dismiss filter, action menu)
422
+ return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures);
423
+ }
424
+ async function runDailyInner(token, options) {
425
+ if (options.json) {
426
+ const result = await executeDailyCheck(token);
427
+ outputJson(result);
428
+ }
429
+ else {
430
+ const result = await executeDailyCheckInternal(token);
431
+ printDigest(result.digest, result.capacity, result.commentedIssues);
432
+ }
433
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Dashboard data fetching and aggregation.
3
+ * Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
4
+ * Separates data concerns from template generation and command orchestration.
5
+ */
6
+ import type { DailyDigest, AgentState, CommentedIssue } from '../core/types.js';
7
+ export interface DashboardFetchResult {
8
+ digest: DailyDigest;
9
+ commentedIssues: CommentedIssue[];
10
+ }
11
+ /**
12
+ * Fetch fresh dashboard data from GitHub.
13
+ * Returns the digest and commented issues, updating state as a side effect.
14
+ * Throws if the fetch fails entirely (caller should fall back to cached data).
15
+ */
16
+ export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
17
+ /**
18
+ * Compute PRs grouped by repository from a digest and state.
19
+ * Used for chart data in both JSON and HTML output.
20
+ */
21
+ export declare function computePRsByRepo(digest: DailyDigest, state: Readonly<AgentState>): Record<string, {
22
+ active: number;
23
+ merged: number;
24
+ closed: number;
25
+ }>;
26
+ /**
27
+ * Compute the top repositories sorted by total PR count.
28
+ */
29
+ export declare function computeTopRepos(prsByRepo: Record<string, {
30
+ active: number;
31
+ merged: number;
32
+ closed: number;
33
+ }>, limit?: number): Array<[string, {
34
+ active: number;
35
+ merged: number;
36
+ closed: number;
37
+ }]>;
38
+ /**
39
+ * Extract monthly activity data from state.
40
+ */
41
+ export declare function getMonthlyData(state: Readonly<AgentState>): {
42
+ monthlyMerged: Record<string, number>;
43
+ monthlyClosed: Record<string, number>;
44
+ monthlyOpened: Record<string, number>;
45
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Dashboard data fetching and aggregation.
3
+ * Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
4
+ * Separates data concerns from template generation and command orchestration.
5
+ */
6
+ import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
7
+ import { toShelvedPRRef } from './daily.js';
8
+ /**
9
+ * Fetch fresh dashboard data from GitHub.
10
+ * Returns the digest and commented issues, updating state as a side effect.
11
+ * Throws if the fetch fails entirely (caller should fall back to cached data).
12
+ */
13
+ export async function fetchDashboardData(token) {
14
+ const stateManager = getStateManager();
15
+ const prMonitor = new PRMonitor(token);
16
+ const issueMonitor = new IssueConversationMonitor(token);
17
+ const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
18
+ prMonitor.fetchUserOpenPRs(),
19
+ prMonitor.fetchRecentlyClosedPRs().catch((err) => {
20
+ console.error(`Warning: Failed to fetch recently closed PRs: ${err instanceof Error ? err.message : err}`);
21
+ return [];
22
+ }),
23
+ prMonitor.fetchRecentlyMergedPRs().catch((err) => {
24
+ console.error(`Warning: Failed to fetch recently merged PRs: ${err instanceof Error ? err.message : err}`);
25
+ return [];
26
+ }),
27
+ prMonitor.fetchUserMergedPRCounts(),
28
+ prMonitor.fetchUserClosedPRCounts(),
29
+ issueMonitor.fetchCommentedIssues().catch((error) => {
30
+ const msg = error instanceof Error ? error.message : String(error);
31
+ if (msg.includes('No GitHub username configured')) {
32
+ console.error(`[DASHBOARD] Issue conversation tracking requires setup: ${msg}`);
33
+ }
34
+ else {
35
+ console.error(`[DASHBOARD] Issue conversation fetch failed: ${msg}`);
36
+ }
37
+ return {
38
+ issues: [],
39
+ failures: [{ issueUrl: 'N/A', error: `Issue conversation fetch failed: ${msg}` }],
40
+ };
41
+ }),
42
+ ]);
43
+ const commentedIssues = fetchedIssues.issues;
44
+ if (fetchedIssues.failures.length > 0) {
45
+ console.error(`[DASHBOARD] ${fetchedIssues.failures.length} issue conversation check(s) failed`);
46
+ }
47
+ if (failures.length > 0) {
48
+ console.error(`Warning: ${failures.length} PR fetch(es) failed`);
49
+ }
50
+ // Store monthly chart data (opened/merged/closed) so charts have data
51
+ const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
52
+ const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
53
+ try {
54
+ stateManager.setMonthlyMergedCounts(monthlyCounts);
55
+ }
56
+ catch (error) {
57
+ console.error('[DASHBOARD] Failed to store monthly merged counts:', error instanceof Error ? error.message : error);
58
+ }
59
+ try {
60
+ stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
61
+ }
62
+ catch (error) {
63
+ console.error('[DASHBOARD] Failed to store monthly closed counts:', error instanceof Error ? error.message : error);
64
+ }
65
+ try {
66
+ const combinedOpenedCounts = { ...openedFromMerged };
67
+ for (const [month, count] of Object.entries(openedFromClosed)) {
68
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
69
+ }
70
+ for (const pr of prs) {
71
+ if (pr.createdAt) {
72
+ const month = pr.createdAt.slice(0, 7);
73
+ combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
74
+ }
75
+ }
76
+ stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
77
+ }
78
+ catch (error) {
79
+ console.error('[DASHBOARD] Failed to store monthly opened counts:', error instanceof Error ? error.message : error);
80
+ }
81
+ const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
82
+ // Apply shelve partitioning for display (auto-unshelve only runs in daily check)
83
+ // Dormant PRs are treated as shelved for display purposes
84
+ const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
85
+ const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || pr.status === 'dormant');
86
+ digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
87
+ digest.autoUnshelvedPRs = [];
88
+ digest.summary.totalActivePRs = prs.length - freshShelved.length;
89
+ stateManager.setLastDigest(digest);
90
+ stateManager.save();
91
+ console.error(`Refreshed: ${prs.length} PRs fetched`);
92
+ return { digest, commentedIssues };
93
+ }
94
+ /**
95
+ * Compute PRs grouped by repository from a digest and state.
96
+ * Used for chart data in both JSON and HTML output.
97
+ */
98
+ export function computePRsByRepo(digest, state) {
99
+ const prsByRepo = {};
100
+ // Count active PRs by repo from digest
101
+ for (const pr of digest.openPRs || []) {
102
+ if (!prsByRepo[pr.repo])
103
+ prsByRepo[pr.repo] = { active: 0, merged: 0, closed: 0 };
104
+ prsByRepo[pr.repo].active++;
105
+ }
106
+ // Add merged/closed counts from repo scores (historical data)
107
+ for (const [repo, score] of Object.entries(state.repoScores || {})) {
108
+ if (!prsByRepo[repo])
109
+ prsByRepo[repo] = { active: 0, merged: 0, closed: 0 };
110
+ prsByRepo[repo].merged = score.mergedPRCount;
111
+ prsByRepo[repo].closed = score.closedWithoutMergeCount;
112
+ }
113
+ return prsByRepo;
114
+ }
115
+ /**
116
+ * Compute the top repositories sorted by total PR count.
117
+ */
118
+ export function computeTopRepos(prsByRepo, limit = 10) {
119
+ return Object.entries(prsByRepo)
120
+ .sort((a, b) => b[1].merged + b[1].active + b[1].closed - (a[1].merged + a[1].active + a[1].closed))
121
+ .slice(0, limit);
122
+ }
123
+ /**
124
+ * Extract monthly activity data from state.
125
+ */
126
+ export function getMonthlyData(state) {
127
+ return {
128
+ monthlyMerged: state.monthlyMergedCounts || {},
129
+ monthlyClosed: state.monthlyClosedCounts || {},
130
+ monthlyOpened: state.monthlyOpenedCounts || {},
131
+ };
132
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Dashboard HTML template generation.
3
+ * Contains all HTML/CSS/JS template strings, escapeHtml(), and rendering helpers.
4
+ * Pure functions with no side effects — all data is passed in as arguments.
5
+ */
6
+ import type { DailyDigest, AgentState, CommentedIssueWithResponse } from '../core/types.js';
7
+ export interface DashboardStats {
8
+ activePRs: number;
9
+ shelvedPRs: number;
10
+ mergedPRs: number;
11
+ closedPRs: number;
12
+ mergeRate: string;
13
+ }
14
+ /**
15
+ * Escape HTML special characters to prevent XSS when interpolating
16
+ * user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
17
+ * Note: This escapes HTML entity characters only. It does not sanitize URL schemes
18
+ * (e.g., javascript:) — callers placing values in href attributes should validate
19
+ * the URL scheme if the source is untrusted. GitHub API URLs are trusted.
20
+ */
21
+ export declare function escapeHtml(text: string): string;
22
+ export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
23
+ export declare function generateDashboardHtml(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>, issueResponses?: CommentedIssueWithResponse[]): string;