@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
@@ -10,17 +10,21 @@
10
10
  * - formatActionHint — human-readable maintainer action hint label
11
11
  * - formatBriefSummary / formatSummary / printDigest — rendering
12
12
  */
13
- import type { FetchedPR, FetchedPRStatus, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
13
+ import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
14
14
  import type { CapacityAssessment, ActionableIssue, ActionMenu } from '../formatters/json.js';
15
15
  /**
16
- * Statuses indicating maintainer engagement or action needed from the contributor.
17
- * Used both for auto-unshelving shelved PRs and for counting critical issues in capacity assessment.
16
+ * Statuses indicating action needed from the contributor.
17
+ * Used for auto-unshelving shelved PRs.
18
18
  */
19
19
  export declare const CRITICAL_STATUSES: ReadonlySet<FetchedPRStatus>;
20
- /** Statuses indicating active maintainer engagement (reviews, feedback, merges). */
21
- export declare const ACTIVE_MAINTAINER_STATUSES: ReadonlySet<FetchedPRStatus>;
22
- /** Statuses indicating stalenessmaintainer comments during these statuses don't count as responsive. */
23
- export declare const STALE_STATUSES: ReadonlySet<FetchedPRStatus>;
20
+ /**
21
+ * ActionReason values that indicate high-priority issues blocking capacity.
22
+ * `incomplete_checklist` is excluded it's actionable but not blocking.
23
+ * Used in capacity assessment to determine if user can take on new work.
24
+ */
25
+ export declare const CRITICAL_ACTION_REASONS: ReadonlySet<ActionReason>;
26
+ /** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
27
+ export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
24
28
  /**
25
29
  * Map a full FetchedPR to a lightweight ShelvedPRRef for digest output.
26
30
  * Only the fields needed for display are retained, reducing JSON payload size.
@@ -33,9 +37,8 @@ export declare function toShelvedPRRef(pr: FetchedPR): ShelvedPRRef;
33
37
  export declare function groupPRsByRepo(prs: FetchedPR[]): RepoGroup[];
34
38
  /**
35
39
  * Compute per-repo maintainer signals from observed open PR data.
36
- * - isResponsive: true if any PR in the repo has a maintainer comment and status
37
- * is not in STALE_STATUSES
38
- * - hasActiveMaintainers: true if any PR in the repo has a status in ACTIVE_MAINTAINER_STATUSES
40
+ * - isResponsive: true if any PR in the repo has a maintainer comment and stalenessTier is active
41
+ * - hasActiveMaintainers: true if any non-stale PR exists in the repo (stalenessTier is 'active')
39
42
  */
40
43
  export declare function computeRepoSignals(prs: FetchedPR[]): Map<string, ComputedRepoSignals>;
41
44
  /**
@@ -16,24 +16,22 @@ import { warn } from './logger.js';
16
16
  // Constants
17
17
  // ---------------------------------------------------------------------------
18
18
  /**
19
- * Statuses indicating maintainer engagement or action needed from the contributor.
20
- * Used both for auto-unshelving shelved PRs and for counting critical issues in capacity assessment.
19
+ * Statuses indicating action needed from the contributor.
20
+ * Used for auto-unshelving shelved PRs.
21
21
  */
22
- export const CRITICAL_STATUSES = new Set([
22
+ export const CRITICAL_STATUSES = new Set(['needs_addressing']);
23
+ /**
24
+ * ActionReason values that indicate high-priority issues blocking capacity.
25
+ * `incomplete_checklist` is excluded — it's actionable but not blocking.
26
+ * Used in capacity assessment to determine if user can take on new work.
27
+ */
28
+ export const CRITICAL_ACTION_REASONS = new Set([
23
29
  'needs_response',
24
30
  'needs_changes',
25
31
  'failing_ci',
26
32
  'merge_conflict',
27
33
  ]);
28
- /** Statuses indicating active maintainer engagement (reviews, feedback, merges). */
29
- export const ACTIVE_MAINTAINER_STATUSES = new Set([
30
- 'healthy',
31
- 'waiting_on_maintainer',
32
- 'changes_addressed',
33
- 'needs_response',
34
- 'needs_changes',
35
- ]);
36
- /** Statuses indicating staleness — maintainer comments during these statuses don't count as responsive. */
34
+ /** Staleness tiers indicating staleness maintainer comments during these tiers don't count as responsive. */
37
35
  export const STALE_STATUSES = new Set(['dormant', 'approaching_dormant']);
38
36
  // ---------------------------------------------------------------------------
39
37
  // Internal helpers
@@ -86,16 +84,15 @@ export function groupPRsByRepo(prs) {
86
84
  }
87
85
  /**
88
86
  * Compute per-repo maintainer signals from observed open PR data.
89
- * - isResponsive: true if any PR in the repo has a maintainer comment and status
90
- * is not in STALE_STATUSES
91
- * - hasActiveMaintainers: true if any PR in the repo has a status in ACTIVE_MAINTAINER_STATUSES
87
+ * - isResponsive: true if any PR in the repo has a maintainer comment and stalenessTier is active
88
+ * - hasActiveMaintainers: true if any non-stale PR exists in the repo (stalenessTier is 'active')
92
89
  */
93
90
  export function computeRepoSignals(prs) {
94
91
  const repoMap = buildRepoMap(prs, 'COMPUTE_SIGNALS');
95
92
  const result = new Map();
96
93
  for (const [repo, repoPRs] of repoMap) {
97
- const isResponsive = repoPRs.some((pr) => pr.lastMaintainerComment && !STALE_STATUSES.has(pr.status));
98
- const hasActiveMaintainers = repoPRs.some((pr) => ACTIVE_MAINTAINER_STATUSES.has(pr.status));
94
+ const isResponsive = repoPRs.some((pr) => pr.lastMaintainerComment && !STALE_STATUSES.has(pr.stalenessTier));
95
+ const hasActiveMaintainers = repoPRs.some((pr) => !STALE_STATUSES.has(pr.stalenessTier));
99
96
  result.set(repo, { isResponsive, hasActiveMaintainers });
100
97
  }
101
98
  return result;
@@ -106,7 +103,7 @@ export function computeRepoSignals(prs) {
106
103
  */
107
104
  export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
108
105
  const activePRCount = activePRs.length;
109
- const criticalIssueCount = activePRs.filter((pr) => CRITICAL_STATUSES.has(pr.status)).length;
106
+ const criticalIssueCount = activePRs.filter((pr) => pr.status === 'needs_addressing' && pr.actionReason && CRITICAL_ACTION_REASONS.has(pr.actionReason)).length;
110
107
  // Has capacity if: under PR limit AND no critical issues
111
108
  const underPRLimit = activePRCount < maxActivePRs;
112
109
  const noCriticalIssues = criticalIssueCount === 0;
@@ -145,37 +142,56 @@ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
145
142
  */
146
143
  export function collectActionableIssues(prs, snoozedUrls = new Set()) {
147
144
  const issues = [];
148
- // 1. Needs Response (highest priority - someone is waiting for you)
149
- for (const pr of prs) {
150
- if (pr.status === 'needs_response') {
151
- issues.push({ type: 'needs_response', pr, label: '[Needs Response]' });
152
- }
153
- }
154
- // 2. Needs Changes (review requested changes, contributor hasn't pushed new code)
155
- for (const pr of prs) {
156
- if (pr.status === 'needs_changes') {
157
- issues.push({ type: 'needs_changes', pr, label: '[Needs Changes]' });
158
- }
159
- }
160
- // 3. CI Failing (include check names so user can distinguish real CI from validation bots)
161
- // Skip snoozed PRs — their CI failures are known and temporarily dismissed
162
- for (const pr of prs) {
163
- if (pr.status === 'failing_ci' && !snoozedUrls.has(pr.url)) {
164
- const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
165
- issues.push({ type: 'ci_failing', pr, label: `[CI Failing${checkInfo}]` });
166
- }
167
- }
168
- // 4. Merge Conflicts
169
- for (const pr of prs) {
170
- if (pr.status === 'merge_conflict') {
171
- issues.push({ type: 'merge_conflict', pr, label: '[Merge Conflict]' });
172
- }
173
- }
174
- // 5. Incomplete Checklist
175
- for (const pr of prs) {
176
- if (pr.status === 'incomplete_checklist') {
177
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
178
- issues.push({ type: 'incomplete_checklist', pr, label: `[Incomplete Checklist${stats}]` });
145
+ const actionPRs = prs.filter((pr) => pr.status === 'needs_addressing');
146
+ const reasonOrder = [
147
+ 'needs_response',
148
+ 'needs_changes',
149
+ 'failing_ci',
150
+ 'merge_conflict',
151
+ 'incomplete_checklist',
152
+ ];
153
+ for (const reason of reasonOrder) {
154
+ for (const pr of actionPRs) {
155
+ if (pr.actionReason !== reason)
156
+ continue;
157
+ if (reason === 'failing_ci' && snoozedUrls.has(pr.url))
158
+ continue;
159
+ let label;
160
+ let type;
161
+ switch (reason) {
162
+ case 'needs_response':
163
+ label = '[Needs Response]';
164
+ type = 'needs_response';
165
+ break;
166
+ case 'needs_changes':
167
+ label = '[Needs Changes]';
168
+ type = 'needs_changes';
169
+ break;
170
+ case 'failing_ci': {
171
+ const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
172
+ label = `[CI Failing${checkInfo}]`;
173
+ type = 'ci_failing';
174
+ break;
175
+ }
176
+ case 'merge_conflict':
177
+ label = '[Merge Conflict]';
178
+ type = 'merge_conflict';
179
+ break;
180
+ case 'incomplete_checklist': {
181
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
182
+ label = `[Incomplete Checklist${stats}]`;
183
+ type = 'incomplete_checklist';
184
+ break;
185
+ }
186
+ default:
187
+ // Defensive fallback for ActionReason values not explicitly handled
188
+ // above (e.g. ci_not_running, needs_rebase, missing_required_files).
189
+ // These aren't in reasonOrder today but this guards future additions.
190
+ warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
191
+ label = `[${reason}]`;
192
+ type = 'needs_response';
193
+ }
194
+ issues.push({ type, pr, label });
179
195
  }
180
196
  }
181
197
  return issues;
@@ -252,7 +268,7 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
252
268
  * Format a brief one-liner summary for the action-first flow
253
269
  */
254
270
  export function formatBriefSummary(digest, issueCount, issueResponseCount = 0) {
255
- const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all healthy';
271
+ const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all on track';
256
272
  const issueReplyText = issueResponseCount > 0 ? ` | ${issueResponseCount} issue repl${issueResponseCount === 1 ? 'y' : 'ies'}` : '';
257
273
  return `\u{1F4CA} ${digest.summary.totalActivePRs} Active PRs | ${attentionText}${issueReplyText}`;
258
274
  }
@@ -267,80 +283,21 @@ export function formatSummary(digest, capacity, issueResponses = []) {
267
283
  lines.push(`\u{1F4CA} **${digest.summary.totalActivePRs} Active PRs** | ${digest.summary.totalMergedAllTime} Merged | ${digest.summary.mergeRate}% Merge Rate`);
268
284
  lines.push('\u2713 Dashboard generated \u2014 say "open dashboard" to view in browser');
269
285
  lines.push('');
270
- // CI Failing
271
- if (digest.ciFailingPRs.length > 0) {
272
- lines.push('### \u274C CI Failing');
273
- for (const pr of digest.ciFailingPRs) {
274
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
275
- if (pr.failingCheckNames.length > 0) {
276
- lines.push(` \u2514\u2500 Failing: ${pr.failingCheckNames.join(', ')}`);
277
- }
278
- }
279
- lines.push('');
280
- }
281
- // Merge Conflicts
282
- if (digest.mergeConflictPRs.length > 0) {
283
- lines.push('### \u26A0\uFE0F Merge Conflicts');
284
- for (const pr of digest.mergeConflictPRs) {
285
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
286
- }
287
- lines.push('');
288
- }
289
- // Needs Response
290
- if (digest.prsNeedingResponse.length > 0) {
291
- lines.push('### \u{1F4AC} Needs Response');
292
- for (const pr of digest.prsNeedingResponse) {
293
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
294
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
295
- lines.push(` \u2514\u2500 @${maintainer} commented`);
296
- if (pr.maintainerActionHints.length > 0) {
297
- const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
298
- lines.push(` \u2514\u2500 Action: ${hintLabels}`);
299
- }
300
- }
301
- lines.push('');
302
- }
303
- // Needs Changes (review requested changes, no new commits yet)
304
- if (digest.needsChangesPRs.length > 0) {
305
- lines.push('### \u{1F527} Needs Changes');
306
- for (const pr of digest.needsChangesPRs) {
286
+ // Needs Addressing
287
+ if (digest.needsAddressingPRs.length > 0) {
288
+ lines.push('### \u274C Needs Addressing');
289
+ for (const pr of digest.needsAddressingPRs) {
307
290
  lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
308
- lines.push(` \u2514\u2500 Review requested changes \u2014 push commits to address`);
291
+ lines.push(` \u2514\u2500 ${pr.displayLabel} ${pr.displayDescription}`);
309
292
  }
310
293
  lines.push('');
311
294
  }
312
- // Incomplete Checklist
313
- if (digest.incompleteChecklistPRs.length > 0) {
314
- lines.push('### \u{1F4CB} Incomplete Checklist');
315
- for (const pr of digest.incompleteChecklistPRs) {
316
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
317
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${stats}`);
318
- }
319
- lines.push('');
320
- }
321
- // Changes Addressed (waiting for maintainer re-review)
322
- if (digest.changesAddressedPRs.length > 0) {
323
- lines.push('### \u{1F4E4} Changes Addressed');
324
- for (const pr of digest.changesAddressedPRs) {
325
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
326
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
327
- lines.push(` \u2514\u2500 Waiting for @${maintainer} to re-review`);
328
- }
329
- lines.push('');
330
- }
331
- // Waiting on Maintainer (approved, no action needed from user)
295
+ // Waiting on Maintainer
332
296
  if (digest.waitingOnMaintainerPRs.length > 0) {
333
297
  lines.push('### \u23F3 Waiting on Maintainer');
334
298
  for (const pr of digest.waitingOnMaintainerPRs) {
335
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title} (approved)`);
336
- }
337
- lines.push('');
338
- }
339
- // Healthy PRs
340
- if (digest.healthyPRs.length > 0) {
341
- lines.push('### \u2705 Healthy');
342
- for (const pr of digest.healthyPRs) {
343
299
  lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
300
+ lines.push(` \u2514\u2500 ${pr.displayDescription}`);
344
301
  }
345
302
  lines.push('');
346
303
  }
@@ -409,63 +366,19 @@ export function printDigest(digest, capacity, commentedIssues = []) {
409
366
  console.log(`Merge Rate: ${digest.summary.mergeRate}%`);
410
367
  console.log(`\nCapacity: ${capacity.hasCapacity ? '\u2705 Ready for new work' : '\u26A0\uFE0F Focus on existing work'}`);
411
368
  console.log(` ${capacity.reason}\n`);
412
- if (digest.ciFailingPRs.length > 0) {
413
- console.log('\u274C CI Failing:');
414
- for (const pr of digest.ciFailingPRs) {
415
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
416
- if (pr.failingCheckNames.length > 0) {
417
- console.log(` Failing: ${pr.failingCheckNames.join(', ')}`);
418
- }
419
- }
420
- console.log('');
421
- }
422
- if (digest.mergeConflictPRs.length > 0) {
423
- console.log('\u26A0\uFE0F Merge Conflicts:');
424
- for (const pr of digest.mergeConflictPRs) {
425
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
426
- }
427
- console.log('');
428
- }
429
- if (digest.prsNeedingResponse.length > 0) {
430
- console.log('\u{1F4AC} Needs Response:');
431
- for (const pr of digest.prsNeedingResponse) {
432
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
433
- if (pr.maintainerActionHints.length > 0) {
434
- const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
435
- console.log(` Action: ${hintLabels}`);
436
- }
437
- }
438
- console.log('');
439
- }
440
- if (digest.needsChangesPRs.length > 0) {
441
- console.log('\u{1F527} Needs Changes:');
442
- for (const pr of digest.needsChangesPRs) {
369
+ if (digest.needsAddressingPRs.length > 0) {
370
+ console.log('\u274C Needs Addressing:');
371
+ for (const pr of digest.needsAddressingPRs) {
443
372
  console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
444
- console.log(` Review requested changes \u2014 push commits to address`);
445
- }
446
- console.log('');
447
- }
448
- if (digest.incompleteChecklistPRs.length > 0) {
449
- console.log('\u{1F4CB} Incomplete Checklist:');
450
- for (const pr of digest.incompleteChecklistPRs) {
451
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
452
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${stats}`);
453
- }
454
- console.log('');
455
- }
456
- if (digest.changesAddressedPRs.length > 0) {
457
- console.log('\u{1F4E4} Changes Addressed:');
458
- for (const pr of digest.changesAddressedPRs) {
459
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
460
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
461
- console.log(` Waiting for @${maintainer} to re-review`);
373
+ console.log(` ${pr.displayLabel} ${pr.displayDescription}`);
462
374
  }
463
375
  console.log('');
464
376
  }
465
377
  if (digest.waitingOnMaintainerPRs.length > 0) {
466
378
  console.log('\u23F3 Waiting on Maintainer:');
467
379
  for (const pr of digest.waitingOnMaintainerPRs) {
468
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title} (approved)`);
380
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
381
+ console.log(` ${pr.displayDescription}`);
469
382
  }
470
383
  console.log('');
471
384
  }
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Display Utils - Human-readable display label computation for PR statuses.
3
3
  * Extracted from PRMonitor to isolate presentation logic (#263).
4
+ *
5
+ * Uses two reason-keyed maps (ACTION_DISPLAY / WAIT_DISPLAY) instead of a
6
+ * single status-keyed map, reflecting the 2-status taxonomy where the
7
+ * granular reason lives in `actionReason` / `waitReason`.
4
8
  */
5
9
  import { FetchedPR } from './types.js';
6
10
  /** Compute display label and description for a FetchedPR (#79). */
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Display Utils - Human-readable display label computation for PR statuses.
3
3
  * Extracted from PRMonitor to isolate presentation logic (#263).
4
+ *
5
+ * Uses two reason-keyed maps (ACTION_DISPLAY / WAIT_DISPLAY) instead of a
6
+ * single status-keyed map, reflecting the 2-status taxonomy where the
7
+ * granular reason lives in `actionReason` / `waitReason`.
4
8
  */
5
9
  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 = {
10
+ const ACTION_DISPLAY = {
12
11
  needs_response: {
13
12
  label: '[Needs Response]',
14
13
  description: (pr) => pr.lastMaintainerComment ? `@${pr.lastMaintainerComment.author} commented` : 'Maintainer awaiting response',
@@ -33,25 +32,20 @@ const STATUS_DISPLAY = {
33
32
  return 'One or more CI checks are failing';
34
33
  },
35
34
  },
36
- ci_blocked: {
37
- label: '[CI Blocked]',
38
- description: (pr) => {
39
- const checks = pr.classifiedChecks || [];
40
- if (checks.length > 0 && checks.every((c) => c.category !== 'actionable')) {
41
- const categories = [...new Set(checks.map((c) => c.category))];
42
- return `All failing checks are non-actionable (${categories.join(', ')})`;
43
- }
44
- return 'CI checks are failing but no action is needed from you';
45
- },
35
+ merge_conflict: {
36
+ label: '[Merge Conflict]',
37
+ description: () => 'PR has merge conflicts with the base branch',
38
+ },
39
+ incomplete_checklist: {
40
+ label: '[Incomplete Checklist]',
41
+ description: (pr) => pr.checklistStats
42
+ ? `${pr.checklistStats.checked}/${pr.checklistStats.total} items checked`
43
+ : 'PR body has unchecked required checkboxes',
46
44
  },
47
45
  ci_not_running: {
48
46
  label: '[CI Not Running]',
49
47
  description: () => 'No CI checks have been triggered',
50
48
  },
51
- merge_conflict: {
52
- label: '[Merge Conflict]',
53
- description: () => 'PR has merge conflicts with the base branch',
54
- },
55
49
  needs_rebase: {
56
50
  label: '[Needs Rebase]',
57
51
  description: () => 'PR branch is significantly behind upstream',
@@ -60,48 +54,53 @@ const STATUS_DISPLAY = {
60
54
  label: '[Missing Files]',
61
55
  description: (pr) => pr.missingRequiredFiles ? `Missing: ${pr.missingRequiredFiles.join(', ')}` : 'Required files are missing',
62
56
  },
63
- incomplete_checklist: {
64
- label: '[Incomplete Checklist]',
65
- description: (pr) => pr.checklistStats
66
- ? `${pr.checklistStats.checked}/${pr.checklistStats.total} items checked`
67
- : 'PR body has unchecked required checkboxes',
68
- },
69
- changes_addressed: {
70
- label: '[Changes Addressed]',
71
- description: (pr) => pr.lastMaintainerComment
72
- ? `Waiting for @${pr.lastMaintainerComment.author} to re-review`
73
- : 'Waiting for maintainer re-review',
74
- },
75
- waiting: {
76
- label: '[Waiting]',
77
- description: () => 'CI pending or awaiting review',
57
+ };
58
+ const WAIT_DISPLAY = {
59
+ pending_review: {
60
+ label: '[Waiting on Maintainer]',
61
+ description: () => 'Awaiting review',
78
62
  },
79
- waiting_on_maintainer: {
63
+ pending_merge: {
80
64
  label: '[Waiting on Maintainer]',
81
65
  description: () => 'Approved and CI passes — waiting for merge',
82
66
  },
83
- healthy: {
84
- label: '[Healthy]',
85
- description: () => 'Everything looks good — normal review cycle',
86
- },
87
- approaching_dormant: {
88
- label: '[Approaching Dormant]',
89
- description: (pr) => `No activity for ${pr.daysSinceActivity} days`,
67
+ changes_addressed: {
68
+ label: '[Waiting on Maintainer]',
69
+ description: (pr) => {
70
+ if (pr.hasUnrespondedComment && pr.lastMaintainerComment) {
71
+ return `Changes addressed — waiting for @${pr.lastMaintainerComment.author} to re-review`;
72
+ }
73
+ return 'Changes addressed awaiting re-review';
74
+ },
90
75
  },
91
- dormant: {
92
- label: '[Dormant]',
93
- description: (pr) => `No activity for ${pr.daysSinceActivity} days`,
76
+ ci_blocked: {
77
+ label: '[CI Blocked]',
78
+ description: (pr) => {
79
+ const checks = pr.classifiedChecks || [];
80
+ if (checks.length > 0 && checks.every((c) => c.category !== 'actionable')) {
81
+ const categories = [...new Set(checks.map((c) => c.category))];
82
+ return `All failing checks are non-actionable (${categories.join(', ')})`;
83
+ }
84
+ return 'CI checks are failing but no action is needed from you';
85
+ },
94
86
  },
95
87
  };
96
88
  /** Compute display label and description for a FetchedPR (#79). */
97
89
  export function computeDisplayLabel(pr) {
98
- const entry = STATUS_DISPLAY[pr.status];
99
- if (!entry) {
100
- warn(MODULE, `Unknown status "${pr.status}" for PR #${pr.number} (${pr.url})`);
101
- return { displayLabel: `[${pr.status}]`, displayDescription: 'Unknown status' };
90
+ if (pr.status === 'needs_addressing' && pr.actionReason) {
91
+ const entry = ACTION_DISPLAY[pr.actionReason];
92
+ if (entry)
93
+ return { displayLabel: entry.label, displayDescription: entry.description(pr) };
94
+ }
95
+ if (pr.status === 'waiting_on_maintainer' && pr.waitReason) {
96
+ const entry = WAIT_DISPLAY[pr.waitReason];
97
+ if (entry)
98
+ return { displayLabel: entry.label, displayDescription: entry.description(pr) };
99
+ }
100
+ // Fallback for missing reason — log so we can identify data issues
101
+ warn('display-utils', `PR ${pr.url} has status "${pr.status}" but no matching reason (actionReason=${pr.actionReason}, waitReason=${pr.waitReason})`);
102
+ if (pr.status === 'needs_addressing') {
103
+ return { displayLabel: '[Needs Addressing]', displayDescription: 'Action required' };
102
104
  }
103
- return {
104
- displayLabel: entry.label,
105
- displayDescription: entry.description(pr),
106
- };
105
+ return { displayLabel: '[Waiting on Maintainer]', displayDescription: 'Awaiting maintainer action' };
107
106
  }
@@ -2,6 +2,10 @@
2
2
  * Custom error type hierarchy for oss-autopilot.
3
3
  * Provides structured error codes and specific error classes
4
4
  * for different failure categories.
5
+ *
6
+ * Error strategy: Rate-limit and auth errors (429, 401, 403+rate-limit) always
7
+ * propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
8
+ * Other errors degrade gracefully — modules return partial results and log warnings.
5
9
  */
6
10
  /**
7
11
  * Base error for all oss-autopilot errors.
@@ -31,3 +35,7 @@ export declare function errorMessage(e: unknown): string;
31
35
  * Returns undefined if the error doesn't have a numeric `status` property.
32
36
  */
33
37
  export declare function getHttpStatusCode(error: unknown): number | undefined;
38
+ /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
39
+ export declare function isRateLimitError(error: unknown): boolean;
40
+ /** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
41
+ export declare function isRateLimitOrAuthError(err: unknown): boolean;
@@ -2,6 +2,10 @@
2
2
  * Custom error type hierarchy for oss-autopilot.
3
3
  * Provides structured error codes and specific error classes
4
4
  * for different failure categories.
5
+ *
6
+ * Error strategy: Rate-limit and auth errors (429, 401, 403+rate-limit) always
7
+ * propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
8
+ * Other errors degrade gracefully — modules return partial results and log warnings.
5
9
  */
6
10
  /**
7
11
  * Base error for all oss-autopilot errors.
@@ -49,3 +53,25 @@ export function getHttpStatusCode(error) {
49
53
  }
50
54
  return undefined;
51
55
  }
56
+ /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
57
+ export function isRateLimitError(error) {
58
+ const status = getHttpStatusCode(error);
59
+ if (status === 429)
60
+ return true;
61
+ if (status === 403) {
62
+ const msg = errorMessage(error).toLowerCase();
63
+ return msg.includes('rate limit');
64
+ }
65
+ return false;
66
+ }
67
+ /** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
68
+ export function isRateLimitOrAuthError(err) {
69
+ const status = getHttpStatusCode(err);
70
+ if (status === 401 || status === 429)
71
+ return true;
72
+ if (status === 403) {
73
+ const msg = errorMessage(err).toLowerCase();
74
+ return msg.includes('rate limit') || msg.includes('abuse detection');
75
+ }
76
+ return false;
77
+ }
@@ -3,7 +3,7 @@
3
3
  * Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
- import { ClosedPR, MergedPR } from './types.js';
6
+ import { ClosedPR, MergedPR, type StarFilter } from './types.js';
7
7
  /** TTL for cached PR count results (24 hours — these stats change slowly). */
8
8
  export declare const PR_COUNTS_CACHE_TTL_MS: number;
9
9
  /** Return type shared by both merged and closed PR count functions. */
@@ -19,7 +19,7 @@ export declare function emptyPRCountsResult<R>(): PRCountsResult<R>;
19
19
  * Fetch merged PR counts and latest merge dates per repository for the configured user.
20
20
  * Also builds a monthly histogram of all merges for the contribution timeline.
21
21
  */
22
- export declare function fetchUserMergedPRCounts(octokit: Octokit, githubUsername: string): Promise<PRCountsResult<{
22
+ export declare function fetchUserMergedPRCounts(octokit: Octokit, githubUsername: string, starFilter?: StarFilter): Promise<PRCountsResult<{
23
23
  count: number;
24
24
  lastMergedAt: string;
25
25
  }>>;
@@ -27,7 +27,7 @@ export declare function fetchUserMergedPRCounts(octokit: Octokit, githubUsername
27
27
  * Fetch closed-without-merge PR counts per repository for the configured user.
28
28
  * Used to populate closedWithoutMergeCount in repo scores for accurate merge rate.
29
29
  */
30
- export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername: string): Promise<PRCountsResult<number>>;
30
+ export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername: string, starFilter?: StarFilter): Promise<PRCountsResult<number>>;
31
31
  /**
32
32
  * Fetch PRs closed without merge in the last N days.
33
33
  * Returns lightweight ClosedPR objects for surfacing in the daily digest.