@oss-autopilot/core 0.53.1 → 0.55.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 (43) hide show
  1. package/dist/cli.bundle.cjs +63 -63
  2. package/dist/commands/comments.js +0 -1
  3. package/dist/commands/config.js +45 -5
  4. package/dist/commands/daily.js +197 -162
  5. package/dist/commands/dashboard-data.js +37 -30
  6. package/dist/commands/dashboard-server.js +8 -1
  7. package/dist/commands/dismiss.js +0 -6
  8. package/dist/commands/init.js +0 -1
  9. package/dist/commands/local-repos.js +1 -2
  10. package/dist/commands/move.js +12 -11
  11. package/dist/commands/setup.d.ts +2 -1
  12. package/dist/commands/setup.js +166 -130
  13. package/dist/commands/shelve.js +10 -10
  14. package/dist/commands/startup.js +30 -14
  15. package/dist/core/ci-analysis.d.ts +6 -0
  16. package/dist/core/ci-analysis.js +91 -12
  17. package/dist/core/daily-logic.js +24 -33
  18. package/dist/core/display-utils.js +22 -2
  19. package/dist/core/github-stats.d.ts +1 -1
  20. package/dist/core/github-stats.js +1 -1
  21. package/dist/core/index.d.ts +2 -1
  22. package/dist/core/index.js +2 -1
  23. package/dist/core/issue-discovery.d.ts +7 -44
  24. package/dist/core/issue-discovery.js +83 -188
  25. package/dist/core/issue-eligibility.d.ts +35 -0
  26. package/dist/core/issue-eligibility.js +126 -0
  27. package/dist/core/issue-vetting.d.ts +6 -21
  28. package/dist/core/issue-vetting.js +15 -279
  29. package/dist/core/pr-monitor.d.ts +14 -16
  30. package/dist/core/pr-monitor.js +26 -90
  31. package/dist/core/repo-health.d.ts +24 -0
  32. package/dist/core/repo-health.js +193 -0
  33. package/dist/core/repo-score-manager.js +2 -0
  34. package/dist/core/search-phases.d.ts +55 -0
  35. package/dist/core/search-phases.js +155 -0
  36. package/dist/core/state.d.ts +11 -0
  37. package/dist/core/state.js +63 -4
  38. package/dist/core/status-determination.d.ts +2 -0
  39. package/dist/core/status-determination.js +82 -22
  40. package/dist/core/types.d.ts +23 -2
  41. package/dist/core/types.js +7 -0
  42. package/dist/formatters/json.d.ts +1 -1
  43. package/package.json +1 -1
@@ -5,6 +5,8 @@
5
5
  * granular action/wait reasons, and staleness tier for a single PR based on
6
6
  * its CI, review, merge-conflict, and timeline signals.
7
7
  */
8
+ /** Days of inactivity after which an actionable CI failure is demoted to stale_ci_failure (#675). */
9
+ export const STALE_CI_DEMOTION_DAYS = 5;
8
10
  /**
9
11
  * CI-fix bots that push commits as a direct result of the contributor's push (#568).
10
12
  * Their commits represent contributor work and should count as addressing feedback.
@@ -43,35 +45,89 @@ export function isCommitAfterComment(commitDate, commentDate) {
43
45
  }
44
46
  return commitMs - commentMs >= MIN_RESPONSE_GAP_MS;
45
47
  }
48
+ /**
49
+ * Resolve the latest commit date, filtering out non-contributor commits (#547, #568).
50
+ * Returns undefined when the commit was by a non-contributor or when no date is available.
51
+ */
52
+ function resolveContributorCommitDate(input) {
53
+ const { latestCommitDate, latestCommitAuthor, contributorUsername } = input;
54
+ if (!latestCommitDate)
55
+ return undefined;
56
+ return isContributorCommit(latestCommitAuthor, contributorUsername) ? latestCommitDate : undefined;
57
+ }
58
+ /** Check whether an unresponded comment has been addressed by a subsequent contributor commit. */
59
+ function isCommentAddressedByCommit(commitDate, commentDate, changesRequestedDate) {
60
+ if (!commitDate || !commentDate)
61
+ return false;
62
+ if (!isCommitAfterComment(commitDate, commentDate))
63
+ return false;
64
+ // Safety net (#431): if a CHANGES_REQUESTED review came after the commit, it's not addressed
65
+ if (changesRequestedDate && commitDate < changesRequestedDate)
66
+ return false;
67
+ return true;
68
+ }
69
+ /** Check whether a changes_requested review has been addressed by a subsequent contributor commit. */
70
+ function isChangesAddressedByCommit(commitDate, changesRequestedDate) {
71
+ if (!commitDate || !changesRequestedDate)
72
+ return false;
73
+ return commitDate >= changesRequestedDate;
74
+ }
75
+ /**
76
+ * Collect all applicable action reasons independently, without short-circuiting (#675).
77
+ * Used alongside the priority-based decision tree to surface secondary issues.
78
+ */
79
+ function collectAllActionReasons(input) {
80
+ const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
81
+ const commitDate = resolveContributorCommitDate(input);
82
+ const reasons = [];
83
+ if (hasUnrespondedComment &&
84
+ !isCommentAddressedByCommit(commitDate, lastMaintainerCommentDate, latestChangesRequestedDate)) {
85
+ reasons.push('needs_response');
86
+ }
87
+ if (reviewDecision === 'changes_requested' &&
88
+ latestChangesRequestedDate &&
89
+ !isChangesAddressedByCommit(commitDate, latestChangesRequestedDate)) {
90
+ reasons.push('needs_changes');
91
+ }
92
+ if (ciStatus === 'failing' && hasActionableCIFailure) {
93
+ reasons.push('failing_ci');
94
+ }
95
+ if (hasMergeConflict) {
96
+ reasons.push('merge_conflict');
97
+ }
98
+ if (hasIncompleteChecklist) {
99
+ reasons.push('incomplete_checklist');
100
+ }
101
+ return reasons.length > 0 ? reasons : undefined;
102
+ }
46
103
  /**
47
104
  * Determine the overall status of a PR based on its signals.
48
105
  */
49
106
  export function determineStatus(input) {
50
- const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate: rawCommitDate, latestCommitAuthor, contributorUsername, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
107
+ const primary = determinePrimaryStatus(input);
108
+ const actionReasons = collectAllActionReasons(input);
109
+ if (actionReasons) {
110
+ return { ...primary, actionReasons };
111
+ }
112
+ return primary;
113
+ }
114
+ /**
115
+ * Priority-based decision tree for the primary status classification.
116
+ * Returns the single highest-priority status; `determineStatus` augments
117
+ * this with the full `actionReasons` array.
118
+ */
119
+ function determinePrimaryStatus(input) {
120
+ const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
51
121
  // Compute staleness tier (independent of status)
52
122
  let stalenessTier = 'active';
53
123
  if (daysSinceActivity >= dormantThreshold)
54
124
  stalenessTier = 'dormant';
55
125
  else if (daysSinceActivity >= approachingThreshold)
56
126
  stalenessTier = 'approaching_dormant';
57
- // Only count the latest commit if it was authored by the contributor or a
58
- // CI bot (#547, #568). Non-contributor commits (maintainer merge commits,
59
- // GitHub suggestion commits) should not mask unaddressed feedback.
60
- const latestCommitDate = rawCommitDate && isContributorCommit(latestCommitAuthor, contributorUsername) ? rawCommitDate : undefined;
127
+ const commitDate = resolveContributorCommitDate(input);
61
128
  // Priority order: needs_addressing (response/changes/ci/conflict/checklist) > waiting_on_maintainer (review/merge/addressed/ci_blocked)
62
129
  if (hasUnrespondedComment) {
63
- // If the contributor pushed a commit after the maintainer's comment,
64
- // the changes have been addressed — waiting for maintainer re-review.
65
- // Require a minimum 2-minute gap to avoid false positives from race
66
- // conditions (pushing while review is being submitted) (#547).
67
- if (latestCommitDate &&
68
- lastMaintainerCommentDate &&
69
- isCommitAfterComment(latestCommitDate, lastMaintainerCommentDate)) {
70
- // Safety net (#431): if a CHANGES_REQUESTED review was submitted after
71
- // the commit, the maintainer still expects changes — don't mask it
72
- if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
73
- return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
74
- }
130
+ if (isCommentAddressedByCommit(commitDate, lastMaintainerCommentDate, latestChangesRequestedDate)) {
75
131
  if (ciStatus === 'failing' && hasActionableCIFailure)
76
132
  return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
77
133
  // Non-actionable CI failures (infrastructure, fork, auth) don't block changes_addressed —
@@ -81,9 +137,8 @@ export function determineStatus(input) {
81
137
  return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
82
138
  }
83
139
  // Review requested changes but no unresponded comment.
84
- // If the latest commit is before the review, the contributor hasn't addressed it yet.
85
140
  if (reviewDecision === 'changes_requested' && latestChangesRequestedDate) {
86
- if (!latestCommitDate || latestCommitDate < latestChangesRequestedDate) {
141
+ if (!isChangesAddressedByCommit(commitDate, latestChangesRequestedDate)) {
87
142
  return { status: 'needs_addressing', actionReason: 'needs_changes', stalenessTier };
88
143
  }
89
144
  // Commit is after review — changes have been addressed
@@ -93,9 +148,14 @@ export function determineStatus(input) {
93
148
  return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
94
149
  }
95
150
  if (ciStatus === 'failing') {
96
- return hasActionableCIFailure
97
- ? { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier }
98
- : { status: 'waiting_on_maintainer', waitReason: 'ci_blocked', stalenessTier };
151
+ if (hasActionableCIFailure) {
152
+ // Demote stale CI failures: if failing for 5+ days with no activity, likely pre-existing (#675)
153
+ if (daysSinceActivity >= STALE_CI_DEMOTION_DAYS) {
154
+ return { status: 'waiting_on_maintainer', waitReason: 'stale_ci_failure', stalenessTier };
155
+ }
156
+ return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
157
+ }
158
+ return { status: 'waiting_on_maintainer', waitReason: 'ci_blocked', stalenessTier };
99
159
  }
100
160
  if (hasMergeConflict) {
101
161
  return { status: 'needs_addressing', actionReason: 'merge_conflict', stalenessTier };
@@ -25,7 +25,7 @@ export interface ClassifiedCheck {
25
25
  category: CIFailureCategory;
26
26
  conclusion?: string;
27
27
  }
28
- /** Return type for PRMonitor.getCIStatus(). */
28
+ /** CI status result returned by getCIStatus(). */
29
29
  export interface CIStatusResult {
30
30
  status: CIStatus;
31
31
  failingCheckNames: string[];
@@ -67,6 +67,8 @@ export interface DetermineStatusResult {
67
67
  actionReason?: ActionReason;
68
68
  waitReason?: WaitReason;
69
69
  stalenessTier: StalenessTier;
70
+ /** All applicable action reasons, ordered by priority. */
71
+ actionReasons?: ActionReason[];
70
72
  }
71
73
  /**
72
74
  * Granular reason why a PR needs addressing (contributor's turn).
@@ -77,7 +79,7 @@ export interface DetermineStatusResult {
77
79
  */
78
80
  export type ActionReason = 'needs_response' | 'needs_changes' | 'failing_ci' | 'merge_conflict' | 'incomplete_checklist' | 'ci_not_running' | 'needs_rebase' | 'missing_required_files';
79
81
  /** Granular reason why a PR is waiting on the maintainer. */
80
- export type WaitReason = 'pending_review' | 'pending_merge' | 'changes_addressed' | 'ci_blocked';
82
+ export type WaitReason = 'pending_review' | 'pending_merge' | 'changes_addressed' | 'ci_blocked' | 'stale_ci_failure';
81
83
  /** How stale is the PR based on days since activity. Orthogonal to status. */
82
84
  export type StalenessTier = 'active' | 'approaching_dormant' | 'dormant';
83
85
  /**
@@ -110,6 +112,8 @@ export interface FetchedPR {
110
112
  actionReason?: ActionReason;
111
113
  /** Granular reason for waiting_on_maintainer status. Undefined when needs_addressing. */
112
114
  waitReason?: WaitReason;
115
+ /** All applicable action reasons, ordered by priority. Primary reason is first. */
116
+ actionReasons?: ActionReason[];
113
117
  /** How stale the PR is based on activity age. Independent of status — a PR can be both needs_addressing and dormant. */
114
118
  stalenessTier: StalenessTier;
115
119
  /** Human-readable status label for consistent display (#79). E.g., "[CI Failing]", "[Needs Response]". */
@@ -264,6 +268,8 @@ export interface RepoScore {
264
268
  signals: RepoSignals;
265
269
  /** GitHub star count, fetched during daily check for dashboard filtering. */
266
270
  stargazersCount?: number;
271
+ /** Primary programming language of the repo, fetched during daily check. */
272
+ language?: string | null;
267
273
  }
268
274
  /** Full set of qualitative signals about a repo's maintainer culture. */
269
275
  export interface RepoSignals {
@@ -285,6 +291,14 @@ export interface RepoScoreUpdate {
285
291
  lastMergedAt?: string;
286
292
  signals?: Partial<RepoSignals>;
287
293
  stargazersCount?: number;
294
+ /** Primary programming language of the repo. */
295
+ language?: string | null;
296
+ }
297
+ /** Repo metadata entry used in dashboard API responses. Shared between server and SPA. */
298
+ export interface RepoMetadataEntry {
299
+ /** Star count, derived from RepoScore.stargazersCount. */
300
+ stars?: number;
301
+ language?: string | null;
288
302
  }
289
303
  /**
290
304
  * Event types recorded in the {@link AgentState} audit log.
@@ -454,6 +468,8 @@ export interface AgentConfig {
454
468
  languages: string[];
455
469
  /** GitHub labels to filter issues by (e.g., `["good first issue", "help wanted"]`). */
456
470
  labels: string[];
471
+ /** Issue scope tiers to search (e.g., `["beginner", "intermediate"]`). When set, scope tier labels are merged with custom `labels`. When absent, only `labels` is used (legacy behavior). */
472
+ scope?: IssueScope[];
457
473
  /** Repos to exclude from issue discovery/search, in `"owner/repo"` format. */
458
474
  excludeRepos: string[];
459
475
  /** Organizations to exclude from issue discovery/search (case-insensitive match on owner segment). */
@@ -484,6 +500,8 @@ export interface AgentConfig {
484
500
  dismissedIssues?: Record<string, string>;
485
501
  /** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
486
502
  statusOverrides?: Record<string, StatusOverride>;
503
+ /** Path to the user's curated issue list file. Replaces config.md as the primary source for detectIssueList(). */
504
+ issueListPath?: string;
487
505
  /** Project categories the user is interested in (e.g., devtools, nonprofit). Used to prioritize search results. */
488
506
  projectCategories?: ProjectCategory[];
489
507
  /** GitHub organizations the user wants to prioritize in issue search. Org names only (not owner/repo). */
@@ -528,6 +546,9 @@ export declare const DEFAULT_CONFIG: AgentConfig;
528
546
  export declare const INITIAL_STATE: AgentState;
529
547
  export declare const PROJECT_CATEGORIES: readonly ["nonprofit", "devtools", "infrastructure", "web-frameworks", "data-ml", "education"];
530
548
  export type ProjectCategory = (typeof PROJECT_CATEGORIES)[number];
549
+ export declare const ISSUE_SCOPES: readonly ["beginner", "intermediate", "advanced"];
550
+ export type IssueScope = (typeof ISSUE_SCOPES)[number];
551
+ export declare const SCOPE_LABELS: Record<IssueScope, string[]>;
531
552
  /** Priority tier for issue search results. Ordered: merged_pr > preferred_org > starred > normal. */
532
553
  export type SearchPriority = 'merged_pr' | 'preferred_org' | 'starred' | 'normal';
533
554
  export interface IssueCandidate {
@@ -49,3 +49,10 @@ export const PROJECT_CATEGORIES = [
49
49
  'data-ml',
50
50
  'education',
51
51
  ];
52
+ // -- Issue scope types --
53
+ export const ISSUE_SCOPES = ['beginner', 'intermediate', 'advanced'];
54
+ export const SCOPE_LABELS = {
55
+ beginner: ['good first issue', 'help wanted', 'easy', 'up-for-grabs', 'first-timers-only', 'beginner'],
56
+ intermediate: ['enhancement', 'feature', 'feature-request', 'contributions welcome'],
57
+ advanced: ['proposal', 'RFC', 'accepted', 'design'],
58
+ };
@@ -5,7 +5,7 @@
5
5
  import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef } from '../core/types.js';
6
6
  import type { ContributionStats } from '../core/stats.js';
7
7
  import type { PRCheckFailure } from '../core/pr-monitor.js';
8
- import type { SearchPriority } from '../core/issue-discovery.js';
8
+ import type { SearchPriority } from '../core/types.js';
9
9
  export interface JsonOutput<T = unknown> {
10
10
  success: boolean;
11
11
  data?: T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.53.1",
3
+ "version": "0.55.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {