@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,512 @@
1
+ /**
2
+ * Domain logic extracted from src/commands/daily.ts.
3
+ *
4
+ * Pure or near-pure functions that operate on FetchedPR data:
5
+ * - computeRepoSignals / groupPRsByRepo — per-repo aggregations
6
+ * - assessCapacity — capacity assessment against active PRs
7
+ * - collectActionableIssues — ordered list of issues needing attention
8
+ * - computeActionMenu — pre-computed action menu for orchestration
9
+ * - toShelvedPRRef — lightweight projection for digest display
10
+ * - formatActionHint — human-readable maintainer action hint label
11
+ * - formatBriefSummary / formatSummary / printDigest — rendering
12
+ */
13
+ import { formatRelativeTime } from './utils.js';
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Statuses indicating maintainer engagement or action needed from the contributor.
19
+ * Used both for auto-unshelving shelved PRs and for counting critical issues in capacity assessment.
20
+ */
21
+ export const CRITICAL_STATUSES = new Set([
22
+ 'needs_response',
23
+ 'needs_changes',
24
+ 'failing_ci',
25
+ 'merge_conflict',
26
+ ]);
27
+ /** Statuses indicating active maintainer engagement (reviews, feedback, merges). */
28
+ export const ACTIVE_MAINTAINER_STATUSES = new Set([
29
+ 'healthy',
30
+ 'waiting_on_maintainer',
31
+ 'changes_addressed',
32
+ 'needs_response',
33
+ 'needs_changes',
34
+ ]);
35
+ /** Statuses indicating staleness — maintainer comments during these statuses don't count as responsive. */
36
+ export const STALE_STATUSES = new Set(['dormant', 'approaching_dormant']);
37
+ // ---------------------------------------------------------------------------
38
+ // Internal helpers
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Build a map grouping PRs by repository, skipping PRs with empty repo fields.
42
+ * Shared by groupPRsByRepo and computeRepoSignals.
43
+ */
44
+ function buildRepoMap(prs, label) {
45
+ const repoMap = new Map();
46
+ for (const pr of prs) {
47
+ if (!pr.repo) {
48
+ console.warn(`[${label}] Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
49
+ continue;
50
+ }
51
+ const existing = repoMap.get(pr.repo) || [];
52
+ existing.push(pr);
53
+ repoMap.set(pr.repo, existing);
54
+ }
55
+ return repoMap;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Domain functions
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Map a full FetchedPR to a lightweight ShelvedPRRef for digest output.
62
+ * Only the fields needed for display are retained, reducing JSON payload size.
63
+ */
64
+ export function toShelvedPRRef(pr) {
65
+ return {
66
+ number: pr.number,
67
+ url: pr.url,
68
+ title: pr.title,
69
+ repo: pr.repo,
70
+ daysSinceActivity: pr.daysSinceActivity,
71
+ status: pr.status,
72
+ };
73
+ }
74
+ /**
75
+ * Group PRs by repository (#80).
76
+ * Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
77
+ */
78
+ export function groupPRsByRepo(prs) {
79
+ const repoMap = buildRepoMap(prs, 'GROUP_BY_REPO');
80
+ const groups = [];
81
+ for (const [repo, repoPRs] of repoMap) {
82
+ groups.push({ repo, prs: repoPRs });
83
+ }
84
+ return groups;
85
+ }
86
+ /**
87
+ * Compute per-repo maintainer signals from observed open PR data.
88
+ * - isResponsive: true if any PR in the repo has a maintainer comment and status
89
+ * is not in STALE_STATUSES
90
+ * - hasActiveMaintainers: true if any PR in the repo has a status in ACTIVE_MAINTAINER_STATUSES
91
+ */
92
+ export function computeRepoSignals(prs) {
93
+ const repoMap = buildRepoMap(prs, 'COMPUTE_SIGNALS');
94
+ const result = new Map();
95
+ for (const [repo, repoPRs] of repoMap) {
96
+ const isResponsive = repoPRs.some((pr) => pr.lastMaintainerComment && !STALE_STATUSES.has(pr.status));
97
+ const hasActiveMaintainers = repoPRs.some((pr) => ACTIVE_MAINTAINER_STATUSES.has(pr.status));
98
+ result.set(repo, { isResponsive, hasActiveMaintainers });
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Assess whether user has capacity for new issues.
104
+ * Only active (non-shelved) PRs count against the limit.
105
+ */
106
+ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
107
+ const activePRCount = activePRs.length;
108
+ const criticalIssueCount = activePRs.filter((pr) => CRITICAL_STATUSES.has(pr.status)).length;
109
+ // Has capacity if: under PR limit AND no critical issues
110
+ const underPRLimit = activePRCount < maxActivePRs;
111
+ const noCriticalIssues = criticalIssueCount === 0;
112
+ const hasCapacity = underPRLimit && noCriticalIssues;
113
+ // Generate reason
114
+ let reason;
115
+ const shelvedNote = shelvedPRCount > 0 ? ` + ${shelvedPRCount} shelved` : '';
116
+ if (hasCapacity) {
117
+ reason = `You have capacity: ${activePRCount}/${maxActivePRs} active PRs${shelvedNote}, no critical issues`;
118
+ }
119
+ else {
120
+ const reasons = [];
121
+ if (!underPRLimit) {
122
+ reasons.push(`at PR limit (${activePRCount}/${maxActivePRs}${shelvedNote})`);
123
+ }
124
+ if (!noCriticalIssues) {
125
+ reasons.push(`${criticalIssueCount} critical issue${criticalIssueCount === 1 ? '' : 's'} need attention`);
126
+ }
127
+ reason = `No capacity: ${reasons.join(', ')}`;
128
+ }
129
+ return {
130
+ hasCapacity,
131
+ activePRCount,
132
+ maxActivePRs,
133
+ shelvedPRCount,
134
+ criticalIssueCount,
135
+ reason,
136
+ };
137
+ }
138
+ /**
139
+ * Collect all actionable issues across PRs for the action-first flow.
140
+ * Order: Needs response -> Needs changes -> CI failing -> Merge conflicts -> Incomplete checklist
141
+ *
142
+ * Note: Recently closed PRs are informational only and excluded from this list.
143
+ * They are available separately in digest.recentlyClosedPRs (#156).
144
+ */
145
+ export function collectActionableIssues(prs, snoozedUrls = new Set()) {
146
+ const issues = [];
147
+ // 1. Needs Response (highest priority - someone is waiting for you)
148
+ for (const pr of prs) {
149
+ if (pr.status === 'needs_response') {
150
+ issues.push({ type: 'needs_response', pr, label: '[Needs Response]' });
151
+ }
152
+ }
153
+ // 2. Needs Changes (review requested changes, contributor hasn't pushed new code)
154
+ for (const pr of prs) {
155
+ if (pr.status === 'needs_changes') {
156
+ issues.push({ type: 'needs_changes', pr, label: '[Needs Changes]' });
157
+ }
158
+ }
159
+ // 3. CI Failing (include check names so user can distinguish real CI from validation bots)
160
+ // Skip snoozed PRs — their CI failures are known and temporarily dismissed
161
+ for (const pr of prs) {
162
+ if (pr.status === 'failing_ci' && !snoozedUrls.has(pr.url)) {
163
+ const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
164
+ issues.push({ type: 'ci_failing', pr, label: `[CI Failing${checkInfo}]` });
165
+ }
166
+ }
167
+ // 4. Merge Conflicts
168
+ for (const pr of prs) {
169
+ if (pr.status === 'merge_conflict') {
170
+ issues.push({ type: 'merge_conflict', pr, label: '[Merge Conflict]' });
171
+ }
172
+ }
173
+ // 5. Incomplete Checklist
174
+ for (const pr of prs) {
175
+ if (pr.status === 'incomplete_checklist') {
176
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
177
+ issues.push({ type: 'incomplete_checklist', pr, label: `[Incomplete Checklist${stats}]` });
178
+ }
179
+ }
180
+ return issues;
181
+ }
182
+ /**
183
+ * Format a maintainer action hint as a human-readable label
184
+ */
185
+ export function formatActionHint(hint) {
186
+ switch (hint) {
187
+ case 'demo_requested':
188
+ return 'demo/screenshot requested';
189
+ case 'tests_requested':
190
+ return 'tests requested';
191
+ case 'changes_requested':
192
+ return 'code changes requested';
193
+ case 'docs_requested':
194
+ return 'documentation requested';
195
+ case 'rebase_requested':
196
+ return 'rebase requested';
197
+ }
198
+ }
199
+ /**
200
+ * Compute the action menu from PR data and capacity.
201
+ * The orchestration layer can insert issue-list options (e.g., "Pick from list")
202
+ * using the context flags.
203
+ */
204
+ export function computeActionMenu(actionableIssues, capacity, commentedIssues = []) {
205
+ const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
206
+ const items = [];
207
+ const hasActionableIssues = actionableIssues.length > 0;
208
+ const hasIssueResponses = issueResponses.length > 0;
209
+ if (hasActionableIssues) {
210
+ items.push({
211
+ key: 'address_all',
212
+ label: `Work through all ${actionableIssues.length} issue${actionableIssues.length === 1 ? '' : 's'} (Recommended)`,
213
+ description: 'Run maintenance in parallel, then address code changes one at a time',
214
+ });
215
+ }
216
+ // Issue replies — positioned after address_all but before search
217
+ if (hasIssueResponses) {
218
+ items.push({
219
+ key: 'issue_replies',
220
+ label: `Review ${issueResponses.length} issue repl${issueResponses.length === 1 ? 'y' : 'ies'}`,
221
+ description: 'Maintainers responded to your comments on issues',
222
+ });
223
+ }
224
+ // The orchestration layer (commands/oss.md Action Menu section) may insert issue-list
225
+ // options before the search item when a curated list is available.
226
+ items.push({
227
+ key: 'search',
228
+ label: 'Search for new issues',
229
+ description: 'Look for new contribution opportunities',
230
+ });
231
+ items.push({
232
+ key: 'done',
233
+ label: 'Done for now',
234
+ description: 'End session with summary',
235
+ });
236
+ return {
237
+ items,
238
+ context: {
239
+ hasActionableIssues,
240
+ actionableCount: actionableIssues.length,
241
+ hasCapacity: capacity.hasCapacity,
242
+ hasIssueResponses,
243
+ issueResponseCount: issueResponses.length,
244
+ },
245
+ };
246
+ }
247
+ // ---------------------------------------------------------------------------
248
+ // Rendering / formatting
249
+ // ---------------------------------------------------------------------------
250
+ /**
251
+ * Format a brief one-liner summary for the action-first flow
252
+ */
253
+ export function formatBriefSummary(digest, issueCount, issueResponseCount = 0) {
254
+ const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all healthy';
255
+ const issueReplyText = issueResponseCount > 0 ? ` | ${issueResponseCount} issue repl${issueResponseCount === 1 ? 'y' : 'ies'}` : '';
256
+ return `\u{1F4CA} ${digest.summary.totalActivePRs} Active PRs | ${attentionText}${issueReplyText}`;
257
+ }
258
+ /**
259
+ * Format summary as markdown (used in JSON output for Claude to display verbatim)
260
+ */
261
+ export function formatSummary(digest, capacity, issueResponses = []) {
262
+ const lines = [];
263
+ // Header
264
+ lines.push('## OSS Dashboard');
265
+ lines.push('');
266
+ lines.push(`\u{1F4CA} **${digest.summary.totalActivePRs} Active PRs** | ${digest.summary.totalMergedAllTime} Merged | ${digest.summary.mergeRate}% Merge Rate`);
267
+ lines.push('\u2713 Dashboard generated \u2014 say "open dashboard" to view in browser');
268
+ lines.push('');
269
+ // CI Failing
270
+ if (digest.ciFailingPRs.length > 0) {
271
+ lines.push('### \u274C CI Failing');
272
+ for (const pr of digest.ciFailingPRs) {
273
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
274
+ if (pr.failingCheckNames.length > 0) {
275
+ lines.push(` \u2514\u2500 Failing: ${pr.failingCheckNames.join(', ')}`);
276
+ }
277
+ }
278
+ lines.push('');
279
+ }
280
+ // Merge Conflicts
281
+ if (digest.mergeConflictPRs.length > 0) {
282
+ lines.push('### \u26A0\uFE0F Merge Conflicts');
283
+ for (const pr of digest.mergeConflictPRs) {
284
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
285
+ }
286
+ lines.push('');
287
+ }
288
+ // Needs Response
289
+ if (digest.prsNeedingResponse.length > 0) {
290
+ lines.push('### \u{1F4AC} Needs Response');
291
+ for (const pr of digest.prsNeedingResponse) {
292
+ const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
293
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
294
+ lines.push(` \u2514\u2500 @${maintainer} commented`);
295
+ if (pr.maintainerActionHints.length > 0) {
296
+ const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
297
+ lines.push(` \u2514\u2500 Action: ${hintLabels}`);
298
+ }
299
+ }
300
+ lines.push('');
301
+ }
302
+ // Needs Changes (review requested changes, no new commits yet)
303
+ if (digest.needsChangesPRs.length > 0) {
304
+ lines.push('### \u{1F527} Needs Changes');
305
+ for (const pr of digest.needsChangesPRs) {
306
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
307
+ lines.push(` \u2514\u2500 Review requested changes \u2014 push commits to address`);
308
+ }
309
+ lines.push('');
310
+ }
311
+ // Incomplete Checklist
312
+ if (digest.incompleteChecklistPRs.length > 0) {
313
+ lines.push('### \u{1F4CB} Incomplete Checklist');
314
+ for (const pr of digest.incompleteChecklistPRs) {
315
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
316
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${stats}`);
317
+ }
318
+ lines.push('');
319
+ }
320
+ // Changes Addressed (waiting for maintainer re-review)
321
+ if (digest.changesAddressedPRs.length > 0) {
322
+ lines.push('### \u{1F4E4} Changes Addressed');
323
+ for (const pr of digest.changesAddressedPRs) {
324
+ const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
325
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
326
+ lines.push(` \u2514\u2500 Waiting for @${maintainer} to re-review`);
327
+ }
328
+ lines.push('');
329
+ }
330
+ // Waiting on Maintainer (approved, no action needed from user)
331
+ if (digest.waitingOnMaintainerPRs.length > 0) {
332
+ lines.push('### \u23F3 Waiting on Maintainer');
333
+ for (const pr of digest.waitingOnMaintainerPRs) {
334
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title} (approved)`);
335
+ }
336
+ lines.push('');
337
+ }
338
+ // Healthy PRs
339
+ if (digest.healthyPRs.length > 0) {
340
+ lines.push('### \u2705 Healthy');
341
+ for (const pr of digest.healthyPRs) {
342
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
343
+ }
344
+ lines.push('');
345
+ }
346
+ // Recently Merged (wins!)
347
+ if (digest.recentlyMergedPRs.length > 0) {
348
+ lines.push('### \u{1F389} Recently Merged');
349
+ for (const pr of digest.recentlyMergedPRs) {
350
+ const mergedDate = pr.mergedAt ? new Date(pr.mergedAt).toLocaleDateString() : '';
351
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${mergedDate ? ` (merged ${mergedDate})` : ''}`);
352
+ }
353
+ lines.push('');
354
+ }
355
+ // Recently Closed (closed without merge)
356
+ if (digest.recentlyClosedPRs.length > 0) {
357
+ lines.push('### \u{1F6AB} Recently Closed');
358
+ for (const pr of digest.recentlyClosedPRs) {
359
+ const closedDate = pr.closedAt ? new Date(pr.closedAt).toLocaleDateString() : '';
360
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${closedDate ? ` (closed ${closedDate})` : ''}`);
361
+ }
362
+ lines.push('');
363
+ }
364
+ // Auto-unshelved (important: maintainer engagement on shelved PRs)
365
+ if (digest.autoUnshelvedPRs.length > 0) {
366
+ lines.push('### \u{1F514} Auto-Unshelved');
367
+ lines.push('> These PRs were shelved but a maintainer engaged \u2014 moved back to active.');
368
+ for (const pr of digest.autoUnshelvedPRs) {
369
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title} (${pr.status.replace(/_/g, ' ')})`);
370
+ }
371
+ lines.push('');
372
+ }
373
+ // Shelved PRs (dimmed, excluded from capacity)
374
+ if (digest.shelvedPRs.length > 0) {
375
+ lines.push('### \u{1F4E6} Shelved');
376
+ for (const pr of digest.shelvedPRs) {
377
+ lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
378
+ }
379
+ lines.push('');
380
+ }
381
+ // Issue Replies
382
+ if (issueResponses.length > 0) {
383
+ lines.push('### \u{1F4AC} Issue Replies');
384
+ for (const issue of issueResponses) {
385
+ lines.push(`- [${issue.repo}#${issue.number}](${issue.url}): ${issue.title}`);
386
+ const timeAgo = formatRelativeTime(issue.lastResponseAt);
387
+ lines.push(` \u2514\u2500 @${issue.lastResponseAuthor}: "${issue.lastResponseBody.slice(0, 80)}${issue.lastResponseBody.length > 80 ? '...' : ''}"${timeAgo ? ` (${timeAgo})` : ''}`);
388
+ }
389
+ lines.push('');
390
+ }
391
+ // Capacity
392
+ const capacityIcon = capacity.hasCapacity ? '\u2705' : '\u26A0\uFE0F';
393
+ const capacityLabel = capacity.hasCapacity ? 'Ready for new work' : 'Focus on existing PRs';
394
+ const shelvedNote = capacity.shelvedPRCount > 0 ? ` + ${capacity.shelvedPRCount} shelved` : '';
395
+ lines.push(`**Capacity:** ${capacityIcon} ${capacityLabel} (${capacity.activePRCount}/${capacity.maxActivePRs} PRs${shelvedNote})`);
396
+ return lines.join('\n');
397
+ }
398
+ /**
399
+ * Print digest to console (simple text output).
400
+ * Unified renderer: uses the same section ordering as formatSummary
401
+ * but outputs plain text with console.log instead of markdown links.
402
+ */
403
+ export function printDigest(digest, capacity, commentedIssues = []) {
404
+ console.log('\n\u{1F4CA} OSS Daily Check\n');
405
+ console.log(`Active PRs: ${digest.summary.totalActivePRs}`);
406
+ console.log(`Needing Attention: ${digest.summary.totalNeedingAttention}`);
407
+ console.log(`Merged (all time): ${digest.summary.totalMergedAllTime}`);
408
+ console.log(`Merge Rate: ${digest.summary.mergeRate}%`);
409
+ console.log(`\nCapacity: ${capacity.hasCapacity ? '\u2705 Ready for new work' : '\u26A0\uFE0F Focus on existing work'}`);
410
+ console.log(` ${capacity.reason}\n`);
411
+ if (digest.ciFailingPRs.length > 0) {
412
+ console.log('\u274C CI Failing:');
413
+ for (const pr of digest.ciFailingPRs) {
414
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
415
+ if (pr.failingCheckNames.length > 0) {
416
+ console.log(` Failing: ${pr.failingCheckNames.join(', ')}`);
417
+ }
418
+ }
419
+ console.log('');
420
+ }
421
+ if (digest.mergeConflictPRs.length > 0) {
422
+ console.log('\u26A0\uFE0F Merge Conflicts:');
423
+ for (const pr of digest.mergeConflictPRs) {
424
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
425
+ }
426
+ console.log('');
427
+ }
428
+ if (digest.prsNeedingResponse.length > 0) {
429
+ console.log('\u{1F4AC} Needs Response:');
430
+ for (const pr of digest.prsNeedingResponse) {
431
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
432
+ if (pr.maintainerActionHints.length > 0) {
433
+ const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
434
+ console.log(` Action: ${hintLabels}`);
435
+ }
436
+ }
437
+ console.log('');
438
+ }
439
+ if (digest.needsChangesPRs.length > 0) {
440
+ console.log('\u{1F527} Needs Changes:');
441
+ for (const pr of digest.needsChangesPRs) {
442
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
443
+ console.log(` Review requested changes \u2014 push commits to address`);
444
+ }
445
+ console.log('');
446
+ }
447
+ if (digest.incompleteChecklistPRs.length > 0) {
448
+ console.log('\u{1F4CB} Incomplete Checklist:');
449
+ for (const pr of digest.incompleteChecklistPRs) {
450
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
451
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${stats}`);
452
+ }
453
+ console.log('');
454
+ }
455
+ if (digest.changesAddressedPRs.length > 0) {
456
+ console.log('\u{1F4E4} Changes Addressed:');
457
+ for (const pr of digest.changesAddressedPRs) {
458
+ const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
459
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
460
+ console.log(` Waiting for @${maintainer} to re-review`);
461
+ }
462
+ console.log('');
463
+ }
464
+ if (digest.waitingOnMaintainerPRs.length > 0) {
465
+ console.log('\u23F3 Waiting on Maintainer:');
466
+ for (const pr of digest.waitingOnMaintainerPRs) {
467
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title} (approved)`);
468
+ }
469
+ console.log('');
470
+ }
471
+ if (digest.recentlyMergedPRs.length > 0) {
472
+ console.log('\u{1F389} Recently Merged:');
473
+ for (const pr of digest.recentlyMergedPRs) {
474
+ const mergedDate = pr.mergedAt ? new Date(pr.mergedAt).toLocaleDateString() : '';
475
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${mergedDate ? ` (merged ${mergedDate})` : ''}`);
476
+ }
477
+ console.log('');
478
+ }
479
+ if (digest.recentlyClosedPRs.length > 0) {
480
+ console.log('\u{1F6AB} Recently Closed:');
481
+ for (const pr of digest.recentlyClosedPRs) {
482
+ const closedDate = pr.closedAt ? new Date(pr.closedAt).toLocaleDateString() : '';
483
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${closedDate ? ` (closed ${closedDate})` : ''}`);
484
+ }
485
+ console.log('');
486
+ }
487
+ if (digest.autoUnshelvedPRs.length > 0) {
488
+ console.log('\u{1F514} Auto-Unshelved:');
489
+ for (const pr of digest.autoUnshelvedPRs) {
490
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title} (${pr.status.replace(/_/g, ' ')})`);
491
+ }
492
+ console.log('');
493
+ }
494
+ if (digest.shelvedPRs.length > 0) {
495
+ console.log('\u{1F4E6} Shelved:');
496
+ for (const pr of digest.shelvedPRs) {
497
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
498
+ }
499
+ console.log('');
500
+ }
501
+ const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
502
+ if (issueResponses.length > 0) {
503
+ console.log('\u{1F4AC} Issue Replies:');
504
+ for (const issue of issueResponses) {
505
+ console.log(` - ${issue.repo}#${issue.number}: ${issue.title}`);
506
+ console.log(` @${issue.lastResponseAuthor}: ${issue.lastResponseBody.slice(0, 80)}${issue.lastResponseBody.length > 80 ? '...' : ''}`);
507
+ }
508
+ console.log('');
509
+ }
510
+ console.log('Run with --json for structured output');
511
+ console.log('Run "dashboard --open" for browser view');
512
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Display Utils - Human-readable display label computation for PR statuses.
3
+ * Extracted from PRMonitor to isolate presentation logic (#263).
4
+ */
5
+ import { FetchedPR } from './types.js';
6
+ /** Compute display label and description for a FetchedPR (#79). */
7
+ export declare function computeDisplayLabel(pr: FetchedPR): {
8
+ displayLabel: string;
9
+ displayDescription: string;
10
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Display Utils - Human-readable display label computation for PR statuses.
3
+ * Extracted from PRMonitor to isolate presentation logic (#263).
4
+ */
5
+ import { warn } from './logger.js';
6
+ const MODULE = 'display-utils';
7
+ /**
8
+ * Deterministic mapping from FetchedPRStatus -> human-readable display label (#79).
9
+ * Ensures consistent label text across sessions — agents no longer derive these.
10
+ */
11
+ const STATUS_DISPLAY = {
12
+ needs_response: {
13
+ label: '[Needs Response]',
14
+ description: (pr) => pr.lastMaintainerComment ? `@${pr.lastMaintainerComment.author} commented` : 'Maintainer awaiting response',
15
+ },
16
+ needs_changes: {
17
+ label: '[Needs Changes]',
18
+ description: () => 'Review requested changes — push commits to address',
19
+ },
20
+ failing_ci: {
21
+ label: '[CI Failing]',
22
+ description: (pr) => {
23
+ const checks = pr.classifiedChecks || [];
24
+ const actionable = checks.filter((c) => c.category === 'actionable');
25
+ if (actionable.length > 0)
26
+ return `${actionable.length} check${actionable.length === 1 ? '' : 's'} failed: ${actionable.map((c) => c.name).join(', ')}`;
27
+ const infrastructure = checks.filter((c) => c.category === 'infrastructure');
28
+ if (infrastructure.length > 0)
29
+ return `${infrastructure.length} check${infrastructure.length === 1 ? '' : 's'} cancelled/timed out (infrastructure)`;
30
+ const failingNames = pr.failingCheckNames || [];
31
+ if (failingNames.length > 0)
32
+ return `${failingNames.length} check${failingNames.length === 1 ? '' : 's'} failed`;
33
+ return 'One or more CI checks are failing';
34
+ },
35
+ },
36
+ ci_blocked: {
37
+ label: '[CI Blocked]',
38
+ description: () => 'CI cannot run (first-time contributor approval needed)',
39
+ },
40
+ ci_not_running: {
41
+ label: '[CI Not Running]',
42
+ description: () => 'No CI checks have been triggered',
43
+ },
44
+ merge_conflict: {
45
+ label: '[Merge Conflict]',
46
+ description: () => 'PR has merge conflicts with the base branch',
47
+ },
48
+ needs_rebase: {
49
+ label: '[Needs Rebase]',
50
+ description: () => 'PR branch is significantly behind upstream',
51
+ },
52
+ missing_required_files: {
53
+ label: '[Missing Files]',
54
+ description: (pr) => pr.missingRequiredFiles ? `Missing: ${pr.missingRequiredFiles.join(', ')}` : 'Required files are missing',
55
+ },
56
+ incomplete_checklist: {
57
+ label: '[Incomplete Checklist]',
58
+ description: (pr) => pr.checklistStats
59
+ ? `${pr.checklistStats.checked}/${pr.checklistStats.total} items checked`
60
+ : 'PR body has unchecked required checkboxes',
61
+ },
62
+ changes_addressed: {
63
+ label: '[Changes Addressed]',
64
+ description: (pr) => pr.lastMaintainerComment
65
+ ? `Waiting for @${pr.lastMaintainerComment.author} to re-review`
66
+ : 'Waiting for maintainer re-review',
67
+ },
68
+ waiting: {
69
+ label: '[Waiting]',
70
+ description: () => 'CI pending or awaiting review',
71
+ },
72
+ waiting_on_maintainer: {
73
+ label: '[Waiting on Maintainer]',
74
+ description: () => 'Approved and CI passes — waiting for merge',
75
+ },
76
+ healthy: {
77
+ label: '[Healthy]',
78
+ description: () => 'Everything looks good — normal review cycle',
79
+ },
80
+ approaching_dormant: {
81
+ label: '[Approaching Dormant]',
82
+ description: (pr) => `No activity for ${pr.daysSinceActivity} days`,
83
+ },
84
+ dormant: {
85
+ label: '[Dormant]',
86
+ description: (pr) => `No activity for ${pr.daysSinceActivity} days`,
87
+ },
88
+ };
89
+ /** Compute display label and description for a FetchedPR (#79). */
90
+ export function computeDisplayLabel(pr) {
91
+ const entry = STATUS_DISPLAY[pr.status];
92
+ if (!entry) {
93
+ warn(MODULE, `Unknown status "${pr.status}" for PR #${pr.number} (${pr.url})`);
94
+ return { displayLabel: `[${pr.status}]`, displayDescription: 'Unknown status' };
95
+ }
96
+ return {
97
+ displayLabel: entry.label,
98
+ displayDescription: entry.description(pr),
99
+ };
100
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Custom error type hierarchy for oss-autopilot.
3
+ * Provides structured error codes and specific error classes
4
+ * for different failure categories.
5
+ */
6
+ /**
7
+ * Base error for all oss-autopilot errors.
8
+ */
9
+ export declare class OssAutopilotError extends Error {
10
+ readonly code: string;
11
+ constructor(message: string, code: string);
12
+ }
13
+ /**
14
+ * Configuration errors (missing setup, invalid config).
15
+ */
16
+ export declare class ConfigurationError extends OssAutopilotError {
17
+ constructor(message: string);
18
+ }
19
+ /**
20
+ * Input validation errors (invalid URLs, out-of-range values).
21
+ */
22
+ export declare class ValidationError extends OssAutopilotError {
23
+ constructor(message: string);
24
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Custom error type hierarchy for oss-autopilot.
3
+ * Provides structured error codes and specific error classes
4
+ * for different failure categories.
5
+ */
6
+ /**
7
+ * Base error for all oss-autopilot errors.
8
+ */
9
+ export class OssAutopilotError extends Error {
10
+ code;
11
+ constructor(message, code) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = 'OssAutopilotError';
15
+ }
16
+ }
17
+ /**
18
+ * Configuration errors (missing setup, invalid config).
19
+ */
20
+ export class ConfigurationError extends OssAutopilotError {
21
+ constructor(message) {
22
+ super(message, 'CONFIGURATION_ERROR');
23
+ this.name = 'ConfigurationError';
24
+ }
25
+ }
26
+ /**
27
+ * Input validation errors (invalid URLs, out-of-range values).
28
+ */
29
+ export class ValidationError extends OssAutopilotError {
30
+ constructor(message) {
31
+ super(message, 'VALIDATION_ERROR');
32
+ this.name = 'ValidationError';
33
+ }
34
+ }