@oss-autopilot/core 0.44.3 → 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.
@@ -2,7 +2,7 @@
2
2
  * State management for the OSS Contribution Agent
3
3
  * Persists state to a JSON file in ~/.oss-autopilot/
4
4
  */
5
- import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo } from './types.js';
5
+ import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus } from './types.js';
6
6
  /**
7
7
  * Acquire an advisory file lock using exclusive-create (`wx` flag).
8
8
  * If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
@@ -257,6 +257,27 @@ export declare class StateManager {
257
257
  * @returns Array of PR URLs whose snoozes were expired.
258
258
  */
259
259
  expireSnoozes(): string[];
260
+ /**
261
+ * Set a manual status override for a PR.
262
+ * @param url - The full GitHub PR URL.
263
+ * @param status - The target status to override to.
264
+ * @param lastActivityAt - The PR's current updatedAt timestamp (for auto-clear detection).
265
+ */
266
+ setStatusOverride(url: string, status: FetchedPRStatus, lastActivityAt: string): void;
267
+ /**
268
+ * Clear a status override for a PR.
269
+ * @param url - The full GitHub PR URL.
270
+ * @returns true if found and removed, false if no override existed.
271
+ */
272
+ clearStatusOverride(url: string): boolean;
273
+ /**
274
+ * Get the status override for a PR, if one exists and hasn't been auto-cleared.
275
+ * @param url - The full GitHub PR URL.
276
+ * @param currentUpdatedAt - The PR's current updatedAt from GitHub. If newer than
277
+ * the stored lastActivityAt, the override is stale and auto-cleared.
278
+ * @returns The override metadata, or undefined if none exists or it was auto-cleared.
279
+ */
280
+ getStatusOverride(url: string, currentUpdatedAt?: string): StatusOverride | undefined;
260
281
  /**
261
282
  * Get the score record for a repository.
262
283
  * @param repo - Repository in "owner/repo" format.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { INITIAL_STATE, } from './types.js';
7
+ import { INITIAL_STATE, isBelowMinStars, } from './types.js';
8
8
  import { getStatePath, getBackupDir, getDataDir } from './utils.js';
9
9
  import { ValidationError, errorMessage } from './errors.js';
10
10
  import { debug, warn } from './logger.js';
@@ -843,6 +843,53 @@ export class StateManager {
843
843
  }
844
844
  return expired;
845
845
  }
846
+ // === Status Overrides ===
847
+ /**
848
+ * Set a manual status override for a PR.
849
+ * @param url - The full GitHub PR URL.
850
+ * @param status - The target status to override to.
851
+ * @param lastActivityAt - The PR's current updatedAt timestamp (for auto-clear detection).
852
+ */
853
+ setStatusOverride(url, status, lastActivityAt) {
854
+ if (!this.state.config.statusOverrides) {
855
+ this.state.config.statusOverrides = {};
856
+ }
857
+ this.state.config.statusOverrides[url] = {
858
+ status,
859
+ setAt: new Date().toISOString(),
860
+ lastActivityAt,
861
+ };
862
+ }
863
+ /**
864
+ * Clear a status override for a PR.
865
+ * @param url - The full GitHub PR URL.
866
+ * @returns true if found and removed, false if no override existed.
867
+ */
868
+ clearStatusOverride(url) {
869
+ if (!this.state.config.statusOverrides || !(url in this.state.config.statusOverrides)) {
870
+ return false;
871
+ }
872
+ delete this.state.config.statusOverrides[url];
873
+ return true;
874
+ }
875
+ /**
876
+ * Get the status override for a PR, if one exists and hasn't been auto-cleared.
877
+ * @param url - The full GitHub PR URL.
878
+ * @param currentUpdatedAt - The PR's current updatedAt from GitHub. If newer than
879
+ * the stored lastActivityAt, the override is stale and auto-cleared.
880
+ * @returns The override metadata, or undefined if none exists or it was auto-cleared.
881
+ */
882
+ getStatusOverride(url, currentUpdatedAt) {
883
+ const override = this.state.config.statusOverrides?.[url];
884
+ if (!override)
885
+ return undefined;
886
+ // Auto-clear if the PR has new activity since the override was set
887
+ if (currentUpdatedAt && currentUpdatedAt > override.lastActivityAt) {
888
+ this.clearStatusOverride(url);
889
+ return undefined;
890
+ }
891
+ return override;
892
+ }
846
893
  // === Repository Scoring ===
847
894
  /**
848
895
  * Get the score record for a repository.
@@ -1060,6 +1107,8 @@ export class StateManager {
1060
1107
  for (const [repoKey, score] of Object.entries(this.state.repoScores)) {
1061
1108
  if (this.isExcluded(repoKey))
1062
1109
  continue;
1110
+ if (isBelowMinStars(score.stargazersCount, this.state.config.minStars ?? 50))
1111
+ continue;
1063
1112
  totalTracked++;
1064
1113
  totalMerged += score.mergedPRCount;
1065
1114
  totalClosed += score.closedWithoutMergeCount;
@@ -17,9 +17,11 @@ export function makeFetchedPR(overrides = {}) {
17
17
  repo,
18
18
  number,
19
19
  title: 'Test PR',
20
- status: 'healthy',
21
- displayLabel: '[Healthy]',
22
- displayDescription: 'Everything looks good',
20
+ status: 'waiting_on_maintainer',
21
+ waitReason: 'pending_review',
22
+ stalenessTier: 'active',
23
+ displayLabel: '[Waiting on Maintainer]',
24
+ displayDescription: 'Awaiting review',
23
25
  createdAt: '2025-06-01T00:00:00Z',
24
26
  updatedAt: '2025-06-15T00:00:00Z',
25
27
  daysSinceActivity: 2,
@@ -41,20 +43,8 @@ export function makeDailyDigest(overrides = {}) {
41
43
  return {
42
44
  generatedAt: '2025-06-20T00:00:00Z',
43
45
  openPRs: [],
44
- prsNeedingResponse: [],
45
- ciFailingPRs: [],
46
- ciBlockedPRs: [],
47
- ciNotRunningPRs: [],
48
- mergeConflictPRs: [],
49
- needsRebasePRs: [],
50
- missingRequiredFilesPRs: [],
51
- incompleteChecklistPRs: [],
52
- needsChangesPRs: [],
53
- changesAddressedPRs: [],
46
+ needsAddressingPRs: [],
54
47
  waitingOnMaintainerPRs: [],
55
- approachingDormant: [],
56
- dormantPRs: [],
57
- healthyPRs: [],
58
48
  recentlyClosedPRs: [],
59
49
  recentlyMergedPRs: [],
60
50
  shelvedPRs: [],
@@ -52,37 +52,35 @@ export interface DetermineStatusInput {
52
52
  dormantThreshold: number;
53
53
  approachingThreshold: number;
54
54
  latestCommitDate?: string;
55
+ /** GitHub login of the HEAD commit's author (from `repos.getCommit`). */
56
+ latestCommitAuthor?: string;
57
+ /** GitHub login of the PR contributor (configured username). */
58
+ contributorUsername?: string;
55
59
  lastMaintainerCommentDate?: string;
56
60
  latestChangesRequestedDate?: string;
57
61
  /** True if at least one failing CI check is classified as 'actionable'. */
58
62
  hasActionableCIFailure?: boolean;
59
63
  }
60
64
  /**
61
- * Computed status for a {@link FetchedPR}, determined by `PRMonitor.determineStatus()`.
62
- * Statuses are checked in priority order — the first match wins.
63
- *
64
- * **Action required (contributor must act):**
65
- * - `needs_response` — Maintainer commented after the contributor's last activity
66
- * - `needs_changes` — Reviewer requested changes (via review, not just a comment)
67
- * - `failing_ci` One or more CI checks are failing (at least one is actionable)
68
- * - `ci_not_running` No CI checks have been triggered *(reserved)*
69
- * - `merge_conflict` PR has merge conflicts with the base branch
70
- * - `needs_rebase` PR branch is significantly behind upstream *(reserved)*
71
- * - `missing_required_files` Required files like changesets or CLA are missing *(reserved)*
72
- * - `incomplete_checklist` — PR body has unchecked required checkboxes
73
- *
74
- * **Waiting (no action needed right now):**
75
- * - `ci_blocked` — All failing CI checks are non-actionable (infrastructure, fork limitation, auth gate)
76
- * - `changes_addressed` — Contributor pushed commits after reviewer feedback; awaiting re-review
77
- * - `waiting` — CI is pending or no specific action needed
78
- * - `waiting_on_maintainer` — PR is approved and CI passes; waiting for maintainer to merge
79
- * - `healthy` — Everything looks good; normal review cycle
65
+ * Granular reason why a PR needs addressing (contributor's turn).
66
+ * Active values (produced by determineStatus): needs_response, needs_changes,
67
+ * failing_ci, merge_conflict, incomplete_checklist.
68
+ * Reserved (display mappings exist but detection not yet wired): ci_not_running,
69
+ * needs_rebase, missing_required_files.
70
+ */
71
+ export type ActionReason = 'needs_response' | 'needs_changes' | 'failing_ci' | 'merge_conflict' | 'incomplete_checklist' | 'ci_not_running' | 'needs_rebase' | 'missing_required_files';
72
+ /** Granular reason why a PR is waiting on the maintainer. */
73
+ export type WaitReason = 'pending_review' | 'pending_merge' | 'changes_addressed' | 'ci_blocked';
74
+ /** How stale is the PR based on days since activity. Orthogonal to status. */
75
+ export type StalenessTier = 'active' | 'approaching_dormant' | 'dormant';
76
+ /**
77
+ * Top-level classification of a PR's state. Only two values:
78
+ * - `needs_addressing` Contributor's turn. See `actionReason` for what to do.
79
+ * - `waiting_on_maintainer` — Maintainer's turn. See `waitReason` for why.
80
80
  *
81
- * **Staleness warnings:**
82
- * - `approaching_dormant` — No activity for `approachingDormantDays` (default 25)
83
- * - `dormant` — No activity for `dormantThresholdDays` (default 30)
81
+ * Staleness (active/approaching_dormant/dormant) is tracked separately in `stalenessTier`.
84
82
  */
85
- export type FetchedPRStatus = 'needs_response' | 'failing_ci' | 'ci_blocked' | 'ci_not_running' | 'merge_conflict' | 'needs_rebase' | 'missing_required_files' | 'incomplete_checklist' | 'needs_changes' | 'changes_addressed' | 'waiting' | 'waiting_on_maintainer' | 'healthy' | 'approaching_dormant' | 'dormant';
83
+ export type FetchedPRStatus = 'needs_addressing' | 'waiting_on_maintainer';
86
84
  /**
87
85
  * Hints about what a maintainer is asking for in their review comments.
88
86
  * Extracted from comment text by keyword matching.
@@ -101,6 +99,12 @@ export interface FetchedPR {
101
99
  title: string;
102
100
  /** Computed by `PRMonitor.determineStatus()` based on the fields below. */
103
101
  status: FetchedPRStatus;
102
+ /** Granular reason for needs_addressing status. Undefined when waiting_on_maintainer. */
103
+ actionReason?: ActionReason;
104
+ /** Granular reason for waiting_on_maintainer status. Undefined when needs_addressing. */
105
+ waitReason?: WaitReason;
106
+ /** How stale the PR is based on activity age. Independent of status — a PR can be both needs_addressing and dormant. */
107
+ stalenessTier: StalenessTier;
104
108
  /** Human-readable status label for consistent display (#79). E.g., "[CI Failing]", "[Needs Response]". */
105
109
  displayLabel: string;
106
110
  /** Brief description of what's happening (#79). E.g., "3 checks failed", "@maintainer commented". */
@@ -321,21 +325,10 @@ export interface DailyDigest {
321
325
  generatedAt: string;
322
326
  /** All open PRs authored by the user, fetched from GitHub Search API. */
323
327
  openPRs: FetchedPR[];
324
- prsNeedingResponse: FetchedPR[];
325
- ciFailingPRs: FetchedPR[];
326
- ciBlockedPRs: FetchedPR[];
327
- ciNotRunningPRs: FetchedPR[];
328
- mergeConflictPRs: FetchedPR[];
329
- needsRebasePRs: FetchedPR[];
330
- missingRequiredFilesPRs: FetchedPR[];
331
- incompleteChecklistPRs: FetchedPR[];
332
- needsChangesPRs: FetchedPR[];
333
- changesAddressedPRs: FetchedPR[];
328
+ /** PRs where the contributor needs to take action. Subset of openPRs where status === 'needs_addressing'. */
329
+ needsAddressingPRs: FetchedPR[];
330
+ /** PRs waiting on the maintainer. Subset of openPRs where status === 'waiting_on_maintainer'. */
334
331
  waitingOnMaintainerPRs: FetchedPR[];
335
- /** PRs with no activity for 25+ days (configurable via `approachingDormantDays`). */
336
- approachingDormant: FetchedPR[];
337
- dormantPRs: FetchedPR[];
338
- healthyPRs: FetchedPR[];
339
332
  /** PRs closed without merge in the last 7 days. Surfaced to alert the contributor. */
340
333
  recentlyClosedPRs: ClosedPR[];
341
334
  /** PRs merged in the last 7 days. Surfaced as wins in the dashboard. */
@@ -354,7 +347,7 @@ export interface DailyDigest {
354
347
  totalActivePRs: number;
355
348
  /** Count of PRs requiring contributor action (response, CI fix, conflict resolution, etc.). */
356
349
  totalNeedingAttention: number;
357
- /** Lifetime merged PR count across all repos, derived from {@link RepoScore} data. */
350
+ /** Lifetime merged PR count across all repos, derived from RepoScore data. */
358
351
  totalMergedAllTime: number;
359
352
  /** Percentage of all-time PRs that were merged (merged / (merged + closed)). */
360
353
  mergeRate: number;
@@ -411,6 +404,24 @@ export interface SnoozeInfo {
411
404
  snoozedAt: string;
412
405
  expiresAt: string;
413
406
  }
407
+ /** Filter for excluding repos below a minimum star count from PR count queries. */
408
+ export interface StarFilter {
409
+ minStars: number;
410
+ knownStarCounts: ReadonlyMap<string, number>;
411
+ }
412
+ /**
413
+ * Check if a repo should be excluded based on its star count.
414
+ * Returns true if the repo is known to be below the threshold.
415
+ * Repos with unknown star counts pass through (fail-open).
416
+ */
417
+ export declare function isBelowMinStars(stargazersCount: number | undefined, minStars: number): boolean;
418
+ /** Manual status override for a PR, set via dashboard or CLI. Auto-clears when new activity is detected. */
419
+ export interface StatusOverride {
420
+ status: FetchedPRStatus;
421
+ setAt: string;
422
+ /** PR's updatedAt at the time the override was set. Used to detect new activity for auto-clear. */
423
+ lastActivityAt: string;
424
+ }
414
425
  /** User-configurable settings, populated via `/setup-oss` and stored in {@link AgentState}. */
415
426
  export interface AgentConfig {
416
427
  /** False until the user completes initial setup via `/setup-oss`. */
@@ -457,6 +468,8 @@ export interface AgentConfig {
457
468
  dismissedIssues?: Record<string, string>;
458
469
  /** PR URLs with snoozed CI failures, mapped to snooze metadata. Snoozed PRs are excluded from actionable CI failure list until expiry. */
459
470
  snoozedPRs?: Record<string, SnoozeInfo>;
471
+ /** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
472
+ statusOverrides?: Record<string, StatusOverride>;
460
473
  }
461
474
  /** Status of a user's comment thread on a GitHub issue. */
462
475
  export type IssueConversationStatus = 'new_response' | 'waiting' | 'acknowledged';
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * Core types for the Open Source Contribution Agent
3
3
  */
4
+ /**
5
+ * Check if a repo should be excluded based on its star count.
6
+ * Returns true if the repo is known to be below the threshold.
7
+ * Repos with unknown star counts pass through (fail-open).
8
+ */
9
+ export function isBelowMinStars(stargazersCount, minStars) {
10
+ return stargazersCount !== undefined && stargazersCount < minStars;
11
+ }
4
12
  /** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
5
13
  export const DEFAULT_CONFIG = {
6
14
  setupComplete: false,
@@ -77,20 +77,8 @@ export interface DailyDigestCompact {
77
77
  generatedAt: string;
78
78
  /** All open PRs authored by the user — the single source of truth for full PR objects. */
79
79
  openPRs: FetchedPR[];
80
- prsNeedingResponse: string[];
81
- ciFailingPRs: string[];
82
- ciBlockedPRs: string[];
83
- ciNotRunningPRs: string[];
84
- mergeConflictPRs: string[];
85
- needsRebasePRs: string[];
86
- missingRequiredFilesPRs: string[];
87
- incompleteChecklistPRs: string[];
88
- needsChangesPRs: string[];
89
- changesAddressedPRs: string[];
80
+ needsAddressingPRs: string[];
90
81
  waitingOnMaintainerPRs: string[];
91
- approachingDormant: string[];
92
- dormantPRs: string[];
93
- healthyPRs: string[];
94
82
  recentlyClosedPRs: DailyDigest['recentlyClosedPRs'];
95
83
  recentlyMergedPRs: DailyDigest['recentlyMergedPRs'];
96
84
  shelvedPRs: ShelvedPRRef[];
@@ -12,20 +12,8 @@ export function deduplicateDigest(digest) {
12
12
  return {
13
13
  generatedAt: digest.generatedAt,
14
14
  openPRs: digest.openPRs,
15
- prsNeedingResponse: toUrls(digest.prsNeedingResponse),
16
- ciFailingPRs: toUrls(digest.ciFailingPRs),
17
- ciBlockedPRs: toUrls(digest.ciBlockedPRs),
18
- ciNotRunningPRs: toUrls(digest.ciNotRunningPRs),
19
- mergeConflictPRs: toUrls(digest.mergeConflictPRs),
20
- needsRebasePRs: toUrls(digest.needsRebasePRs),
21
- missingRequiredFilesPRs: toUrls(digest.missingRequiredFilesPRs),
22
- incompleteChecklistPRs: toUrls(digest.incompleteChecklistPRs),
23
- needsChangesPRs: toUrls(digest.needsChangesPRs),
24
- changesAddressedPRs: toUrls(digest.changesAddressedPRs),
15
+ needsAddressingPRs: toUrls(digest.needsAddressingPRs),
25
16
  waitingOnMaintainerPRs: toUrls(digest.waitingOnMaintainerPRs),
26
- approachingDormant: toUrls(digest.approachingDormant),
27
- dormantPRs: toUrls(digest.dormantPRs),
28
- healthyPRs: toUrls(digest.healthyPRs),
29
17
  recentlyClosedPRs: digest.recentlyClosedPRs,
30
18
  recentlyMergedPRs: digest.recentlyMergedPRs,
31
19
  shelvedPRs: digest.shelvedPRs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.44.3",
3
+ "version": "0.44.15",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  "commander": "^14.0.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "^20.0.0",
54
+ "@types/node": "^25.3.3",
55
55
  "@vitest/coverage-v8": "^4.0.18",
56
56
  "esbuild": "^0.27.3",
57
57
  "tsx": "^4.21.0",